Visual Basic でOpenCV⑧ - UIとOpenCvを分離

クラスを導入し、ユーザーインターフェースとOpenCvSharpを分離したものを紹介します。

UIとOpenCVを分離

ユーザーインターフェースとOpenCvSharp に関するコードを分離する例を紹介します。これまでのプログラムはForm に対応したソースファイルがOpenCvSharp を参照しており、ユーザーインターフェースOpenCV が混在していました。本プログラムは、OpenCvSharpをクラス内に封じ込め、ユーザーインターフェースOpenCV を完全に分離します。これによって、プログラムを読みやすくするとともにメンテナンス性の向上も目指します。
Form は従来のVisual Basic で開発したプログラムと同様とし、クラスはOpenCV の共通機能を実現したメソッドなどで構成されたベースクラスと特定のアプリケーションに依存した派生クラスから成り立ちます。OpenCV に対応したクラスの構造を図に示します。

これまでは、ユーザーインターフェースとOpenCvSharpが同居していたため、System 名前空間と OpenCvSharp 名前空間の両方に存在する同一名のクラスなどは、どちらのものであるか明示する必要がありました。本プログラム以降は、ユーザーインターフェースOpenCVを利用するファイルを分離したため、System 名前空間や OpenCvSharp 名前空間を明示的に示す必要はありません。例えばForm1などのSizeはSystem.Drawing.Sizeであり、CCvクラスのSizeはOpenCvSharp.Sizeです。これ以降は名前空間を明示的に示す必要はありませんが、理解を助けるために明示しても構いません。

共通に使われる機能を、スーパークラス(基底クラス)のCCv クラスに実装し、各プログラム特有の機能をCCv クラスのサブクラス(派生クラス)であるCCvFunc クラスに実装します。

Form1.vb

まず、読み込んだ画像を表示するフォームに対するコードが記述されているForm1.vb を示します。これまでのプログラムと異なる部分のみを示します。

Imports System
Imports System.IO

Imports Filters.CCvLibrary

Public Class Form1
    Private ReadOnly ttl As String = "sample"
    Private ReadOnly mForm2 As Form2 = Nothing
    Private ccvfunc As CCvFunc = Nothing

    Public Sub New()
        :
        mForm2 = New Form2

        ccvfunc = New CCvFunc()
    End Sub

    :

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

        PBox.Image = result.Item2
        PBox.Size = PBox.Image.Size

        AdjustWinSize(PBox.Image)                       ' ウィンドウサイズ調整

        toolSSLbl.Text = Path.GetFileName(result.Item1) 'ファイル名表示

        If mForm2.Validate() Then
            mForm2.Hide()
        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 ToolMenuEffect_Click(sender As Object, e As EventArgs) _
                                                Handles ToolMenuEffect.Click
        Try
            If PBox.Image Is Nothing Then Return         ' 読み込んでいるか

            Cursor = Cursors.WaitCursor
            mForm2.DoCvShow(ccvfunc)

        Catch ex As Exception
            MessageBox.Show(ex.Message)
        Finally
            Cursor = Cursors.[Default]
        End Try
    End Sub
    :

ユーザーインターフェースとOpenCvSharp に関するコードを分離したため、「Imports OpenCvSharp 」がなくなり、開発したクラスを利用するための「Imports Filters.CCvLibrary 」を追加します。
コンストラクター内でフォームのタイトルやステータスバーを設定するのは、これまでと同様です。コンストラクターの最後で、CCv クラスのサブクラスCCvFunc をインスタンス化します。
OpenFile メソッドは、ファイルの読み込みと表示などをまとめたものです。これまではCIo クラスを利用していましたが、本プログラムはコンストラクターインスタンス化したCCvFunc クラスのOpenFileCv メソッドを使用します。
[開く]メニュー項目が選択されたときに呼び出されるFileMenuOpen_Click メソッドも単純化され、OpenFile メソッドを呼び出します。
[処理]メニュー項目が選択されたときに呼び出されるToolMenuEffect_Click メソッドは、CCvFunc クラスのインスタンスを引数にForm2 のDoCvShow メソッドを呼び出します。

Form2.vb

処理結果を表示するフォームに対するコードが記述されているForm2.vb を示します。これまでのプログラムと異なる部分を主に示します。

Imports System

Imports Filters.CCvLibrary

Public Class Form2
    Private ReadOnly ttl As String = "処理結果"
    Private mCcvfunc As CCvFunc = Nothing

    :

    Private Sub FileMenuSaveAs_Click(sender As Object, e As EventArgs) _
                                                Handles FileMenuSaveAs.Click
        Try
            mCcvfunc.SaveAS()
        Catch ex As Exception
            MessageBox.Show(ex.Message)
        End Try
    End Sub

    :

    Public Sub DoCvShow(ByVal ccvfunc As CCvFunc)
        mCcvfunc = ccvfunc

        Dim bmp As Bitmap = mCcvfunc.DoCvFunction()
        If bmp IsNot Nothing Then
            PBox.Image = bmp
            AdjustWinSize(PBox.Image)
            Show()
        End If
    End Sub
    :

Form1 同様、ユーザーインターフェースとOpenCvSharp に関するコードを分離したため、「Imports OpenCvSharp 」の代わりに「 Imports Filters.CCvLibrary 」を追加します。Form1 から渡されるCCvFunc クラスのインスタンスを保持するため、Private フィールドmCcvfunc を宣言します。
[名前を付けて保存]メニュー項目が選択されたときに制御が渡るのがFileMenuSaveAs_Click メソッドです。単純にmCcvfunc のSaveAS メソッドを呼び出すだけです。実際にOpenCvSharp を使用して保存する処理はクラスにカプセル化されます。
DoCvShow メソッドは、Form1 で[処理]メニュー項目が選択されたときに呼び出されます。渡されたCCvFunc クラスのインスタンスをmCcvfunc へ保存したのち、DoCvFunction メソッドを呼び出し、受け取ったBitmap オブジェクトを表示します。画像処理自体はDoCvFunctionメソッドで行われます。
CCvFunc クラスのSaveAS メソッドやDoCvFunction メソッドは、これまでのプログラムのフォームに対応したソースコードで実行していたものを、クラス内へ移動しただけです。このようにクラスを利用すると、同じコードを何回も記述する必要はなくなります。

CCvFunc クラス

以降にCCvFunc クラスのソースコードを示します。

Imports OpenCvSharp

Namespace CCvLibrary
    Public Class CCvFunc
        Inherits CCv

        'コンストラクタ
        Public Sub New()
            MyBase.New()
        End Sub

        ' OpenCVを使用して処理
        Public Function DoCvFunction() As Bitmap
            mDst = New Mat()
            Return OpenCvSharp.Extensions.BitmapConverter.ToBitmap(mDst)
        End Function

    End Class
End Namespace

単純に、既に紹介した「ブラー処理」のプログラムをクラス化しただけです。DoCvFunction メソッドの内容を書き換えるだけで、いろいろな画像処理へ対応できます。

CCv クラス

本クラスに基本的な機能を実装します。後で使用するメソッドやプロパティも実装しています。簡単ですので、各メソッドの説明は省きます。

Imports OpenCvSharp

Namespace CCvLibrary
    Public Class CCv
        Private _mSrc As Mat
        Private _mDst As Mat

        Protected Property mSrc() As Mat
            Get
                Return _mSrc
            End Get
            Set
                _mSrc = Value
            End Set
        End Property

        Protected Property mDst() As Mat
            Get
                Return _mDst
            End Get
            Set
                _mDst = Value
            End Set
        End Property

        '----------------------------------------------------------------
        'コンストラクタ
        Public Sub New()
        End Sub

        '----------------------------------------------------------------
        ' 読み込みファイル名を取得、
        '                ダイアログを使用して読み込みファイルを選択させる
        Public Function GetReadFile(Optional filter As _
                String = "画像ファイル(*.jpg,*.bmp,*.png)|*.jpg;*.bmp;*.png|" _
                                    & "すべてのファイル(*.*)|*.*") As String
            Dim fname As String = Nothing

            Using openDlg As OpenFileDialog = New OpenFileDialog()
                openDlg.CheckFileExists = True
                openDlg.Filter = filter
                openDlg.FilterIndex = 1
                If openDlg.ShowDialog() = DialogResult.OK _
                    Then fname = openDlg.FileName
            End Using
            Return fname
        End Function

        '----------------------------------------------------------------
        ' 書き込みファイル名を取得、
        '               ダイアログを使用して読み込みファイルを選択させる
        Public Function GetWriteFile(Optional filter As _
                String = "画像ファイル(*.jpg,*.bmp,*.png)|*.jpg;*.bmp;*.png|" _
                                    & "すべてのファイル(*.*)|*.*") As String
            Dim fname As String = Nothing

            Using svDlg As SaveFileDialog = New SaveFileDialog()
                svDlg.Filter = filter
                svDlg.FilterIndex = 1
                If svDlg.ShowDialog() = DialogResult.OK _
                    Then fname = svDlg.FileName
            End Using
            Return fname
        End Function

        '----------------------------------------------------------------
        ' ファイルを開く、ファイル名が指定されていない場合は、
        '               ダイアログを使用して読み込みファイルを選択させる
        Public Function OpenFileCv(fname As String) As (String, Bitmap)
            Dim bmp As Bitmap = Nothing
            Dim newfname = fname

            If Equals(fname, Nothing) Then
                newfname = GetReadFile()
            End If

            If Not Equals(newfname, Nothing) Then
                Dim img As Mat = Cv2.ImRead(newfname)
                If Not img.Empty() Then
                    mSrc = img
                    bmp = Extensions.BitmapConverter.ToBitmap(mSrc)
                End If
            End If
            Return (newfname, bmp)
        End Function


        '----------------------------------------------------------------
        ' 名前を付けてファイルを保存
        Public Sub SaveAS()
            Dim fname As String = GetWriteFile()
            If Not Equals(fname, Nothing) Then
                Cv2.ImWrite(fname, mDst)    ' OpenCV
            End If
        End Sub

        '----------------------------------------------------------------
        ' create cos k mat
        Public Function CreateCosMat(rows As Integer, cols As Integer) As Mat

            Dim mat As New Mat(rows, cols, MatType.CV_8UC3, New Scalar(0))
            '画像アクセス用
            Dim indexer_dst As MatIndexer(Of Vec3b) = mat.GetGenericIndexer(Of Vec3b)()

            Dim center As New Point(cols / 2, rows / 2)
            Dim radius = Math.Sqrt(Math.Pow(center.X, 2) + Math.Pow(center.Y, 2))
            'ピクセルアクセス
            For y = 0 To mat.Height - 1
                For x = 0 To mat.Width - 1
                    ' distance from center
                    Dim distance = Math.Sqrt(
                        Math.Pow(center.X - x, 2) + Math.Pow(center.Y - y, 2))
                    ' radius=π, current radian
                    Dim radian = distance / radius * Math.PI
                    ' cosθ, normalize -1.0~1.0 to 0~1.0
                    Dim cd = (Math.Cos(radian) + 1.0) / 2.0
                    ' normalize (Y) 0~1.0 to 0.0~255.0
                    cd *= 255.0
                    indexer_dst(y, x) = New Vec3b(cd, cd, cd)
                    'mat.Set(y, x, New Vec3b(cd, cd, cd)) 'インデックサーを用いない
                Next
            Next
            Return mat
        End Function

        '----------------------------------------------------------------
        ' mulMask
        Public Function MulMat(mat As Mat, table As Mat) As Mat
            Dim dst As New Mat()
            Using mat32f As New Mat(), dst32f As New Mat()
                Dim table32f As New Mat()

                mat.ConvertTo(mat32f, MatType.CV_32FC3)
                table.ConvertTo(table32f, MatType.CV_32FC3)
                table32f /= 255.0F
                Cv2.Multiply(mat32f, table32f, dst32f)
                dst32f.ConvertTo(dst, MatType.CV_8UC3)
            End Using
            Return dst
        End Function


        '---------------------------------------------------------
        ' Size change by Gausian
        '
        '  dst:      Mat
        '  ListRect: areas
        ' toSmall:   0:to Big, 1:to small
        '
        Protected Function doChgObjsGausian(dst As Mat, ListRect As List(Of Rectangle),
                                                    toSmall As Integer) As Drawing.Bitmap
            For Each rr In ListRect
                If rr.Width = 0 OrElse rr.Height = 0 Then Continue For  'skip if area is 0

                Dim rect As New Rect(rr.X, rr.Y, rr.Width, rr.Height)
                Dim obj As New Mat(dst, rect)                       ' set roi

                Dim mapX As New Mat(obj.Size(), MatType.CV_32FC1)   ' map x cord. mat
                Dim mapY As New Mat(obj.Size(), MatType.CV_32FC1)   ' map y cord. mat

                Dim cx As Single = obj.Cols / 2.0F                  ' center cord.
                Dim cy As Single = obj.Rows / 2.0F

                Dim indexerX = mapX.GetGenericIndexer(Of Single)()
                Dim indexerY = mapY.GetGenericIndexer(Of Single)()

                For y As Integer = 0 To obj.Rows - 1        ' calc src cord.
                    For x As Integer = 0 To obj.Cols - 1
                        Dim dx = x - cx                     ' x cord. form center
                        Dim dy = y - cy                     ' y cord. form center
                        Dim r = Math.Sqrt(Math.Pow(dx, 2) + Math.Pow(dy, 2))  ' distabce

                        ' ガウス関数、 u: 0, a: 1, sigma: obj.Cols / 8
                        Dim gauss = gaussf(r, 1.0F, 0.0F, obj.Cols / 8.0F)

                        If toSmall = 0 Then                 ' 変換座標の計算
                            indexerX(y, x) = cx + (dx / (gauss + 1.0F))
                            indexerY(y, x) = cy + (dy / (gauss + 1.0F))
                        Else
                            indexerX(y, x) = cx + (dx * (gauss + 1.0F))
                            indexerY(y, x) = cy + (dy * (gauss + 1.0F))
                        End If
                    Next
                Next
                Cv2.Remap(obj, obj, mapX, mapY,
                                InterpolationFlags.Cubic, BorderTypes.Replicate)
            Next
            Return Extensions.BitmapConverter.ToBitmap(dst)
        End Function

        '---------------------------------------------------------
        ' gaussf
        Private Function gaussf(x As Single, a As Single,
                                mu As Single, sigma As Single) As Single
            Return a * CSng(Math.Exp(-Math.Pow(x - mu, 2) / (2 * Math.Pow(sigma, 2))))
        End Function

        '----------------------------------------------------------------
        ' Size change by Cos Table
        '
        '  src:      source Mat
        '  dst:      destination Mat
        '  ListRect: areas
        ' scale:     scale
        '
        Protected Function DoChgObjs(src As Mat, dst As Mat,
                    ListRect As List(Of Rectangle), scale As Single) As Drawing.Bitmap
            Dim srcobjs As New List(Of Mat)(), dstobjs As New List(Of Mat)()

            For Each r In ListRect
                If r.Width = 0 OrElse r.Height = 0 Then Continue For 'skip if area is 0

                If scale > 1.0F Then   ' to Big

                    '入力切り出し
                    Dim srcrect As New Rect(r.X, r.Y, r.Width, r.Height)
                    Dim srcroi As New Mat(src, srcrect)
                    srcobjs.Add(srcroi)

                    '出力切り出し、少し大きくする
                    Dim deltaW As Integer = CInt(r.Width * (scale - 1.0F)) / 2
                    Dim deltaH As Integer = CInt(r.Height * (scale - 1.0F)) / 2

                    Dim dstrect As New Rect(r.X - deltaW, r.Y - deltaH,
                                           r.Width + (deltaW * 2), r.Height + deltaH * 2)
                    Dim cliprect As Rect = ClipIt(dst.Size(), dstrect)
                    Dim dstroi As New Mat(dst, cliprect)
                    dstobjs.Add(dstroi)   ' to Small
                Else
                    '入力切り出し、少し大きくする
                    Dim deltaW As Integer = CInt(r.Width * (1.0F - scale)) / 2
                    Dim deltaH As Integer = CInt(r.Height * (1.0F - scale)) / 2

                    Dim srcrect As New Rect(r.X - deltaW, r.Y - deltaH,
                                           r.Width + (deltaW * 2), r.Height + (deltaH * 2))
                    Dim cliprect As Rect = ClipIt(dst.Size(), srcrect)
                    Dim srcroi As New Mat(src, cliprect)
                    srcobjs.Add(srcroi)

                    '出力切り出し
                    Dim dstrect As New Rect(r.X, r.Y, r.Width, r.Height)
                    Dim dstroi As New Mat(dst, dstrect)
                    dstobjs.Add(dstroi)
                End If
            Next

            ' 大きさを合わせる
            For i = 0 To srcobjs.Count - 1
                Cv2.Resize(srcobjs(i), srcobjs(i),
                                    New Size(dstobjs(i).Cols, dstobjs(i).Rows))
            Next

            ' マージ、重みづけ加算
            For i = 0 To srcobjs.Count - 1
                Dim weightMat As Mat = CreateCosMat(srcobjs(i).Rows, srcobjs(i).Cols)
                Dim iWeightMat As Mat = Scalar.All(255) - weightMat
                Dim srcWeight As Mat = MulMat(srcobjs(i), weightMat)
                Dim dstWeight As Mat = MulMat(dstobjs(i), iWeightMat)
                Cv2.Add(dstWeight, srcWeight, dstobjs(i))
            Next
            Return Extensions.BitmapConverter.ToBitmap(dst)
        End Function


        '---------------------------------------------------------
        ' clip Rect
        Private Function ClipIt(size As Size, rect As Rect) As Rect
            Dim clip As Rect = rect
            clip.Width = If(rect.X < 0, rect.Width + rect.X, clip.Width)
            clip.X = If(rect.X < 0, 0, clip.X)
            clip.Height = If(rect.Y < 0, rect.Height + rect.Y, clip.Height)
            clip.Y = If(rect.Y < 0, 0, clip.Y)

            clip.Width = If(clip.X + rect.Width >=
                                    size.Width, size.Width - clip.X, clip.Width)

            clip.Height = If(clip.Y + rect.Height >=
                                    size.Height, size.Height - clip.Y, clip.Height)

            Return clip
        End Function

    End Class
End Namespace