VideoProcessorを使ってGPUでNV12をRGBへ変換する
はじめに
前回は、Media FoundationでUSBカメラから取得したNV12画像をCPUでRGBへ変換し、D3DImageへ表示しました。
この方法でも問題なく動作しますが、1920×1080や4Kなどの高解像度映像では、CPUによる色変換の負荷が無視できません。
Windowsには VideoProcessor という映像処理機能が用意されており、GPUを利用して色変換やリサイズなどの処理を高速に実行できます。
今回は、このVideoProcessorを利用して、NV12からRGBへの変換をGPUで行い、そのままD3DImageへ表示する方法を紹介します。
前回との違い
前回はCPUで色変換を行っていました。
Media Foundation
│
NV12
│
CPUでRGBへ変換
│
RGB
│
D3DImage
今回はVideoProcessorを利用することで、GPU上で色変換を行います。
Media Foundation
│
NV12 Texture
│
VideoProcessor
│
RGB Texture
│
D3DImage
CPUが画素データを1ピクセルずつ処理する必要はなくなり、GPU上だけで変換処理が完結します。
VideoProcessorとは
VideoProcessorは、Direct3D 11が提供する映像処理機能です。
映像再生で必要となる様々な処理をGPU上で実行できます。
主な機能は次のとおりです。
- NV12やYUY2などのYUV→RGB変換
- リサイズ
- クロップ
- 回転
- インターレース解除
- 色空間変換
Media FoundationやWindows標準の動画再生機能でも利用されているため、多くのPCではGPUによる高速な映像処理を利用できます。
VideoProcessorを使うと処理はどう変わるのか
前回は、CPUでNV12からRGBへ変換していました。
今回はその部分をVideoProcessorへ置き換えます。
処理全体は次のようになります。
void D3DPreview::UpdateSysSurface(unsigned char* src, int stride)
{
// NV12形式のまま画像データを変換元としてテクスチャにコピー
D3D11_MAPPED_SUBRESOURCE mapped;
m_d3d11_context->Map(m_d3d11_texture2d_cpu, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped);
BYTE* dst = static_cast<BYTE*>(mapped.pData);
for (UINT y = 0; y < m_height; y++)
{
BYTE* srcLine = src + stride * y;
BYTE* dstLine = dst + mapped.RowPitch * y;
memcpy(dstLine, srcLine, m_width);
}
for (UINT y = 0; y < m_height / 2; y++)
{
BYTE* srcLine = src + stride * m_height + stride * y;
BYTE* dstLine = dst + mapped.RowPitch * m_height + mapped.RowPitch * y;
memcpy(dstLine, srcLine, m_width);
}
m_d3d11_context->Unmap(m_d3d11_texture2d_cpu, 0);
// GPUメモリに転送
m_d3d11_context->CopyResource(
m_d3d11_texture2d_src,
m_d3d11_texture2d_cpu);
// NV12 -> RGB に変換
D3D11_VIDEO_PROCESSOR_STREAM stream = {};
stream.Enable = TRUE;
stream.pInputSurface = m_d3d_input_view;
m_d3d_video_context->VideoProcessorBlt(
m_d3d_video_processor,
m_d3d_output_view,
0,
1,
&stream);
// 表示の為に変換処理が終了するのを待つ
m_d3d_context4->Signal(m_d3d_fence, m_fence_value);
if (m_d3d_fence->GetCompletedValue() < m_fence_value)
{
m_d3d_fence->SetEventOnCompletion(
m_fence_value,
m_fence_event);
WaitForSingleObject(
m_fence_event,
3000);
}
m_fence_value++;
}
一見すると処理が増えたように見えますが、色変換そのものは非常にシンプルです。
実際にNV12からRGBへ変換しているのは、次の数行だけです。
D3D11_VIDEO_PROCESSOR_STREAM stream = {};
stream.Enable = TRUE;
stream.pInputSurface = m_d3d_input_view;
m_d3d_video_context->VideoProcessorBlt(
m_d3d_video_processor,
m_d3d_output_view,
0,
1,
&stream);
VideoProcessorBlt()へ入力テクスチャと出力テクスチャを指定するだけで、GPUがNV12からRGBへの色変換を実行してくれます。
CPU側で画素を1ピクセルずつ計算する必要はありません。
なお、このサンプルではGPUによる描画完了を待つためにFenceによる同期も行っています。
同期処理については今回の本題ではないため詳細な説明は省略しますが、VideoProcessorBlt()の完了を待ってからD3DImageへ表示しています。
VideoProcessorはDirect3D11の機能
VideoProcessorはDirect3D11のAPIとして提供されています。
そのため、VideoProcessorを利用するには、Direct3D11のデバイスを作成する必要があります。
ここで少し困ることがあります。
WPFのD3DImageが扱えるのは、Direct3D9のテクスチャだけです。
つまり、
VideoProcessor
│
D3D11 Texture
│
?
│
D3D9 Texture
│
D3DImage
という橋渡しが必要になります。
Direct3D11とDirect3D9でテクスチャを共有する
実際には、Direct3D11のテクスチャをDirect3D9のテクスチャへコピーしているわけではありません。
共有テクスチャを利用することで、同じGPUメモリをDirect3D11とDirect3D9の両方から参照できます。
GPU Memory
+----------------------+
| D3D11 Texture |
+----------------------+
▲
│ Shared Handle
▼
+----------------------+
| D3D9 Texture |
+----------------------+
│
D3DImage
そのため、GPUメモリ間でデータコピーが発生せず、高速に表示できます。
共有テクスチャを作成する
まずは、Direct3D11側で共有可能なテクスチャを作成します。
共有テクスチャを作成するには、D3D11_RESOURCE_MISC_SHAREDを指定します。
D3D11_TEXTURE2D_DESC desc_dst = {};
desc_dst.Width = width;
desc_dst.Height = height;
desc_dst.MipLevels = 1;
desc_dst.ArraySize = 1;
desc_dst.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
desc_dst.SampleDesc.Count = 1;
desc_dst.Usage = D3D11_USAGE_DEFAULT;
desc_dst.BindFlags = D3D11_BIND_RENDER_TARGET;
desc_dst.MiscFlags = D3D11_RESOURCE_MISC_SHARED;
std::vector<uint32_t> pixels(width * height, 0xff000000);
D3D11_SUBRESOURCE_DATA initData = {};
initData.pSysMem = pixels.data();
initData.SysMemPitch = width * 4;
m_d3d11_device->CreateTexture2D(
&desc_dst,
&initData,
&m_d3d11_texture2d_dst
);
テクスチャ作成後、IDXGIResourceから共有ハンドルを取得します。
ComPtr<IDXGIResource> resource;
m_d3d11_texture2d_dst->QueryInterface(IID_PPV_ARGS(&resource));
HANDLE sharedHandle;
resource->GetSharedHandle(&sharedHandle);
取得した共有ハンドルを利用して、Direct3D9側で同じGPUメモリを開きます。
m_d3d_device_ex->CreateTexture(
width,
height,
1,
D3DUSAGE_RENDERTARGET,
D3DFMT_A8R8G8B8,
D3DPOOL_DEFAULT,
&m_d3d_texture9,
&sharedHandle
);
これで、Direct3D11とDirect3D9の両方から同じテクスチャを利用できるようになります。
D3DImageへ表示する
共有テクスチャを取得できれば、その後の処理は前回とほとんど変わりません。
d3dImage.Lock();
d3dImage.SetBackBuffer(
D3DResourceType.IDirect3DSurface9,
surface);
d3dImage.AddDirtyRect(...);
d3dImage.Unlock();
これにより、VideoProcessorが出力したGPU上のRGBテクスチャを、そのままWPFへ表示できます。
VideoProcessorのメリット
VideoProcessorを利用するメリットは、高速な色変換だけではありません。
Windows標準のビデオ処理機能を利用するため、グラフィックドライバが提供する映像補正機能も自動的に反映されます。
例えば、Intel Graphics Command Centerでは、明るさやコントラスト、彩度などの映像調整を行えます。
これらの設定を変更すると、VideoProcessorで生成された映像にもそのまま反映されます。
つまり、アプリケーション側で特別な画質調整機能を実装しなくても、WindowsやGPUドライバが提供する映像処理の恩恵を受けることができます。
VideoProcessorは単なる色変換APIではなく、Windows標準の映像処理パイプラインの一部であることが分かります。
おわりに
WPFには動画表示用のライブラリやコンポーネントが数多く存在し、手軽に映像を表示できるものも少なくありません。
しかし、実際の開発では「思ったように動かない」「特殊な映像フォーマットに対応できない」「パフォーマンスを改善したい」といった場面に遭遇することがあります。そのようなとき、内部の仕組みがブラックボックスになっていると、原因の調査や対処が難しくなることも少なくありません。
Direct3DやMedia FoundationなどのAPIは、初期化手順やインターフェースが多く、最初は分かりづらく感じるかもしれません。
ですが、一度仕組みを理解してしまえば、USBカメラだけでなく、FFmpegやハードウェアデコーダとの連携、GPU上での画像処理など、さまざまな場面へ応用できます。
この連載が、WPFで映像処理を行う際の理解や実装の一助となれば幸いです。
参考
今回紹介したサンプルプログラム一式は、GitHubで公開しています。
記事では説明を分かりやすくするために一部コードを抜粋していますが、実際に動作するプロジェクトとしてまとめています。
Media FoundationによるUSBカメラの取得から、VideoProcessorによるGPUでのNV12→RGB変換、共有テクスチャを利用したD3DImageへの表示まで、一通り実装していますので、実際に試してみたい方は参考にしてください。
次回予告
次回は、WinUI3でSwapChainPanelを使って画像を表示する方法について記述したいと思います。
