C++/CLIで動かしているOpenCVのCvImageをC#.NETのBitmapオブジェクトにして読み込む
橋本商会: Cで書いたOpenCVのコードをC++/CLIで.NET用DLLにしてC# Formアプリから使う の続き。
FormアプリからOpenCVのHighGUIを別ウィンドウで開いていたが、Bitmapオブジェクトにキャストする事で.NETアプリのPictureBoxなどに読み込む事が出来る。
これにより画像処理だけ高速にC++で行い、アプリケーションのUIやネットワーク部分はC#.NETで実装するといういいとこどりが出来る。
■出来た物
FormにLoadImageボタンとPictureBoxを追加。
LoadButtonを押すとShadowCamera.dllから呼び出しているOpenCV側のCvImageが変換されてBitmapになって出てくる関数を作った。

今回作っているShadowCameraは背景を蓄積させて統計を取り、差分で人物の影だけを抜き出すライブラリなので、毎フレーム計算し続ける必要がある。runCapture()の最後でwhileループさせて実現している。
なので、1つBitmapオブジェクトを用意しておき、whileループの最後で処理結果を毎回保存しておいてそれをC#側から適当なタイミングで読みに行く実装にした。
DLLであるC++/OpenCV側のループを毎フレーム回し続けながら、それをブロックする事なくC# Formアプリ側から取得できる。
例によってOpenCV勉強用mercurialリポジトリのchanges19としてcommitしてある。
なお、VisualStudioはC#はutf8なのにC++はshift_jisで保存されてしまい、utf8にするとビルドできなくなってしまうのでリポジトリビューアでは文字化けしてしまっている。悲しい。
文字化けしている部分には大したこと書いてないので無視していいし、必要ならリポジトリの左側からzipでダウンロードできる。
ちなみに今回の方法は、3年以上前の開発合宿の記事に匿名様が
コメントで教えてくれたSharperCVのCvImage型をSystem.Drawing.Bitmap型に変換する方法が書かれていてそれを元にした。
やはりできなかった事とかも書いてみるものだなと思った。もちろん助けをアテにしちゃいけないが
■ShadowCamera.dll側の変更
C++プロジェクトのShadowCameraに、System.Drawingへの参照を追加する。Bitmapクラスを使うため。

「新しい参照の追加」ボタンを押す

.NETタブの下の方にスクロールしていって、System.Drawing を追加する。

ShadowCamera.hにコードを書き加える
OpenCV Study: c9320e90f1be ShadowCamera/ShadowCamera.h
DLLへの参照を追加して
using namespace System::Drawing;
勝手に書き換えられたら嫌なので、privateでグローバル変数 shadow を作っておく
private:
System::Drawing::Bitmap^ shadow;
DLL利用側のためのアクセサを用意する
public:これでDLL内のshadowというBitmapオブジェクトを、DLL外から読み出す事が出来るようになった。
System::Drawing::Bitmap^ ShadowImage(void){
return this->shadow;
}
いまいち ^ や ~ などをつけるルールを把握しきれていないけど、他のC++/CLIアプリがそうやっていたので先人の作法に倣うことにする。
あとはrunCapture()の最後のwhileループ内で、毎回作られるimgResultというIplImage(CvImage互換)オブジェクトをdynamic_castしてshadowに保存する。
this->shadow = dynamic_cast<System::Drawing::Bitmap^>長いので変換部分のみ抜粋。ShadowCamera.hの85~125行目がwhileループです
(gcnew System::Drawing::Bitmap(w,h,w*3,
System::Drawing::Imaging::PixelFormat::Format24bppRgb,
static_cast<System::IntPtr>(imgResult->imageDataOrigin)));
dynamic_castやstatic_castの違いはC++の新しいキャストが参考になった。
オブジェクトの代入のためのcastは.NETの上で動いているからdynamic_castで、unmanaged領域と行き来するSystem::IntPtrと*char間のcastはstatic_castになるという理解でいいのかな?
■ShadowCamera.dllを使うC# Formアプリ側の変更
ボタン(buttonLoadImage)と画像を表示するためのPictureBox(pictureBoxResult)を配置。
PictureBoxはSizeModeをStretchImageにしておくと自動リサイズされて便利。

で、ボタン押下イベントで
pictureBoxResult.Image = cam.ShadowImage();BitmapをPictureBoxのImageプロパティに突っ込むだけで表示できる。楽ちんだ

終わり。
ボタンじゃなくてThreadなどで適当にsleepを入れながら読み込み続ければふつうに影カメラになる。

橋本様、感激しながら読ませていただいております!
まさに今までやりたかったのに実現できなかった手法が
いとも簡単にできてしまっているので感動しております!
C++/CLIを始めてまだ日も浅いのですが、
this->shadow = dynamic_cast
(gcnew System::Drawing::Bitmap(w,h,w*3,
System::Drawing::Imaging::PixelFormat::Format24bppRgb,
static_cast(imgResult->imageDataOrigin)));
の部分について2点質問がございます。
お手すきでしたらアドバイスをいただければ大変光栄です!
1.dynamic_castなしで
this->shadow = gcnew System::Drawing::Bitmap(w,h,w*3,
System::Drawing::Imaging::PixelFormat::Format24bppRgb,
static_cast(imgResult->imageDataOrigin));
でも動いたのですが、dynamic_castは必ず必要になりますでしょうか。。
2.何に着目すれば、橋本様のように
「imgResult->imageDataOrigin が System::IntPtr 型にstatic_cast できる」
とひらめくのでしょうか!
是非ご教授ください!
tomさん
1については、C++/CLIをこの時はじめて使ったのでdynamic_castが必要なのかどうなのか、いまいちわからないです。
2は、
・3年前の記事 http://shokai.org/blog/archives/829 のコメントでShaperCVでのCvImage→Bitmap変換を教わりました
・また以前 http://d.hatena.ne.jp/shokai/20090202/1233589674 で IplImage->ImageDataOrigin配列で画素を座標指定して取り出したりしていた
・Cは配列名でアクセスするとポインタになる
・ポインタはSystem::IntPtrに多分キャストできるだろう
と考えて、書いてみたら動いてしまいました。
はじめまして.
大学の研究でC#を利用してOpenCVを呼び出すということをやりたく,このサイトを参考にさせて頂きました.
が,ソリューションエクスプローラで[参照設定]→[プロジェクト参照]でプログラムを参照することができません…
作ったC++プログラムだけを単体でビルドすると
1>LINK : fatal error LNK1104: ファイル 'highgui.lib' を開くことができません。
というエラーが起きてしまいます.
これが原因なのでしょうか?
もしよろしければ何かアドバイス等よろしくお願い致します.