LoginSignup
2
9

More than 1 year has passed since last update.

System.Drawing.Bitmapとcv::Matの相互変換をするdllのレシピ

Last updated at Posted at 2021-11-09

概要

Winforms(or WPF)を使っていて、画像処理をしたくなった!さぁどうしましょうか。
そうですね、OpenCVSharpですね。
But! C++で築き上げてきた資産が皆様にはあると思います。

そんな資産であるC++アセットのdllに画像を渡すときの方法を共有したく今回執筆しました。
皆様の工数を1秒でも削ることができれば幸いです。

本記事では、C#側のBitmap画像をCLRクラスライブラリ(C++/CLI側)にcv::Matとして渡す、またcv::MatBitmapに変換してC#に渡すといった流れをプロジェクトの作成から実装していきます。
(結論から言うと、BitmapMatと、MatIplImageBitmapといった順番で変換させていきます。)

環境

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」にチェックを付けます。
image.png

ではC++/CLI側に渡すための画像データを定義していきます。

Program.cs
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に変更しておきます。
image.png

それではこの画像をC++/CLI側に持っていきたいと思います。

CLRクラスライブラリを作成する

ソリューションエクスプローラーからソリューションファイルを右クリックして「追加」→「新しいプロジェクト」を選択します。
image.png
出てきたウィンドウでC++言語のCLRクラスライブラリ(.NET Framework)を選択します。
プロジェクト名は「OpenCVTestLibrary」として、バージョンは4.5.2に合わせます。

作成したらヘッダーが表示されると思います。
中身を少しだけ以下のように書き換えました。

OpenCVTestLibrary.h
#pragma once

using namespace System;

namespace OpenCVTestLibrary {
    public ref class ProcMat
    {
        int hoge = 0;
    };
}

クラス名をClass1からProcMatに変えて、hoge変数を定義しています。
それではこいつをビルドしてdllを作成します。
image.png
image.png

dllファイルはここに出力されたようです。
また、今回は個別にビルドしたのでいいのですが、デフォルトのビルドの順序がdllの方が後になっているので、ビルドの順序を設定しておきます。
image.png
プロジェクトタブから「プロジェクトの依存関係」を選択します。
出てきたウィンドウを画像と同じ設定にすることで、MatTestOpenCVTestLibraryに依存していることをVSに伝えられて、ビルドの順序がOpenCVTestLibraryMatTestとなります。
image.png

ここまで出来たら次はC#側に戻ります。
まだこのコンソールアプリはdllの存在を知らないので、dllの場所を教えてあげます。
まず、ソリューションエクスプローラーから「参照」を右クリックして「参照の追加」を選択します。

「プロジェクト」タブから「OpenCVTestLibrary」にチェックを付けることで、先ほどのクラスライブラリを使えるようになります。
image.png

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.libopencv_world3414d.libをそれぞれのモードで入力すれば良いです。
※※デバッグモードの時はopencv_worldXXXd.dll、リリースモードの時はopencv_worldXXX.dllを必要とします。

それでは、dllを使っていきます。

dllファイルを使用する

C#側のProgram.cs内に先ほど作成したdllの名前空間を追加しておきます。

Program.cs
~
using OpenCVTestLibrary;
~

早速インスタンスを作成してみます。
image.png
「System.BadImageFormatException: ファイルまたはアセンブリ 'OpenCVTestLibrary, Version=1.0.7974.29347, Culture=neutral, PublicKeyToken=null'、またはその依存関係の 1 つが読み込めませんでした。間違ったフォーマットのプ ログラムを読み込もうとしました。」
親の顔よりは見ていないファイルがない系の例外です!
この時はプラットフォームをx64にしたつもりだったのですが、「ビルド」タブの「構成マネージャー」を確認したところC#側がany CPUのままだったのでそこを修正すると治りました。
image.png
画像の赤枠で囲ったプラットフォームを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を定義します。

OpenCVTestLibrary.h
#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を渡します。
次に関数本体です。

OpenCVTestLibrary.cpp
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::MatSystem.Drawing.Bitmapに変換し、C#に渡す

対となるMat2Bmp関数を実装します!

OpenCVTestLibrary.h
~
void Mat2Bmp(Bitmap^% bmp);
~

System.Drawing.Bitmapデータを参照渡しする関数としました。
それでは中身です。

OpenCVTestLibrary.cpp
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::MatIplImageに変換してから横の1行ずつをBitmapのメモリにコピーしていくという作戦をとります。(もちろん、4byteになるようにパディングするという作戦もあります。)また、Bitmapは輝度の1パラメータのみで画像を表現するという仕様ではないため、RGB(輝度, 輝度, 輝度)としたカラーパレットを設定してグレースケールを再現します。

今回作成したMat2Bmp関数の仕様としては、

  • 出力のピクセルフォーマットはFormat8bppIndexedFormat24bppRgb
  • cv::Matのチャンネル数が1の時は8bitのグレースケールのBitmap、それ以外の時は24bitのカラーのBitmapとなる

image.png
てなちょーしです。合間の画像処理でMat画像にアルファチャンネルを追加された暁にはエラーを吐く気しかしませんのでどうか透けさせないでください。

最後に以下のようにC#側で保存して今回の実装は終了です!

Program.cs
~
dst.Save(@"C:\Sample\dst.png", System.Drawing.Imaging.ImageFormat.Png);

あとがき

効率の良い方々はOpenCVSharpの便利関数(OpenCvSharp.Extensions.BitmapConverter.ToMat())を使ってサクッと実装してそうだったので、この記事に需要があるかはわかりませんが、一応C++/CLIをかませたC#/C++間でcv::MatSystem.Drawing.BitmapSystem.Drawing.Bitmapcv::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/

2
9
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
9