【C#】OpenCVSharpのMatとNumpyのNDArrayを相互変換する

画像処理でよく使われるものと言えばOpenCVNumpy

先日Pythonの画像処理のプログラムをC#のフォームアプリに移行する機会があり、OpenCVSharpとNumpyを入れてやればそのまま同じコードが使えるだろう~と思ったら、PythonのOpenCVと違ってOpenCVSharpの関数にはNumpyの配列がそのまま渡せない問題にぶつかりました。
特に変換関数も用意されておらず、調べてもそれらしい情報が出てこず苦労しました。

今回は、OpenCVSharpのMatをNumpyのNDArrayに変換する関数、反対にNDArrayをMatに変換する関数についての備忘録です。

環境

Visual Studio 2022、 .NET 6.0で確認しました。

NuGetで以下をインストールします。
・OpenCvSharp4.Windows
・OpenCvSharp4.Extensions
・Numpy(NumSharpじゃない)

変換するコード

変換コードを最初に紹介します。

NDarrayをMatに変換する関数。
MatTypeには、NDArrayのデータ型を渡します。dtypeプロパティで確認できます。

C#
// NDArrayが2次元(白黒)の場合
private Mat NDArrayToMat(NDarray array, MatType type)
{
    try
    {
        int rows = array.shape[0];
        int cols = array.shape[1];

        Mat mat = new Mat(rows, cols, type);
        for (int y = 0; y < rows; y++)
        {
            for (int x = 0; x < cols; x++)
            {
                mat.Set<int>(y, x, (int)(array[y, x]));
            }
        }
        return mat;
    }
    catch (Exception ex)
    {
        return new Mat();
    }
}

// NDArrayが3次元(カラー)の場合
static Mat NDArray3ToMat(NDarray array, MatType type)
{
    try
    {
        int rows = array.shape[0];
        int cols = array.shape[1];

        Mat mat = new Mat(rows, cols, type);
        for (int y = 0; y < rows; y++)
        {
            for (int x = 0; x < cols; x++)
            {
                Vec3b pic = new Vec3b();
                pic[0] = (byte)array[y, x, 0];
                pic[1] = (byte)array[y, x, 1];
                pic[2] = (byte)array[y, x, 2];
                mat.Set<Vec3b>(y, x, pic);
            }
        }
        return mat;
    }
    catch (Exception ex)
    {
        return new Mat();
    }
}

MatをNDarrayに変換する関数。

C#
// 白黒画像の場合
private NDarray MatToNDArray(Mat mat)
{
    try
    {
        int width = mat.Width;
        int height = mat.Height;
        int[,] data = new int[height, width];

        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                data[y,x] = mat.Get<int>(y, x);
            }
        }
        return data;
    }
    catch (Exception ex)
    {
        return (NDarray)0;
    }
}

// カラー画像の場合
static NDarray MatToNDArray3(Mat mat)
{
    try
    {
        int width = mat.Width;
        int height = mat.Height;
        int[,,] data = new int[height, width, 3];

        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                data[y, x, 0] = mat.At<Vec3b>(y, x)[0];
                data[y, x, 1] = mat.At<Vec3b>(y, x)[1];
                data[y, x, 2] = mat.At<Vec3b>(y, x)[2];
            }
        }
        return data;
    }
    catch (Exception ex)
    {
        return (NDarray)0;
    }
}

簡単な説明

数値は適当で意味は特にありません。

2次元のNDArrayは、画像横幅サイズの配列が、画像縦幅サイズ個集まった2次元配列で、例えば
[[100, 56, …, 134] ,
[17, 66, …, 190],

[23, 45, …, 82]]
みたいな感じ。

これが画像(Mat)ならこんな感じ。

NDArrayからMatへの変換は、NDArrayの0行0列目、0行1列目……、画像縦幅サイズ-1行画像横幅サイズ-1列目までを2重ループで順に取り出して、Matの対応するピクセルに代入しています。
反対にMatからNDArrayへの変換は、1ピクセルずつ取り出して、配列の対応するインデックスに入れるという逆のことをしています。

3次元(カラー)の場合は、各ピクセルにつき、RGBの値が入るため3次元配列になります。
NDArrayは、
[[[123, 59, 134], [8, 213, 11] …, [0, 45, 29]],
[[97, 25, 186], [22, 62, 73] …, [53, 40, 6]],

[[33, 67, 161], [2, 143, 82] …, [100, 50, 255]]]
みたいな感じ。

Matは以下。

対応する座標に加えて、それぞれの座標でRGB値も代入しています。

使ってみる

上の画像をグレースケールに変換してみます。

読み込む用の.npyファイルをPythonで作りました。

Python
from PIL import Image
import numpy as np

// ↑の画像を読み込み
im = np.array(Image.open("test.jpg"))

// .npyファイルとして保存
np.save('test.npy', im)

先に作ったtest.npyを読み込んで、Cv2.CvtColor()でグレースケールに変換。CvtColorの引数はMatなので、NDArrayから変換して渡してやります。

C#
public static Bitmap GrayScale()
{
    var imagePath = @"test.npy";
    var imageArray = np.load(imagePath);
    var type = imageArray.dtype; // uint8
    // カラー画像を.npyにしたので、3次元の方で変換する
    var inputMat = NDArray3ToMat(imageArray, MatType.CV_8UC3);
    var mat = new Mat();
    Cv2.CvtColor(inputMat, mat, ColorConversionCodes.BGR2GRAY);
    return BitmapConverter.ToBitmap(mat);
}

無事グレースケールにできました!

全体のコードはこちら(GitHub)にあります。
ボタンが押されると指定したパスの.npyを読み込んで変換し、変換後の画像をPictureBoxに表示しています。

備考

時間がかかる

MatのSetメソッド・Atメソッドはとても時間がかかるらしい。
今回使用した画像は1024×683でしたが、Stopwatchクラスで測定したところ、NDArrayからMatに変換するのに7.3秒かかりました。
ちなみにMatに変換したやつをNDArrayに戻す処理は182ミリ秒でした。Setと比べればAtの方はまだ速いんでしょうか。

ポインタ操作を使えば速く処理できるらしく、実際ポインタを用いることで変換しようとしてる?ものもいくつか見つけました。でもよく分からなかったというかうまくいかなかったんですよね……。

処理速度が求められる場合は、今回のコードはオススメできません。
余裕があればポインタにも再チャレンジしたいですね。

np.load()の謎挙動

1番最初に読み込む(実行して1番最初にnp.load()に入り、そして終わる)までにとても時間がかかります。でも1回通過してしまえばあまりかからなくなるのが更に謎……。
ちなみに、.Frameworkの方では処理が終わらず実装を断念しました。

また、プログラムを終了してもバックグラウンドに何か処理が残ってしまうという問題もあります。
現状解決方法が不明のため、プログラム終了時にプロセスを殺す処理が必要そうです。

C#
public static void KillProcess()
{
    // 自身のプロセス名で検索して、そいつを殺す
    System.Diagnostics.Process[] ps = System.Diagnostics.Process.GetProcessesByName("xxx");
    foreach (System.Diagnostics.Process p in ps)
    {
        p.Kill();
    }
}

もし上記2つの問題について何かご存じの方がいれば教えていただきたいです。

コメント

タイトルとURLをコピーしました