1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

WPFでD3DImageを使って画像を表示する(第2回)

1
Last updated at Posted at 2026-07-15

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カメラをオープンします。

ここではIMFMediaSourceIMFSourceReaderを生成します。
映像の取得は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のインターフェース実装など、一部のコードは省略しています。

実際のアプリケーションでは、適切なエラー処理やリソース管理を実装してください。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?