近年の深層学習の広がりで顔検出や顔認識の深層学習モデルもたくさん提案されるようになりました。今回はArcFaceなどに代表される顔認識モデルの入力画像を作る際に必要となる画像の正規化処理について書きます。
ArcFaceを例にすると、モデルの入力は顔の写った画像です。この時入力する画像は正規化された顔の画像でなくてはいけません。ここで言う正規化とは顔の大きさや位置が常に一定に揃うようにすることを意味します。例えば大きすぎる顔や小さすぎる顔を一定の大きさにしたり、画像の右寄りに写った顔を中央に移動させたり、首をかしげて斜めに写る顔を垂直に回転させたりといった処理をします。
いくつか実際の画像で例を示します。一番左側の画像に対して正規化を行うと一番左側の画像のように顔の大きさ、位置、角度が揃うように変換されます。
ではどうやってこの正規化処理をやっているかというと、入力となる画像の顔に特徴点を紐付けて、それを決まった位置に合うように変換します。
より詳しく述べると顔の目や鼻、口などの特徴のある座標を扱い、入力画像のこれらの座標をもとに拡大や移動量、回転角度などを算出します。私が参考にした資料では特徴のある座標として右目、左目、鼻、口の右端、口の左端の5箇所を使用していました。この座標はランドマークや器官点とも呼ばれます。以降これら5箇所に設定された座標をランドマーク座標と呼ぶことにします。
次に変換後に理想的な大きさ、位置、角度になるランドマーク座標をテンプレートとして持ちます。そして入力画像のランドマーク座標からこのテンプレートのランドマーク座標に合うように変換します。
ランドマーク座標に合わせる変換をするためには相似変換(Similarity Transform)を使用します。画像の幾何学的変換としてはアフィン変換が有名で、アフィン変換では画像を平行移動、拡大縮小、回転、スキュー(せん断)に対応した変換を行うことができます。相似変換とアフィン変換の違いとして相似変換はスキューを表すことができません。よってアフィン変換の方がより変換の自由度が大きいのですが顔認識の前の正規化では相似変換が使われることが多いようです。スキューの変換は必要ないということなのでしょう。
アフィン変換などの幾何学的変換についてはこれらのサイトが参考になります。
これらのサイトを参考に相似変換を定式化しておきます。
変換前の画素の位置を\( (x, y) \) , 変換後の画素の位置を \( (x’, y’) \) として、画像を縦と横共に \(s\) 倍に拡大(縮小)してから原点を中心に反時計回りに \( \theta \) ラジアン回転させ、X軸方向に \( t_x \) と Y軸方向に \( t_y \) だけ平行移動させる変換は次のように表せます。
$$ \begin{bmatrix} x’ \\ y’ \\ 1 \end{bmatrix} = \begin{bmatrix} s \cos \theta & -s \sin \theta & t_x \\ s \sin \theta & s \cos \theta & t_y \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} $$
変換行列の要素に着目すると左上の2行2列は符号の違いはあれど2つの成分からなっており、3行目の(0, 0, 1)は定数です。つまり変数は4つあれば表現できることになります。ここで変換行列を4要素で表してみます。
$$ M = \begin{bmatrix} s \cos \theta & -s \sin \theta & t_x \\ s \sin \theta & s \cos \theta & t_y \\ 0 & 0 & 1 \end{bmatrix} = \begin{bmatrix} a & -b & c \\ b & a & d \\ 0 & 0 & 1 \end{bmatrix} $$
ここで \(a, b, c, d \) は下記の通りとします。
$$ \begin{eqnarray*} a & = & s \cos \theta \\ b & = & s \sin \theta \\ c & = & t_x \\ d & = & t_y \\ \end{eqnarray*} $$
次に変換行列を求める方法を考えます。変換行列を用いた相似変換では、入力画像のランドマーク座標を変換してテンプレートのランドマーク座標に合うようにするのでした。なので入力画像とテンプレートのランドマーク座標の対応から変換行列を求めます。変換行列の成分は4つですが、ランドマークの対応は5個の座標値でそれぞれXの座標値とYの座標値を持つので合計10個になります。
この10個の対応から最小二乗法を用いて変換行列の4成分を求めます。最小二乗法のために先程の相似変換の式を少し変換します。
前述の相似変換の式に入力画像のランドマーク座標 \(x_1, y_1, x_2, y_2, …, x_5, y_5 \) とテンプレートのランドマーク座標 \(x’_1, y’_1, x’_2, y’_2, …, x’_5, y’_5 \) を当てはめると次のようになります。
$$
\begin{eqnarray*}
\begin{bmatrix} x’ \\ y’ \\ 1 \end{bmatrix} & = & \begin{bmatrix} a & -b & c \\ b & a & d \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} \\
\begin{bmatrix}
x’_1 & x’_2 & \dots & x’_5 \\
y’_1 & y’_2 & … & y’_5 \\
1 & 1 & … & 1
\end{bmatrix} & = & \begin{bmatrix} a & -b & c \\ b & a & d \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix}
x_1 & x_2 & … & x_5 \\
y_1 & y_2 & … & y_5 \\
1 & 1 & … & 1
\end{bmatrix}
\end{eqnarray*}
$$
そしてこれを10個の対応になるように変形します。
$$
\begin{bmatrix}
x’_1 \\ x’_2 \\ \vdots \\ x’_5 \\ y’_1 \\ y’_2 \\ \vdots \\ y’_5
\end{bmatrix} = \begin{bmatrix}
x_1 & -y_1 & 1 & 0 \\
x_2 & -y_2 & 1 & 0 \\
\vdots & \vdots & \vdots & \vdots\\
x_5 & -y_5 & 1 & 0 \\
y_1 & x_1 & 0 & 1 \\
y_2 & x_2 & 0 & 1 \\
\vdots & \vdots & \vdots & \vdots\\
y_5 & x_5 & 0 & 1 \\
\end{bmatrix} \begin{bmatrix} a \\ b \\ c \\ d \end{bmatrix}
$$
ベクトルや行列の成分を毎回書くと大変なので次のようにおきます。ここで \( \boldsymbol{x}, \boldsymbol{b} \) はベクトルなので注意してください。ベクトルは太字で書くのでベクトルの \( \boldsymbol{b} \) とパラメータの \( b \) を区別してください。
$$
\begin{eqnarray*}
\boldsymbol{x} & = & \begin{bmatrix} a \\ b \\ c \\ d \end{bmatrix} \\
A & = & \begin{bmatrix}
x_1 & -y_1 & 1 & 0 \\
x_2 & -y_2 & 1 & 0 \\
\vdots & \vdots & \vdots & \vdots\\
x_5 & -y_5 & 1 & 0 \\
y_1 & x_1 & 0 & 1 \\
y_2 & x_2 & 0 & 1 \\
\vdots & \vdots & \vdots & \vdots\\
y_5 & x_5 & 0 & 1 \\
\end{bmatrix} \\
\boldsymbol{b} & = & \begin{bmatrix}
x’_1 \\ x’_2 \\ \vdots \\ x’_5 \\ y’_1 \\ y’_2 \\ \vdots \\ y’_5
\end{bmatrix}
\end{eqnarray*}
$$
これで先程の式を次のように書くことができます。
$$
\boldsymbol{b} = A \boldsymbol{x}
$$
これの誤差は次のように定義できます。
$$
\| A \boldsymbol{x} – \boldsymbol{b} \|
$$
今回のパラメータは \( \boldsymbol{x} \) ですのでこれを最小二乗法で最小化していきます。
最小二乗法についてはこちらのサイトが参考になります。以降の式は文字が多くてミスするかもしれないのでこのサイトの方が信頼できると思います。
さて、ではまず誤差を二乗にします。
$$
\| A \boldsymbol{x} – \boldsymbol{b} \|^2
$$
\( A \boldsymbol{x} – \boldsymbol{b} \) はベクトルになるので、ベクトルのノルムの性質 \( \| \boldsymbol{v} \| = \sqrt{ \boldsymbol{v} \cdot \boldsymbol{v} } = \sqrt{ \boldsymbol{v} ^\mathsf{T} \boldsymbol{v} } \) と 行列の転置の性質 \( ( C + D )^\mathsf{T} = C^\mathsf{T} + D^\mathsf{T} \) 、 \( ( C D )^\mathsf{T} = D^\mathsf{T} + C^\mathsf{T} \) より
$$
\begin{eqnarray*}
\| A \boldsymbol{x} – \boldsymbol{b} \|^2 & = & ( A \boldsymbol{x} – \boldsymbol{b} )^\mathsf{T} ( A \boldsymbol{x} – \boldsymbol{b} ) \\
& = & ( \boldsymbol{x}^\mathsf{T} A^\mathsf{T} – \boldsymbol{b}^\mathsf{T} ) ( A \boldsymbol{x} – \boldsymbol{b} ) \\
& = & \boldsymbol{x}^\mathsf{T} A^\mathsf{T} A \boldsymbol{x} – \boldsymbol{x}^\mathsf{T} A^\mathsf{T} \boldsymbol{b} – \boldsymbol{b}^\mathsf{T} A \boldsymbol{x} + \boldsymbol{b}^\mathsf{T} \boldsymbol{b} \\
\end{eqnarray*}
$$
ここで、 \( \boldsymbol{b}^\mathsf{T} A \boldsymbol{x} \) はスカラーとなるのでスカラーを転置させても値が変わらないことを利用して、
$$
\begin{eqnarray*}
\boldsymbol{b}^\mathsf{T} A \boldsymbol{x} & = & ( \boldsymbol{b}^\mathsf{T} A \boldsymbol{x} )^\mathsf{T} \\
& = & A \boldsymbol{x}^\mathsf{T} ( \boldsymbol{b}^\mathsf{T} )^\mathsf{T} \\
& = & \boldsymbol{x}^\mathsf{T} A^\mathsf{T} \boldsymbol{b}
\end{eqnarray*}
$$
となり、先程の式をさらに次のように書くことができます。
$$
\begin{eqnarray*}
\| A \boldsymbol{x} – \boldsymbol{b} \|^2 & = & \boldsymbol{x}^\mathsf{T} A^\mathsf{T} A \boldsymbol{x} – \boldsymbol{x}^\mathsf{T} A^\mathsf{T} \boldsymbol{b} – \boldsymbol{b}^\mathsf{T} A \boldsymbol{x} + \boldsymbol{b}^\mathsf{T} \boldsymbol{b} \\
& = & \boldsymbol{x}^\mathsf{T} A^\mathsf{T} A \boldsymbol{x} – 2 \boldsymbol{x}^\mathsf{T} A^\mathsf{T} \boldsymbol{b} + \boldsymbol{b}^\mathsf{T} \boldsymbol{b} \\
\end{eqnarray*}
$$
これを \( \boldsymbol{x} \) で微分します。微分で対称行列 \( P \) に関する微分は \( \frac{d}{dx} \boldsymbol{x}^\mathsf{T} P \boldsymbol{x} = 2 P \boldsymbol{x} \) となり、 \( \frac{d}{dx} \boldsymbol{x}^\mathsf{T} \boldsymbol{v} = \boldsymbol{v} \) となることから、
$$
\frac{d}{dx} \boldsymbol{x}^\mathsf{T} A^\mathsf{T} A \boldsymbol{x} – 2 \boldsymbol{x}^\mathsf{T} A^\mathsf{T} \boldsymbol{b} + \boldsymbol{b}^\mathsf{T} \boldsymbol{b} =
2 A^\mathsf{T} A \boldsymbol{x} – 2 A^\mathsf{T} \boldsymbol{b}
$$
となり、誤差の関数の二乗 \( \| A \boldsymbol{x} – \boldsymbol{b} \|^2 \) が最小になるための必要条件である勾配がゼロになる式を得ることができます。そして今回求めたい \( \boldsymbol{x} \) についての式にすることで誤差が最小になるパラメータを求めることができました。
$$
\begin{eqnarray*}
\frac{d}{dx} \| A \boldsymbol{x} – \boldsymbol{b} \|^2 & = & 0 \\
2 A^\mathsf{T} A \boldsymbol{x} – 2 A^\mathsf{T} \boldsymbol{b} & = & 0 \\
A^\mathsf{T} A \boldsymbol{x} & = & A^\mathsf{T} \boldsymbol{b} \\
\boldsymbol{x} & = & ( A^\mathsf{T} A )^{-1} A^\mathsf{T} \boldsymbol{b} \\
\end{eqnarray*}
$$
注意としてここで、 \( A^\mathsf{T} A \) が正則である (逆行列を求めることができる) ことが条件になります。
さて、相似変換の変換行列を求める式を得ることができたので実際に計算してみたいと思います。実装にはPythonを使ってライブラリとしては行列演算用にNumPyと画像の扱いのためにOpenCVを利用します。
相似変換の例として次の画像を使います。画像の左側に写る人を今回の対象にします。そして5箇所のランドマーク座標を赤い点で描画したものが右側の画像です。
これをテンプレートのランドマーク座標に合うようにします。今回使用するテンプレートは次の画像のようにしました。
このテンプレートに合うように変換行列を求めていきます。ランドマーク座標を設定して実装したコードがこちらです。
import numpy as np
input_data = np.array((194,397,367,432,274,515,181,557,320,581)).reshape((5, 2))
target_data = np.array((320,351,684,351,508,552,342,714,695,714)).reshape((5, 2))
b = np.append(target_data[:,0], target_data[:,1])
A = np.zeros((10, 4))
tmp1 = np.ones(5)
A[0:5,2] = tmp1
A[5:10,3] = tmp1
A[0:5,0] = input_data[:,0]
A[5:10,0] = input_data[:,1]
A[0:5,1] = -1.0 * input_data[:,1]
A[5:10,1] = input_data[:,0]
x = np.dot(np.dot(np.linalg.inv(np.dot(A.T, A)), A.T), b)
M = np.zeros((3, 3))
M[0,0] = x[0]
M[0,1] = - x[1]
M[1,0] = x[1]
M[1,1] = x[0]
M[0,2] = x[2]
M[1,2] = x[3]
M[2,2] = 1
print(f"M=\n{M}")
np.savetxt('matrix.csv', M[:2,:], fmt='%.10f', delimiter=',')
これを実行します。
$ python3 calc_mat_update.py
M=
[[ 2.20445398 0.47002717 -312.55159296]
[ -0.47002717 2.20445398 -432.29969677]
[ 0. 0. 1. ]]
実行して求めた変換行列は次のようになりました。(少数第二位まで、三位以降は切り捨てした)
$$
M = \begin{bmatrix}
2.20 & 0.47 & -312.55 \\
-0.47 & 2.20 & -432.29 \\
0 & 0 & 1
\end{bmatrix}
$$
この変換行列を使用して入力画像を相似変換してみます。相似変換のためにはOpenCVのアフィン変換処理関数がそのまま使えます。このアフィン変換用の関数にさきほど求めた変換行列を入れます。
import cv2
import numpy as np
import sys
img = cv2.imread(sys.argv[1])
mat = np.loadtxt('matrix.csv', delimiter=',')
aff = cv2.warpAffine(img, mat, img.shape[:2])
cv2.imwrite('out.jpg', aff)
変換して得られた画像がこちらです。隣に並べたテンプレートのランドマークと比較しても良さそうな位置に顔があります。入力画像と比べると少し斜めになっていた顔が垂直になり意図した通りです。
他の画像を入力しても良さそうな結果が得られたので式の導出と実装は正しく動いているようです。
今回は顔の認識用AIモデルに関連して利用される画像の相似変換について式を追いながら理解し、Pythonで実装することで実際の画像でその動作を試すところまでを行いました。
「アフィン変換みたいな変換してるらしい」、や「最小二乗法って聞いたことあるしだいたい知ってるよ」みたいな浅い理解だったのが今回の作業でしっかりと理解できたと思うのでまたこのような式を扱う記事を書けたらいいなと思います。
コメント