C#でOpenCVの画像を表示させたくて苦労する

Visual Studio 2012が手に入ったので、今まで経験のしたことのないC#を勉強しようと思い、いろいろ挑戦中。

ただ専門は画像処理ということもあり、速度が大事なのでC#だけで完結するのは難しいので主にC/C++でコアの処理を書いて、見た目の部分にC#を使うことなりそう。

取りあえずやりたいのがC#で作ったGUIのボタンを押したら、C++で画像処理して、その結果画像をGUIで表示するということ。
調べてみるとOpenCVC#向けのライブラリもあるらしいけど、C#アルゴリズムを書きたいわけではないので止めておく。
C#C++間で連携するには、dllを呼び出す方法やC++/CLIを使う方法があるらしい。dllを呼び出す方法は記法がややこしいので止めておくことにすると、C++/CLIの選択肢が残った。

サンプルプロジェクト

試行錯誤というか実験用にサンプルプロジェクトを作成する。プロジェクト名は画像処理っぽくPeopleDetectとした。
ソリューションはC#C++/CLIの二つのプロジェクトを持っている。

二つを連帯させるために、C++/CLIの方は構成の種類をdllとしている。
C#の方は参照の追加でC++/CLIのプロジェクトを追加している。

それでC#のコードでボタンを押したら、画像処理するといった処理を記述した。
画像処理は時間がかかるので別のスレッドで処理をさせている。
MainWindow.xaml.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

using CppCLIWrapper;
using System.Drawing;
using System.IO;

namespace PeopleDetect
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        [System.Runtime.InteropServices.DllImport("GDI32.dll")]
        public static extern bool DeleteObject(IntPtr objectHandle);

        private CppWrapper cppwrapper = new CppWrapper();

        public MainWindow()
        {
            InitializeComponent();
        }
        // BitmapからBitmapSourceに変換
        private System.Windows.Media.Imaging.BitmapSource ToBitmapSource(System.Drawing.Bitmap image)
        {
            var hbitmap = image.GetHbitmap();
            var bitmapsource = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(
                    hbitmap,
                    IntPtr.Zero,
                    System.Windows.Int32Rect.Empty,
                    BitmapSizeOptions.FromWidthAndHeight(image.Width, image.Height));
            DeleteObject(hbitmap);

            return bitmapsource;
        }
        private async void Button_Clicked(object sender, RoutedEventArgs e)
        {
            var bitmap = await Task.Run(() => cppwrapper.calcBinaryImage());
 
            this.image.Source = ToBitmapSource(bitmap);
            this.image.Width = image.Width;
            this.image.Height = image.Height;
        }
    }
}

C++C#を連携するためにC++/CLIを使用している。そのソースは以下の通りになった。
CppCLIWrapper.h

#pragma once

class PeopleDetect;

namespace CppCLIWrapper 
{
	public ref class CppWrapper
	{
	public:
		CppWrapper();
		~CppWrapper();
		!CppWrapper();

		System::Drawing::Bitmap^ calcBinaryImage();
	public:
		PeopleDetect *people_detect_;
	};
}

CppCLIWrapper.cpp

#include "CppCLIWrapper.h"

#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>

#include "PeopleDetect.h"

namespace CppCLIWrapper
{
	CppWrapper::CppWrapper()
	{
		people_detect_ = new PeopleDetect();
	}

	CppWrapper::~CppWrapper()
	{
		this->!CppWrapper();
	}

	CppWrapper::!CppWrapper()
	{
		if (people_detect_)
		{
			delete people_detect_;
			people_detect_ = nullptr;
		}
	}

	System::Drawing::Bitmap^ GetBitmap(const cv::Mat_<cv::Vec3b>& img)
	{  
		uchar *c = new uchar[img.step*img.rows];
		System::IntPtr ip(c);
		memcpy(ip.ToPointer(), img.data, img.step*img.rows);

		System::Drawing::Bitmap^ bmp = gcnew System::Drawing::Bitmap(
			img.cols, img.rows, img.step,  
			System::Drawing::Imaging::PixelFormat::Format24bppRgb, 
			ip);

		// deleteしなくていい?
		//delete c;
		return bmp;       
	}  

	System::Drawing::Bitmap^ CppWrapper::calcBinaryImage()
	{
                // c++での処理
		cv::Mat_<cv::Vec3b> image = people_detect_->calcBinaryImage();

		return GetBitmap(image);
	}
} // namwspace CppCLIWrapper

最後に処理のコア部分のC++ソース。
グレースケール化の処理をしているんだけど、せっかくVC11にしたのでC++ AMPのtextureを使用している。
PeopleDetect.h

#pragma once

#include <opencv2/core/core.hpp>

class PeopleDetect
{
public:
	PeopleDetect();
	~PeopleDetect();

	cv::Mat_<cv::Vec3b> calcBinaryImage();
};

PeopleDetect.cpp

#include "PeopleDetect.h"

#include <iostream>
#include <amp.h>
#include <amp_graphics.h>

#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>

using namespace concurrency;
using namespace concurrency::graphics;

void toGrayscaleAMP(cv::Mat_<cv::Vec3b>& image)
{
	cv::Mat rgba;
	cv::cvtColor(image, rgba, CV_BGR2BGRA);
	cv::Mat output_rgba = rgba.clone();

	extent<2> e(image.rows, image.cols);

	texture<uint_4, 2> image_texture(e, rgba.data, e.size()*4U, 8U);
	texture<uint_4, 2> output_texture(e, output_rgba.data, e.size()*4U, 8U);
	writeonly_texture_view<uint_4, 2> view(output_texture);

	parallel_for_each(view.extent, [=, &image_texture](index<2> idx) restrict(amp)
	{
		uint_4 color = image_texture[idx];

		unsigned int gray = (color.r + color.g + color.b) / 3U;
		uint_4 gray4(gray, gray, gray, 255);
		view.set(idx, gray4);
	});
	
	copy(output_texture, output_rgba.data, output_texture.extent.size()*4U);
	//cv::cvtColor(output_rgba, image, CV_BGRA2BGR); // error
	for (int y = 0; y < image.rows; y++)
	{
		for (int x = 0; x < image.cols; x++)
		{
			for (int i = 0; i < 3; i++)
			{
				image(y, x)[i] = output_rgba.at<cv::Vec4b>(y, x)[i];
			}
		}
	}
}

cv::Mat_<cv::Vec3b> PeopleDetect::calcBinaryImage()
{
	cv::Mat_<cv::Vec3b> image = cv::imread("C:/test.png");

	toGrayscaleAMP(image);

	return image;
}

コンパイルしてみるとグレースケール化できてたので、何とか動いているみたい。
入力画像

処理結果

C#C++/CLI.net framework と知らないことが多くてかなり大変だった。
初心者のコードなので参考にするとえらい目にあうかも?

追記 2012/12/28
結局C#の勉強はしてないので、よくわかっていないんだけど
久しぶりに動かしてみるとカラーがうまく動かないのでちょっと修正。
c++

Bitmap^ Wrapper::process(System::String^ filename)
{
	using namespace msclr::interop;
	cv::Mat_<cv::Vec3b> image = cv::imread(marshal_as<std::string>(filename));

	return getBitmap(image);
}

Bitmap^ Wrapper::getBitmap(cv::Mat_<cv::Vec3b>& image)
{
	const int alignment = 4;
	const int new_cols = cv::alignSize(image.cols, alignment);

	if (p_mat_ == nullptr)
	{
		p_mat_ = new cv::Mat_<cv::Vec3b>(image.rows, new_cols);
	}
	else if (p_mat_->cols != new_cols || p_mat_->rows != image.rows)
	{
		delete p_mat_;
		p_mat_ = new cv::Mat_<cv::Vec3b>(image.rows, new_cols);
	}
	cv::Mat_<cv::Vec3b> roi_mat(*p_mat_, cv::Rect(0, 0, image.cols, image.rows));
	image.copyTo(roi_mat);

	Bitmap^ dst = gcnew Bitmap(roi_mat.cols, 
		roi_mat.rows, 
		roi_mat.step, 
		System::Drawing::Imaging::PixelFormat::Format24bppRgb, 
		System::IntPtr(roi_mat.data));

	return dst;  
}

C#

string filename = dialog.FileName;//ファイル名

Bitmap bitmap = _wrapper.process(filename);
var source = ToBitmapSource(bitmap);
ImageView.Source = source; //GUI

その2へ続く
C#でOpenCVの画像を表示させたくて苦労する2 - wildpieの日記