勉強しないとな~blog

ちゃんと勉強せねば…な電気設計エンジニアです。

OpenCVやってみる - 33. アフィン変換行列推定、"0"以外の文字比較

前回の続きです。
今回は実際に文字テンプレート - 比較対象輪郭間のアフィン変換行列の推定、比較を行ってみたいと思います。

下準備

今まで通りの画像読み込み、下処理です。

import cv2
import numpy as np
%matplotlib inline
from matplotlib import pyplot as plt
import math

img1 = cv2.imread('harupan_190428_1.jpg')
img2 = cv2.imread('harupan_190428_2.jpg')
img3 = cv2.imread('harupan_200317_1.jpg')
img4 = cv2.imread('harupan_210227_2.jpg')
img5 = cv2.imread('harupan_210402_1.jpg')
img6 = cv2.imread('harupan_210402_2.jpg')
img7 = cv2.imread('harupan_210414_1.jpg')

f:id:nokixa:20211121023052p:plain

アフィン変換パラメータの推定

アフィン変換ではパラメータ推定のために2画像のマッチング点を最低3組与える必要があります。
今まで使ったSIFTなどの特徴量検出を使ってもいいかもしれませんが、今回は輪郭データが得られているので、これに含まれる座標が使えないか?
前回の点数文字輪郭で試してみたいと思います。

輪郭データの確認

前回少し見てみましたが、輪郭データは輪郭上の点の座標のリストになっていました。
実際の点数文字輪郭で確認してみます。

# image: Input image, BGR format
def calculate_harupan(image, debug):
    h, w, chs = image.shape
    if h > 800 or w > 800:
        k = 800.0/h if w > h else 800.0/w
    else:
        k = 1.0
    img = cv2.resize(image, None, fx=k, fy=k, interpolation=cv2.INTER_AREA)
    if debug:
        print('Resized to ', img.shape)
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    # Convert hue value (rotation, mask by saturation)
    hsv[:,:,0] = np.where(hsv[:,:,0] < 50, hsv[:,:,0]+180, hsv[:,:,0])
    hsv[:,:,0] = np.where(hsv[:,:,1] < 100, 0, hsv[:,:,0])
    # Thresholding with cv2.inRange()
    th_hue = cv2.inRange(hsv[:,:,0], 135, 190)
    contours, hierarchy = cv2.findContours(th_hue, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    indices0 = [i for i,hier in enumerate(hierarchy[0,:,:]) if hier[3] == -1]
    indices1 = [i for i,hier in enumerate(hierarchy[0,:,:]) if hier[3] in indices0]
    if debug:
        print('Number of contours: ', len(contours))
        print('Number of indices0: ', len(indices0), 'indices1: ', len(indices1))
    contours1 = [contours[i] for i in indices1]
    contours1_filtered = [ctr for ctr in contours1 if cv2.contourArea(ctr) > 800*800/4000]
    if debug:
        return contours1_filtered, img
    else:
        return contours1_filtered
ctrs1, img1_resize = calculate_harupan(img1, True)

idx_zero = 26; ctrs1_zero = ctrs1[idx_zero]
idx_one = 27; ctrs1_one = ctrs1[idx_one]
idx_two = 24; ctrs1_two = ctrs1[idx_two]
idx_three = 33; ctrs1_three = ctrs1[idx_three]
idx_five = 35; ctrs1_five = ctrs1[idx_five]
ctrs1_numbers = [ctrs1_zero, ctrs1_one, ctrs1_two, ctrs1_three, ctrs1_five]
[print(ctr.shape[0]) for ctr in ctrs1_numbers];
Resized to  (1067, 800, 3)
Number of contours:  2514
Number of indices0:  1448 indices1:  875
62
38
99
94
67

いずれも100未満の座標の数になっています。
画像上にこれらの座標を示してみたいと思います。ここではcv2.drawMarker()を使ってみました。

def create_contour_area_image(img, ctr):
    x,y,w,h = cv2.boundingRect(ctr)
    rtn_img = img[y:y+h,x:x+w,:].copy()
    rtn_ctr = ctr.copy()
    origin = np.array([x,y])
    for c in rtn_ctr:
        c[0,:] -= origin
    return rtn_img, rtn_ctr

plt.figure(figsize=(6.4,4.8), dpi=100)
for i,ctr in enumerate(ctrs1_numbers):
    subimg, subctr = create_contour_area_image(img1_resize, ctr)
    [cv2.drawMarker(subimg, p, (0,255,0), markerType=cv2.MARKER_CROSS, markerSize=3) for p in subctr[:,0,:]];
    plt.subplot(1,5,1+i), plt.imshow(cv2.cvtColor(subimg, cv2.COLOR_BGR2RGB)), plt.xticks([]), plt.yticks([])
plt.show()

f:id:nokixa:20220124001042p:plain

文字の角やカーブしている部分に輪郭の点が多く見られるのと、直線部分でも斜めになっていると点が存在しています。

パラメータ推定方法

アフィン変換パラメータを推定するには、テンプレート画像上の点と比較対象画像上の点の対応付けをしないといけません。どうしようかと考えましたが、前にも見たケンブリッジ大の教科書に載っていたICPという手法が使えそうでした。

http://www.computervisionmodels.com/
https://www.amazon.co.jp/Computer-Vision-Models-Learning-Inference/dp/1107011795

ICP(iterative closest point)

上記教科書Chapter 17 "Models for shape"の中で紹介されていました。
この中でも、"17.3 Shape templates"の節で、ここでは検出したい形状のテンプレートがあり、また、対象画像上にはそれが変換を施されて現れるので、この変換を求める、というのがテーマになっています。まさに今やろうとしているのと同じ内容です。

ICPアルゴリズムの手順は、

  1. 変換パラメータ\psiの初期値を設定
  2. テンプレート上の点(landmark point)w_n (n=1...N)\psiで変換、w'_nを得る
  3. w'_nと対象画像上の点y_nについて、最も近いものを対応付ける
  4. w_ny_nの対応付けから、変換パラメータ\psiを算出
  5. 2.に戻る、これを収束するまで(対応付けが変わらなくなるまで?)繰り返す

というものです。

OpenCVではppf_match_3dの中にICPクラスがありますが、これは3次元の点群向けのようなので、今回は自作してみたいと思います。

https://docs.opencv.org/3.4/dc/d9b/classcv_1_1ppf__match__3d_1_1ICP.html

"0"の文字の検出について

ICPアルゴリズムを使っていきたいと思いますが、"0"の文字だけちょっと問題が。
他の数字では角がありますが、"0"だけ楕円形状をしていて、輪郭点が必ずしも同じ位置に現れない可能性があります。また、楕円だとアフィン変換をしても結局楕円になる、ということもあるので、以下のような手法を考えてみます。

  • 楕円で近似、近似した楕円とテンプレートマッチングを実施、一致度が閾値以上であれば"0"の文字であると判定

OpenCVのアフィン変換パラメータ推定関数

OpenCVでのアフィン変換パラメータ推定ですが、以下の2つの関数があります。

調べていてestimateRigidTransform()というのもありましたが、こちらは既に非推奨になっていて、今使っているバージョン(4.5.3)ではなくなっていました。
http://opencv.jp/opencv-2svn/cpp/structural_analysis_and_shape_descriptors.html#cv-estimaterigidtransform
https://campkougaku.com/2020/07/16/estimateaffine2d/

今回は1つ目だと意味がない(3組だと、その3組がきっちりマッチングする変換行列が返ってくるだけ)ので、2つ目を使おうと思いますが、今回対応点のマッチングに当たる部分は自分で用意するので、RANSAC等が動くのは余分かなと。

単純に3組より多い対応点から最適な変換行列を求める、ということであれば、最小二乗誤差になる変換行列、という条件で、これはClosed formの解があります。このあたりも上記の教科書に書いてあります。

4組以上の対応点で最小二乗誤差を取るアフィン変換パラメータの計算

ここでは、教科書にならってアフィン変換行列を


\begin{bmatrix} \boldsymbol{\Phi} & \boldsymbol{\tau} \end{bmatrix} = 
\begin{bmatrix} \phi_{11} & \phi_{12} & \tau_x \\ \phi_{21} & \phi_{22} & \tau_y \end{bmatrix}

と書きます。
対応点群のうち変換元を\boldsymbol{w}_i=[u_i v_i \rbrack ^T (i=1...N)、変換先を\boldsymbol{x}_i=[x_i y_i \rbrack ^Tとして、 行列\boldsymbol{A}_i、ベクトル\boldsymbol{b}


\boldsymbol{A}_i = 
\begin{bmatrix} u_i & v_i & 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & u_i & v_i & 1 \end{bmatrix} \\
\boldsymbol{b} = \begin{bmatrix} \phi_{11} & \phi_{12} & \tau_x & \phi_{21} & \phi_{22} & \tau_y \end{bmatrix} ^T

とします。\boldsymbol{b}は変換パラメータを要素に持つベクトルとなり、最適値\boldsymbol{\hat{b}}を求めることとなります。


\boldsymbol{\hat{b}} = 
    \underset{\boldsymbol{b}}{\rm{argmin}} \lbrack \sum_{i=1}^N (\boldsymbol{x}_i - \boldsymbol{A}_i\boldsymbol{b})^T (\boldsymbol{x}_i - \boldsymbol{A}_i\boldsymbol{b}) \rbrack

行列\boldsymbol{A}、ベクトル\boldsymbol{x}


\boldsymbol{A} = 
\begin{bmatrix}
u_1 & v_1 & 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & u_1 & v_1 & 1 \\
u_2 & v_2 & 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & u_2 & v_2 & 1 \\
\vdots & & & & & \\
u_N & v_N & 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & u_N & v_N & 1 \\
\end{bmatrix} \\
    \boldsymbol{x} = \begin{bmatrix} x_1 & y_1 & x_2 & y_2 & \cdots & x_N & y_N \end{bmatrix}^T

とすると、先ほどの最適値\boldsymbol{\hat{b}}は、


\boldsymbol{\hat{b}} = (\boldsymbol{A}^T \boldsymbol{A})^{-1}\boldsymbol{A}^T \boldsymbol{x}

となります。

こちらのサイトにも同様なことが書かれていました。
https://tukurutanoshi.hateblo.jp/entry/2019/02/27/165340

以下、詳細を検討していきます。

ICPで"0"以外の変換パラメータ推定

landmark pointの選択

まずは各文字テンプレートの輪郭点から、landmark pointとして使える角の点を選びます。
(輪郭の直線、もしくは緩いカーブだと、必ず同じところに輪郭点が現れるとは限らないので)
前回のトラックバーを使うと探しやすいと思うので、使います。
また、今後の利用のため、輪郭周辺のみの画像と、原点をこれに合わせた輪郭データを残しておきます。

from ipywidgets import interact
subimgs1_numbers = []
subctrs1_numbers = []
for ctr in ctrs1_numbers:
    subimg, subctr = create_contour_area_image(img1_resize, ctr)
    subimgs1_numbers += [subimg]
    subctrs1_numbers += [subctr]
def plot_contour_point(img, ctr, i_point):
    img_copy = img.copy()
    cv2.drawMarker(img_copy, ctr[i_point,0,:], (0,255,0), markerType=cv2.MARKER_CROSS, markerSize=3);
    plt.imshow(cv2.cvtColor(img_copy, cv2.COLOR_BGR2RGB)), plt.xticks([]), plt.yticks([])
    plt.show()

def plot_contour_point_one(i_point):
    plot_contour_point(subimgs1_numbers[1], subctrs1_numbers[1], i_point)

def plot_contour_point_two(i_point):
    plot_contour_point(subimgs1_numbers[2], subctrs1_numbers[2], i_point)
    
def plot_contour_point_three(i_point):
    plot_contour_point(subimgs1_numbers[3], subctrs1_numbers[3], i_point)
    
def plot_contour_point_five(i_point):
    plot_contour_point(subimgs1_numbers[4], subctrs1_numbers[4], i_point)
interact(plot_contour_point_one, i_point=(0, ctrs1_numbers[1].shape[0]-1));

f:id:nokixa:20220124005014p:plain

subctrs1_one = subctrs1_numbers[1]
pts1_one_idx = [0, 6, 18, 23, 31, 34]
pts1_one = np.zeros([len(pts1_one_idx),2])
for i,idx in enumerate(pts1_one_idx):
    pts1_one[i,:] = subctrs1_one[idx,0,:].copy()
interact(plot_contour_point_two, i_point=(0, ctrs1_numbers[2].shape[0]-1));

f:id:nokixa:20220124005016p:plain

subctrs1_two = subctrs1_numbers[2]
pts1_two_idx = [29, 34, 39, 52, 84, 88]
pts1_two = np.zeros([len(pts1_two_idx),2])
for i,idx in enumerate(pts1_two_idx):
    pts1_two[i,:] = subctrs1_two[idx,0,:].copy()
interact(plot_contour_point_three, i_point=(0, ctrs1_numbers[3].shape[0]-1));

f:id:nokixa:20220124005018p:plain

subctrs1_three = subctrs1_numbers[3]
pts1_three_idx = [13, 48, 49, 62, 63, 79, 80]
pts1_three = np.zeros([len(pts1_three_idx),2])
for i,idx in enumerate(pts1_three_idx):
    pts1_three[i,:] = subctrs1_three[idx,0,:].copy()
interact(plot_contour_point_five, i_point=(0, ctrs1_numbers[4].shape[0]-1));

f:id:nokixa:20220124005020p:plain

subctrs1_five = subctrs1_numbers[4]
pts1_five_idx = [2, 4, 8, 9, 35, 36, 58, 63]
pts1_five = np.zeros([len(pts1_five_idx),2])
for i,idx in enumerate(pts1_five_idx):
    pts1_five[i,:] = subctrs1_five[idx,0,:].copy()

選んだので、まとめて表示してみます。
また、今回のやり方では、テンプレート画像について前回やったような角度調整は不要なので、角度調整なしのものを用意しておきます。

pts1_numbers = [pts1_one, pts1_two, pts1_three, pts1_five]
ctrs1_templates = []
plt.figure(figsize=(6.4,4.8), dpi=100)
for i,ctr in enumerate(subctrs1_numbers[1:5]):
    img = subimgs1_numbers[i+1]
    for p in pts1_numbers[i]:
        img = cv2.drawMarker(img, p.astype('uint'), (0,255,0), markerType=cv2.MARKER_CROSS, markerSize=3)
    template = np.zeros((img.shape[0], img.shape[1]), 'uint8')
    cv2.drawContours(template, [ctr], -1, 255, -1)
    ctrs1_templates += [template]
    plt.subplot(2,4,1+i), plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)), plt.xticks([]), plt.yticks([])
    plt.subplot(2,4,5+i), plt.imshow(template, cmap='gray'), plt.xticks([]), plt.yticks([])
plt.show()

f:id:nokixa:20220124001045p:plain

ICPアルゴリズム実装

まず変換行列の初期値が必要になります。
今回は

  • 対象画像の外接矩形(回転考慮)をテンプレートの外接矩形(回転考慮)に合わせるような変換を初期値とする

というので考えました。この変換だけでももしかしたら十分かも?
外接矩形だと90°単位の回転も考えておいたほうがいいかも。 後は上で書いた通り実装するだけです。

# pts: list of 2D points, or ndarray of shape (n,2)
# query: 2D point to find nearest neighbor
def find_nearest_neighbor(pts, query):
    min_distance = float('inf')
    min_idx = 0
    for i, p in enumerate(pts):
        d = np.linalg.norm(query - p)
        if(d < min_distance):
            min_distance = d
            min_idx = i
    return min_idx, min_distance

def get_initial_transform(src_ctr, dst_ctr):
    src_box = cv2.boxPoints(cv2.minAreaRect(src_ctr))
    dst_box = cv2.boxPoints(cv2.minAreaRect(dst_ctr))
    # Rotated patterns are created when starting index is slided
    dst_box = np.vstack([dst_box, dst_box])
    # Area of converted image
    dst_rect = cv2.boundingRect(dst_ctr)
    
    src_pts = [p for p in src_ctr[:,0,:]]
    dst_pts = [p for p in dst_ctr[:,0,:]]
    min_sum_distance = float('inf')
    for i in range(4):
        M = cv2.getAffineTransform(src_box[0:3], dst_box[i:i+3])
        sum_distance = 0
        for p in src_pts:
            p2 = M @ np.array([p[0], p[1], 1])
            idx, d = find_nearest_neighbor(dst_pts, p2)
            sum_distance += d
        if(sum_distance < min_sum_distance):
            M_rtn = M
            min_sum_distance = sum_distance
    return M_rtn

変換行列初期値を試しに出してみます。 まずは"1"のテンプレートとの比較から。

for i, ctr in enumerate(ctrs1[0:20]):
    subimg, subctr = create_contour_area_image(img1_resize, ctr)
    M = get_initial_transform(subctrs1_numbers[1], subctr)
    converted_img = cv2.warpAffine(subimgs1_numbers[1], M, (subimg.shape[1], subimg.shape[0]))
    plt.figure(figsize=(3.2,2.4), dpi=100)
    print('No. ', i)
    plt.subplot(1,2,1), plt.imshow(cv2.cvtColor(converted_img, cv2.COLOR_BGR2RGB)), plt.title('Template'), plt.xticks([]), plt.yticks([])
    plt.subplot(1,2,2), plt.imshow(cv2.cvtColor(subimg, cv2.COLOR_BGR2RGB)), plt.title('Target'), plt.xticks([]), plt.yticks([])
    plt.show()

No. 0

f:id:nokixa:20220124001047p:plain

No. 1

f:id:nokixa:20220124001050p:plain

No. 2

f:id:nokixa:20220124001052p:plain

No. 3

f:id:nokixa:20220124001055p:plain

No. 4

f:id:nokixa:20220124001057p:plain

No. 5

f:id:nokixa:20220124001059p:plain

No. 6

f:id:nokixa:20220124001102p:plain

No. 7

f:id:nokixa:20220124001104p:plain

No. 8

f:id:nokixa:20220124001107p:plain

No. 9

f:id:nokixa:20220124001109p:plain

No. 10

f:id:nokixa:20220124001112p:plain

No. 11

f:id:nokixa:20220124001114p:plain

No. 12

f:id:nokixa:20220124001116p:plain

No. 13

f:id:nokixa:20220124001119p:plain

No. 14

f:id:nokixa:20220124001121p:plain

No. 15

f:id:nokixa:20220124001123p:plain

No. 16

f:id:nokixa:20220124001126p:plain

No. 17

f:id:nokixa:20220124001128p:plain

No. 18

f:id:nokixa:20220124001130p:plain

No. 19

f:id:nokixa:20220124001132p:plain

この初期値だけで"1"の文字はきちんとチェックできそうですが、同じ撮影角度からなのでまあそうなるんだろうか。
次はICPを実装、試してみます。

気になったのは、最近傍点がかぶって、対応点の重複が起きてしまったらどうなるかというところ。
アフィン変換行列を計算するときの行列\boldsymbol{A}に対応点座標が入ってきますが、逆行列の計算があるので、変なことが起こってしまいそうな気がします。

ということで、これを避けるように実装します。

ついでにテンプレートマッチングも行って、一致度を見てみます。ICPの効果の確認のため、初期推定行列での一致度も見てみます。
cv2.matchTemplate()では、比較方法としてcv2.TM_CCORR_NORMEDを使おうと思います。結果の最大値は1.0で、2つの画像が完全一致したときにその値になります。
あともう一つ、matchShapes()関数でも形状比較ができるようなので、やってみます。 こちらでは値が小さいほど一致度が高いということです。

https://docs.opencv.org/4.5.5/d5/d45/tutorial_py_contours_more_functions.html

# src, dst: ndarray, shape is (n,2) (n: number of points)
def estimate_affine_2d(src, dst):
    n = min(src.shape[0], dst.shape[0])
    x = dst[0:n].flatten()
    A = np.zeros((2*n,6))
    for i in range(n):
        A[i*2,0] = src[i,0]
        A[i*2,1] = src[i,1]
        A[i*2,2] = 1
        A[i*2+1,3] = src[i,0]
        A[i*2+1,4] = src[i,1]
        A[i*2+1,5] = 1
    M = np.linalg.inv(A.T @ A) @ A.T @ x
    return M.reshape([2,3])

# Find optimum affine matrix using ICP algorithm
# src_pts: ndarray, shape is (n_s,2) (n_s: number of points)
# dst_pts: ndarray, shape is (n_d,2) (n_d: number of points, n_d should be larger or equal to n_s)
# initial_matrix: ndarray, shape is (2,3)
def icp(src_pts, dst_pts, max_iter=1000, initial_matrix=np.array([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]])):
    default_affine_matrix = np.array([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]])
    if dst_pts.shape[0] < src_pts.shape[0]:
        print("icp: Insufficient destination points")
        return default_affine_matrix
    if initial_matrix.shape != (2,3):
        print("icp: Illegal shape of initial_matrix")
        return default_affine_matrix
    M = initial_matrix
    # Store indices of the nearest neighbor point of dst_pts to the converted point of src_pts
    nn_idx = []
    for i in range(max_iter):
        nn_idx_tmp = []
        dst_pts_list = [p for p in dst_pts]
        idx_list = list(range(0,dst_pts.shape[0]))
        for p in src_pts:
            p2 = M @ np.array([p[0], p[1], 1])
            idx, d = find_nearest_neighbor(dst_pts_list, p2)
            nn_idx_tmp += [idx_list[idx]]
            del dst_pts_list[idx]
            del idx_list[idx]
        if __debug__:
            print("icp: nn_idx: ", nn_idx_tmp)
        if nn_idx != [] and nn_idx == nn_idx_tmp:
            if __debug__:
                print("icp: converged in ", i, " iteration(s)")
            break
        dst_pts2 = np.zeros_like(src_pts)
        for j,idx in enumerate(nn_idx_tmp):
            dst_pts2[j,:] = dst_pts[idx,:]
        M = estimate_affine_2d(src_pts, dst_pts2)
        nn_idx = nn_idx_tmp
        if i == max_iter -1:
            print("icp: Not converged")
    return M
binimg1_one = np.zeros_like(subimgs1_numbers[1][:,:,0])
binimg1_one = cv2.drawContours(binimg1_one, [subctrs1_one], -1, 255, -1)

for i, ctr in enumerate(ctrs1[0:20]):
    print("-- No. ", i, " --")
    subimg, subctr = create_contour_area_image(img1_resize, ctr)
    binimg = np.zeros_like(subimg[:,:,0])
    pts = np.zeros((subctr.shape[0], 2))
    for i,p in enumerate(subctr[:,0,:]):
        pts[i] = p
    binimg = cv2.drawContours(binimg, [subctr], -1, 255, -1)
    M_init = get_initial_transform(subctrs1_one, subctr)
    M = icp(pts1_one, pts, max_iter=100, initial_matrix=M_init)
    print("Affine matrix: ")
    print(M)
    subimg_one_converted = cv2.warpAffine(subimgs1_numbers[1], M, (subimg.shape[1],subimg.shape[0]))
    subctr_one_converted = np.zeros_like(subctrs1_one)
    subctr_one_converted_init = np.zeros_like(subctrs1_one)
    for i in range(subctrs1_one.shape[0]):
        subctr_one_converted[i,0,:] = (M[:,0:2] @ subctrs1_one[i,0,:]) + M[:,2]
        subctr_one_converted_init[i,0,:] = (M_init[:,0:2] @ subctrs1_one[i,0,:]) + M_init[:,2]
    binimg_one = np.zeros_like(subimg[:,:,0])
    binimg_one = cv2.drawContours(binimg_one, [subctr_one_converted], -1, 255, -1)
    binimg_one_init = np.zeros_like(subimg[:,:,0])
    binimg_one_init = cv2.drawContours(binimg_one_init, [subctr_one_converted_init], -1, 255, -1)
    similarity1 = cv2.matchTemplate(binimg.copy(), binimg_one, cv2.TM_CCORR_NORMED)
    similarity1_init = cv2.matchTemplate(binimg.copy(), binimg_one_init, cv2.TM_CCORR_NORMED)
    similarity2 = cv2.matchShapes(subctr, subctr_one_converted, cv2.CONTOURS_MATCH_I2, 0.0)
    print("similarity1: ", similarity1, "(", similarity1_init, " with initial matrix)", ", similarity2: ", similarity2)
    plt.figure(figsize=(6.4,2.4), dpi=100)
    plt.subplot(1,4,1), plt.imshow(cv2.cvtColor(subimg_one_converted, cv2.COLOR_BGR2RGB)), plt.title('Template'), plt.xticks([]), plt.yticks([])
    plt.subplot(1,4,2), plt.imshow(cv2.cvtColor(subimg, cv2.COLOR_BGR2RGB)), plt.title('Target'), plt.xticks([]), plt.yticks([])
    plt.subplot(1,4,3), plt.imshow(binimg_one, cmap='gray'), plt.title('Template'), plt.xticks([]), plt.yticks([])
    plt.subplot(1,4,4), plt.imshow(binimg, cmap='gray'), plt.title('Target'), plt.xticks([]), plt.yticks([])
    plt.show()
-- No.  0  --
icp: nn_idx:  [24, 5, 13, 15, 2, 23]
icp: nn_idx:  [24, 5, 12, 15, 2, 23]
icp: nn_idx:  [24, 5, 12, 15, 2, 23]
icp: converged in  2  iteration(s)
Affine matrix: 
[[ 3.77521764  0.35939223 -1.62896427]
 [-0.11927385  0.98120093 -0.4825973 ]]
similarity1:  [[0.83907473]] ( [[0.8470252]]  with initial matrix) , similarity2:  2.070841343730711

f:id:nokixa:20220124001135p:plain

-- No.  1  --
icp: nn_idx:  [5, 11, 13, 19, 4, 3]
icp: nn_idx:  [5, 11, 13, 19, 4, 3]
icp: converged in  1  iteration(s)
Affine matrix: 
[[ 0.09102535 -0.21797439 11.83687946]
 [ 0.62207725  0.06364548 -0.50776546]]
similarity1:  [[0.7998894]] ( [[0.83268374]]  with initial matrix) , similarity2:  2.069956761410079

f:id:nokixa:20220124001137p:plain

-- No.  2  --
icp: nn_idx:  [24, 6, 10, 12, 21, 23]
icp: nn_idx:  [24, 6, 11, 12, 21, 23]
icp: nn_idx:  [24, 6, 11, 12, 21, 23]
icp: converged in  2  iteration(s)
Affine matrix: 
[[ 0.63722433  0.05213959 -0.56842827]
 [-0.01010113  0.28285502  0.67956106]]
similarity1:  [[0.85370934]] ( [[0.8228804]]  with initial matrix) , similarity2:  3.097868971398144

f:id:nokixa:20220124001139p:plain

-- No.  3  --
icp: nn_idx:  [10, 14, 25, 30, 11, 9]
icp: nn_idx:  [10, 14, 25, 30, 11, 8]
icp: nn_idx:  [10, 14, 25, 30, 11, 8]
icp: converged in  2  iteration(s)
Affine matrix: 
[[ 0.11168581 -0.27227208 13.14478209]
 [ 0.63847995  0.04773183 -0.98538065]]
similarity1:  [[0.8774381]] ( [[0.8368629]]  with initial matrix) , similarity2:  2.0255038136645815

f:id:nokixa:20220124001142p:plain

-- No.  4  --
icp: nn_idx:  [8, 16, 23, 31, 9, 7]
icp: nn_idx:  [8, 16, 23, 31, 9, 7]
icp: converged in  1  iteration(s)
Affine matrix: 
[[-1.06272935e-02 -2.96027105e-01  1.52971042e+01]
 [ 6.03319977e-01  3.05329968e-02  7.12554116e-01]]
similarity1:  [[0.8454327]] ( [[0.85770833]]  with initial matrix) , similarity2:  3.1988653569571213

f:id:nokixa:20220124001144p:plain

-- No.  5  --
icp: nn_idx:  [9, 13, 16, 3, 12, 8]
icp: nn_idx:  [9, 13, 16, 4, 12, 8]
icp: nn_idx:  [9, 13, 16, 4, 12, 8]
icp: converged in  2  iteration(s)
Affine matrix: 
[[-5.36774428e-01 -6.36147697e-02  1.40442789e+01]
 [-5.81606616e-03 -2.55624851e-01  1.52168842e+01]]
similarity1:  [[0.8033988]] ( [[0.81920767]]  with initial matrix) , similarity2:  3.95210735229086

f:id:nokixa:20220124001146p:plain

-- No.  6  --
icp: nn_idx:  [7, 27, 39, 63, 18, 4]
icp: nn_idx:  [6, 27, 39, 63, 17, 4]
icp: nn_idx:  [5, 27, 38, 63, 17, 4]
icp: nn_idx:  [5, 27, 38, 63, 17, 4]
icp: converged in  3  iteration(s)
Affine matrix: 
[[-0.7778685  -0.35529497 34.39894773]
 [ 0.91020511 -0.16915753  8.47497258]]
similarity1:  [[0.71735805]] ( [[0.7633234]]  with initial matrix) , similarity2:  1.88512766720494

f:id:nokixa:20220124001149p:plain

-- No.  7  --
icp: nn_idx:  [1, 14, 33, 40, 0, 57]
icp: nn_idx:  [1, 13, 33, 42, 0, 54]
icp: nn_idx:  [1, 12, 33, 42, 0, 53]
icp: nn_idx:  [1, 11, 33, 42, 0, 53]
icp: nn_idx:  [1, 11, 33, 42, 0, 53]
icp: converged in  4  iteration(s)
Affine matrix: 
[[ 1.09282845 -0.47169988 21.60848193]
 [ 0.57396     0.71953624 -5.82698916]]
similarity1:  [[0.7672092]] ( [[0.8546155]]  with initial matrix) , similarity2:  0.9234014436533062

f:id:nokixa:20220124001151p:plain

-- No.  8  --
icp: nn_idx:  [41, 0, 6, 25, 50, 37]
icp: nn_idx:  [40, 0, 6, 25, 50, 37]
icp: nn_idx:  [40, 0, 6, 25, 50, 36]
icp: nn_idx:  [40, 0, 6, 25, 50, 35]
icp: nn_idx:  [40, 0, 6, 25, 50, 35]
icp: converged in  4  iteration(s)
Affine matrix: 
[[ 3.15292202e-01  4.29651332e-01  4.86435813e-01]
 [-1.34699268e+00 -1.62861226e-02  3.10887500e+01]]
similarity1:  [[0.74493146]] ( [[0.74549276]]  with initial matrix) , similarity2:  1.6186110812682086

f:id:nokixa:20220124001153p:plain

-- No.  9  --
icp: nn_idx:  [46, 5, 21, 25, 39, 42]
icp: nn_idx:  [46, 5, 21, 25, 39, 42]
icp: converged in  1  iteration(s)
Affine matrix: 
[[ 0.80616591 -0.05393659  0.46875474]
 [-0.03735035  0.95663612  0.44759342]]
similarity1:  [[0.9512335]] ( [[0.97588533]]  with initial matrix) , similarity2:  0.1421256423484834

f:id:nokixa:20220124001156p:plain

-- No.  10  --
icp: nn_idx:  [32, 42, 1, 8, 53, 31]
icp: nn_idx:  [32, 42, 1, 8, 54, 30]
icp: nn_idx:  [32, 42, 1, 8, 54, 30]
icp: converged in  2  iteration(s)
Affine matrix: 
[[-1.04624547  0.17536172 23.32801419]
 [-0.22475023 -0.54176223 31.41222332]]
similarity1:  [[0.7661245]] ( [[0.789921]]  with initial matrix) , similarity2:  0.5005787273262177

f:id:nokixa:20220124001158p:plain

-- No.  11  --
icp: nn_idx:  [29, 43, 63, 7, 30, 26]
icp: nn_idx:  [29, 41, 63, 8, 27, 26]
icp: nn_idx:  [29, 40, 64, 8, 27, 25]
icp: nn_idx:  [29, 39, 64, 8, 27, 25]
icp: nn_idx:  [29, 39, 64, 8, 27, 25]
icp: converged in  4  iteration(s)
Affine matrix: 
[[-1.12407669e+00  6.83733951e-02  3.36880211e+01]
 [-2.44519307e-02 -9.50621868e-01  5.19999215e+01]]
similarity1:  [[0.7463398]] ( [[0.80455464]]  with initial matrix) , similarity2:  1.2619150345061985

f:id:nokixa:20220124001200p:plain

-- No.  12  --
icp: nn_idx:  [36, 49, 88, 2, 24, 30]
icp: nn_idx:  [35, 49, 88, 3, 27, 30]
icp: nn_idx:  [35, 49, 88, 3, 27, 30]
icp: converged in  2  iteration(s)
Affine matrix: 
[[-1.56806219 -0.30534418 47.03348041]
 [ 0.47604111 -0.85860263 43.51690626]]
similarity1:  [[0.72022676]] ( [[0.6975103]]  with initial matrix) , similarity2:  1.3914075613092929

f:id:nokixa:20220124001204p:plain

-- No.  13  --
icp: nn_idx:  [0, 6, 16, 19, 28, 29]
icp: nn_idx:  [0, 5, 16, 20, 28, 29]
icp: nn_idx:  [0, 5, 16, 20, 28, 29]
icp: converged in  2  iteration(s)
Affine matrix: 
[[ 0.93756138  0.03949807 -0.76644805]
 [-0.03855138  0.96085223  0.36815656]]
similarity1:  [[0.95289135]] ( [[0.9058108]]  with initial matrix) , similarity2:  0.38096457366596537

f:id:nokixa:20220124001206p:plain

-- No.  14  --
icp: nn_idx:  [37, 58, 113, 1, 23, 31]
icp: nn_idx:  [37, 56, 113, 1, 24, 30]
icp: nn_idx:  [37, 56, 113, 1, 24, 30]
icp: converged in  2  iteration(s)
Affine matrix: 
[[-1.23288405 -0.48977163 47.8703411 ]
 [ 0.57970655 -0.84490737 41.08824725]]
similarity1:  [[0.6762236]] ( [[0.70227975]]  with initial matrix) , similarity2:  1.2474046314232978

f:id:nokixa:20220124001209p:plain

-- No.  15  --
icp: nn_idx:  [0, 2, 6, 17, 25, 24]
icp: nn_idx:  [0, 2, 5, 17, 25, 24]
icp: nn_idx:  [0, 2, 5, 17, 25, 24]
icp: converged in  2  iteration(s)
Affine matrix: 
[[ 0.70935123  0.04373579  2.2724023 ]
 [-0.07280603  0.357142    1.50096987]]
similarity1:  [[0.7443389]] ( [[0.8030085]]  with initial matrix) , similarity2:  1.5579271874548668

f:id:nokixa:20220124001211p:plain

-- No.  16  --
icp: nn_idx:  [20, 26, 45, 2, 35, 18]
icp: nn_idx:  [20, 26, 46, 2, 35, 18]
icp: nn_idx:  [20, 26, 46, 2, 35, 18]
icp: converged in  2  iteration(s)
Affine matrix: 
[[-0.63087244 -0.01974989 16.31276532]
 [ 0.06772852 -0.36275534 18.16710691]]
similarity1:  [[0.81694937]] ( [[0.76340884]]  with initial matrix) , similarity2:  1.640027037212645

f:id:nokixa:20220124001214p:plain

-- No.  17  --
icp: nn_idx:  [0, 12, 40, 49, 69, 73]
icp: nn_idx:  [0, 12, 40, 49, 69, 73]
icp: converged in  1  iteration(s)
Affine matrix: 
[[ 0.9943615  -0.16510235  4.43565989]
 [ 0.13401419  0.99049185 -0.04637385]]
similarity1:  [[0.9555899]] ( [[0.9649432]]  with initial matrix) , similarity2:  0.08709025018016892

f:id:nokixa:20220124001216p:plain

-- No.  18  --
icp: nn_idx:  [111, 18, 57, 70, 99, 104]
icp: nn_idx:  [111, 18, 57, 70, 98, 104]
icp: nn_idx:  [111, 18, 57, 70, 98, 104]
icp: converged in  2  iteration(s)
Affine matrix: 
[[ 0.95356303 -0.34740354 13.56925455]
 [ 0.32619587  0.91220825  0.67636115]]
similarity1:  [[0.95916927]] ( [[0.95294887]]  with initial matrix) , similarity2:  0.4385846009540594

f:id:nokixa:20220124001219p:plain

-- No.  19  --
icp: nn_idx:  [26, 35, 0, 6, 47, 25]
icp: nn_idx:  [26, 35, 0, 6, 48, 24]
icp: nn_idx:  [26, 35, 0, 6, 48, 24]
icp: converged in  2  iteration(s)
Affine matrix: 
[[-0.90189543  0.11394542 21.30119745]
 [-0.12584142 -0.56866086 31.567395  ]]
similarity1:  [[0.7807008]] ( [[0.7943264]]  with initial matrix) , similarity2:  0.7837848069205069

f:id:nokixa:20220124001221p:plain

__debug__を使ってデバッグ表示も入れてみました。
デフォルトでTrueとのことなので、今回は表示されています。

この結果を見た感じだと、

  • ICPアルゴリズムで点のマッチングが改善している様子が見られる
  • アフィン変換によるせん断変形が見られる(元のテンプレート画像で直角だった部分が斜めになっている)
  • ICPアルゴリズムで、初期推定パラメータから改善したかというと微妙…どちらかというと悪くなっている傾向があるような。
    もしかしたら解像度を上げたりすると改善するのかもと思っています。
  • テンプレートと対象輪郭の比較としては、cv2.matchTemplate()、というよりcv2.CCORR_NORMEDによる比較のほうが分かりやすそう、こちらは最大値が1と決まっているので
  • cv2.matchTemplate()での比較では、"1"の文字の検出がいい感じにできそう
    だいたい0.9ぐらいの閾値にすればよさそうです。
  • cv2.matchShapes()のほうは値の範囲がどれくらいかよくわからない、また、閾値も難しい

という感じです。
ひとまず"0"以外の文字については、アフィン変換行列をICPアルゴリズムで計算する、テンプレートとの一致度をcv2.matchTemplate()で比較する、という方針で進めようと思います。

また一区切り

あと"0"の文字の比較、判定が残っていますが、一区切りにしたいと思います。