重い画像処理を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#から使う