Media FoundationでUSBカメラの映像を表示する
はじめに
前回は、D3DImageを利用してDirect3D9Exで作成した黒い画像をWPFへ表示しました。
今回は、黒い画像の代わりにUSBカメラの映像を表示してみます。
USBカメラの映像はMedia Foundationを利用して取得します。
取得した映像はNV12形式のため、そのままではD3DImageへ表示できません。
今回はCPUでNV12からRGBへ変換し、前回作成したD3DImageへ表示します。
今回作成するプログラム
今回の構成は次のようになります。
USB Camera
│
▼
Media Foundation
│
▼
NV12形式の画像
│
CPUでRGB形式の画像に変換
│
▼
SYSTEMMEM Surface
│
UpdateSurface()
▼
GPU Surface
│
▼
D3DImage
│
▼
Image
1. USBカメラを検索して開く
最初に、接続されているUSBカメラを列挙します。
Media Foundationでは、MFEnumDeviceSources()を利用することで、接続されている映像入力デバイスを取得できます。
次に、検索した1つ目のUSBカメラをオープンします。
ここではIMFMediaSourceとIMFSourceReaderを生成します。
映像の取得はIMFSourceReaderを利用します。
CameraDevice.cpp
bool CaemeraDevice::Open()
{
bool ret = false;
IMFAttributes* pAttributes = NULL;
IMFActivate** ppDevices = NULL;
UINT32 count = 0;
// カメラを列挙する
MFCreateAttributes(&pAttributes, 1);
pAttributes->SetGUID(
MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE,
MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID
);
MFEnumDeviceSources(pAttributes, &ppDevices, &count);
if (count == 0)
{
goto done;
}
// デバイスをオープンする
HRESULT hr = ppDevices[0]->ActivateObject(IID_PPV_ARGS(&pSource));
if (FAILED(hr))
{
goto done;
}
m_pSource = pSource;
m_pSource->AddRef();
ret = true;
done:
SafeRelease(&pAttributes);
for (DWORD i = 0; i < count; i++)
{
SafeRelease(&ppDevices[i]);
}
CoTaskMemFree(ppDevices);
return ret;
}
Media Foundationでは、検索したデバイスの中から今回は1台目のUSBカメラをオープンしています。
2. カメラが対応しているフォーマットを取得する
USBカメラは複数の解像度やピクセルフォーマットに対応しています。
今回は利用可能なフォーマットを列挙し、その中からNV12形式を選択します。
例えば次のようなフォーマットが取得できます。
| 解像度 | フォーマット |
|---|---|
| 640×480 | YUY2 |
| 640×480 | NV12 |
| 1280×720 | NV12 |
| 1920×1080 | MJPG |
今回はNV12形式を使用します。
フォーマットの選択は、カメラオープン時に作成したm_pSourceから取得する以下のインターフェイスを使用して行います。
IMFPresentationDescriptor
IMFStreamDescriptor
IMFMediaTypeHandler
IMFMediaType
Media Foundationでは、複数のインターフェースを経由して利用可能なフォーマットへアクセスします。
今回は処理の流れを分かりやすくするため、ソースコードは省略します。
3. キャプチャを開始する
フォーマットを設定後、ReadSample()を繰り返し呼び出すことで映像を取得できます。
取得した映像は次のような流れで画像データを取得します。
IMFSample
│
▼
IMFMediaBuffer
│
▼
BYTE*
CameraDevice.cpp
class SourceReaderCB : public IMFSourceReaderCallback
{
private:
D3DPreview* m_d3d_preview;
public:
SourceReaderCB(HANDLE hEvent, D3DPreview* d3d_preview) : m_hEvent(hEvent), m_d3d_preview(d3d_preview)
{
InitializeCriticalSection(&m_critsec);
}
// IMFSourceReaderCallback methods
STDMETHODIMP OnReadSample(HRESULT hrStatus, DWORD dwStreamIndex,
DWORD dwStreamFlags, LONGLONG llTimestamp, IMFSample* pSample)
{
if (SUCCEEDED(hrStatus))
{
if (pSample)
{
if (dwStreamFlags & MF_SOURCE_READERF_ERROR)
{
return S_OK;
}
// キャプチャした画像のバッファを取得する
IMFMediaBuffer* mediaBuffer = nullptr;
pSample->ConvertToContiguousBuffer(&mediaBuffer);
IMF2DBuffer* buffer2D = nullptr;
mediaBuffer->QueryInterface(IID_PPV_ARGS(&buffer2D));
BYTE* scan0 = nullptr;
LONG stride = 0;
buffer2D->Lock2D(&scan0, &stride);
// DirectXの画像に反映する
m_d3d_preview->UpdateSysSurface(scan0, stride);
buffer2D->Unlock2D();
SafeRelease(&buffer2D);
SafeRelease(&mediaBuffer);
// 次の画像に進む
SetEvent(m_hEvent);
}
}
}
bool Wait(DWORD dwMilliseconds)
{
DWORD dwResult = WaitForSingleObject(m_hEvent, dwMilliseconds);
if (dwResult == WAIT_OBJECT_0)
{
return true;
}
return false;
}
}
bool CaemeraDevice::Start(int format_index, D3DPreview* d3d_preview)
{
m_format_index = format_index;
m_d3d_preview = d3d_preview;
m_is_running = true;
// カメラからの画像受信スレッド
m_capture_thread = std::thread([this]()
{
// キャプチャ画像を受け取るIMFSourceReaderCallbackのオブジェクトを生成する
HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
SourceReaderCB* pCallback = new (std::nothrow) SourceReaderCB(hEvent, d3d_preview);
// コールバックを設定する
IMFAttributes* pAttributes = nullptr;
MFCreateAttributes(&pAttributes, 1);
pAttributes->SetUnknown(MF_SOURCE_READER_ASYNC_CALLBACK, pCallback);
// ソースリーダーを取得する
IMFSourceReader* pReader = NULL;
MFCreateSourceReaderFromMediaSource(m_pSource, pAttributes, &pReader);
SafeRelease(&pAttributes);
// キャプチャ終了が指示されるまで繰り返す
while (m_is_running)
{
// 画像取得を開始する
DWORD flags;
IMFSample* pSample = nullptr;
HRESULT hr = pReader->ReadSample(
MF_SOURCE_READER_FIRST_VIDEO_STREAM,
0,
NULL,
NULL,
NULL,
NULL
);
if (SUCCEEDED(hr))
{
while (m_is_running)
{
// 画像取得完了を待つ
if (pCallback->Wait(1000))
{
break;
}
}
}
else
{
break;
}
}
SafeRelease(&pReader);
SafeRelease(&pCallback);
});
return true;
}
4. NV12をRGBへ変換する
今回カメラから取得できる映像はNV12形式です。
D3DImageはRGB形式のSurfaceを表示するため、そのままでは表示できません。
そこで、今回はCPUでNV12からA8R8G8B8へ変換します。
NV12
│
▼
CPUでRGB変換
│
▼
A8R8G8B8
D3DPreview.cpp
void D3DPreview::UpdateSysSurface(unsigned char* src, int stride)
{
D3DLOCKED_RECT lr;
m_d3d_sys_surface9->LockRect(&lr, NULL, 0);
BYTE* dst = static_cast<BYTE*>(lr.pBits);
// NV12 -> RGBに変換しつつ、CPUのSurfaceに画像データを転送する
for (UINT y = 0; y < m_height; y++)
{
BYTE* srcY = src + stride * y;
BYTE* srcUV = src + stride * m_height + stride * (y / 2);
BYTE* dstLine = dst + lr.Pitch * y;
for (UINT x = 0; x < m_width; x++)
{
int Y = srcY[x];
int U = srcUV[(x / 2) * 2 + 0] - 128;
int V = srcUV[(x / 2) * 2 + 1] - 128;
int C = Y;
int R = C + (int)(1.402 * V);
int G = C - (int)(0.344 * U + 0.714 * V);
int B = C + (int)(1.772 * U);
R = max(0, min(255, R));
G = max(0, min(255, G));
B = max(0, min(255, B));
dstLine[x * 4 + 0] = (BYTE)B;
dstLine[x * 4 + 1] = (BYTE)G;
dstLine[x * 4 + 2] = (BYTE)R;
dstLine[x * 4 + 3] = 0xff;
}
}
m_d3d_sys_surface9->UnlockRect();
// GPUのSurfaceに画像データを転送する。
m_d3d_device_ex->UpdateSurface(
m_d3d_sys_surface9,
nullptr,
m_d3d_surface9,
nullptr
);
}
ちなみに、NV12とはY成分とUV成分が分かれて格納されているフォーマットです。
Y plane ... 輝度
----------
YYYYYYYYYYYYYYYY
YYYYYYYYYYYYYYYY
YYYYYYYYYYYYYYYY
YYYYYYYYYYYYYYYY
:
UV plane ... 色差
----------
UVUVUVUVUVUVUVUV
UVUVUVUVUVUVUVUV
:
5. D3DImageへ表示する
RGBへ変換した画像は、前回作成したD3DPreviewクラスを利用して表示します。
表示処理は前回と同じ流れになりますが、今回は簡易的に1msごとに表示更新することで、動画となるようにしました。
MainWindow.xaml.cs
private void StartCamera_Click(object sender, RoutedEventArgs e)
{
// キャプチャを開始する
if (!cameraControler.Start())
{
return;
}
previewStop = false;
displayThread = new Thread(() =>
{
while (!previewStop)
{
unsafe
{
Dispatcher.BeginInvoke((Action)(() =>
{
if (previewStop)
{
return;
}
d3dimg.Lock();
if (!backBufferInitialized)
{
IntPtr surface = (IntPtr)cameraControler.GetSurface();
d3dimg.SetBackBuffer(D3DResourceType.IDirect3DSurface9, surface, true);
backBufferInitialized = true;
}
d3dimg.AddDirtyRect(new Int32Rect(0, 0, d3dimg.PixelWidth, d3dimg.PixelHeight));
d3dimg.Unlock();
}));
// 約1ms毎に表示を更新する(本来はカメラから画像を受信した後で表示を更新)
Thread.Sleep(1);
}
}
});
displayThread.Start();
}
まとめ
今回はMedia Foundationを利用してUSBカメラの映像を取得し、NV12からRGBへ変換してD3DImageへ表示しました。
内容は次の通りです。
- Media FoundationでUSBカメラを検索しオープンする
- 対応フォーマットを取得する
- NV12形式で映像を取得する
- CPUでRGBへ変換する
- D3DImageへ表示する
今回は処理の流れを分かりやすくするため、CPUでNV12からRGBへ変換しました。しかし、高解像度の映像ではCPU負荷が大きくなります。次回はDirect3D11のVideo Processorを利用し、GPUで高速に色変換を行う方法を紹介します。
本記事では、D3DImageの動作の流れを分かりやすくすることを目的としているため、エラー処理やリソースの解放処理、C++/CLIのインターフェース実装など、一部のコードは省略しています。
実際のアプリケーションでは、適切なエラー処理やリソース管理を実装してください。