SourceChord

C#とXAML好きなプログラマの備忘録。最近はWPF系の話題が中心です。

OpenCvSharp3入門

前回に続き、再度OpenCvSharpネタです。
色々使ってみたので、使い方などを一通りメモしときます。

参考情報

OpenCvSharpのC++API対応についての説明
http://schima.hatenablog.com/entry/2014/03/29/140106

OpenCvSharpのWiki。ここのC++ APIの部分など。
https://github.com/shimat/opencvsharp/wiki

http://opencv.jp/reference_manual

OpenCvSharp3ではC APIは廃止して、C++ APIを使うようになっています。
まずは、この辺のページを見て、OpenCVC++ APIをざっと把握するとよいと思います。

画像の入出力

以下の関数で、画像ファイルからMatクラスのデータを作ったり、、Matクラスのインスタンスの内容をファイルに書き出したりできます。

  • Cv2.Imread・・・画像ファイルの読み込み
  • Cv2.Imwrite・・・画像ファイルへの書き出し
            var src = Cv2.ImRead(@"Images/1.jpg");
            Cv2.ImWrite(@"output.jpg", src);

画像データの形式(Matクラス)

Imreadで読み込んだデータは、Matという型のインスタンスとして返されます。
これは、画像データをはじめ、各種行列形式のデータを汎用的に扱うための型とのこと。

詳しいことは、以下のリンクなどを参照してください。
http://opencv.jp/cookbook/opencv_mat.html
http://opencv.jp/opencv-2svn/cpp/core_basic_structures.html#mat

Matクラス作成時の画像読み込み

Imreadメソッドを使わなくても、以下のようにMatクラスのコンストラクタで、画像ファイルのパスを指定することで画像の読み込みができます。

インスタンス作成時に、画像読み込みまでしたい場合は、こちらの方が便利。
この呼び出し方はOpenCV本家のものではなく、OpenCvSharpnの独自拡張かな?たぶん。
細かい部分ですが、使いやすくなるよう配慮されてていいライブラリですね。

            var img = new Mat(@"Images/1.jpg");
            Cv2.ImShow("test", img);
            Cv2.WaitKey();

f:id:minami_SC:20160815234922p:plain

他にも、OpenCV本家と同様な、多数のコンストラクタのオーバーロードがあります。
こんな風に、塗りつぶし色を指定して、Matのインスタンスを作ることもできます。

            var img = new Mat(240, 320, MatType.CV_8UC3, new Scalar(0, 0, 255));
            Cv2.ImShow("test", img);
            Cv2.WaitKey();

f:id:minami_SC:20160815234954p:plain

ウィンドウ表示

今までもサラッとImShowなどの関数を使ってきましたが、
ここらで改めて、ウィンドウ表示関連の機能をまとめておきます。

以下のような関数があります。

  • Cv2.ImShow・・・・・引数で指定した内容をウィンドウ表示する
  • Cv2.WaitKey・・・・・キー入力が行われるまで、処理を止めて待機
  • Cv2.DestroyWindow・・引数で指定したウィンドウを破棄
  • Cv2.DestroyAllWindows・・OpenCVで作成したすべてのウィンドウを破棄
            var img = new Mat(@"Images/1.jpg");
            // 「test」という名前のウィンドウを作りimgの内容を表示
            Cv2.ImShow("test", img);

            // キー入力があるまで待機
            Cv2.WaitKey();

            // 明示的に全画像の破棄を指示
            Cv2.DestroyAllWindows();

Windowクラスを使ったウィンドウ表示

OpenCvSharpでは、Windowというクラスが定義されており、以下のようにWinowを作成して表示することができます。

            var img = new Mat(@"Images/1.jpg");
            using(var win = new Window("Sample Window", img))
            {
                Cv2.WaitKey();
            }

前回の記事でも書きましたが、WPF環境などで使う場合は、Windowというクラス名がSystem.WindowsOpenCvSharpそれぞれの名前空間に存在するので、両方の名前空間をusingしている場合には注意が必要です。

Matクラスへのアクセス

各種画像の情報取得

画像の幅・高さや、チャンネル数・ビット深度情報、などなどの情報を表示してみます。

            // Matクラスの各種情報を確認
            var img = new Mat(@"Images/1.jpg");
            Console.WriteLine($"Width: {img.Width}, Height: {img.Height}");
            Console.WriteLine($"Cols: {img.Cols}, Rows: {img.Rows}");
            Console.WriteLine($"Channels: {img.Channels()}, Depth: {img.Depth()}");

f:id:minami_SC:20160815235019p:plain

このDepthメソッドで取得できる値は、Matの各要素のビット深度を表します。ただし、この値はビット深度をビット数やバイト数で表した数値ではなく、OpenCV内で定義されたビット深度の種別を示す定数値となっているので注意が必要です。
http://opencv.jp/opencv-2svn/cpp/core_basic_structures.html#cv-mat-depth
このサンプルでは「0」という値になっていますが、これはOpenCvで定義されている定数のCV_8Uにあたるもので、8ビット符号なしデータとなります。

画像の加工/コピーなど

            var src = new Mat(@"Images/1.jpg");
            var dst = new Mat();

            // リサイズ後の画像サイズを指定してリサイズする場合
            Cv2.Resize(src, dst, new Size(320, 240), 0, 0, InterpolationFlags.Cubic);

            // 倍率指定でリサイズする場合
            Cv2.Resize(src, dst, Size.Zero, 1, 0.5, InterpolationFlags.Cubic);

            // X軸方向に画像反転
            Cv2.Flip(src, dst, FlipMode.X);

            // Y軸方向に画像反転
            Cv2.Flip(src, dst, FlipMode.Y);


            // 画像データの複製
            var clone = src.Clone();

            // 画像の一部分を切り出して複製
            var partialClone = src.Clone(new Rect(100, 100, 200, 150));

ピクセルデータの取得、設定

続いて、Matクラスとして扱っている画像データの、各ピクセルデータにアクセスしてみます。

ここでは何通りかの方法を用いてピクセルデータを直接操作し、ネガポジ反転を行ってみます。

            // Get/Setメソッドを使ってアクセス
            var src = new Mat(@"Images/1.jpg");
            for (var y = 0; y < src.Height; y++)
            {
                for (var x = 0; x < src.Width; x++)
                {
                    var px = src.Get<Vec3b>(y, x);
                    px[0] = (byte)(255 - px[0]);
                    px[1] = (byte)(255 - px[1]);
                    px[2] = (byte)(255 - px[2]);
                    src.Set(y, x, px);
                }
            }
            Cv2.ImShow("image", src);
            Cv2.WaitKey();
            // インデクサを使ったアクセス
            var src = new Mat(@"Images/1.jpg");
            var indexer = src.GetGenericIndexer<Vec3b>();
            for (var y = 0; y < src.Height; y++)
            {
                for (var x = 0; x < src.Width; x++)
                {
                    var px = indexer[y, x];
                    px[0] = (byte)(255 - px[0]);
                    px[1] = (byte)(255 - px[1]);
                    px[2] = (byte)(255 - px[2]);
                    indexer[y, x] = px;
                }
            }
            Cv2.ImShow("image", src);
            Cv2.WaitKey();
            // 特定のデータ型に特化したMat派生クラスを使ったアクセス
            var src = new Mat(@"Images/1.jpg");
            var mat3 = new MatOfByte3(src);
            var indexer = mat3.GetIndexer();
            for (var y = 0; y < src.Height; y++)
            {
                for (var x = 0; x < src.Width; x++)
                {
                    var px = indexer[y, x];
                    px[0] = (byte)(255 - px[0]);
                    px[1] = (byte)(255 - px[1]);
                    px[2] = (byte)(255 - px[2]);
                    indexer[y, x] = px;
                }
            }
            Cv2.ImShow("image", src);
            Cv2.WaitKey();

f:id:minami_SC:20160815235032p:plain

↓のページを見てみると、MatOfByte3など、特定のデータ型に特化したMat派生クラスを通してアクセスするのが最も高速なようです。
https://github.com/shimat/opencvsharp/wiki/%5BCpp%5D-Accessing-Pixel

とりあえず、最低限このようなピクセルデータへのアクセスだけできれば、自分で画像処理のロジックをガリガリ書いて色々遊べますね。

OpenCVの機能を使ってネガポジ反転

上で書いたサンプルでは、ピクセルデータを直接操作してネガポジ反転を行いましたが、単純にネガポジ反転するだけであれば、以下のようにOpenCVの機能でシンプルに書くこともできます。

            // ネガポジ反転するだけなら、以下のようにNot演算で可能
            var src = new Mat(@"Images/1.jpg");
            var result1 = new Mat();
            Cv2.BitwiseNot(src, result1);
            // ↓こんな書き方もあり
            var result2 = ~src;

各種アルゴリズム

Matクラスのデータに対して、様々な処理を行う関数が多数用意されてます。
ここでは初めの一歩として、ぼかし効果を与えるblurメソッドを呼び出してみたいと思います。

            var src = new Mat(@"Images/1.jpg");
            var dst = new Mat();
            Cv2.Blur(src, dst, new Size(5, 5));
            Cv2.ImShow("blur", dst);
            Cv2.WaitKey();

他にも多数のアルゴリズムが実装されています。
それぞれの使い方は、必要に応じてリファレンスを参照すればよいかと思います。

図形の描画

OpenCVでは、Mat型の画像に対して、線分やテキストなどの図形データを描画する方法が用意されてます。
こんな風に呼び出して、図形などを描画することができます。

            var img = new Mat(240, 320, MatType.CV_8UC3, new Scalar(0, 0, 0));

            // 直線を描画
            Cv2.Line(img, new Point(10, 10), new Point(300, 10), new Scalar(0, 0, 255));
            Cv2.Line(img, new Point(10, 30), new Point(300, 30), new Scalar(0,255, 0), 2);

            // 矩形を描画
            Cv2.Rectangle(img, new Rect(50, 50, 100, 100), new Scalar(255, 0, 0), 2);

            // 文字を描画
            Cv2.PutText(img, "Hello OpenCvSharp!!", new Point(10, 180), HersheyFonts.HersheyComplexSmall, 1, new Scalar(255, 0, 255), 1, LineTypes.AntiAlias);

            Cv2.ImShow("image", img);
            Cv2.WaitKey();

f:id:minami_SC:20160815235102p:plain

Matクラスのお作法

今までは、特に何も注意せずにMatクラスのインスタンスを作って使ってきましたが、ここではMat型のデータのメモリ管理を少し意識してみたいと思います。

OpenCvSharpでは、MatクラスをIDisposableなクラスとして定義しています。
なので、using構文などを用いて、リソース破棄のタイミングを厳密に管理することができます。

逆に言うと、using使ったり、Dispose()メソッドの呼び出しをしたりしないと、
Matクラスを作って確保されたメモリ領域がいつ破棄されるかは、.NetのGCがかかるタイミング次第、、、という事になってしまいます。

Matで扱うデータは、主に画像などのメモリを大量に消費するものになります。
基本的にはちゃんとusing使って、破棄のタイミングを適切に管理するのが望ましいかと思います。

            using (var src = new Mat(@"Images/1.jpg"))
            using (var dst = new Mat())
            {
                // ぼかしをかける
                Cv2.Blur(src, dst, new Size(5, 5));
                // 結果表示用のウィンドウ作成
                using (var win = new Window("result", dst))
                {
                    Cv2.WaitKey();
                }
            }

動画データを扱う

最後に動画データの扱いについてサラッと試してみます。
動画ファイルや、カメラからのキャプチャなど、動画データのソースとなるものを読み込むには、VideoCaptureクラスを用いることで簡単に利用できるようになります。

動画ファイルの読み込み

まずは、動画ファイルを読み込みしてみます。

このようにVideoCaptureクラスのコンストラクタで、動画ファイルのパスを渡すと、動画の各フレームデータを取得できるようになります。

以下のようなコードで動画ファイルの再生ができるはず、、、なのですが、
自分の環境では再生できませんでした。 OpenCvSharp2.4では、同様のコードで再生できたんだけど・・・
この辺は、原因調査中です。。。

2017/11/26追記 OpenCvSharp 3.2.0.20170107以降のバージョンでは、問題なく動画ファイルの読み込みできるようになってました。

            var capture = new VideoCapture(@"Path/To/MovieFile.xxx");
            using (var win = new Window("capture"))
            using (var mat = new Mat())
            {
                while (true)
                {
                    capture.Read(mat);
                    // 読み込めるフレームがなくなったら終了
                    if (mat.Empty()) { break; }

                    win.ShowImage(mat);
                    Cv2.WaitKey(33);
                }
            }

Webカメラからのキャプチャ

Webカメラからのキャプチャも、画像ファイルを扱うのと同じような感じでコーディングできます。

以下のように、VideoCaptureクラスのコンストラクタに、デバイスIDの数値を渡すことで、指定したデバイスでキャプチャを開始することができます。
このとき、「0」を指定するとデフォルトのデバイスをオープンします。

            // デフォルトのカメラをオープン
            var capture = new VideoCapture(0);
            using (var win = new Window("capture"))
            using (var mat = new Mat())
            {
                while (true)
                {
                    capture.Read(mat);
                    win.ShowImage(mat);
                    if (Cv2.WaitKey(30) >= 0) { break; }
                }
            }

こちらは、OpenCvSharp3系のバージョンでもうまく動きました。

動画データの保存

以下のように、VideoWriterクラスを使って、動画データの保存ができます。
ただし、こちらも動画読み込みと同じように、自分の環境では正しく動画データの出力できず・・・

実行すると、エンコーダの選択画面が出るのですが、選んだものによってはファイルが出力されない、、とか、ファイル出力されても、再生できなかったり、、、などなど。

この辺も、もう少し調べてみようと思います。

            // デフォルトのカメラをオープン
            using (var capture = new VideoCapture(0))
            using (var writer = new VideoWriter("test.avi", FourCC.Default, capture.Fps, new Size(capture.FrameWidth, capture.FrameHeight)))
            using (var win = new Window("capture"))
            using (var mat = new Mat())
            using (var dst = new Mat())
            {
                while (true)
                {
                    capture.Read(mat);
                    Cv2.CvtColor(mat, dst, ColorConversionCodes.BGR2GRAY);
                    win.ShowImage(dst);
                    writer.Write(dst);
                    if (Cv2.WaitKey(30) >= 0) { break; }
                }
            }