[Python] OpenCV matchTemplate

程式語言:Python
Package:opencv-python
官方網址
OpenCV-Python Tutorials

功能:找出圖片中類似的東西

演算法

  1. 輸入兩張影像,分別為 image、template
  2. 不斷滑動 template,得到 image 上各個位置的比較值,比較值代表相似程度
    然後將 image 左上角位置,作為 result 比較值的存放位置
  3. 完成後可得到 result
    可用 minMaxLoc() 函式,找出結果圖的最大或最小值,定位出搜尋位置

限制

  • 物體有旋轉時,會找不到
  • 物體大小改變時,會找不到

result = cv2.matchTemplate(image, templ, method[, result])

  • 參數
    • image
      • 被尋找的圖片
      • 必須為 8-bit or 32-bit
    • templ
      • 尋找的物品圖片
      • size 不能大於 image,且格式需一致
    • method
      • 比對的方法
    • result
      • 比較的結果,格式為 numpy.ndarray (dtype=float32)
      • 可傳入想儲存結果的 array
      • 因 image 大小為 \(W \times H\) 且 templ 為 \(w \times h\) ,所以大小為 \((W-w+1) \times (H-h+1)\)

比對方法

\(I\) 表示 image,\(T\) 表示 template,\(R\) 表示 result
  • CV_TM_SQDIFF
    • 平方差,越小越相似
    • \(R(x,y)= \sum _{x',y'} (T(x',y')-I(x+x',y+y'))^2\)
  • CV_TM_SQDIFF_NORMED
    • 正規化平方差,越小越相似
    • 保證當 pixel 亮度都乘上同一係數時,相似度不變
    • \(R(x,y)= \frac{\sum_{x',y'} (T(x',y')-I(x+x',y+y'))^2}{\sqrt{\sum_{x',y'}T(x',y')^2 \cdot \sum_{x',y'} I(x+x',y+y')^2}}\)
  • CV_TM_CCORR
    • 相關係數,越大越相似
    • \(R(x,y)= \sum _{x',y'} (T(x',y') \cdot I(x+x',y+y'))\)
  • CV_TM_CCORR_NORMED
    • 正規化相關係數,越大越相似
    • 保證當 pixel 亮度都乘上同一係數時,相似度不變
    • \(R(x,y)= \frac{\sum_{x',y'} (T(x',y') \cdot I(x+x',y+y'))}{\sqrt{\sum_{x',y'}T(x',y')^2 \cdot \sum_{x',y'} I(x+x',y+y')^2}}\)
  • CV_TM_CCOEFF
    • 去掉直流成份的相關係數,越大越相似
    • \(R(x,y)= \sum _{x',y'} (T'(x',y') \cdot I'(x+x',y+y'))\)
      where
      \(\begin{array}{l} T'(x',y')=T(x',y') - 1/(w \cdot h) \cdot \sum _{x'',y''} T(x'',y'') \\ I'(x+x',y+y')=I(x+x',y+y') - 1/(w \cdot h) \cdot \sum _{x'',y''} I(x+x'',y+y'') \end{array}\)
  • CV_TM_CCOEFF_NORMED
    • 正規化 去掉直流成份的相關係數
    • 保證當 pixel 亮度都乘上同一係數時,相似度不變
    • 計算出的相關係數被限制在了 -1 到 1 之間
      • 1 表示完全相同
      • -1 表示亮度正好相反
      • 0 表示没有線性相關
    • \(R(x,y)= \frac{ \sum_{x',y'} (T'(x',y') \cdot I'(x+x',y+y')) }{ \sqrt{\sum_{x',y'}T'(x',y')^2 \cdot \sum_{x',y'} I'(x+x',y+y')^2} }\)
      where
      \(\begin{array}{l} T'(x',y')=T(x',y') - 1/(w \cdot h) \cdot \sum _{x'',y''} T(x'',y'') \\ I'(x+x',y+y')=I(x+x',y+y') - 1/(w \cdot h) \cdot \sum _{x'',y''} I(x+x'',y+y'') \end{array}\)

範例

尋找特定物品
程式碼
import cv2
import numpy as np
from matplotlib import pyplot as plt

img = cv2.imread('image.png')
img2 = img.copy()
template = cv2.imread('template.jpg')
w = template.shape[1]
h = template.shape[0]

# All the 6 methods for comparison in a list
methods = ['cv2.TM_CCOEFF', 'cv2.TM_CCOEFF_NORMED', 'cv2.TM_CCORR',
            'cv2.TM_CCORR_NORMED', 'cv2.TM_SQDIFF', 'cv2.TM_SQDIFF_NORMED']

for meth in methods:
    img = img2.copy()
    method = eval(meth)

    # Apply template Matching
    res = cv2.matchTemplate(img,template,method)
    min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)

    # If the method is TM_SQDIFF or TM_SQDIFF_NORMED, take minimum
    if method in [cv2.TM_SQDIFF, cv2.TM_SQDIFF_NORMED]:
        top_left = min_loc
    else:
        top_left = max_loc
    bottom_right = (top_left[0] + w, top_left[1] + h)

    cv2.rectangle(img,top_left, bottom_right, 255, 2)

    plt.subplot(121),plt.imshow(res,cmap = 'gray')
    plt.title('Matching Result'), plt.xticks([]), plt.yticks([])
    plt.subplot(122),plt.imshow(img)
    plt.title('Detected Point'), plt.xticks([]), plt.yticks([])
    plt.suptitle(meth)

    plt.show()
結果
標示所有相同物品
程式碼
import cv2
import numpy as np
from matplotlib import pyplot as plt

def getPoints(img, template, threshold, method=cv2.TM_CCOEFF_NORMED):
    result = cv2.matchTemplate(img, template, method)

    if method in [cv2.TM_SQDIFF, cv2.TM_SQDIFF_NORMED]:
        loc = np.where(result <= threshold) #回傳為 y, x
    else:
        loc = np.where(result >= threshold) #回傳為 y, x
    
    pts = zip(*loc[::-1])
    
    return removeSame(pts, min(template.shape[0], template.shape[1]))
    

# 去掉太過接近的座標
def removeSame(pts, threshold):
    elements = []
    for x,y in pts:
        for ele in elements:
            if ((x-ele[0])**2 + (y-ele[1])**2) < threshold**2:
                break
        else:
            elements.append((x,y))
    
    return elements
    
    
def drawRectangle(img, pts, w, h, color=(0,0,255), lineW=2):
    for pt in pts:
        cv2.rectangle(img, pt, (pt[0] + w, pt[1] + h), color, lineW) #因後面會反向顏色順序,所以這邊顯示紅色要反向
    

if __name__ == '__main__':
    img = cv2.imread('image.png')
    img_result = img.copy()
    template = cv2.imread('template.png')
    w = template.shape[1]
    h = template.shape[0]
    
    threshold = 0.4
    
    elements = getPoints(img, template, threshold)  
    total_n = len(elements)
    drawRectangle(img_result, elements, w=template.shape[1], h=template.shape[0])  

    # 逆時針旋轉 90
    template_r90 = cv2.transpose(template)
    cv2.flip(template_r90, 0)
    pts1 = getPoints(img, template_r90, threshold)
    # 順時針旋轉 90
    template_r90 = cv2.transpose(template)
    cv2.flip(template_r90, 1)
    pts2 = getPoints(img, template_r90, threshold)
    
    elements = removeSame(pts1+pts2, w)  
    total_90 = len(elements)
    drawRectangle(img_result, elements, w=template_r90.shape[1], h=template_r90.shape[0], color=(0,255,0)) 
        
    plt.subplot(121)
    plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) #因 openCV 記錄的顏色順序是反過來的
    plt.title('Original'), plt.xticks([]), plt.yticks([])
    plt.subplot(122)
    plt.imshow(cv2.cvtColor(img_result, cv2.COLOR_BGR2RGB)) #因 openCV 記錄的顏色順序是反過來的
    plt.title('Detected Point'), plt.xticks([]), plt.yticks([])
    plt.suptitle('Total:{}\nnomral:{}, 90:{}'.format(total_n+total_90, total_n, total_90))

    plt.show()
    # cv2.imwrite('result.png', img_result)

參考

Template Matching
Template Matching

留言