Edited at

【UWP】Hololensで得た画像をC++のDLLで処理し,リアルタイムで画像データを返す

More than 1 year has passed since last update.


動機

C#,ないしUnityプロジェクトでは諭吉を出さない限り

画像処理界隈の大好きなOpenCVが使えないことが難でした.

しかしそれは,C++ベースのDLLを組み込むことで解決されました.

【UWPアプリでOpenCVを無料で使おう!】C#(.NET) UWPアプリへのC++ダイナミックライブラリ(dll)の組み込み方

それもC++ベースで書けるという

OpenCVをこよなく愛する皆さんに朗報です.

ここでは,HololensのPhotoCaptureで得た画像をC++DLLで処理し,リアルタイムでC#に画像を返すことをしたいと思います.


C#: C++関数へ画像データを参照渡し

今回はリアルタイム性重視なので,ガンガン高速化を試みました.

photoCaptureFrame.CopyRawImageDataIntoBuffer()は,中身のデータを1個ずつ(並列化されていたら並列数分ずつですが)コピーする関数で,これを使っていると重いです

試したところ,130msくらいかかっていました.

大きなbyte配列は参照渡しが無難ですが,C#はポインタの扱いが難癖あるので,ここで紹介します.


Byte列のポインタをIntPtrで取得

unsafe{}コード内で,以下の関数によりByte列のポインタを取得します.

IntPtr src_ptr = photoCaptureFrame.GetUnsafePointerToBuffer();

Marshalで確保していて,Marshal.Release(src_ptr)により解放しなきゃいけないなど,扱いが少々厄介です.


出力用配列を用意

byte列で出力用配列を用意します.普通の用意の仕方です.

今回はTextureに出力結果を上げたいので,配列出力とします.

byte[] output = new byte[channels * dst_rows * dst_cols];

こっちも参照渡しでいいのでは…?と考える方,多いと思いますが,


  • テクスチャにbyte列を渡すtexture.LoadRawTextureData(byte[])はbyte列を引数とするので,配列が必須です.

  • C#ではポインタから配列への変換ができないそうです(出典後述)

Marshal.Copy メソッド (IntPtr, Byte[], Int32, Int32 ... - MSDN - Microsoftもありますが,結局データコピーするだけで,値渡しなので意味なしです.

textureに上げる用途である限りは,配列出力は止むを得ません.


C++のDLL関数に画像データを送る

DLLの関数定義式が以下だったとします.

今回は参照渡しをするのでunsafeを頭につけます.


TestCpp()

[DllImport("CppPlugin")]

unsafe private static extern void Hengento(
IntPtr src_ptr, int rows, int cols, int channels, byte[] output
);


スクリプト内で関数に素直に値を入れて,Marshal.ReleaseでIntPtrを解放してあげましょう.

// int rows, int colsはphotoCaptureのresolutionを参考に定義しましょう.

// int channelsは,photoCaptureFrameのフレームである限りはrgbaなので,4を代入しましょう.
TestCpp(src_ptr, rows, cols, channels, output);
Marshal.Release(src_ptr);


C#スクリプトまとめ


OnCapturedPhotoToMemory()


private void OnCapturedPhotoToMemory(PhotoCapture.PhotoCaptureResult result, PhotoCaptureFrame photoCaptureFrame)
{

if (result.success)
{

#if WINDOWS_UWP

unsafe
{

IntPtr src_ptr = photoCaptureFrame.GetUnsafePointerToBuffer();

byte[] output = new byte[channels * dst_rows * dst_cols];

Hengento(src_ptr, src_rows, src_cols, channels, output);
Marshal.Release(src_ptr);

Texture2D texOutput = new Texture2D(cols, rows, TextureFormat.BGRA32, false);
texOutput.LoadRawTextureData(output);
texOutput.Apply();
texOutput.wrapMode = TextureWrapMode.Clamp;

// rawImageCanvas直下のGameObject, RawImageのコンポーネントあり
// オブジェクトを生成
rawImage.GetComponent<RectTransform>().sizeDelta = new Vector2(cols, rows);
Obj.GetComponentInChildren<RawImage>().texture = texOutput;

}

#endif
}

capture.StopPhotoModeAsync(OnStoppedPhotoMode);

}


ビルドの際に,VSの各プロジェクトのプロパティでunsafe スクリプトの許可をenabledにするのを忘れずに!


C++: 画像データを受け取り,処理して返す


IntPtrはC++ではvoid*扱い

型が分からないポインタという意味で,IntPtrはC++ではvoid*扱いです.

C++での関数定義はこうなります.


TestCpp()

void __stdcall TestCpp(

void* src_ptr, int rows, int cols, uchar* output
);


IMFMediaBufferで,不明型のポインタからint列を取得

次に型不明のポインタからint配列(intポインタ)を起こすのに,IMFMediaBufferを使います.

headerファイルに#include <mfidl.h>を書いて使ってください.

    IMFMediaBuffer *media_buffer = NULL;

HRESULT res = reinterpret_cast<IUnknown *>((int*)(src_ptr))->QueryInterface<IMFMediaBuffer>(&media_buffer);

if (SUCCEEDED(res))
{
BYTE* photo_buffer = NULL;

res = media_buffer->Lock(&photo_buffer, NULL, NULL);

if (SUCCEEDED(res))
{
uchar* ptr = photo_buffer + 4 * src_cols * rows_start + 4 * cols_start;

// ptr[]にはb, g, r, a の順で0-255で画素値が格納されている
... // 以下,処理
}

}

IMFMediaBufferとはなんぞや? …私の解釈では,他アプリからデータ列をポインタ経由で受け取るときに使うもので,ビデオ読み込み等にも使われるものだということです.

間違ってたらご指摘ください.

自分が試した中では最も高速です.

但しmedia_buffer->Lock()の処理は幾分か時間がかかります.


高速処理のためのTips

画像処理はどうしても繰り返し処理で時間がかかってしまうので,OpenCVの関数ではなく,できるだけ直書きでトライするのがおすすめです.

当然ですがポインタ参照が高速です -> [OpenCV][Mat]画素へのアクセススピード比較

多くの関数はOpenCV CookBookの定義をそのまま適用するだけで直書き実装が可能です.(そのうち他の投稿にUPします)

高速化には,VSでOpenMPのサポートをオンにし,

#pragma omp parallel forをfor文の前に置くだけでOKです.


高速な配列出力のためのTips

Unityのテクスチャのチャネルは4次元が必須らしく,

仕方なしにoutputは4channels*rows*colsの長さのbyte列を用意しておきます.

エッジ検出や2値化等のグレースケール処理でよくても,仕方なしに4チャネルで出力します.

output配列にそのまま出力するのが最も高速でしょう.

C#でbyte列を確保した際,その初期値は0なので,それを利用します.

以下はLaplacianエッジ検出の例です.


Laplacian()


// ラプラシアン
// 端から1行or1列分は透明でいいので,初期値の0のままにする.
offset = 2;
int px = 255;// 128;
#pragma omp parallel for
for (int j = offset; j < dst_rows - offset; j++) {
int* gus_ptr_ = imgGus.ptr<int>(j) + offset;
uchar* out_ptr = &output[4 * dst_cols * j] + 4 * offset;

for (int i = offset; i < dst_cols - offset; i++) {

if (*(gus_ptr_ - dst_cols) + *(gus_ptr_ - 1) + *(gus_ptr_ + 1) + *(gus_ptr_ + dst_cols) - 4 * *(gus_ptr_) > 2) {
*out_ptr = px;
*(out_ptr + 1) = px;
*(out_ptr + 2) = px;
*(out_ptr + 3) = px;

}

out_ptr += 4; ++gus_ptr_;
}
}


一度cv::Matとした画像からoutput列に出力したいのであれば,スレッドナンバーに応じた処理に書き換えてあげるのがおすすめです.


int OMP_NUM = 0;
#ifdef _OPENMP
OMP_NUM = omp_get_max_threads();
omp_set_num_threads(OMP_NUM);
#endif

// 結果をimgDstからoutputに出力
int num = 4 * dst_rows * dst_cols / OMP_NUM;
#pragma omp parallel
{
int thread_id = omp_get_thread_num();
int thread_part = 4 * dst_rows * thread_id;

uchar* out_ptr = &output[thread_part];
uchar* dst_ptr = &imgDst.data[thread_part];
for (int i = 0; i < num; i++) {
*out_ptr = *dst_ptr;
}
}


結果

クロップし,ラプラシアンエッジをかけた画像を出力した場合,

600*600pxへクロップした画像に対しては,60-70ms程度で処理します.

200*200pxへの画像へは,30-40ms程度です.

Hololensのフレームレートは60fpsで,15ms程度に収まるのが理想.ラプラシアンではなくSobelを使い,ガウシアンフィルタを介さないと処理が軽くなる等あると思います.

画像は今度載せます.すみません…