C#でOpenCVの画像を表示させたくて苦労する
Visual Studio 2012が手に入ったので、今まで経験のしたことのないC#を勉強しようと思い、いろいろ挑戦中。
ただ専門は画像処理ということもあり、速度が大事なのでC#だけで完結するのは難しいので主にC/C++でコアの処理を書いて、見た目の部分にC#を使うことなりそう。
取りあえずやりたいのがC#で作ったGUIのボタンを押したら、C++で画像処理して、その結果画像をGUIで表示するということ。
調べてみるとOpenCVのC#向けのライブラリもあるらしいけど、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; }
string filename = dialog.FileName;//ファイル名 Bitmap bitmap = _wrapper.process(filename); var source = ToBitmapSource(bitmap); ImageView.Source = source; //GUI