【VBA×画像処理】ビットマップ画像でテンプレートマッチングを行う

本ページでは、VBAで読み込んだビットマップ(BMP)画像を内部的に処理し、別途用意したテンプレート画像と最も類似した領域を画像内から探索する「テンプレートマッチング」の実装方法を解説していきます。テンプレートマッチングは画像内の特定パターンの検出や位置合わせ、製造業の検査画像処理など幅広い分野で利用される基本的な画像処理手法です。本ページでは代表的な3つのマッチング手法(SSD・SAD・ZNCC)を実装します。

icon-warning 注意事項

本ページではビットマップ(BMP)画像の読み込みと書き出しページで実装したクラスに画像処理を追加していきます。前提の機能となる画像の入出力機能を実装しているため事前に一読ください。また内部処理としてグレースケール化を、結果の可視化に長方形描画を利用するため、これらも実装済みであることを前提としています。

テンプレートマッチング

テンプレートマッチングとは、別途用意したテンプレート画像と最も類似した領域を入力画像内から探索する処理のことを指します。具体的には入力画像上をテンプレート画像と同サイズの「窓」でずらしながら走査し、各位置でテンプレート画像との類似度(あるいは差分)を計算して、最も一致する位置を特定します。式で表すと下記の通りです。(※ここで \( I \) は入力画像、\( T \) はテンプレート画像、\( N, M \) はテンプレート画像の幅と高さ、\( S(x, y) \) は位置 \( (x, y) \) でのスコア)

\[
S(x, y) = f\bigl( I(x+i, y+j),\, T(i, j) \bigr) \quad (i = 0, 1, \dots, N-1,\, j = 0, 1, \dots, M-1)
\]

類似度の計算方法 \( f \) によってマッチング手法が異なります。本ページでは画像処理で広く利用される代表的な3つの手法 SSDSADZNCC を実装します。なお、計算コストを抑えるためいずれの手法でも入力画像とテンプレート画像はあらかじめグレースケール化した上でマッチングを行います。

SSD (Sum of Squared Difference)

SSDは入力画像の比較領域とテンプレート画像の各ピクセルの輝度値の差を2乗して足し合わせた値です。値が小さいほど両画像が類似しており、最小値となる位置がマッチング箇所となります。

計算が単純で実装が容易な反面、画像全体の明るさが変化するとスコアも大きく変動するため、明るさ変化に弱いのが欠点です。

\[
\text{SSD}(x, y) = \sum_{j=0}^{M-1} \sum_{i=0}^{N-1} \bigl( I(x+i, y+j) – T(i, j) \bigr)^2
\]

SAD (Sum of Absolute Difference)

SADは入力画像の比較領域とテンプレート画像の各ピクセルの輝度値の差の絶対値を足し合わせた値です。SSDと同様に値が小さいほど類似しており、最小値となる位置がマッチング箇所となります。

SSDよりさらに計算が単純で2乗の計算がない分わずかに高速ですが、SSDと同じく明るさ変化には弱い特性があります。

\[
\text{SAD}(x, y) = \sum_{j=0}^{M-1} \sum_{i=0}^{N-1} \bigl| I(x+i, y+j) – T(i, j) \bigr|
\]

ZNCC (Zero-mean Normalized Cross-Correlation)

ZNCCは入力画像の比較領域とテンプレート画像のそれぞれから平均値を引いた上で正規化した相互相関値です。SSDやSADとは異なり、値は -1 〜 1 の範囲をとり 最大値となる位置がマッチング箇所となります。

計算式が複雑な分計算量は多くなりますが、各画像の平均値を引いてから計算するため明るさ変化に強いという大きな利点があります。実用的には最も使われることが多い手法です。

\[
\text{ZNCC}(x, y) = \frac{\displaystyle \sum_{j=0}^{M-1} \sum_{i=0}^{N-1} \bigl( I(x+i, y+j) – \bar{I} \bigr)\bigl( T(i, j) – \bar{T} \bigr)}{\sqrt{\displaystyle \sum_{j=0}^{M-1} \sum_{i=0}^{N-1} \bigl( I(x+i, y+j) – \bar{I} \bigr)^2 \cdot \sum_{j=0}^{M-1} \sum_{i=0}^{N-1} \bigl( T(i, j) – \bar{T} \bigr)^2 }}
\]

(※ここで \( \bar{I}, \bar{T} \) はそれぞれ比較領域・テンプレート画像の輝度平均値)

3手法の比較

3手法の特性をまとめると下表の通りです。基本的には精度を優先する場合はZNCC、速度を優先する場合はSADやSSDを利用します。

手法 判定基準 値域 計算量 明るさ変化耐性
SSD 最小値 0以上 ×
SAD 最小値 0以上 ×
ZNCC 最大値 -1〜1
icon-warning  VBAでの処理時間に関する注意

テンプレートマッチングは「入力画像の全位置 × テンプレート画像の全ピクセル」の二重ループで計算されるため、画像サイズが大きくなると計算量が爆発的に増加します。

 
たとえば 400×300 の入力画像に対して 50×50 のテンプレート画像でマッチングを行うと、比較位置数は約87,500箇所、各位置で2,500ピクセルの比較が行われるため、合計で 2億回以上の演算 が発生します。VBAはインタプリタ言語のためC++やPythonと比べて実行速度が遅く、上記程度の処理でも数分〜十数分の処理時間がかかる可能性があります。
 
処理時間を短縮するには下記の対策が有効です。
 ・ 探索ストライドを大きくする:探索点を間引いて処理時間を短縮できるが精度は下がる
 ・ 画像のサイズを小さくする:事前に画像を縮小することで計算量を削減できる
 ・ SAD/SSDを使う:ZNCCより計算量が少なく明るさ変化が問題ない用途で有効

サンプルコード

以下はSSD・SAD・ZNCCの3手法によるテンプレートマッチングを行うためのコードです。事前に用意しておいたclsBitmap内に下記コードをコピペすることで利用可能になります。各手法のPublic関数は内部の共通処理関数 TemplateMatching をアルゴリズム名付きで呼び出すラッパー方式となっており、共通処理関数内では指定アルゴリズムに応じてスコア計算とベストスコア判定を行います。

クラスモジュール (clsBitmapに追記)

'============================================================================
'=  テンプレートマッチング (SSD / SAD / ZNCC)
'=
'=  【依存関数】下記の処理を事前に実装しておく必要があります
'=    ・ Grayscale                : グレースケール変換
'=        https://liclog.net/vba-bitmap-image-processing-grayscale
'=    ・ Rectangle (任意)         : 検出箇所の枠描画 (結果可視化に利用)
'=        https://liclog.net/vba-bitmap-image-processing-draw-rectangle
'============================================================================

'****************************************************************************
'*  SSD(Sum of Squared Difference)によるテンプレートマッチング
'*
'*      sPathTmp    :テンプレート画像ファイルパス(.bmp)
'*      (lStride)   :探索ストライド[px] (省略時は1)
'*      (lMethod)   :グレースケール変換手法
'*                    0 = 平均法 / 1 = 加重平均法 / 2 = 輝度法
'*      戻り値      :判定結果エリア座標(左上X,Y座標,右下X,Y座標)
'****************************************************************************
Public Function SSD(ByVal sPathTmp As String, _
                    Optional lStride As Long = 1, _
                    Optional lMethod As Long = 2) As Long()
                    
    SSD = TemplateMatching("SSD", sPathTmp, lStride, lMethod)

End Function

'****************************************************************************
'*  SAD(Sum of Absolute Difference)によるテンプレートマッチング
'*
'*      sPathTmp    :テンプレート画像ファイルパス(.bmp)
'*      (lStride)   :探索ストライド[px] (省略時は1)
'*      (lMethod)   :グレースケール変換手法
'*                    0 = 平均法 / 1 = 加重平均法 / 2 = 輝度法
'*      戻り値      :判定結果エリア座標(左上X,Y座標,右下X,Y座標)
'****************************************************************************
Public Function SAD(ByVal sPathTmp As String, _
                    Optional lStride As Long = 1, _
                    Optional lMethod As Long = 2) As Long()
                    
    SAD = TemplateMatching("SAD", sPathTmp, lStride, lMethod)

End Function

'****************************************************************************
'*  ZNCC(Zero-mean Normalized Cross-Correlation)によるテンプレートマッチング
'*
'*      sPathTmp    :テンプレート画像ファイルパス(.bmp)
'*      (lStride)   :探索ストライド[px] (省略時は1)
'*      (lMethod)   :グレースケール変換手法
'*                    0 = 平均法 / 1 = 加重平均法 / 2 = 輝度法
'*      戻り値      :判定結果エリア座標(左上X,Y座標,右下X,Y座標)
'****************************************************************************
Public Function ZNCC(ByVal sPathTmp As String, _
                     Optional lStride As Long = 1, _
                     Optional lMethod As Long = 2) As Long()
                    
    ZNCC = TemplateMatching("ZNCC", sPathTmp, lStride, lMethod)

End Function

'----------------------------------------------------------------------------
'-  テンプレートマッチング共通処理
'----------------------------------------------------------------------------
Private Function TemplateMatching(ByVal sAlgorithm As String, _
                                  ByVal sPathTmp As String, _
                                  Optional lStride As Long = 1, _
                                  Optional lMethod As Long = 2) As Long()
 
    Dim rgbDataOrg()      As RGBTRIPLE '元画像RGBデータ
    Dim rgbDataTmp()      As RGBTRIPLE 'テンプレート画像RGBデータ
    Dim rgbDataOrgGray()  As RGBTRIPLE '元画像RGBデータ(グレースケール)
    Dim rgbDataTmpGray()  As RGBTRIPLE 'テンプレート画像RGBデータ(グレースケール)
    Dim rgbDataCompArea() As RGBTRIPLE '比較エリア
    
    Dim lCoord(3)       As Long
    Dim lHeight         As Long
    Dim lWidth          As Long
    Dim lHeightTmp      As Long
    Dim lWidthTmp       As Long
    Dim lX              As Long
    Dim lY              As Long
    
    Dim dScore          As Double
    Dim dScoreBest      As Double
    Dim bIsLargerBetter As Boolean
    
    Dim lMinX As Long
    Dim lMinY As Long
    Dim lMaxX As Long
    Dim lMaxY As Long
    
    'アルゴリズム別の初期値設定 (SSD/SADは最小値探索、ZNCCは最大値探索)
    Select Case sAlgorithm
        Case "SSD", "SAD"
            bIsLargerBetter = False
            dScoreBest = 1E+30  '最小値を探すので大きな値で初期化
        Case "ZNCC"
            bIsLargerBetter = True
            dScoreBest = -1E+30 '最大値を探すので小さな値で初期化
    End Select

    '元画像 & テンプレート画像を読込
    rgbDataOrg = m_rgbImageData
    rgbDataTmp = ImportDataFromBitmapFile(sPathTmp)
    
    '元画像 & テンプレート画像をグレースケール変換
    rgbDataOrgGray = Grayscale(rgbDataOrg, lMethod)
    rgbDataTmpGray = Grayscale(rgbDataTmp, lMethod)
    
    lHeight = UBound(rgbDataOrg, 1) + 1
    lWidth = UBound(rgbDataOrg, 2) + 1
    lHeightTmp = UBound(rgbDataTmp, 1) + 1
    lWidthTmp = UBound(rgbDataTmp, 2) + 1
    
    '元画像をテンプレートサイズの窓で走査
    For lY = 0 To lHeight - lHeightTmp Step lStride
        For lX = 0 To lWidth - lWidthTmp Step lStride
          
            '比較領域を抽出
            rgbDataCompArea = ExtractCompareArea(rgbDataOrgGray, lX, lY, lWidthTmp, lHeightTmp)
          
            'アルゴリズム別にスコアを計算
            Select Case sAlgorithm
                Case "SSD"
                    dScore = SumOfSquaredDifference(rgbDataCompArea, rgbDataTmpGray)
                Case "SAD"
                    dScore = SumOfAbsoluteDifference(rgbDataCompArea, rgbDataTmpGray)
                Case "ZNCC"
                    dScore = ZeroMeanNCC(rgbDataCompArea, rgbDataTmpGray)
            End Select

            '最良スコアを更新
            If (bIsLargerBetter And dScore > dScoreBest) Or _
               (Not bIsLargerBetter And dScore < dScoreBest) Then
                dScoreBest = dScore
                lMinX = lX
                lMinY = lY
                lMaxX = lX + lWidthTmp - 1
                lMaxY = lY + lHeightTmp - 1
            End If
            
        Next
    Next
    
    lCoord(0) = lMinX
    lCoord(1) = lMinY
    lCoord(2) = lMaxX
    lCoord(3) = lMaxY
    
    TemplateMatching = lCoord
    
End Function

'----------------------------------------------------------------------------
'-  比較領域を抽出
'----------------------------------------------------------------------------
Private Function ExtractCompareArea(rgbDataOrg() As RGBTRIPLE, _
                                    ByVal lStartX As Long, ByVal lStartY As Long, _
                                    ByVal lAreaWidth As Long, ByVal lAreaHeight As Long) As RGBTRIPLE()
    
    Dim rgbDataArea() As RGBTRIPLE
    Dim lX As Long
    Dim lY As Long

    ReDim rgbDataArea(lAreaHeight - 1, lAreaWidth - 1)

    For lY = 0 To lAreaHeight - 1
        For lX = 0 To lAreaWidth - 1
            rgbDataArea(lY, lX) = rgbDataOrg(lStartY + lY, lStartX + lX)
        Next
    Next

    ExtractCompareArea = rgbDataArea
    
End Function

'----------------------------------------------------------------------------
'-  SSD : 輝度値の差の2乗和
'----------------------------------------------------------------------------
Private Function SumOfSquaredDifference(rgbDataA() As RGBTRIPLE, _
                                        rgbDataB() As RGBTRIPLE) As Double
    
    Dim dSum  As Double
    Dim lDiff As Long
    Dim lX    As Long
    Dim lY    As Long
    Dim lH    As Long
    Dim lW    As Long

    lH = UBound(rgbDataA, 1)
    lW = UBound(rgbDataA, 2)
    
    dSum = 0
    For lY = 0 To lH
        For lX = 0 To lW
            lDiff = CLng(rgbDataA(lY, lX).rgbRed) - CLng(rgbDataB(lY, lX).rgbRed)
            dSum = dSum + lDiff * lDiff
        Next
    Next

    SumOfSquaredDifference = dSum
    
End Function

'----------------------------------------------------------------------------
'-  SAD : 輝度値の差の絶対値の和
'----------------------------------------------------------------------------
Private Function SumOfAbsoluteDifference(rgbDataA() As RGBTRIPLE, _
                                         rgbDataB() As RGBTRIPLE) As Double
    
    Dim dSum  As Double
    Dim lDiff As Long
    Dim lX    As Long
    Dim lY    As Long
    Dim lH    As Long
    Dim lW    As Long

    lH = UBound(rgbDataA, 1)
    lW = UBound(rgbDataA, 2)
    
    dSum = 0
    For lY = 0 To lH
        For lX = 0 To lW
            lDiff = CLng(rgbDataA(lY, lX).rgbRed) - CLng(rgbDataB(lY, lX).rgbRed)
            dSum = dSum + Abs(lDiff)
        Next
    Next

    SumOfAbsoluteDifference = dSum
    
End Function

'----------------------------------------------------------------------------
'-  ZNCC : 平均値を引いた正規化相互相関
'----------------------------------------------------------------------------
Private Function ZeroMeanNCC(rgbDataA() As RGBTRIPLE, _
                             rgbDataB() As RGBTRIPLE) As Double
    
    Dim dMeanA  As Double
    Dim dMeanB  As Double
    Dim dNumer  As Double '分子
    Dim dDenomA As Double '分母項A
    Dim dDenomB As Double '分母項B
    Dim dDevA   As Double
    Dim dDevB   As Double
    Dim lX      As Long
    Dim lY      As Long
    Dim lH      As Long
    Dim lW      As Long
    Dim lN      As Long

    lH = UBound(rgbDataA, 1)
    lW = UBound(rgbDataA, 2)
    lN = (lH + 1) * (lW + 1)
    
    '平均値を計算
    dMeanA = 0
    dMeanB = 0
    For lY = 0 To lH
        For lX = 0 To lW
            dMeanA = dMeanA + rgbDataA(lY, lX).rgbRed
            dMeanB = dMeanB + rgbDataB(lY, lX).rgbRed
        Next
    Next
    dMeanA = dMeanA / lN
    dMeanB = dMeanB / lN
    
    '相関係数を計算
    dNumer = 0
    dDenomA = 0
    dDenomB = 0
    For lY = 0 To lH
        For lX = 0 To lW
            dDevA = rgbDataA(lY, lX).rgbRed - dMeanA
            dDevB = rgbDataB(lY, lX).rgbRed - dMeanB
            dNumer = dNumer + dDevA * dDevB
            dDenomA = dDenomA + dDevA * dDevA
            dDenomB = dDenomB + dDevB * dDevB
        Next
    Next
    
    'ゼロ除算回避
    If dDenomA = 0 Or dDenomB = 0 Then
        ZeroMeanNCC = 0
    Else
        ZeroMeanNCC = dNumer / Sqr(dDenomA * dDenomB)
    End If
    
End Function

 
標準モジュール

Option Explicit
Sub main()

    Dim sPathBmpSrc As String
    Dim sPathBmpTmp As String
    Dim sPathBmpExport As String
    Dim oBitmap As clsBitmap
    Dim lCoord() As Long

    sPathBmpSrc = "C:\...\source.bmp"      '入力画像
    sPathBmpTmp = "C:\...\template.bmp"    'テンプレート画像
    sPathBmpExport = "C:\...\output.bmp"   '出力画像

    Set oBitmap = New clsBitmap
    Call oBitmap.LoadBitmap(sPathBmpSrc)                                          '画像読み込み
    lCoord = oBitmap.SSD(sPathBmpTmp)                                             'SSDによるテンプレートマッチング
    'lCoord = oBitmap.SAD(sPathBmpTmp)                                            'SADによるテンプレートマッチング
    'lCoord = oBitmap.ZNCC(sPathBmpTmp)                                           'ZNCCによるテンプレートマッチング
    Call oBitmap.Rectangle(lCoord(0), lCoord(1), lCoord(2), lCoord(3), _
                           RGB(0, 0, 255), 3)                                     '検出箇所を青枠で可視化
    Call oBitmap.ExportBitmap24(sPathBmpExport)                                   '画像出力

End Sub

使い方は上記コードのように、画像読み込み後にいずれかのマッチング手法を呼び出すだけです。

戻り値として検出された矩形領域の座標 (左上X, 左上Y, 右下X, 右下Y) がLong型配列で返るため、これを長方形の描画処理にそのまま渡すことで、検出箇所を画像上に枠線で可視化することができます。


 

関連情報

VBA×画像処理ページ

次回 >> 画像の平滑化(ガウシアンフィルタ)
前回 >> 画像に長方形を描画
画像処理のメインページへ戻る

参考

外部リンク:Template matching – Wikipedia
     :Cross-correlation – Wikipedia

Excel,VBA,画像処理