アフィン変換を使った特徴点マッチングで雑すぎるコラ画像を作る<!-- --> | <!-- -->magmagchi<!-- -->
magmagchi

アフィン変換を使った特徴点マッチングで雑すぎるコラ画像を作る

July 14, 2022

はじめに

特徴点マッチングにおいてのアフィン行列を求める記事です。この記事では特徴点を手動で決めることとします。 affine.gif こんな感じで2枚の画像の特徴点をクリックして特徴点を指定して、それに合うように片方の画像をアフィン変換によって変形させて重ねることが目的です。

特徴点マッチング

アフィン行列

画像の各座標を変換させる行列計算

(xy1)=(abtxcdty001)(xy1)\left( \begin{array}{c} x^{'}\\ y^{'}\\ 1 \end{array} \right) = \left( \begin{array}{ccc} a & b & t_{x}\\ c & d & t_{y}\\ 0 & 0 & 1 \end{array} \right) \left( \begin{array}{c} x\\ y\\ 1 \end{array} \right)

A=(abtxcdty001)A= \left( \begin{array}{ccc} a & b & t_{x}\\ c & d & t_{y}\\ 0 & 0 & 1 \end{array} \right)

の部分がアフィン行列。 この行列だけで画像の回転、拡大、縮小、移動、反転、せん断を表せる優れもの!!

アフィン変換については以下の記事を参考にさせていただきました。 完全に理解するアフィン変換 行列によるアフィン変換(拡大縮小・回転・剪断・移動) ~Python画像処理の再発明家~

アフィン行列の算出

2つの画像の特徴点がN(N3)N(N\geqq3)個あったとき、変換前の画像の特徴点の座標を

(xnyn)\left( \begin{array}{c} x_{n}\\ y_{n} \end{array} \right)

変換後の座標を

(xnyn)\left( \begin{array}{c} x^{'}_{n}\\ y^{'}_{n} \end{array} \right)

としてNN個の座標すべてにアフィン変換をする行列式は

(x1x2xNy1x2xN111)=(abtxcdty001)(x1x2xNy1x2xN111)\left( \begin{array}{c} x^{'}_{1}&x^{'}_{2}&\cdots&x^{'}_{N}\\ y^{'}_{1}&x^{'}_{2}&\cdots&x^{'}_{N}\\ 1&1&\cdots&1 \end{array} \right) = \left( \begin{array}{ccc} a & b & t_{x}\\ c & d & t_{y}\\ 0 & 0 & 1 \end{array} \right) \left( \begin{array}{c} x_{1}&x_{2}&\cdots&x_{N}\\ y_{1}&x_{2}&\cdots&x_{N}\\ 1&1&\cdots&1 \end{array} \right)

で表すことが出来ます。このa,b,c,d,tx,tya,b,c,d,t_{x},t_{y}の最もらしい値を求めることが目的となります。 ここで1組の変換前後の座標

(xnyn),(xnyn)\left( \begin{array}{c} x_{n}\\ y_{n} \end{array} \right) , \left( \begin{array}{c} x^{'}_{n}\\ y^{'}_{n} \end{array} \right)

に対してアフィン変換

(xnyn1)=(abtxcdty001)(xnyn1)\left( \begin{array}{c} x^{'}_{n}\\ y^{'}_{n}\\ 1 \end{array} \right) = \left( \begin{array}{ccc} a & b & t_{x}\\ c & d & t_{y}\\ 0 & 0 & 1 \end{array} \right) \left( \begin{array}{c} x_{n}\\ y_{n}\\ 1 \end{array} \right)

を展開すると

xn=axn+byn+txyn=cxn+dyn+ty\begin{aligned} x^{'}_{n}&=ax_{n} + by_{n} + t_{x}\\ y^{'}_{n}&=cx_{n} + dy_{n} + t_{y} \end{aligned}

になります。

w1=(abtx),w2=(cdty),pn=(xnyn1),pn=(xnyn1)\boldsymbol{w}_{1}= \left( \begin{array}{c} a\\ b\\ t_{x} \end{array} \right) , \boldsymbol{w}_{2}= \left( \begin{array}{c} c\\ d\\ t_{y} \end{array} \right) , \boldsymbol{p}_{n}= \left( \begin{array}{c} x_{n} & y_{n} & 1 \end{array} \right) , \boldsymbol{p}^{'}_{n}= \left( \begin{array}{c} x^{'}_{n} & y^{'}_{n} & 1 \end{array} \right)

というようなベクトルを用意すれば

xn=pnw1yn=pnw2\begin{aligned} x^{'}_{n}&=\boldsymbol{p}_{n}\boldsymbol{w}_{1}\\ y^{'}_{n}&=\boldsymbol{p}_{n}\boldsymbol{w}_{2} \end{aligned}

と書けます。変換先の座標とアフィン変換による変換後の座標の距離を誤差関数にして、誤差関数が一番小さくなる時の w1\boldsymbol{w}_{1}w2\boldsymbol{w}_{2}を求めます。 誤差関数EE

E=n=1N((xnpnw1)2+(ynpnw2)2)E=\sum_{n=1}^{N} \Bigl( (x^{'}_{n} - \boldsymbol{p}_{n}\boldsymbol{w}_{1})^{2} + (y^{'}_{n} - \boldsymbol{p}_{n}\boldsymbol{w}_{2})^{2} \Bigr)

に設定して、これを行列形式で表すために

x=(x1xN),y=(y1yN),P=(p1pN)=(x1y21xNyN1)\boldsymbol{x}^{'} = \left( \begin{array}{c} x^{'} _{1}\\ \vdots\\ x^{'} _{N} \end{array} \right) , \boldsymbol{y}^{'} = \left( \begin{array}{c} y^{'} _{1}\\ \vdots\\ y^{'} _{N} \end{array} \right) , \boldsymbol{P}= \left( \begin{array}{c} \boldsymbol{p}_{1}\\ \vdots\\ \boldsymbol{p}_{N} \end{array} \right) = \left( \begin{array}{c} x_{1} & y_{2} & 1\\ &\vdots&\\ x_{N} & y_{N} & 1 \end{array} \right)

とすれば

E=(xPw1)T(xPw1)+(yPw2)T(yPw2)E= (\boldsymbol{x}^{'} - \boldsymbol{P}\boldsymbol{w}_{1})^{T}(\boldsymbol{x}^{'} - \boldsymbol{P}\boldsymbol{w}_{1}) + (\boldsymbol{y}^{'} - \boldsymbol{P}\boldsymbol{w}_{2})^T(\boldsymbol{y}^{'} - \boldsymbol{P}\boldsymbol{w}_{2})

と書けます。展開すると

E=(xT(Pw1)T)(xPw1)+(yT(Pw2)T)(yPw2)=xTxxTPw1(Pw1)Tx+(Pw1)T(Pw1)+yTyyTPw2(Pw2)Ty+(Pw2)T(Pw2)=xTxw1TPTxw1TPTx+w1TPTPw1+yTyw2TPTyw2TPTy+w2TPTPw2=xTx2w1TPTx+w1TPTPw1+yTy2w2TPTy+w2TPTPw2\begin{aligned} E&=({\boldsymbol{x}^{'} }^{T} - (\boldsymbol{P}\boldsymbol{w}_{1})^{T})(\boldsymbol{x}^{'} - \boldsymbol{P}\boldsymbol{w}_{1}) + ({\boldsymbol{y}^{'} }^{T} - (\boldsymbol{P}\boldsymbol{w}_{2})^{T})(\boldsymbol{y}^{'} -\boldsymbol{P}\boldsymbol{w}_{2})\\ &={\boldsymbol{x}^{'} }^{T}\boldsymbol{x}^{'} - {\boldsymbol{x}^{'} }^{T}\boldsymbol{P}\boldsymbol{w}_{1} - (\boldsymbol{P}\boldsymbol{w}_{1})^{T}\boldsymbol{x}^{'} + (\boldsymbol{P}\boldsymbol{w}_{1})^{T}(\boldsymbol{P}\boldsymbol{w}_{1}) + {\boldsymbol{y}^{'} }^{T}\boldsymbol{y}^{'} - {\boldsymbol{y}^{'} }^{T}\boldsymbol{P}\boldsymbol{w}_{2} - (\boldsymbol{P}\boldsymbol{w}_{2})^{T}\boldsymbol{y}^{'} + (\boldsymbol{P}\boldsymbol{w}_{2})^{T}(\boldsymbol{P}\boldsymbol{w}_{2})\\ &={\boldsymbol{x}^{'} }^{T}\boldsymbol{x}^{'} - \boldsymbol{w}^{T}_{1}\boldsymbol{P}^{T} \boldsymbol{x}^{'} - \boldsymbol{w}^{T}_{1}\boldsymbol{P}^{T} \boldsymbol{x}^{'} + \boldsymbol{w}^{T}_{1}\boldsymbol{P}^{T}\boldsymbol{P}\boldsymbol{w}_{1} + {\boldsymbol{y}^{'} }^{T}\boldsymbol{y}^{'} - \boldsymbol{w}^{T}_{2}\boldsymbol{P}^{T} \boldsymbol{y}^{'} - \boldsymbol{w}^{T}_{2}\boldsymbol{P}^{T} \boldsymbol{y}^{'} + \boldsymbol{w}^{T}_{2}\boldsymbol{P}^{T}\boldsymbol{P}\boldsymbol{w}_{2}\\ &={\boldsymbol{x}^{'} }^{T}\boldsymbol{x}^{'} - 2\boldsymbol{w}^{T}_{1}\boldsymbol{P}^{T} \boldsymbol{x}^{'} + \boldsymbol{w}^{T}_{1}\boldsymbol{P}^{T}\boldsymbol{P}\boldsymbol{w}_{1} + {\boldsymbol{y}^{'} }^{T}\boldsymbol{y}^{'} - 2\boldsymbol{w}^{T}_{2}\boldsymbol{P}^{T} \boldsymbol{y}^{'} + \boldsymbol{w}^{T}_{2}\boldsymbol{P}^{T}\boldsymbol{P}\boldsymbol{w}_{2}\\ \end{aligned}

となります。ここでは

xTPw1=((Pw1)Tx)T=(w1TPTx)T=w1TPTxxTPw2=((Pw2)Tx)T=(w2TPTx)T=w2TPTx\begin{aligned} {\boldsymbol{x}^{'} }^{T}\boldsymbol{P}\boldsymbol{w}_{1} &= \left ( (\boldsymbol{P}\boldsymbol{w}_1)^{T}\boldsymbol{x}^{'} \right )^{T} = (\boldsymbol{w}^{T}_{1} \boldsymbol{P}^{T} \boldsymbol{x}^{'})^{T} = \boldsymbol{w}^{T}_{1} \boldsymbol{P}^{T} \boldsymbol{x}^{'} \\ {\boldsymbol{x}^{'} }^{T}\boldsymbol{P}\boldsymbol{w}_{2} &= \left ( (\boldsymbol{P}\boldsymbol{w}_2)^{T}\boldsymbol{x}^{'} \right )^{T} = (\boldsymbol{w}^{T}_{2} \boldsymbol{P}^{T} \boldsymbol{x}^{'})^{T} = \boldsymbol{w}^{T}_{2} \boldsymbol{P}^{T} \boldsymbol{x}^{'} \end{aligned}

を用いました。w1TPTx\boldsymbol{w}^{T}_{1} \boldsymbol{P}^{T} \boldsymbol{x}^{'}1×11\times 1行列になることから(w1TPTx)T=w1TPTx(\boldsymbol{w}^{T}_{1} \boldsymbol{P}^{T} \boldsymbol{x}^{'})^{T} = \boldsymbol{w}^{T}_{1} \boldsymbol{P}^{T} \boldsymbol{x}^{'}が成り立ちます。そして、w1\boldsymbol{w}_{1}w2\boldsymbol{w}_{2}で偏微分してEEが小さくなる時を求めると

Ew1=2PTx+2PTPw1=02PTPw1=2PTxw1=(PTP)1PTxEw2=2PTy+2PTPw2=02PTPw2=2PTyw2=(PTP)1PTy\begin{aligned} \frac{\partial E}{\partial \boldsymbol{w}_{1}}=-2\boldsymbol{P}^{T}\boldsymbol{x}^{'} + 2\boldsymbol{P}^{T}\boldsymbol{P}\boldsymbol{w}_{1}&=0\\ 2\boldsymbol{P}^{T}\boldsymbol{P}\boldsymbol{w}_{1}&=2\boldsymbol{P}^{T}\boldsymbol{x}^{'} \\ \boldsymbol{w}_{1}&=(\boldsymbol{P}^{T}\boldsymbol{P})^{-1}\boldsymbol{P}^{T}\boldsymbol{x}^{'} \\ \frac{\partial E}{\partial \boldsymbol{w}_{2}}=-2\boldsymbol{P}^{T}\boldsymbol{y}^{'} + 2\boldsymbol{P}^{T}\boldsymbol{P}\boldsymbol{w}_{2}&=0\\ 2\boldsymbol{P}^{T}\boldsymbol{P}\boldsymbol{w}_{2}&=2\boldsymbol{P}^{T}\boldsymbol{y}^{'} \\ \boldsymbol{w}_{2}&=(\boldsymbol{P}^{T}\boldsymbol{P})^{-1}\boldsymbol{P}^{T}\boldsymbol{y}^{'} \end{aligned}

これによりw1\boldsymbol{w}_{1}w2\boldsymbol{w}_{2}を求められたのでアフィン行列は

A=(w1Tw2T001)A= \left( \begin{array}{c} &\boldsymbol{w}_{1}^{T} &\\ &\boldsymbol{w}_{2}^{T} &\\ 0&0&1 \end{array} \right)

として求められました。

実装

Pythonで実装して終わります。

import numpy as np
import math
from PIL import Image
from matplotlib import pyplot as plt


# 参照画像の範囲超えたやつは配列の最後を参照するようにする関数
def clip_xy(ref_xy, img_shape):
    # x座標について置換
    ref_x = np.where((0 \<= ref_xy[:, 0]) & (ref_xy[:, 0] \< img_shape[1]), ref_xy[:, 0], -1)
    # y座標について置換
    ref_y = np.where((0 \<= ref_xy[:, 1]) & (ref_xy[:, 1] \< img_shape[0]), ref_xy[:, 1], -1)

    # 結合して返す
    return np.vstack([ref_x, ref_y]).T


# アフィン変換
def affine(data, affine, draw_area_size):
    # data:アフィン変換させる画像データ
    # affine:アフィン行列
    #:draw_area_size:dataのshapeと同じかそれ以上がいいかも

    # アフィン行列の逆行列
    inv_affine = np.linalg.inv(affine)

    x = np.arange(0, draw_area_size[1], 1)
    y = np.arange(0, draw_area_size[0], 1)
    X, Y = np.meshgrid(x, y)

    XY = np.dstack([X, Y, np.ones_like(X)])
    xy = XY.reshape(-1, 3).T

    # 参照座標の計算
    ref_xy = inv_affine @ xy
    ref_xy = ref_xy.T

    # 参照座標の周りの座標
    liner_xy = {}
    liner_xy['downleft'] = ref_xy[:, :2].astype(int)
    liner_xy['upleft'] = liner_xy['downleft'] + [1, 0]
    liner_xy['downright'] = liner_xy['downleft'] + [0, 1]
    liner_xy['upright'] = liner_xy['downleft'] + [1, 1]

    # 線形補間での重み計算
    liner_diff = ref_xy[:, :2] - liner_xy['downleft']

    liner_weight = {}
    liner_weight['downleft'] = (1 - liner_diff[:, 0]) * (1 - liner_diff[:, 1])
    liner_weight['upleft'] = (1 - liner_diff[:, 0]) * liner_diff[:, 1]
    liner_weight['downright'] = liner_diff[:, 0] * (1 - liner_diff[:, 1])
    liner_weight['upright'] = liner_diff[:, 0] * liner_diff[:, 1]

    # 重み掛けて足す
    liner_with_weight = {}
    for direction in liner_weight.keys():
        l_xy = liner_xy[direction]
        l_xy = clip_xy(l_xy, data.shape)
        l_xy = np.dstack([l_xy[:, 0].reshape([draw_area_size[0], draw_area_size[1]]), l_xy[:, 1].reshape([draw_area_size[0], draw_area_size[1]])])
        l_weight = liner_weight[direction].reshape([draw_area_size[0], draw_area_size[1]])
        l_weight = np.repeat(np.expand_dims(l_weight, axis=-1), 4, axis=-1) # chanel 方向にコピー
        liner_with_weight[direction] = data[l_xy[:, :, 1], l_xy[:, :, 0], :] * l_weight

    data_linear = sum(liner_with_weight.values())
    return np.ceil(data_linear).astype(np.int64)


# 特徴点からアフィン行列求める関数
def registration(P, x_dash, y_dash):
    w1 = np.linalg.inv(P.T @ P) @ P.T @ x_dash
    w2 = np.linalg.inv(P.T @ P) @ P.T @ y_dash
    affine_matrix = np.array([[1.0, 0.0, 0.0],
                              [0.0, 1.0, 0.0],
                              [0.0, 0.0, 1.0]])
    affine_matrix[0, :] = w1
    affine_matrix[1, :] = w2
    return affine_matrix


# クリックした特徴点保存する配列
future_points1 = np.array([[1, 1]])
future_points2 = np.array([[1, 1]])
count_fp1 = 0
count_fp2 = 0


# クリックで特徴点決める
def onclick(event):
    global future_points1
    global future_points2
    global count_fp1
    global count_fp2

    click_axes = event.inaxes
    x = math.floor(event.xdata)
    y = math.floor(event.ydata)
    if click_axes == ax1:
        if count_fp1 == 0:
            future_points1[0, :] = (x, y)
            count_fp1 = 1
        else:
            future_points1 = np.vstack([future_points1, np.array([x, y])])
            count_fp1 += count_fp1
    if click_axes == ax2:
        if count_fp2 == 0:
            future_points2[0, :] = (x, y)
            count_fp2 = 1
        else:
            future_points2 = np.vstack([future_points2, np.array([x, y])])
            count_fp2 += count_fp2
    click_axes.scatter(x, y)
    fig.canvas.draw_idle()


# エンターを押すと画像重ね合わせ
def onEnter(event):
    if event.key == 'enter' and future_points1.size == future_points2.size and future_points1.size >= 3:
        # P:変換元の座標行列([[x,y,1],[x,y,1],...]
        # x_dash:変換先のx座標ベクトル
        # y_dash:変換先のy座標ベクトル
        vec_one = np.ones((future_points2.shape[0], 1))
        P = np.hstack([future_points2, vec_one])
        x_dash = future_points1[:, 0]
        y_dash = future_points1[:, 1]
        affine_matrix = registration(P, x_dash, y_dash)
        # アフィン変換後の画像求める
        affined_image = affine(image2, affine_matrix, image1.shape)
        ax3.imshow(affined_image)
        ax4.imshow(affined_image, alpha=0.5)
        fig.canvas.draw_idle()



# 画像読み込み
image1_path = 'images/Lenna.bmp'
image2_path = 'images/Mandrill.bmp'
image1 = Image.open(image1_path)
image1.putalpha(255)
image1 = np.array(image1)
image2 = Image.open(image2_path)
image2.putalpha(255)
image2 = np.array(image2)

# 画像の最後にbg_colorの色追加
bg_color = np.array([[[255,255,255,0]]])
image2 = np.hstack([image2, bg_color * np.ones((image2.shape[0], 1, 1), int)])
image2 = np.vstack([image2, bg_color * np.ones((1, image2.shape[1], 1), int)])

x_image1 = np.arange(0, image1.shape[1], 1)
y_image1 = np.arange(0, image1.shape[0], 1)

X1, Y1 = np.meshgrid(x_image1, y_image1)

x_image2 = np.arange(0, image2.shape[1], 1)
y_image2 = np.arange(0, image2.shape[0], 1)

X2, Y2 = np.meshgrid(x_image2, y_image2)

fig = plt.figure(figsize=(8, 8))
ax1 = fig.add_subplot(221)
ax1.set_title("image1")
mesh1 = ax1.imshow(image1)
ax2 = fig.add_subplot(223)
ax2.set_title("image2")
mesh2 = ax2.imshow(image2)
ax3 = fig.add_subplot(222)
ax3.set_title("paste")
mesh3 = ax3.imshow(image1)
ax4 = fig.add_subplot(224)
ax4.set_title("alpha blend")
mesh4 = ax4.imshow(image1, alpha=0.5)

cid = fig.canvas.mpl_connect('button_press_event', onclick)
cid = fig.canvas.mpl_connect('key_press_event', onEnter)
plt.show()