Visual Basic でOpenCV⑫ - 画像合成

Visual BasicOpenCVを使用し、画像を合成するものをいくつか紹介します。マスクやROI を使用して、一部のエリアだけを処理対象とする例も紹介します。

加算

2 つの画像を加算するプログラムを紹介します。2 つの画像を開いて、それらを加算して結果を表示します。画像ファイルはドロップ、あるいはメニューから[開く]を選択します。2つの画像サイズは同じでなければなりません。

フォーム

画像処理の説明に先立ち、フォームを簡単に説明します。本プログラムのフォームは1 つだけです。

フォームには、StatusStrip、MenuStrip、Panel、およびPictureBox の4 つのコントロールを配置します。スクロールバー表示を行うので、PictureBox コントロールはPanel コントロールの上に配置します。
メニューを以降に示します。メニューに対応するメソッドは、メニュー項目をダブルクリックすると、自動的にメソッドが定義され、該当のソースファイル部分へカーソルが移動します。その部分に、適宜コードを記述します。

ソースリスト

Form1.vb

以降に、ソースリストを示します。

Imports System
Imports System.IO

Imports Sample.CCvLibrary

Public Class Form1
    Private ReadOnly ttl As String = "sample"
    Private mFname As List(Of String)
    Private ccvfunc As CCvFunc = Nothing

    Public Sub New()
        MyBase.New
        InitializeComponent()

        Text = ttl
        toolSSLbl.Text = "Status"
        Panel1.Dock = DockStyle.Fill    'スクロール対応
        Panel1.AutoScroll = True
        PBox.Location = New Point(0, 0)

        AllowDrop = True
        AddHandler DragEnter, AddressOf Form1_DragEnter
        AddHandler DragDrop, AddressOf Form1_DragDrop

        mFname = New List(Of String)()
        ccvfunc = New CCvFunc()
    End Sub

    ' adjust window size
    Private Sub AdjustWinSize(img As Image)
        PBox.Size = img.Size        'スクロール対応

        ClientSize = New Size(      ' ウィンドウサイズ調整
                         img.Width, img.Height + MenuStrip1.Height _
                         + StatusStrip1.Height)
    End Sub

    ' open file
    Private Sub OpenFile(Optional fname As String = Nothing)
        Dim result = ccvfunc.OpenFileCv(fname)
        If result.Item2 Is Nothing Then
            Return
        End If

        mFname.Add(result.Item1)
        If mFname.Count > 2 Then
            mFname.RemoveAt(0)
        End If

        If mFname.Count = 2 Then
            toolSSLbl.Text = Path.GetFileName(mFname(0)) &
                        " + " & Path.GetFileName(mFname(1)) 'ファイル名表示
            Effect()
        Else
            toolSSLbl.Text = Path.GetFileName(mFname(0))
        End If
    End Sub

    ' 「開く」メニュー項目
    Private Sub FileMenuOpen_Click(sender As Object, e As EventArgs) _
                                                Handles FileMenuOpen.Click
        Try
            OpenFile()
        Catch ex As Exception
            MessageBox.Show(ex.Message)
        End Try
    End Sub

    ' 処理
    Private Sub Effect()
        Try
            Cursor = Cursors.WaitCursor

            Dim bmp As Bitmap = ccvfunc.DoCvFunction(mFname)
            If bmp Is Nothing Then
                MessageBox.Show("error", "size error")
                Return
            End If

            PBox.Image = bmp
            PBox.Size = PBox.Image.Size

            AdjustWinSize(PBox.Image)           ' ウィンドウサイズ調整
        Catch ex As Exception
            MessageBox.Show(ex.Message)
        Finally
            Cursor = Cursors.Default
        End Try
    End Sub

    ' 「名前を付けて保存」メニュー項目
    Private Sub FileMenuSaveAs_Click(sender As Object, e As EventArgs) _
                                                        Handles FileMenuSaveAs.Click
        Try
            If PBox.Image IsNot Nothing Then
                ccvfunc.SaveAS()
            End If
        Catch ex As Exception
            MessageBox.Show(ex.Message)
        End Try
    End Sub

    'ドラッグエンター
    Private Sub Form1_DragEnter(sender As Object, e As DragEventArgs)
        If e.Data.GetDataPresent(DataFormats.FileDrop) Then
            e.Effect = DragDropEffects.All
        Else
            e.Effect = DragDropEffects.None
        End If
    End Sub

    'ドロップ
    Private Sub Form1_DragDrop(sender As Object, e As DragEventArgs)
        Try
            Dim fname = CType(e.Data.GetData(DataFormats.FileDrop, False), String())
            For Each it In fname
                OpenFile(it)
            Next
        Catch ex As Exception
            MessageBox.Show(ex.Message)
        End Try
    End Sub

    ' 「閉じる」メニュー項目
    Private Sub FileMenuClose_Click(sender As Object, e As EventArgs) _
                                                Handles FileMenuClose.Click
        Close()
    End Sub

End Class

まず、ユーザーインターフェースの説明を行います。コンストラクターで、これまでと異なるのは、画像ファイル名を管理するmFname を生成することです。mFname はList(Of String) で、複数のファイル名を保持します。
OpenFile メソッドは、これまでと違い、複数の画像ファイル名を管理します。すでに画像ファイルを2 つ読み込み済みの場合、先に読み込んだファイルを破棄します。読み込んだ画像が2 つの場合は、Effectメソッドを呼び出し、[処理]メニュー項目を選択したときと同じ動作を行います。
[開く]メニュー項目が選択されたときの処理は、これまでのプログラムと同様です。
Effectメソッドが呼び出されたら、画像ファイル名が格納されているmFname を引数にして、CCvFunc クラスのDoCvFunction メソッドを呼び出します。このDoCvFunction メソッドでOpenCvSharp を使った処理が行われます。DoCvFunction メソッドは、処理後のBitmap オブジェクトを返します。それをPictureBox のImage へ設定したのち、フォームのサイズを画像サイズに合わせます。もし、DoCvFunction メソッドが返すBitmap オブジェクトがNothingの場合は、2つの画像が同じサイズでなかったと判断し、エラーメッセージをダイアログで表示します。
[名前を付けて保存]メニュー項目が選択されたら、FileMenuSaveAs_Click メソッドが呼び出されます。CCvFunc クラスのSaveAS メソッドを呼び出し、処理後の画像をファイルに保存します。
ファイルをドロップされたら、Form1_DragDrop メソッドに制御が移ります。これまでは複数のファイルがドロップされても最初のファイルしか処理しませんでした。このプログラムは、ドロップされた全ファイルを処理対象とします。fname に格納されているファイルを" For Each " 文で、すべて処理します。

CCvFunc.vb

本プログラム特有の処理を受け持つCCvFunc クラスのDoCvFunction メソッドを示します。

:
Public Function DoCvFunction(ByVal mFname As List(Of String)) As Drawing.Bitmap
    Using src1 As Mat = Cv2.ImRead(mFname(0))
        Using src2 As Mat = Cv2.ImRead(mFname(1))
            If src1.Width <> src2.Width OrElse src1.Height <> src2.Height Then
                Return Nothing
            End If
            mDst = New Mat()
            Cv2.Add(src1, src2, mDst)
        End Using
    End Using
    Return OpenCvSharp.Extensions.BitmapConverter.ToBitmap(mDst)
End Function
:

本プログラムは、Cv2.Add を使用し、2 つの画像を加算します。画像のサイズが異なる場合、Nothing を返します。

実行

以降に、2つの入力画像と処理結果を示します。


処理結果フォームのステータスバーに、対象となったファイル名が表示されます。

ブレンド

2 つの画像をブレンドする例と差分を取る例を紹介します。先のプログラムと異なるのはDoCvFunction メソッド中の1 行のみです。まず、Cv2.AddWeightedメソッドで、2 つの画像をブレンドする例を示します。
Cv2.Add(src1, src2, mDst)

Cv2.AddWeighted(src1, 0.5, src2, 0.5, .0, mDst)
へ変更します。

実行

以降に実行例を示します。

Cv2.AddWeightedメソッドへ与える値によってブレンドの比率を変更できます。この例では、両方に0.5を与えていますので、両方の画像が同じ比率でブレンドされます。

差分

次に、2 つの画像の差分を取る例を紹介します。先ほど同様、
Cv2.Add(src1, src2, mDst)

Cv2.Absdiff(src1, src2, mDst)
へ変更します。

マスクを使った2 つの画像加算

2 つの画像を加算しますが、その際にマスクを使用するプログラムを紹介します。これまでと異なるCCvFuncクラスのDoCvFunctionメソッドを示します。

Public Function DoCvFunction(ByVal mFname As List(Of String)) As Drawing.Bitmap
    Using src1 As Mat = Cv2.ImRead(mFname(0))
        Using src2 As Mat = Cv2.ImRead(mFname(1))
            If src1.Width <> src2.Width OrElse src1.Height <> src2.Height Then
                Return Nothing
            End If
            Using mask As Mat = New Mat(src1.Size(), MatType.CV_8UC1, Scalar.Black)
                Dim p0 As New Point(mask.Width / 4, mask.Height / 4)
                Dim p1 As New Point(mask.Width * 3 / 4, mask.Height * 3 / 4)
                Cv2.Rectangle(mask, p0, p1, Scalar.White, -1)
                mDst = src1.Clone()
                Cv2.Add(src1, src2, mDst, mask)
            End Using
        End Using
    End Using
    Return OpenCvSharp.Extensions.BitmapConverter.ToBitmap(mDst)
End Function

本プログラムは、最初のプログラムに近く、Cv2.Add メソッドを使用し2 つの画像を加算します。最初のプログラムと異なるのはマスクを使用することです。マスクは、中心部が0 以外で、外周部が0 の値を持つMat オブジェクトです。中心部の、四角形の範囲だけが処理対象となります。

実行

以降に、2つの入力画像と処理結果を示します。


加算だけではなく、差分や論理和でも同じようにマスクを使って、特定の範囲を処理対象とすることができます。以降に、プログラム内で生成したマスクを示します。

ここで使用するCv2.Add メソッドは、すでに紹介したメソッドですが、オーバーロードを使用します。

次に、マスクの形状を変更した例を示します。

Public Function DoCvFunction(ByVal mFname As List(Of String)) As Drawing.Bitmap
    Using src1 As Mat = Cv2.ImRead(mFname(0))
        Using src2 As Mat = Cv2.ImRead(mFname(1))
            If src1.Width <> src2.Width OrElse src1.Height <> src2.Height Then
                Return Nothing
            End If
            Using mask As Mat = New Mat(src1.Size(), MatType.CV_8UC1, Scalar.Black)
                Dim p0 As New Point(mask.Width / 8, mask.Height / 8)
                Dim p1 As New Point(mask.Width * 7 / 8, mask.Height * 7 / 8)
                Cv2.Rectangle(mask, p0, p1, Scalar.White, -1)
                Dim center As New Point(mask.Width / 2, mask.Height / 2)
                Cv2.Circle(mask, center, mask.Height / 8, Scalar.Black, -1)
                mDst = src1.Clone()
                Cv2.Add(src1, src2, mDst, mask)
            End Using
        End Using
    End Using
    Return OpenCvSharp.Extensions.BitmapConverter.ToBitmap(mDst)
End Function

以降に、実行結果とプログラム内で生成したマスクを示します。

少し複雑なマスクの例も示します。

Public Function DoCvFunction(ByVal mFname As List(Of String)) As Drawing.Bitmap
            :
            Using mask As Mat = New Mat(src1.Size(), MatType.CV_8UC1, Scalar.Black)
                Dim xUnit = mask.Width / 8
                Dim yUnit = mask.Height / 8
                Dim LLPoint2D As New List(Of List(Of Point)) From {
                            New List(Of Point)(New Point() {
                                New Point(4 * xUnit, 1 * yUnit),
                                New Point(7 * xUnit, 6 * yUnit),
                                New Point(1 * xUnit, 6 * yUnit)}),
                            New List(Of Point)(New Point() {
                                New Point(1 * xUnit, 2 * yUnit),
                                New Point(7 * xUnit, 2 * yUnit),
                                New Point(4 * xUnit, 7 * yUnit)})}
                Cv2.FillPoly(mask, LLPoint2D, Scalar.White)
                mDst = src1.Clone()
                Cv2.Add(src1, src2, mDst, mask)
            End Using
            :
End Function

以降に、実行結果とプログラム内で生成したマスクを示します。

ROIを設定した2 つの画像加算

次に、2 つの画像を加算しますが、Mat オブジェクトにROI を設定し、ROI の範囲だけが処理される例を示します。

Public Function DoCvFunction(ByVal mFname As List(Of String)) As Drawing.Bitmap
            :
            Dim roi As New Rect(src1.Cols / 4, src1.Rows / 4,
                                                src1.Cols / 2, src1.Rows / 2)
            mDst = src1.Clone()
            Using src1Roi As New Mat(src1, roi)
                Using src2Roi As New Mat(src2, roi)
                    Using dstRoi As New Mat(mDst, roi)
                        Cv2.Add(src1Roi, src2Roi, dstRoi)
                    End Using
                End Using
            End Using
            :
End Function

本プログラムは、最初の例をマスクからROI へ変更したものです。Cv2.Add メソッドを使用し2 つの画像を加算しますが、先の例と異なり、マスクの代わりにROI を使用します。Rect オブジェクトroi にROI の座標を設定します。このROI を各画像に設定し、加算を行います。あるいは、roi の範囲のサブマトリックスを指定して加算を行うと表現しても良いでしょう。これによってROI の部分だけが処理対象となります。

以降に、2つの入力画像と処理結果を示します。


加算だけではなく、差分や論理和でも同じようにROIを使って、特定の範囲を処理対象とすることができます。

Matオブジェクトの要素アクセス

Matオブジェクトの各要素を直接操作したい場合があります。いくつか方法はありますが、ここではMatオブジェクトのGetGenericIndexerメソッドを利用し、Matオブジェクトの型を操作する例を示します。速度などを気にしないなら一般的なGet/Setメソッドを利用するのが良いでしょう。ここでは、Get/Setメソッドを利用する例は紹介しません。

CV_8UC1

グレイスケール画像を格納する際に頻繁に利用される、1チャンネル符号なし8ビットの例を示します。
Dim indexer = img.GetGenericIndexer(Of Byte)()
indexer(y, x) = 200

x,yはMatオブジェクト要素を指す座標値です。1チャンネルのため、スカラー値を指定します。

CV_8UC3

カラー画像を格納する際に頻繁に利用される、3チャンネル符号なし8ビットの例を示します。
Dim indexer = img.GetGenericIndexer(Of Vec3b)()
indexer(y, x) = New Vec3b(255, 0, 0)

この例では設定を行っています。indexerの指す要素に、Vec3bの値を設定します。

CV_32FC1

1チャンネル浮動小数点数32ビットの例を示します。
Dim indexer = img.GetGenericIndexer(Of Single)()
indexer(y, x) = 128.0F

CV_64FC3

3チャンネル浮動小数点数64ビットの例を示します。
Dim indexer = img.GetGenericIndexer(Of Vec3d)()
indexer(y, x) = New Vec3d(0.0D, 0.0D, 255D)