OpenCVを使ったテンプレートマッチングをPythonで試す時、まず参考にするのが
Template Matching in OpenCVまたはTemplate Matching with Multiple Objectsというサンプルコードだと思います。これらのサンプルコードのおおまかな内容は理解が簡単なのですが、テンプレートマッチングの関数cv.matchTemplate()の戻り値をどのように扱えばよいのかはっきりしなかったので、詳しく確かめてみました。
解析したサンプルコード
Template Matching with Multiple Objectsというサンプルコードについて、cv.matchTemplate()の戻り値の構造を確かめました。
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img_rgb = cv.imread('target.png')
img_gray = cv.cvtColor(img_rgb, cv.COLOR_BGR2GRAY)
template = cv.imread('template.png',0)
w, h = template.shape[::-1]
res = cv.matchTemplate(img_gray,template,cv.TM_CCOEFF_NORMED)
threshold = 0.8
loc = np.where( res >= threshold)
for pt in zip(*loc[::-1]):
cv.rectangle(img_rgb, pt, (pt[0] + w, pt[1] + h), (0,0,255), 2)
cv.imwrite('res.png',img_rgb)
オリジナルのサンプルコードでは、テンプレートマッチング対象画像としてmario.pngが使われていますが、target.pngに差し替えています。また、オリジナルのサンプルコードでは、テンプレート画像としてmario_coin.pngが使われていますが、template.pngに差し替えています。
|
使用した画像 |
サンプルの解析
なぜcv.TM_CCOEFF_NORMEDなのか
このサンプルは、ある画像中の複数のマッチング部分を検出します。_NORMEDのモードを指定すると、マッチング結果が-1.0から1.0の間に正規化されるようです。_NORMED以外のモードを指定すると、マッチング結果から最大値、最小値を求めることはできますが、「ある値以上はマッチしたと考える」といった処理を行うときに、その境界値を決めることができません。このため、画像中のマッチング部分を特定することができません。マッチング結果が正規化されていると、「0.8以上はマッチングしたとみなす」といった境界値を設定することができるようになります。
threshold = 0.8の意味
マッチングの境界値を決めています。cv.TM_CCOEFF_NORMEDは、マッチング率が高いほど値が大きくなります。この値は、状況に応じて調整が必要になります。正規化されているので、最大値は1.0です。
cv.matchTemplate(img_gray,template,cv.TM_CCOEFF_NORMED)の戻り値の構造
cv.matchTemplate(img_gray,template,cv.TM_CCOEFF_NORMED)の戻り値resは、2次元の配列になります。配列の大きさは、マッチング対象画像の大きさが(W,H)、テンプレート画像の大きさが(w,h)の時、(W-w+1)x(H-h+1)です。この例で使用した画像の場合、(W,H)=(640,480)、(w,h)=(89,305)なので、戻り値resの大きさは、(640-89+1,480-305+1)=(552,176)です。print(res)を実行すると、次の様に表示されます。具体的には、len(res)=176であり、len(res[0])=552です。1次元側がw(x方向)、2次元側がh(y方向)に対応します。
res[[ 0.6472297 0.64573884 0.6484621 ... 0.3446521 0.34587955
0.34714523]
[ 0.64773846 0.64624274 0.6487364 ... 0.3454256 0.3465633
0.34753427]
[ 0.64942294 0.6476835 0.64918137 ... 0.3461068 0.3472825
0.34812936]
...
[ 0.3363744 0.3467935 0.35590363 ... -0.04257199 -0.05481663
-0.06681015]
[ 0.32571808 0.33642587 0.34692764 ... -0.05835321 -0.06999504
-0.08208057]
[ 0.31470156 0.32549503 0.33650377 ... -0.07385409 -0.08541072
-0.09782302]]
この結果を見るとわかるように、値は-1.0から1.0の間に正規化されています。
loc = np.where( res >= threshold)の意味
この部分は、resの中で0.8以上の要素を探し、その配列のインデックスがlocとして返されます。locの構造は、2次元配列になります。locがなぜ2次元配列になるかというと、resが2次元配列だからです。locの2次元配列は、resを行列としてみた場合の列と行(またはx,y座標)になります。print(loc)を実行すると、次の様に表示されます。
loc(array([122, 123, 127]), array([94, 94, 95]))
上の結果の場合、loc[0][0]とloc[1][0]がペアで行列の0.8以上の要素の行列インデックス(x,y座標)を表します(2次元配列の[0]がy座標、[1]がx座標)。同様に、loc[0][1]とloc[1][1]とloc[0][2]とloc[1][2]のペアが行列の0.8以上の要素の行列インデックス(y,x座標)を表します。
for pt in zip(*loc[::-1]):
この部分は、locの結果から、検出部分のボックスを描画するループです。zip(*loc[::-1])は、まずloc[::-1]でloc[0]=y座標、loc[1]=x座標であったのを、loc[0]=x座標、loc[1]=y座標と逆にしています。次にzip(*loc)でlocをpt(y,x)配列としてループさせています。
cv.rectangle(img_rgb, pt, (pt[0] + w, pt[1] + h), (0,0,255), 2)
ここで、ptには長方形の1点の(x,y)、 (pt[0] + w, pt[1] + h)には反対側の頂点の(x,y)が入ります。pt[0]がx座標、pt[1]がy座標です。ptには、0.8以上でマッチした領域の左上の座標が入っているので、その値のx(pt[0])にテンプレート画像のwを足し、y(pt[1])にテンプレート画像のhを足すと、テンプレートマッチング対象画像上にマッチした領域を重ねて描画できます。