概要
Winforms(or WPF)を使っていて、画像処理をしたくなった!さぁどうしましょうか。
そうですね、OpenCVSharpですね。
But! C++で築き上げてきた資産が皆様にはあると思います。
そんな資産であるC++アセットのdllに画像を渡すときの方法を共有したく今回執筆しました。
皆様の工数を1秒でも削ることができれば幸いです。
本記事では、C#側のBitmap
画像をCLRクラスライブラリ(C++/CLI側)にcv::Mat
として渡す、またcv::Mat
をBitmap
に変換してC#に渡すといった流れをプロジェクトの作成から実装していきます。
(結論から言うと、Bitmap
→Mat
と、Mat
→IplImage
→Bitmap
といった順番で変換させていきます。)
環境
Windows 10
Visual Studio 2019
.NET Framework 4.5.2
OpenCV 4.5.0
C++14
実装
まずはプロジェクトを作っていきます。
今回はコンソールアプリで簡単に実装していきたいので.NET Frameworkのコンソールアプリを選択します。
プロジェクト名は「MatTest」とします。
Bitmapの準備
C#側でBitmapを使うために、System.Drawingをインポートする準備をします。
まず、ソリューションエクスプローラーから「参照」を右クリックして「参照の追加」を選択します。
そして「参照マネージャー」タブから「System.Drawing」にチェックを付けます。
ではC++/CLI側に渡すための画像データを定義していきます。
using System.Drawing;
namespace MatTest
{
class Program
{
static void Main(string[] args)
{
Bitmap bmp = new Bitmap(@"C:\Sample\lena.png");
}
}
}
これでbmp
にlena.pngを格納できました。
最後に、後でOpenCVを使うので、ソリューションプラットフォームをany CPU
からx64
に変更しておきます。
それではこの画像をC++/CLI側に持っていきたいと思います。
CLRクラスライブラリを作成する
ソリューションエクスプローラーからソリューションファイルを右クリックして「追加」→「新しいプロジェクト」を選択します。
出てきたウィンドウでC++言語のCLRクラスライブラリ(.NET Framework)を選択します。
プロジェクト名は「OpenCVTestLibrary」として、バージョンは4.5.2に合わせます。
作成したらヘッダーが表示されると思います。
中身を少しだけ以下のように書き換えました。
# pragma once
using namespace System;
namespace OpenCVTestLibrary {
public ref class ProcMat
{
int hoge = 0;
};
}
クラス名をClass1からProcMatに変えて、hoge変数を定義しています。
それではこいつをビルドしてdllを作成します。
dllファイルはここに出力されたようです。
また、今回は個別にビルドしたのでいいのですが、デフォルトのビルドの順序がdllの方が後になっているので、ビルドの順序を設定しておきます。
プロジェクトタブから「プロジェクトの依存関係」を選択します。
出てきたウィンドウを画像と同じ設定にすることで、MatTest
がOpenCVTestLibrary
に依存していることをVSに伝えられて、ビルドの順序がOpenCVTestLibrary
→MatTest
となります。
ここまで出来たら次はC#側に戻ります。
まだこのコンソールアプリはdllの存在を知らないので、dllの場所を教えてあげます。
まず、ソリューションエクスプローラーから「参照」を右クリックして「参照の追加」を選択します。
「プロジェクト」タブから「OpenCVTestLibrary」にチェックを付けることで、先ほどのクラスライブラリを使えるようになります。
dllファイルを直接参照する方法もありますが、ReleaseモードとDebugモードで実行のたびに参照先のdllを変えるのが面倒くさいので、Release/Debugが連動してくれるプロジェクト参照にしました。
(※csprojの中身をいい感じに書き換えることでできなくはないので、気になる方は調べてみてください。参考リンク:https://qiita.com/tera1707/items/208d658c06fe1ddd2396 )
最後に、後々使うOpenCVまでのパスを通していきます。
自分が今回使用するOpenCVは4.5.0バージョンです。バージョンを合わせる必要はないと思いますので、皆さんの持っているマイOpenCVを使ってください。
(OpenCVのビルド参考リンク:https://swallow-incubate.com/archives/blog/20200508/)
以下の構成でC++側のプロパティにパスを通してあげてください。
タブ | 項目 | 設定 |
---|---|---|
C/C++ | 追加のインクルードディレクトリ | C:\opencv\include |
リンカー/全般 | 追加のライブラリディレクトリ | C:\opencv\x64\vc16\lib |
リンカー/入力 | 追加の依存ファイル | (Releaseモード選択時)opencv_world450.lib;(Debugモード選択時)opencv_world450d.lib; |
※追加の依存ファイルでは、追加のライブラリディレクトリ内にあるlibファイルを参照します。したがって、libフォルダ内にあるlibフォルダ名をコピーします。仮にOpenCV 3.4.14をビルドした場合、opencv_world3414.lib とopencv_world3414d.lib をそれぞれのモードで入力すれば良いです。 |
||
※※デバッグモードの時はopencv_worldXXXd.dll 、リリースモードの時はopencv_worldXXX.dll を必要とします。 |
それでは、dllを使っていきます。
dllファイルを使用する
C#側のProgram.cs内に先ほど作成したdllの名前空間を追加しておきます。
~
using OpenCVTestLibrary;
~
早速インスタンスを作成してみます。
「System.BadImageFormatException: ファイルまたはアセンブリ 'OpenCVTestLibrary, Version=1.0.7974.29347, Culture=neutral, PublicKeyToken=null'、またはその依存関係の 1 つが読み込めませんでした。間違ったフォーマットのプ ログラムを読み込もうとしました。」
親の顔よりは見ていないファイルがない系の例外です!
この時はプラットフォームをx64
にしたつもりだったのですが、「ビルド」タブの「構成マネージャー」を確認したところC#側がany CPU
のままだったのでそこを修正すると治りました。
画像の赤枠で囲ったプラットフォームをany CPU
からx64
に修正して実行します。
デバッグ用のdllを沢山読み込み終えると無事実行できました!pm.hoge
にはきちんと0
が入っています。
ついでに後々C++/CLI側をデバッグしたくなると思うので、C#側のプロパティのデバッグからネイティブコードデバッグを有効にしておきます。
それでは本題に入っていきます!
C#からSystem.Drawing.Bitmap
を渡し、C++/CLIでcv::Mat
に変換する
それではまずはC++/CLIにBitmapを渡します。
上の方に書いてあった同様の手順で「OpenCVTestLibrary」の参照にSystem.Drawing
を追加します。
すると、System::Drawing::Bitmap
を使用できるようになります。
まず、変換関数のBmp2Mat
を定義します。
# pragma once
# include <opencv2/opencv.hpp>
# include <opencv2/imgproc/imgproc_c.h>
using namespace System;
using namespace System::Drawing;
using namespace cv;
namespace OpenCVTestLibrary {
public ref class ProcMat
{
public:
void Bmp2Mat(Bitmap^ img, bool toGray);
private:
Mat* mat = new Mat();
};
}
プライベートメンバのmat
に変換後のデータを格納することにします。
第1引数にはSystem::Drawing::Bitmap
を渡し、第2引数にはグレースケールに変換するか否かのboolean
を渡します。
次に関数本体です。
void ProcMat::Bmp2Mat(Bitmap^ img, bool toGray) {
// bitの深さ
int color_bit_cnt = img->GetPixelFormatSize(img->PixelFormat);
// 1行当たりのメモリサイズが4の倍数で固定されるため4との積をとる
int stride = ((img->Width * color_bit_cnt + 31) / 32) * 4;
Imaging::BitmapData^ bmp_data = img->LockBits(
Rectangle(0, 0, img->Width, img->Height),
Imaging::ImageLockMode::ReadWrite,
img->PixelFormat);
try {
if (color_bit_cnt == 8) {
*mat = cv::Mat(img->Height, img->Width, CV_8UC1, bmp_data->Scan0.ToPointer(), stride);
}
else if (color_bit_cnt == 24){
*mat = cv::Mat(img->Height, img->Width, CV_8UC3, bmp_data->Scan0.ToPointer(), stride);
if (toGray) cvtColor(*mat, *mat, CV_BGR2GRAY);
}
else {
*mat = cv::Mat(img->Height, img->Width, CV_8UC4, bmp_data->Scan0.ToPointer(), stride);
if (toGray) cvtColor(*mat, *mat, CV_BGRA2GRAY);
else cvtColor(*mat, *mat, CV_BGRA2BGR); // 32bitの画像は扱わないものとする
}
}
finally {
img->UnlockBits(bmp_data);
}
}
仕様をまとめると、
- 入力のBitmapの形式は問わない
- 出力されるmatの型は
unsigned char
型とする - toGrayがtrueの時は、matに変換した後、グレースケールに変換する
- toGrayがfalseの時かつ、アルファチャンネルを持つ画像が入力されても出力は24bitに変換される
てなちょーしです。matにしてしまえばこっちのもんなのでこの後に煮るなり焼くなりホモグラフィするなり皆様の好きなようにしてください。
C++/CLIでcv::Mat
をSystem.Drawing.Bitmap
に変換し、C#に渡す
対となるMat2Bmp
関数を実装します!
~
void Mat2Bmp(Bitmap^% bmp);
~
System.Drawing.Bitmap
データを参照渡しする関数としました。
それでは中身です。
void ProcMat::Mat2Bmp(Bitmap^% bmp) {
// matの深度に応じてピクセルフォーマットを設定
Imaging::PixelFormat pf = mat->channels() == 1 ? Imaging::PixelFormat::Format8bppIndexed : Imaging::PixelFormat::Format24bppRgb;
// 1行ずつコピーするため、IplImageに変換
IplImage iplImage = cvIplImage(*mat);
bmp = gcnew Bitmap(iplImage.width, iplImage.height, pf);
// カラーパレットをグレースケール用に設定
if (mat->channels() == 1) {
Imaging::ColorPalette^ pal = bmp->Palette;
for (int i = 0; i < 256; ++i)
{
pal->Entries[i] = Color::FromArgb(i, i, i);
}
bmp->Palette = pal;
}
Drawing::Imaging::BitmapData^ bd = bmp->LockBits(Rectangle(0, 0, iplImage.width, iplImage.height), Drawing::Imaging::ImageLockMode::WriteOnly, pf);
for (int i = 0; i < iplImage.height; i++) {
uchar* p = (uchar*)bd->Scan0.ToPointer() + i * bd->Stride;
memcpy(p, iplImage.imageData + iplImage.widthStep * i, iplImage.widthStep);
}
bmp->UnlockBits(bd);
}
Bitmapデータは横幅のデータ長が4の倍数で固定されているため、迂闊にcv::Mat
をそのままIplImage
のコンストラクタに渡すと「GDI+ で汎用エラーが発生しました。」や配列外参照、「保護されたメモリへのアクセス…」といった沼エラーが続々と発生します。
なので、一旦cv::Mat
をIplImage
に変換してから横の1行ずつをBitmapのメモリにコピーしていくという作戦をとります。(もちろん、4byteになるようにパディングするという作戦もあります。)また、Bitmapは輝度の1パラメータのみで画像を表現するという仕様ではないため、RGB(輝度, 輝度, 輝度)
としたカラーパレットを設定してグレースケールを再現します。
今回作成したMat2Bmp関数の仕様としては、
- 出力のピクセルフォーマットは
Format8bppIndexed
かFormat24bppRgb
-
cv::Mat
のチャンネル数が1の時は8bitのグレースケールのBitmap、それ以外の時は24bitのカラーのBitmapとなる
てなちょーしです。合間の画像処理でMat画像にアルファチャンネルを追加された暁にはエラーを吐く気しかしませんのでどうか透けさせないでください。
最後に以下のようにC#側で保存して今回の実装は終了です!
~
dst.Save(@"C:\Sample\dst.png", System.Drawing.Imaging.ImageFormat.Png);
あとがき
効率の良い方々はOpenCVSharp
の便利関数(OpenCvSharp.Extensions.BitmapConverter.ToMat())を使ってサクッと実装してそうだったので、この記事に需要があるかはわかりませんが、一応C++/CLIをかませたC#/C++間でcv::Mat
→System.Drawing.Bitmap
とSystem.Drawing.Bitmap
→cv::Mat
を実装することができました。
愚痴ります。
Bitmapのそもそもの仕様である横幅が4byte固定ということを知らずに適当にmatを渡して、適当なストライドと適当なピクセルフォーマットでBitmapを作成するという行為で数時間も詰まりました。最初から「配列外参照してるよ!」と言ってくれればもう少し早く勘づけたかもしれないのにずっと「GDI+ で汎用エラーが発生しました。」「GDI+ で汎用エラーが発生しました。」「保護されたメモリへのアクセス…」「GDI+ で汎用エラーが発生しました。」…
自分みたいな凡庸にはもっと明瞭であってほしいもんです。画像回りの全メモリを解放させたと思います(大袈裟
最後に全く関係ない話ですが、私はこの記事を最初OpenCVSharpのMatをC++/CLIで定義した構造体を経由してC++側のMatにコピーするという方針で半分くらい執筆していました。が、そもそもOpenCVSharp使ってる時点でそっちで画像処理すればええやんというセルフ論破をしてしまいボツにしたという悲しい歴史も共有しておきます。
参考
Bitmap→Matについて
https://teratail.com/questions/225574
https://stackoverflow.com/questions/59421739/c-cli-convert-bitmap-to-opencv-mat
Mat→IplImageについて
https://stackoverflow.com/questions/4664187/converting-cvmat-to-iplimage
https://imagingsolution.net/program/opencv/iplimage2bitmap/
Mat→Bitmapについて
http://cometlearning.blogspot.com/2018/01/opencvcopencvmatbitmap.html
Bitmapのカラーパレットについて
https://imagingsolution.net/program/csharp/setcolorpalette/