Python独習!

習得したPython知識をペイフォワード

Pythonとopencv4で特徴量パターンマッチングしてから対象部分を抽出する

前回のプログラム、AKAZE検出器を使った特徴量パターンマッチングの改良版。
greenhornprofessional.hatenablog.com
比べる2つの画像の角度、大きさが違っても大丈夫。ちゃんと該当する部分を抽出する、というもの。ただし、画像の画素数が少ないと検出率が悪くなるので注意。
処理の流れは、

  1. 2つの画像から特徴量(正しくはその座標値)を抽出し、画像の回転ズレを揃える。
  2. 回転補正した画像をつかって、再び、特徴量(正しくはその座標値)を抽出し、縦横の縮尺を計算する。
  3. バウンディングボックスを描画する。

次回はこの発展で、抽出した画像との差分検出を実装する、予定。

結果

縦横比が違うのでわかりづらいけど、ちゃんと同じ領域をバウンディングボックスで囲えている。
1番目:オリジナルのサンプル画像
2番目:基準画像との回転ズレを補正したサンプル画像
3番目:回転補正により生じたデータなし部分をカットしたサンプル画像
4番目:基準画像に3番目の画像とマッチする部分をバウンディングボックスで囲ったもの
f:id:greenhornprofessional:20200419010139p:plain
f:id:greenhornprofessional:20200419010158p:plain

プログラム

# 25_PatternMatch_001.py
# python 3.8.1
# opencv-contrib-python 4.2.0.32
# opencv-python         4.1.2.30
# coding: utf-8
#
import cv2 
import numpy as np
import math
import sys

#==============#
# Make img src #
#==============#
# Here, "Train" meanig standard or criterion, "Query" meaning sample to be tested.
## Read images.
img_query = cv2.imread("25_query.jpg")
img_train = cv2.imread("25_train.jpg")

cv2.imshow("Query_original", img_query)

## Convert color image to gray image.
grayImg_query = cv2.cvtColor(img_query, cv2.COLOR_BGR2GRAY)
grayImg_train = cv2.cvtColor(img_train, cv2.COLOR_BGR2GRAY)

cv2.imshow("Query_gray scale", grayImg_query)

#======================#
# Make keypoints array #
#======================#
def makeKeypointsArry(query, train):
    ## Make instance of Detector.
    detector = cv2.AKAZE_create()
#    detector = cv2.ORB_create()

    ## Find keypoints and descriptors.
    kp_query, des_query = detector.detectAndCompute(query, None) 
    kp_train, des_train = detector.detectAndCompute(train, None)

    ## Make instance of Matcher.
    matcher = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=False)
#    matcher = cv2.BFMatcher(cv2.NORM_L2, crossCheck=False) 
#    matcher = cv2.BFMatcher(cv2.NORM_L1, crossCheck=False) 

    ## Match descriptors.
    matches = matcher.knnMatch(des_query, des_train, k=2)

    ## Appy ratio test.
    ratio = 0.4
    good = [] 
    for m, n in matches: 
        if m.distance < ratio * n.distance: 
            good.append(m)

    ## Conncet keypoints with lines.
#    img_result = cv2.drawMatches(img_query, kp_query, img_train, kp_train, good, None, flags=2) 
    ## Show connection.
#    cv2.namedWindow("Connection", cv2.WINDOW_NORMAL)
#    cv2.imshow('Connection', img_result) 
#    cv2.waitKey(0) 
#    cv2.destroyAllWindows()

    ## Extract XY coordinate from DMatch.
    n = len(good)
    if n >= 3:
        kp_query_x = []
        kp_query_y = []
        kp_train_x = []
        kp_train_y = []
        for i in range(n):
            gq = good[i].queryIdx
            gt = good[i].trainIdx
            kp_query_x.append(kp_query[gq].pt[0])
            kp_query_y.append(kp_query[gq].pt[1])
            kp_train_x.append(kp_train[gt].pt[0])
            kp_train_y.append(kp_train[gt].pt[1])

        return kp_query_x, kp_query_y, kp_train_x, kp_train_y

    else:
        print("An error occured. Program will finish.")
        sys.exit()

#=================================#
# Correct rotation of query-image #
#=================================#
def rotationCorrection(query, train):
    ## Make keypoints array.
    query_x, query_y, train_x, train_y = makeKeypointsArry(query, train)

    ## Calculate angle difference between query and train.
    query_deg = math.atan2(max(query_y) - min(query_y), max(query_x) - min(query_x)) * 180 / math.pi
    train_deg = math.atan2(max(train_y) - min(train_y), max(train_x) - min(train_x)) * 180 / math.pi
    angle = query_deg - train_deg

    ## Do affine transformation.
    center = (min(query_x), min(query_y))
    scale = 1.0
    height, width = query.shape
    size = (width, height)

    rotation_matrix = cv2.getRotationMatrix2D(center, angle, scale)
    img_rot = cv2.warpAffine(query, rotation_matrix, size, flags=cv2.INTER_CUBIC)

    cv2.imshow("Query_rotation corrected", img_rot)

    ## Shave off 4 edges of img_rot. 
    cut_rate = 0.05
    cut_y = int(height * cut_rate)
    cut_x = int(width  * cut_rate)
    img_crop = img_rot[cut_y : height - cut_y, cut_x : width - cut_x]

    cv2.imshow("Query_cropped", img_crop)

    return img_crop

#=================================#
# Draw boundingbox on train-image #
#=================================#
def extractImage(query, train):
    ## Make keypoints array.
    query_x, query_y, train_x, train_y = makeKeypointsArry(query, train)

    ## Calcularate size difference between query and train.
    x_mag = (max(query_x) - min(query_x)) / (max(train_x) - min(train_x))
    y_mag = (max(query_y) - min(query_y)) / (max(train_y) - min(train_y))

    ## Determine boundingbox position.
    height, width = query.shape
    x1 = int(min(train_x) - min(query_x) / x_mag)
    x2 = int(x1 + width / x_mag)
    y1 = int(min(train_y) - min(query_y) / y_mag)
    y2 = int(y1 + height / y_mag)

    ## Draw boundingbox.
    cv2.rectangle(img_train, (x1, y1), (x2, y2), (0, 255, 0), 2)
    cv2.namedWindow("Result", cv2.WINDOW_NORMAL)
    cv2.imshow("Result", img_train)

#======#
# Main #
#======#
grayImg_query_crop = rotationCorrection(grayImg_query, grayImg_train)
extractImage(grayImg_query_crop, grayImg_train)

参考サイト

こちらを主に参考にした。
細かい計算方法は違うが、アプローチは一緒。
OpenCVを使ったパターンマッチングで画像中の物体抽出 with Python - Qiita

今回、一番助かったサイト。
この記事と出会わなければ積んでいた。
OpenCVで特徴量の座標を取得する - Qiita

"Train" "Query" はどっちがどっちかわからない。
StackOverflowで解説を見つけた。とりあえず、以下のように思っておけばよさそう。
 Train → 基準、データベース
 Query → サンプル、検査(照会)にかけられる
c++ - What is `query` and `train` in openCV features2D - Stack Overflow

Detector(検出器)とDiscriptor(記述子)の違い
image processing - difference between feature detector and descriptor? - Signal Processing Stack Exchange

/* -----codeの行番号----- */