3

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になって出てくる関数を作った。
C++/OpenCVのCvImageを.NETの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クラスを使うため。
C++/OpenCVのCvImageを.NETのBitmapに変換できた


「新しい参照の追加」ボタンを押す
C++/OpenCVのCvImageを.NETのBitmapに変換できた


.NETタブの下の方にスクロールしていって、System.Drawing を追加する。
C++/OpenCVのCvImageを.NETのBitmapに変換できた


ShadowCamera.hにコードを書き加える

OpenCV Study: c9320e90f1be ShadowCamera/ShadowCamera.h
DLLへの参照を追加して

using namespace System::Drawing;


勝手に書き換えられたら嫌なので、privateでグローバル変数 shadow を作っておく
private:
    System::Drawing::Bitmap^ shadow;


DLL利用側のためのアクセサを用意する
public:
    System::Drawing::Bitmap^ ShadowImage(void){
        return this->shadow;
    }
これでDLL内のshadowというBitmapオブジェクトを、DLL外から読み出す事が出来るようになった。
いまいち ^ や ~ などをつけるルールを把握しきれていないけど、他のC++/CLIアプリがそうやっていたので先人の作法に倣うことにする。


あとはrunCapture()の最後のwhileループ内で、毎回作られるimgResultというIplImage(CvImage互換)オブジェクトをdynamic_castしてshadowに保存する。
this->shadow = dynamic_cast<System::Drawing::Bitmap^>
    (gcnew System::Drawing::Bitmap(w,h,w*3, 
    System::Drawing::Imaging::PixelFormat::Format24bppRgb,
    static_cast<System::IntPtr>(imgResult->imageDataOrigin)));
長いので変換部分のみ抜粋。ShadowCamera.hの85~125行目がwhileループです

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にしておくと自動リサイズされて便利。
C++/OpenCVのCvImageを.NETのBitmapに変換できた


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

終わり。

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

2

Cで書いたOpenCVのコードをC++/CLIで.NET用DLLにしてC# Formアプリから使う

重い画像処理をC++の画像処理ライブラリであるOpenCVでやって、ネットワーク処理は慣れ親しんだC#.NETでやりたい。
そこで、色々試行錯誤した結果、C#からは「処理し終わった画像を表示してくれるカメラ」に見えるような、OpenCVの処理部分を含んだDLLをC++/CLIで作ることにした。
(OpenCVそのものについてと開発環境構築は橋本商会 ? OpenCVをはじめたに書いた)


他にも連動のさせかたはSharperCVというC#ラッパーを使うとか、SocketでOpenCVで処理した結果の画像だけ送るとか色々とやり方はあるけど、
前者は2003年でプロジェクトが止まっていてcvPow等の配列関連の関数が無く、後者は俺のMicrosoft SDKのWinUser.hの6373行目あたりから文字化けしていてWinSocketが使えなかったりしたのでDLLを作るやり方に落ち着いた。


この方法のメリットは、まず納得のいくOpenCVの処理をC++単体のプロジェクトでいつもどおり書いて、それをほぼコピペすればDLLができてしまうところ。
ShaperCVを使う場合は全て関数を照らし合わせてC#で書き直さなければならないので面倒。
Socketを使う方はよく知らない。


DLLの作り方はかせいさんとこが参考になった。CLIを使う場合と使わない場合が解説されている。(今回は楽する為にCLIを使う)



■今回つくるもの
mercurialリポジトリをアップロードした。左のバーからzipでダウンロードするか、

hg clone http://shokai.org/projects/opencv-study/ opencv-study

してリポジトリを複製してください。

の2つを作ります。
影カメラライブラリの方は、OpenCV Study: 7537970cfc6b BgSubAvg/bgsubAvg.cppをDLL化したもので、cvRunningAvgを使って背景統計を取って背景差分して人型の影を作るもの → cvRunningAvgを使って背景統計を取り背景差分する – 橋本詳解


完成図はこれ。C#で作ったFormアプリで、ボタンを押すとDLLに書いてあるOpenCVの処理が起動してウィンドウが開き画像処理結果が表示/毎フレーム更新される。
Cで書いたOpenCVのコードをC++/CLIで.NET用DLLにしてC#から使う



■C++でDLLを作る
まず空のソリューションを作る。
ソリューション名を右クリックして新しいプロジェクト追加
Cで書いたOpenCVのコードをC++/CLIで.NET用DLLにしてC#から使う


VC++のクラスライブラリを作成する。名前はShadowCameraにした
Cで書いたOpenCVのコードをC++/CLIで.NET用DLLにしてC#から使う


できたShadowCameraプロジェクトを右クリックしてプロパティを編集
Cで書いたOpenCVのコードをC++/CLIで.NET用DLLにしてC#から使う


OpenCV用のライブラリを読み込む。
すべての構成->入力->追加の依存ファイル に

$(NoInherit) highgui.lib cxts.lib cv.lib cxcore.lib ml.lib cvaux.lib cvhaartraining.lib

を書く
Cで書いたOpenCVのコードをC++/CLIで.NET用DLLにしてC#から使う

ShadowCamera.hのClass名をCameraに変更、さらにint runCapture()関数を書く。
この関数の中にOpenCVのmain()を全部突っ込んでしまえばそのまま動く。今回はbgsubAvg.cppのmain()をコピペしたけど他にも赤色重心追跡や肌色検出、単純なカメラ入力など色々作ったので好きなものをコピペしてよい。
OpenCV関連のヘッダを#includeで読み込むのを忘れずに。
(試しにint asdf()という単に数値を返すだけの関数も作っておいた。これがC#から呼べればちゃんとDLLとして読み込めているはず)

OpenCV Study: 7537970cfc6b ShadowCamera/ShadowCamera.h

// ShadowCamera.h
#include <cv.h>
#include <highgui.h>
#include <ctype.h>
#include <stdio.h>

#pragma once

using namespace System;

namespace ShadowCamera {

    public ref class Camera
    {
        // TODO: このクラスの、ユーザーのメソッドをここに追加してください。
    public:
        int asdf(void){
            return 15;
        }

    public:
        int runCapture(void){

            bool isStop = false;
            const int INIT_TIME = 50;
            const double BG_RATIO = 0.02; // 背景領域更新レート
            const double OBJ_RATIO = 0.005; // 物体領域更新レート
            const double Zeta = 10.0;
            IplImage *img = NULL;

            CvCapture *capture = NULL;
            capture = cvCreateCameraCapture(0);
            //capture = cvCaptureFromAVI("test.avi");
            if(capture == NULL){
                printf("capture device not found!!");
                return -1;
            }

            img = cvQueryFrame(capture);
            int w = img->width;
            int h = img->height;

            IplImage *imgAverage = cvCreateImage(cvSize(w, h), IPL_DEPTH_32F, 3);
            IplImage *imgSgm = cvCreateImage(cvSize(w, h), IPL_DEPTH_32F, 3);
            IplImage *imgTmp = cvCreateImage(cvSize(w, h), IPL_DEPTH_32F, 3);
            IplImage *img_lower = cvCreateImage(cvSize(w, h), IPL_DEPTH_32F, 3);
            IplImage *img_upper = cvCreateImage(cvSize(w, h), IPL_DEPTH_32F, 3);
            IplImage *imgSilhouette = cvCreateImage(cvSize(w, h), IPL_DEPTH_8U, 1);
            IplImage *imgSilhouetteInv = cvCreateImage(cvSize(w, h), IPL_DEPTH_8U, 1);
            IplImage *imgResult = cvCreateImage(cvSize(w, h), IPL_DEPTH_8U, 3);

            printf("背景初期化中…¥n");
            cvSetZero(imgAverage);
            for(int i = 0; i < INIT_TIME; i++){
                img = cvQueryFrame(capture);
                cvAcc(img, imgAverage);
                printf("輝度平均 %d/%d¥n", i, INIT_TIME);
            }
            cvConvertScale(imgAverage, imgAverage, 1.0 / INIT_TIME);
            cvSetZero(imgSgm);
            for(int i = 0; i < INIT_TIME; i++){
                img = cvQueryFrame(capture);
                cvConvert(img, imgTmp);
                cvSub(imgTmp, imgAverage, imgTmp);
                cvPow(imgTmp, imgTmp, 2.0);
                cvConvertScale(imgTmp, imgTmp, 2.0);
                cvPow(imgTmp, imgTmp, 0.5);
                cvAcc(imgTmp, imgSgm);
                printf("輝度振幅 %d/%d¥n", i, INIT_TIME);
            }
            cvConvertScale(imgSgm, imgSgm, 1.0 / INIT_TIME);
            printf("背景初期化完了¥n");

            char winNameCapture[] = "Capture";
            char winNameSilhouette[] = "Silhouette";
            cvNamedWindow(winNameCapture, 0);
            cvNamedWindow(winNameSilhouette, 0);

            while(1){
                if(!isStop){
                    img = cvQueryFrame(capture);
                    if(img == NULL) break;
                    cvConvert(img, imgTmp);

                    // 輝度範囲
                    cvSub(imgAverage, imgSgm, img_lower);
                    cvSubS(img_lower, cvScalarAll(Zeta), img_lower);
                    cvAdd(imgAverage, imgSgm, img_upper);
                    cvAddS(img_upper, cvScalarAll(Zeta), img_upper);
                    cvInRange(imgTmp, img_lower, img_upper, imgSilhouette);

                    // 輝度振幅
                    cvSub(imgTmp, imgAverage, imgTmp);
                    cvPow(imgTmp, imgTmp, 2.0);
                    cvConvertScale(imgTmp, imgTmp, 2.0);
                    cvPow(imgTmp, imgTmp, 0.5);

                    // 背景領域を更新
                    cvRunningAvg(img, imgAverage, BG_RATIO, imgSilhouette);
                    cvRunningAvg(imgTmp, imgSgm, BG_RATIO, imgSilhouette);

                    // 物体領域を更新
                    cvNot(imgSilhouette, imgSilhouetteInv);
                    cvRunningAvg(imgTmp, imgSgm, OBJ_RATIO, imgSilhouetteInv);

                    cvErode(imgSilhouette, imgSilhouette, NULL, 1); // 収縮
                    cvDilate(imgSilhouette, imgSilhouette, NULL, 2); // 膨張
                    cvErode(imgSilhouette, imgSilhouette, NULL, 1); // 収縮

                    cvMerge(imgSilhouette, imgSilhouette, imgSilhouette, NULL, imgResult);
                    cvShowImage(winNameCapture, img);
                    cvShowImage(winNameSilhouette, imgResult);
                }
                int waitKey = cvWaitKey(33);
                if(waitKey == ‘q’) break;
                if(waitKey == ‘ ‘){
                    isStop = !isStop;
                    if(isStop) printf("stop¥n");
                    else printf("start¥n");
                }
            }

            cvReleaseCapture(&capture);
            cvDestroyWindow(winNameCapture);
            cvDestroyWindow(winNameSilhouette);

            return 0;
        }
    };
}



■C#でFormアプリを作る
新しいプロジェクトを作る。
ソリューションエクスプローラで[参照設定]→[プロジェクト参照]でShadowCameraを参照。
Cで書いたOpenCVのコードをC++/CLIで.NET用DLLにしてC#から使う
なお、同一Solution内ならプロジェクト参照が便利だけど、
ReleaseにShadowCamera.dllができているのでそれを他の人に配布して、[ファイルを参照]してもらってdllから呼び出してもらっても同じ機能が使えます


ボタンを1つ置く。buttonStartという名前にして、ダブルクリックしてクリックイベントの雛形を作る
Cで書いたOpenCVのコードをC++/CLIで.NET用DLLにしてC#から使う



ShadowCameraを読み込んで

using ShadowCamera;


新しいCameraクラスのインスタンスを作って、startButtonイベント内でさっき作ったrunCapture関数を呼ぶようにする
OpenCV Study: 7537970cfc6b ShadowCameraSample/ShadowCameraForm.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using ShadowCamera;

namespace ShadowCameraSample
{
    public partial class ShadowCameraForm : Form
    {
        private Camera cam;
        public ShadowCameraForm()
        {
            InitializeComponent();
            cam = new Camera();
        }

        private void buttonStart_Click(object sender, EventArgs e)
        {
            cam.runCapture();
        }
    }
}



ShadowCameraSampleを(右クリックして)スタートアッププロジェクトに設定し、
ビルドするとShadowCameraFormが起動する。ボタンを押すとしばらくカメラを読み込んで、数秒後にOpenCVのHighGUIのウィンドウが起動するはず。
Cで書いたOpenCVのコードをC++/CLIで.NET用DLLにしてC#から使う