Захват и покадровая обработка видеосигнала в среде .Net

Автор: Чечель Андрей Олегович
Опубликовано: 31.05.2012
Версия текста: 1.0
Введение
Покадровая обработка видеосигнала
Библиотека AviCap32.dll
Фреймворк DirectShow
Фреймворк Media Foundation
Выводы
Список литературы

Введение

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

В статье рассматриваются способы захвата видеосигнала в среде .Net для осуществления покадровой обработки. Для решения этой задачи предлагается воспользоваться интерфейсами и фреймворками, разработанными компанией Microsoft: AviCap32, DirectShow, Media Foundation. Главные преимущества выбранного подхода – то, что они внедрены в поставку операционной системы Windows и распространяются с комплектом средств разработки (SDK) для Windows. Исключается необходимость сопровождения и поддержки сторонних библиотек, забота о совместимости с разными версиями операционной системы Windows, а также исключаются финансовая сторона вопроса, связанная с получением дополнительных лицензий.

Предложенные способы обработки видеосигнала выбраны из тех соображений, что они являются достаточно простыми в реализации, а также могут использоваться шаблонно – это позволяет разработчику концентрироваться на поставленной задаче. В статье описываются подходы, повышающие эффективность покадровой обработки для приложений, построенных на базе технологий WindowsForms и WPF. Материалы статьи могут быть полезны C# разработчикам для создания систем распознавания образов, видео трекинга объектов и т.д.

Покадровая обработка видеосигнала

Процесс покадровой обработки видеосигнала в общем случае включает в себя следующие этапы: получение списка устройств-источников видеосигнала; инициализация подключения к требуемому устройству; управление устройством (запуск и остановка воспроизведения); получение отдельных кадров из видео потока; обработка полученного кадра; закрытие соединения с источником видеосигнала.

Стоит отметить, что существует 3 основных метода реализации систем покадровой обработки видеосигналов:

[DllImport("gdi32.dll")]
publicstaticexternbool BitBlt(IntPtr hdcDest, int nXDest, int nYDest, int nWidth, int nHeight,
                 IntPtr hdcSrc, int nXSrc, int nYSrc, int dwRop);

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

Библиотека AviCap32.dll

Библиотека является частью фреймворка VFW (Video for Windows) [1], поставляется вместе с операционной системой Windows и предназначена для работы с устройствами, поддерживающими интерфейс MCI (Media Control Interface), т.е. с такими медиа-устройствами как цифровые видеокамеры, Web-камеры, видеокарты и т.д. Главное назначение AviCap32 – предоставить удобный механизм управления устройствами посредством сообщений. На данный момент библиотека является устаревшей, Microsoft не рекомендует использовать ее при разработке приложений, так как существуют более новые технологии, например DirectShow. В любом случае, обзор этой библиотеки включен в статью, как наиболее простого и удобного средства организации покадровой обработки захваченного видеосигнала. Название DLL-файла объясняется тем, что одним из основных предназначений библиотеки является работа с видеофайлами в формате AVI (Audio Video Interleave), однако, вместе с этим, она также позволяет выполнять захват видеопотока с соответствующих устройств. Для этих целей используется следующий набор функций:

Для получения списка поддерживаемых устройств и итерации по доступным драйверам устройств используется метод capGetDriverDescriptionA. Функция возвращает имя и версию драйвера устройства по указанному индексу. Далее, выбрав подходящий драйвер устройства, необходимо создать окно, в которое будет производиться захват сигнала с устройства. Так как отдельной функции получения единичного кадра в AviCap32 не существует, то в качестве альтернативы можно с нужной периодичностью копировать содержимое созданного окна в объект Bitmap. Для определения момента доступности кадра есть возможность воспользоваться функцией обратного вызова (callback function), для этого необходимо послать сообщение WM_CAP_SET_CALLBACK_FRAME в созданное окно. После создания окна выполняется инициализация подключения к драйверу устройства. Следующий метод выполняет эту задачу:

[DllImport("avicap32.dll", EntryPoint = "capGetDriverDescriptionA")]
publicstaticexternbool GetDriverDescription(
  short wDriverIndex, 
[MarshalAs(UnmanagedType.VBByRefStr)] ref String lpszName,
  int cbName, 
[MarshalAs(UnmanagedType.VBByRefStr)] ref String lpszVer, int cbVer);

[DllImport("avicap32.dll", EntryPoint = "capCreateCaptureWindowA")]
publicstaticexternint CreateCaptureWindow(
  [MarshalAs(UnmanagedType.VBByRefStr)] refstring lpszWindowName,
  int dwStyle, int x, int y, int nWidth, 
  int nHeight, int hWndParent, int nId);

[DllImport("user32")]
publicstaticexternint SetWindowPos(
  int hwnd, int hWndInsertAfter, int x, int y,
  int cx, int cy, int wFlags);

[DllImport("user32", EntryPoint = "SendMessageA")]
publicstaticexternint SendMessage(
  int hwnd, int wMsg, int wParam,
  [MarshalAs(UnmanagedType.AsAny)] object lParam);

[DllImport("user32")]
publicstaticexternbool DestroyWindow(int hwnd);


constint WS_CHILD = 0x40000000;
constint WS_VISIBLE = 0x10000000;
constint WM_CAP_DRIVER_CONNECT = 0x40a;
constint WM_CAP_DRIVER_DISCONNECT = 0x40b;
constint WM_CAP_SET_PREVIEWRATE = 0x434;
constshort SWP_NOSENDCHANGING = 0x0040;

privateint _hWnd;
publicvoid Connect(int deviceId, int width, int height, Control control)
{
  string winName = "AviCap32 Win";
  _hWnd = CreateCaptureWindow(ref winName, WS_CHILD | WS_VISIBLE, 0, 0,
                width, height, control.Handle.ToInt32(), 0);

  if (0 != SendMessage(_hWnd, WM_CAP_DRIVER_CONNECT, deviceId, 0))
  {
    SendMessage(_hWnd, WM_CAP_SET_PREVIEWRATE, 1, 0);
    SetWindowPos(_hWnd, 0, 0, 0, width, height, SWP_NOSENDCHANGING);
  }
  else
  {
    DestroyWindow(_hWnd);
  }
}

В рассматриваемом методе Connectможно подчеркнуть несколько ключевых моментов:

  1. Вызов функции CreateCaptureWindow создает окно для захвата видеосигнала. Окно привязывается к компоненту, который передается в качестве параметра с требуемым значением размера рабочей области компонента и не обязательными флагами, указывающими, является ли окно дочерним и видимым. В коде метода опущена проверка на наличие компонента: если он не инициализирован, должно быть создано временное окно с компонентом заданных размеров. Метод также не отражает сохранение ссылки на этот временный компонент: она может понадобиться для вызова метода Dispose по завершению обработки.
  2. В созданное окно передается сообщение WM_CAP_DRIVER_CONNECT для инициализации подключения окна к драйверу видеоустройства. Метод принимает идентификатор (индекс) видеоустройства в списке, который можно получить, обращаясь к функции capGetDriverDescriptionA. В случае ошибки необходимо уничтожить созданное окно при помощи функции DestroyWindow. Как показывает практика, первая попытка подключения может провалиться – поэтому рекомендуется выполнять 2-3 попытки в цикле.
  3. Если подключение установлено, необходимо задать скорость обновления кадров в окне. Для этого окну посылается сообщение WM_CAP_SET_PREVIEWRATE, в качестве параметра к которому указывается длительность показа единичного кадра в миллисекундах (в примере она равняется 1 мс). Также необходимо отключить обработку сообщения WM_WINDOWPOSCHANGING, для чего вызывается функция SetWindowPos с передачей флага SWP_NOSENDCHANGING. Это делается для того, чтобы зафиксировать размеры и положение созданного окна. Кроме того, манипулируя флагами, можно зафиксировать его положение по оси Z, если в этом есть необходимость.

Функции SendMessage, SetWindowPos и DestroyWindow определяются в библиотеке user32.dll. После вызова метода Connect происходит подключение к драйверу видеоустройства. Управлять воспроизведением можно при помощи сообщения WM_CAP_SET_PREVIEW. Для остановки показа достаточно передать длительность показа кадра, равную 0. Для возобновления – значение, соответствующее требуемой длительности показа кадра (в миллисекундах). Как уже говорилось, для захвата единичного кадра достаточно скопировать поверхность привязанного компонента. Эта операция осуществляется следующей функцией:

      var controlGraphics = control.CreateGraphics();
var bitmapGraphics = Graphics.FromImage(frameBitmap);

publicstaticvoid CopyControlFace(
Graphics controlGraphics, Graphics imageGraphics)
{
  constint SrcCopy = 0xCC0020;
  IntPtr srcHdc = IntPtr.Zero;
  IntPtr dstHdc = IntPtr.Zero;
  RectangleF size = imageGraphics.VisibleClipBounds;

  lock (_locker)
  {
    try
    {
      srcHdc = controlGraphics.GetHdc();
      dstHdc = imageGraphics.GetHdc();

      BitBlt(
        dstHdc, 0, 0, (int) size.Width, (int) size.Height, 
        srcHdc, 0, 0, SrcCopy);
    }
    finally
    {
      controlGraphics.ReleaseHdc(srcHdc);
      imageGraphics.ReleaseHdc(dstHdc);
    }
  }
}

В качестве параметров функция принимает поверхность рисования GDI+ Graphics. Первый параметр – поверхность, ассоциированная с компонентом, вторая – с объектом Bitmap, в который выполняется сохранение кадра. Так как покадровая обработка выполняется в цикле, то имеет смысл зафиксировать (кешировать) объект Bitmap и полученный для него объект Graphics при первой итерации. Такой подход поднимает производительность копирования, так как на получение объектов Graphics может затрачиваться некоторое время. То же самое нужно проделать с поверхностью рисования для компонента, в который производится захват видеосигнала.

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

      public
      void Disconnect(int deviceId)
{
  SendMessage(_hWnd, WM_CAP_DRIVER_DISCONNECT, deviceId, 0);
  DestroyWindow(_hWnd);
}

Фреймворк DirectShow

DirectShow – фреймворк, позволяющий управлять аудио- и видеоустройствами, взаимодействуя с VFW и WDM (Windows Driver Model), путем воспроизведения и захвата мультимедийных потоков [2]. Изначально DirectShow являлся частью DirectX SDK, но был перенесен в Windows SDK, хотя по-прежнему опирается на такие технологии как DirectSound, DirectDraw, Direct3D, входящие в состав DirectX. Фреймворк поддерживает ряд видео и аудио форматов, включая AVI, MPEG, MP3 и др., а также позволяет автоматически задействовать аппаратного обеспечения, использующегося для ускорения обработки.

В рамках фреймворка объекты представляются так называемыми фильтрами, которые условно можно подразделить на 3 категории: фильтры захвата, обработки и визуализации. Разработчик, использующий DirectShow, может объединить требуемый набор фильтров в граф. Примером графа может служить следующая цепочка: фильтр захвата видео, фильтр захвата аудио => фильтр преобразования (кодек, объединение аудио- и видеопотока) => фильтр визуализации (вывод на экран или запись в файл). Чтобы поддержать универсальность фреймворка, DirectShow был разработан на основе компонентной модели COM. Для платформы .Net компания Microsoft не предусмотрела отдельной реализации, поэтому необходимо использовать COM Interop для взаимодействия с функциями и COM-объектами. Более простой способ – использовать, например, свободно распространяемую библиотеку DirectShow .Net (DirectShowLib) [3], на примере которой далее в статье будут разобраны ключевые моменты работы с видеоустройствами.

Для получения списка устройств (фильтров) необходимо воспользоваться следующим методом. В качестве параметра метод принимает идентификатор категории запрашиваемых фильтров. Разнообразие категорий включает в себя устройства ввода, кодеки, контроллеры и т.д. Для получения списка устройств, способных захватывать видеосигнал, в качестве параметра необходимо передать категорию FilterCategory.VideoInputDevice.

      public
      static List<IBaseFilter> GetDevices(Guid filterCategory)
{
  DsDevice[] devices = null;
  var sources = new List<IBaseFilter>();
      
  try
  {
    devices = DsDevice.GetDevicesOfCat(filterCategory);
    foreach (var device in devices)
    {
      if (device.Mon != null)
      {
        object source;
        var iid = typeof(IBaseFilter).GUID;
        device.Mon.BindToObject(null, null, ref iid, out source);
        if (source != null)
        {
          sources.Add((IBaseFilter)source);
        }
      }
    }
  }
  finally
  {
    if (devices != null)
    {
      foreach (var device in devices)
      {
        device.Dispose();
      }
    }
  }

  return sources;
}

Для выполнения покадровой обработки DirectShow позволяет использовать как специально созданный для этой задачи фильтр с программным интерфейсом ISampleGrabber,так и создать окно захвата. Рассмотрим способ, использующий окно захвата. Выбрав интересующее устройство, необходимо выполнить инициализацию. На этом шаге создается менеджер графа, реализующий интерфейс IGraphBuilder, а также построитель графа захвата – объект, который реализует интерфейс ICaptureGraphBuilder2. Построитель графа захвата предоставляет наиболее простой способ подключения к источнику видеосигнала, соединяя его с приемником при помощи функции RenderStream. Менеджер графа в свою очередь реализует интерфейсы IVideoWindow и IMediaControl. Первый интерфейс описывает методы доступа к окну, в котором производится отображение захваченного видеосигнала. Второй интерфейс объявляет методы, использующиеся для управления потоком сигнала (старт, пауза, стоп).

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

      public
      void Connect(IBaseFilter baseFilter)
{
  _baseFilter = baseFilter;
  _graphBuilder = (IGraphBuilder)new FilterGraph();
  _captureGraphBuilder = (ICaptureGraphBuilder2)new CaptureGraphBuilder2();
  _videoWindow = (IVideoWindow)_graphBuilder;
  _mediaControl = (IMediaControl)_graphBuilder;

  int hr = _captureGraphBuilder.SetFiltergraph(_graphBuilder);
  DsError.ThrowExceptionForHR(hr);

  hr = _graphBuilder.AddFilter(_baseFilter, Name);
  DsError.ThrowExceptionForHR(hr);

  hr = _captureGraphBuilder.RenderStream(
    PinCategory.Preview, MediaType.Video, _baseFilter, null, null);
  DsError.ThrowExceptionForHR(hr);

  SetPreviewVisible(true);
}

privatevoid SetPreviewVisible(bool isVisible)
{
  if (isVisible)
  {
    int hr = _videoWindow.put_Owner(_parent.Handle);
    DsError.ThrowExceptionForHR(hr);

    hr = _videoWindow.put_WindowStyle(
      WindowStyle.Child | WindowStyle.ClipChildren);
    DsError.ThrowExceptionForHR(hr);

    hr = _videoWindow.put_Visible(OABool.True);
    DsError.ThrowExceptionForHR(hr);
  }
  else
  {
    int hr = _videoWindow.put_Visible(OABool.False);
    DsError.ThrowExceptionForHR(hr);

    hr = _videoWindow.put_Owner(IntPtr.Zero);
    DsError.ThrowExceptionForHR(hr);
  }
}

Описанный код выполняет подключение к выбранному видеоустройству. Как уже отмечалось, управление потоком выполняется при помощи объекта, реализующего интерфейс IMediaControl, предоставляющий методы: Run, Stop, Pause. Для получения кадра видеосигнала в виде объекта Bitmap необходимо воспользоваться методом CopyControlFace, использующимся в работе с библиотекой AviCap32. По завершении работы с устройством, необходимо скрыть окно захвата и освободить используемые COM-объекты. Эти действия выполняет следующая функция:

      public
      void Disconnect()
{
  int hr = _mediaControl.Stop();
  DsError.ThrowExceptionForHR(hr);
  SetPreviewVisible(false);

  Marshal.ReleaseComObject(_baseFilter);
  Marshal.ReleaseComObject(_mediaControl);
  Marshal.ReleaseComObject(_videoWindow);
  Marshal.ReleaseComObject(_captureGraphBuilder);
  Marshal.ReleaseComObject(_graphBuilder);
}

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

Последний фильтр добавляется как заглушка, для обозначения того, что поток не имеет специального фильтра, выполняющий рендеринг. В случае работы, например, с Web-камерой этот фильтр может быть опущен, однако если в случае источника сигнала является видеофайл, опускать фильтр NullRenderer не стоит – так как это может привести к тому, что поток будет некорректно реагировать на сигналы управления. Для соединения фильтров необходимо воспользоваться следующими методами, устанавливающими связь между входами и выходами фильтров:

      private
      static
      void ConnectFilterToFilter(
IGraphBuilder graphBuilder, IBaseFilter srcFilter, 
  IBaseFilter dstFilter, Guid mediaType)
{
  IEnumPins srcPins;
  int hr = srcFilter.EnumPins(out srcPins);
  DsError.ThrowExceptionForHR(hr);

  var srcPin = new IPin[1];
  while (0 == srcPins.Next(1, srcPin, IntPtr.Zero))
  {
    IPin connectedTo;
    if (0 != srcPin[0].ConnectedTo(out connectedTo))
    {
      if (PinHasMediaType(srcPin[0], mediaType))
      {
        PinDirection pd;
        hr = srcPin[0].QueryDirection(out pd);
        DsError.ThrowExceptionForHR(hr);

        if (pd == PinDirection.Output)
        {
          ConnectPinToFilter(graphBuilder, srcPin[0], dstFilter);
        }
      }
    }
  }
}

privatestaticvoid ConnectPinToFilter(
IGraphBuilder graphBuilder, IPin srcPin, IBaseFilter dstFilter)
{
  IEnumPins dstPins;
  int hr = dstFilter.EnumPins(out dstPins);
  DsError.ThrowExceptionForHR(hr);

  var dstPin = new IPin[1];
  while (0 == dstPins.Next(1, dstPin, IntPtr.Zero))
  {
    IPin connectedTo;
    if (0 != dstPin[0].ConnectedTo(out connectedTo))
    {
      PinDirection pd;
      hr = dstPin[0].QueryDirection(out pd);
      DsError.ThrowExceptionForHR(hr);

      if (pd == PinDirection.Input)
      {
        hr = graphBuilder.Connect(srcPin, dstPin[0]);
        DsError.ThrowExceptionForHR(hr);
      }
    }
  }
}

privatestaticbool PinHasMediaType(IPin pin, Guid mediaType)
{
  IEnumMediaTypes mt;
  var amt = new AMMediaType[1];
  int hr = pin.EnumMediaTypes(out mt);
  DsError.ThrowExceptionForHR(hr);
  if (mt != null)
  {
    while (0 == mt.Next(1, amt, IntPtr.Zero))
    {
      if (amt[0].majorType == mediaType)
      {
        returntrue;
      }
    }
  }

  returnfalse;
}

Метод ConnectFilterToFilter связывает контакты вывода одного фильтра с контактами ввода другого. Связываемые контакты должны поддерживать переданный в виде параметра тип медиаинформации, это условие проверяется при помощи метода PinHasMediaType. В методе ConnectFilterToFilter выполняется итерация по контактам фильтра-источника, для контактов вывода выполняется метод связывания с фильтром-приемником ConnectPinToFilter, в котором выполняется итерация по всем контактам ввода и установление связи между контактами. Перейдем к рассмотрению инициализации подключения к драйверу устройства. Это выполняет следующий метод:

      public
      void Connect()
{
  try
  {
    _filterGraph = new FilterGraph();
    _graphBuilder = (IGraphBuilder)_filterGraph;
    _mediaControl = (IMediaControl)_filterGraph;
    _sampleGrabberObj = new SampleGrabber();
    _sampleGrabber = (ISampleGrabber)_sampleGrabberObj;
    _sampleGrabberFilter = (IBaseFilter)_sampleGrabberObj;

    if (_graphBuilder != null)
    {
      int hr = _graphBuilder.AddFilter(_baseFilter, Name);
      DsError.ThrowExceptionForHR(hr);

      hr = _graphBuilder.AddFilter(
        _sampleGrabberFilter, "SampleGrabberFilter");
      DsError.ThrowExceptionForHR(hr);

      var mediaType = new AMMediaType();
      mediaType.majorType = MediaType.Video;
      mediaType.subType = MediaSubType.ARGB32;
      _sampleGrabber.SetMediaType(mediaType);

      ConnectFilterToFilter(
        _graphBuilder, _baseFilter, _sampleGrabberFilter, MediaType.Video);

      // Add null renderervar nullRender = new NullRenderer();
      var nullRenderFilter = (IBaseFilter)nullRender;
      _graphBuilder.AddFilter(nullRenderFilter, "NullRendererFilter");
      ConnectFilterToFilter(
        _graphBuilder, _sampleGrabberFilter, 
        nullRenderFilter, MediaType.Video);

      _sampleGrabberCb = new SampleGrabberCB(
        _sampleGrabber, PixelFormat.Format32bppArgb);
    }
  }
  catch
  {
    Disconnect();
  }
}

В методе Connect выполняется инициализация менеджера графа, фильтра SampleGrabber. Фильтр источника сигнала и фильтр захвата кадров добавляются в граф. Для объекта SampleGrabberвызывается метод, задающий требуемый тип медиаданных. В приведенном примере используется значение ARGB32. Фильтры соединяются между собой при помощи рассмотренного метода ConnectFilterToFilter. В завершение выполняется создание объекта SampleGrabberCB – это класс-обработчик, содержащий функции обратного вызова, используемые для получения кадров. Класс реализует интерфейс ISampleGrabberCB. В конструкторе выполняется вызов метода фильтра захвата кадров, возвращающего информацию о кадре (размеры, тип), кроме того, класс устанавливает себя в качестве обработчика кадров. Чтобы отписаться от события получения кадров, необходимо снова вызвать метод SetCallBack, передав в качестве параметра значение null. Синхронный метод BufferCB получает обратный вызов в момент готовности кадра. В качестве параметра на вход поступает указатель на участок памяти, содержащий видеокадр, размер участка памяти в байтах и временной код кадра. Стоит отметить, что для формата RGB32 кадр располагается в памяти перевернутым, точнее, отраженным сверху вниз, поэтому необходимо учитывать этот факт при копировании. После того как объект Bitmap подготовлен, выполняется уведомление ожидающих потоков. Метод GetFrame дожидается сигнала о готовности кадра и возвращает полученное изображение.

      private
      readonly ManualResetEvent _frameReadyEvent = 
  new ManualResetEvent(false);
privatereadonlyobject _currFrameLocker = newobject();

public SampleGrabberCB(ISampleGrabber sampleGrabber, PixelFormat pixelFormat)
{
  var am = new AMMediaType();
  sampleGrabber.GetConnectedMediaType(am);
  var videoInfo = (VideoInfoHeader)Marshal.PtrToStructure(
am.formatPtr, typeof(VideoInfoHeader));

  _width = videoInfo.BmiHeader.Width;
  _height = videoInfo.BmiHeader.Height;
  _pixelFormat = pixelFormat;

  var hr = sampleGrabber.SetCallback(this, 1);
  DsError.ThrowExceptionForHR(hr);

  _frameReadyEvent.Reset();
}

publicint BufferCB(double sampleTime, IntPtr pBuffer, int bufferLen)
{
  var newFrame = new Bitmap(_width, _height, _pixelFormat);
  CreateBitmapInversed(newFrame, pBuffer, bufferLen);

  lock (_currFrameLocker)
  {
    if (_currFrame != null)
    {
      _currFrame.Dispose();
    }
    _currFrame = newFrame;
  }

    _frameReadyEvent.Set();
  return 0;
}

public Bitmap GetFrame()
{
  Bitmap frame;
  _frameReadyEvent.WaitOne();
  lock (_currFrameLocker)
  {
    frame = _currFrame;
    _currFrame = null;
    _frameReadyEvent.Reset();
  }
  return frame;
}

По завершении покадровой обработки видеосигнала необходимо выполнить отключение функций обратного вызова и освободить все используемые COM-объекты.

Фреймворк Media Foundation

В завершении рассмотрим применение Media Foundation к задаче покадровой обработки видеосигнала. В ряду представленных в данной статье технологий этот фреймворк стоит на самом верху, являясь передовой разработкой компании Microsoft, способной заменить в будущем устаревшие технологии. В Media Foundation реализована улучшенная, более эффективная поддержка аудио и видео форматов, в том числе форматов видео высокого качества за счет использования специальной обработки видео с аппаратным ускорением; разработан упрощенный программный интерфейс для ускорения разработки приложений и многое другое [4].

Media Foundation, так же как и DirectShow, спроектирован на основе объектной модели компонентов (COM) – что обуславливает необходимость использования COM Interop при программировании на языке C#. И снова, в качестве упрощения ручного труда есть возможность воспользоваться готовой библиотекой MediaFoundation .Net [5]. Но стоит отметить, что некоторые методы и интерфейсы библиотеки находятся в стадии тестирования, поэтому не включены в скомпилированный вариант – так что для полноценной работы необходимо вручную скомпилировать код библиотеки, указав флаг ALLOW_UNTESTED_INTERFACES.

Перед началом использования Media Foundation необходимо вызвать метод MFStartup для инициализации фреймворка, а по завершении работы – MFShutdown.

      int hr = MFExtern.MFStartup(0x20070, MFStartup.Full);
MFError.ThrowExceptionForHR(hr);

hr = MFExtern.MFShutdown();
MFError.ThrowExceptionForHR(hr);

Для получения списка устройств, которые можно использовать для захвата видеосигнала, необходимо воспользоваться методом GetDevices. Метод начинается с создания атрибутов (IMFAttributes) источников сигнала. Здесь в качестве параметра устанавливается флаг MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING в 1 – это значение позволяет объекту SourceReader производить конвертацию исходного формата сигнала YUV в формат RGB32. Кроме того определяется тип источника сигнала – указывается идентификатор категории устройств, позволяющих выполнять захват видеосигнала. Используя функцию MFEnumDeviceSources можно получить коллекцию активаторов устройств, соответствующих указанным атрибутам. Активатор обладает функцией ActivateObject, при помощи которой он создает объект, реализующий интерфейс IMFMediaSource, непосредственно представляющий устройство-источник видеосигнала. Функция MFCreateSourceReaderFromMediaSource используется для создания объекта, реализующего интерфейс IMFSourceReader, этот объект используется для получения данных из источника сигнала, например для получения единичных кадров.

      public List<Tuple<IMFMediaSource, IMFSourceReader>> GetDevices()
{
  int hr = MFExtern.MFStartup(0x20070, MFStartup.Full);
  MFError.ThrowExceptionForHR(hr);

  IMFAttributes a;
  hr = MFErrorMFExtern.MFCreateAttributes(out a, 1);
  MFError.ThrowExceptionForHR(hr);
  
  hr = a.SetUINT32(MFAttributesClsid.MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING, 1);
  MFError.ThrowExceptionForHR(hr);
  
  hr = a.SetGUID(MFAttributesClsid.MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE,
           CLSID.MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID);
  MFError.ThrowExceptionForHR(hr);

  var sources = new List<Tuple<IMFMediaSource, IMFSourceReader>>();
  IMFActivate[] devices = null;
  try
  {
    int devicesCount;
    hr = MFExtern.MFEnumDeviceSources(a, out devices, out devicesCount);
    MFError.ThrowExceptionForHR(hr);

    foreach (var device in devices)
    {
      object source;
      hr = device.ActivateObject(typeof(IMFMediaSource).GUID, out source);
      MFError.ThrowExceptionForHR(hr);

      if (source != null)
      {
        var mediaSource = (IMFMediaSource)source;
        IMFSourceReader sourceReader;
        hr = MFExtern.MFCreateSourceReaderFromMediaSource(mediaSource, a, out sourceReader);
        MFError.ThrowExceptionForHR(hr);
        sources.Add(new Tuple<IMFMediaSource, IMFSourceReader>(mediaSource, sourceReader));
      }
    }

    return sources;
  }
  finally
  {
    if(devices != null)
    {
      foreach (var device in devices)
      {
        Marshal.ReleaseComObject(device);
      }
    }
  }
}

Перед непосредственной работой с видеоустройством необходимо определить формат данных. Как уже отмечалось, для захвата видео информации по умолчанию используется формат YUV. На выходе удобнее работать с цветовой схемой RGB – для этого есть возможность задать значение формата для IMFSourceReader (RGB32). Возможность преобразования была включена в предыдущем методе установкой флага MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING. Следующий метод выполняет установку типа медиа данных для IMFSourceReader:

      public
      void InitializeMediaType()
{
  Guid majorType;
  IMFMediaType currMediaType = null;
  IMFMediaType rgbMediaType = null;

  try
  {
    int hr = _srcReader.GetCurrentMediaType(-4, out currMediaType);
    MFError.ThrowExceptionForHR(hr);

    hr = currMediaType.GetGUID(MFAttributesClsid.MF_MT_MAJOR_TYPE, out majorType);
    MFError.ThrowExceptionForHR(hr);
    hr = MFExtern.MFCreateMediaType(out rgbMediaType);
    MFError.ThrowExceptionForHR(hr);


    hr = rgbMediaType.SetGUID(MFAttributesClsid.MF_MT_MAJOR_TYPE, majorType);
    MFError.ThrowExceptionForHR(hr);
    hr = rgbMediaType.SetGUID(MFAttributesClsid.MF_MT_SUBTYPE, MFMediaType.RGB32);
    MFError.ThrowExceptionForHR(hr);
    hr = _srcReader.SetCurrentMediaType(-4, IntPtr.Zero, rgbMediaType);
    MFError.ThrowExceptionForHR(hr);

  }
  finally
  {
    if (currMediaType != null)
    {
      Marshal.ReleaseComObject(currMediaType);
    }
    if (rgbMediaType != null)
    {
      Marshal.ReleaseComObject(rgbMediaType);
    }
  }
 }

Выполнив всю необходимую инициализацию можно приступить непосредственно к покадровой обработке. Для управления потоком используются методы Start, Pause и Stop объекта источника данных, реализующего интерфейс IMediaSource. После запуска потока можно перейти к получению отдельного кадра. В функции GetShapshot выполняется чтение кадра, определение буфера, в котором хранятся данные кадра. После чего получается указатель на буфер путем вызова функции Lock. Остается создать объект Bitmap формата Rgb32 и скопировать в него данные из буфера. К слову, копирование данных из одного участка неуправляемой памяти в другой составляет интересную задачу. Обычное побайтовое копирование проигрывает в 2 раза по производительности, методу, использующему при копировании 4-х байтные значения, который в свою очередь проигрывает вызову функции RtlMoveMemory, входящей в библиотеку kernel32.dll.

      public Bitmap GetSnapshot()
{
  IMFSample ppSample = null;
  IMFMediaBuffer ppBuffer = null;

  try
  {
    long pllTimestamp;
    int pdwActualStreamIndex;
    int pdwStreamFlags;
    int hr = _srcReader.ReadSample(-4, 0, out pdwActualStreamIndex, out pdwStreamFlags, 
                     out pllTimestamp, out ppSample);
    MFError.ThrowExceptionForHR(hr);

    if (ppSample == null)
    {
      returnnull;
    }

    IntPtr ppbBuffer;
    int pcbMaxLength;
    int currBuffLen;

    hr = ppSample.GetBufferByIndex(0, out ppBuffer);
    MFError.ThrowExceptionForHR(hr);

    hr = ppBuffer.Lock(out ppbBuffer, out pcbMaxLength, out currBuffLen);
    MFError.ThrowExceptionForHR(hr);

    return CreateBitmap(ppbBuffer, currBuffLen);
  }
  finally
  {
    if(ppBuffer != null)
    {
      Marshal.ReleaseComObject(ppBuffer);
    }   
    if(ppSample != null)
    {
      Marshal.ReleaseComObject(ppSample);
    }
  }
}

По завершении работы с MediaFoundation необходимо освободить все использующиеся COM объекты при помощи вызова метода Marshal.ReleaseComObject.

Выводы

В статье были рассмотрены способы реализации захвата видеосигнала с устройства-источника с целью выполнения покадровой обработки при помощи AviCap32, DirectShow, Media Foundation. Алгоритмы апробированы и внедрены в систему видео трекинга объектов, использующую концепцию представления графических образов в виде полевых структур [6]. Результаты замера производительности методов представлены на Рис. 1.


Рис. 1. Результаты замера производительности рассмотренных методов

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

В качестве критериев оценки производительности были выбраны два показателя: полезная нагрузка и среднее время доступа к кадру. Значение полезной нагрузки, выраженное в условных единицах, отображает максимальный объем вычислений, производимых над полученным кадром. Из графика видно, что AviCap32 может обеспечить полезную нагрузку в 2 раза меньше, чем остальные способы – причиной этому снижение производительности во время обновления кадра на форме с использованием метода Invoke. В то же время показатель среднего времени доступа к кадру для AviCap32 сравним со временем доступа при использовании DirectShow. Это означает, что убрав вызовы Invoke, можно добиться идентичного значения полезной нагрузки. В случае с Media Foundation, наоборот, значение доступа к кадру выше остальных. Это связано с тем, что кадры не кэшируются на стороне Media Foundation и при запросе очередного кадра теряется время на его ожидание. Добавив дополнительный поток для кэширования кадров, есть возможность снизить значение доступа к кадру.

Можно сделать вывод, что с точки зрения производительности, рассмотренные способы идентичны. Однако для AviCap32 желательно не использовать вызовы методов Invoke, которые могут привести к потере производительности в 2 раза. Стоит отметить, что наиболее удобным и понятным в разработке средством является фреймворк DirectShow. И хотя Media Foundation может составить конкуренцию, в реализации его методы сложнее, кроме того в задаче покадровой обработки видеосигнала программисты могут столкнуться с проблемой доступа к COM объектам из разных потоков, в случае работы без использования специального диспетчера.

Список литературы

  1. Video for Windows: Video Capture // Windows Dev Center. 2011. URL: http://msdn.microsoft.com/en-us/library/windows/desktop/dd757692.aspx (дата обращения 15.11.2011).
  2. Pesce M. Programming Microsoft DirectShow for Digital Video and Television / Mark D. Pesce // Microsoft Press. - Redmond, 2003.
  3. DirectShow .Net Library. URL: http://directshownet.sourceforge.net/ (дата обращения 15.11.2011).
  4. Polinger A. Developing Microsoft Media Foundation Applications / A. Polinger // O’Reilly Media. - Sebastopol, 2011.
  5. Media Foundation .Net Library. URL: http://mfnet.sourceforge.net/ (дата обращения 15.11.2011).
  6. Чечель А.О. Трекинг графических объектов, представленных в виде полевых структур / А.О. Чечель // Современное телевидение и радиоэлектроника. Труды 20-й Международной научно-технической конференции. 2012, с. 130-133.


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