人脸对齐是大多数人脸分析算法中的一个关键模块,在人脸识别、表情识别、人脸建模等领域有着广泛的应用。人脸对齐获取图像中人脸的几何结构,基于平移、缩放和旋转得到对齐后的标准人脸。
在欧式几何中,如果两个物体具有相同的形状,或者其中一个物体的形状与另一个物体的镜像相同,那么这两个物体是相似的。更准确地说,可以通过均匀缩放(放大或缩小)并叠加必要的平移、旋转和反射来获得另一个。这意味着任意物体都可以重新缩放、重新定位和反射,以便与另一物体精确重合。如果两个物体相似,则每个物体都与另一个物体的特定均匀缩放结果一致。
目前大多数人脸对齐方案在定位关键点后采用 Umeyama 对齐算法进行处理。例如 insightface 中的 similarTransform,dlib 中的 get_face_chip_details。相似性变换由平移变换、旋转变换以及尺度变换组合而成。下面以 skimage.transform.SimilarityTransform 为例进行介绍。
SimilarityTransform
2D 相似性变换。具有以下形式:
X = a0 * x - b0 * y + a1 =
= s * x * cos(rotation) - s * y * sin(rotation) + a1
Y = b0 * x + a0 * y + b1 =
= s * x * sin(rotation) + s * y * cos(rotation) + b1
其中s
是比例因子,齐次变换矩阵为:
[
a
0
b
0
a
1
b
0
a
0
b
1
0
0
1
]
\begin{bmatrix} a_0 & b_0 & a_1 \\ b_0 & a_0 & b_1 \\ 0 & 0 & 1 \end{bmatrix}
a0b00b0a00a1b11
除了旋转和平移参数外,相似性变换还使用单个缩放因子扩展了欧几里德变换。
参数:
matrix
:(3,3)数组,可选。齐次变换矩阵。scale
:float,可选。比例因子。rotation
:float,可选。逆时针旋转角度(以弧度表示)。translation
:(tx, ty)
作为数组、列表或元组,可选。x,y 方向平移参数。
属性:
params
:(3,3)数组,齐次变换矩阵。
[ s cos ( θ ) sin ( θ ) t x sin ( θ ) s cos ( θ ) t y 0 0 1 ] \begin{bmatrix} s \cos(\theta) & \sin(\theta) & t_x\\ \sin(\theta) & s\cos(\theta) & t_y\\ 0 & 0 & 1 \end{bmatrix} scos(θ)sin(θ)0sin(θ)scos(θ)0txty1
def __init__(self, matrix=None, scale=None, rotation=None,
translation=None):
params = any(param is not None
for param in (scale, rotation, translation))
if params and matrix is not None:
raise ValueError("You cannot specify the transformation matrix and"
" the implicit parameters at the same time.")
elif matrix is not None:
if matrix.shape != (3, 3):
raise ValueError("Invalid shape of transformation matrix.")
self.params = matrix
elif params:
if scale is None:
scale = 1
if rotation is None:
rotation = 0
if translation is None:
translation = (0, 0)
self.params = np.array([
[math.cos(rotation), - math.sin(rotation), 0],
[math.sin(rotation), math.cos(rotation), 0],
[ 0, 0, 1]
])
self.params[0:2, 0:2] *= scale
self.params[0:2, 2] = translation
else:
# default to an identity transform
self.params = np.eye(3)
estimate
从一组对应点估计变换。可以使用总体最小二乘法确定过定、确定和欠定的参数。源坐标和目标坐标的数量必须匹配。
参数:
src
:(N,2)数组,源坐标。dst
:(N,2)数组,目标坐标。
返回值:
success
:布尔型,如果模型估计成功,则返回True
。
self.params = _umeyama(src, dst, True)
return True
scale
@property
def scale(self):
if abs(math.cos(self.rotation)) < np.spacing(1):
# sin(self.rotation) == 1
scale = self.params[1, 0]
else:
scale = self.params[0, 0] / math.cos(self.rotation)
return scale
_umeyama
估算是否具有缩放比例的N-D相似度变换。
估计有或无标度的N-D相似变换。
参数:
src
:(M,N)数组,源坐标。dst
:(M,N)数组,目标坐标。estimate_scale
:布尔,是否估计比例因子。
返回值:
T
:(N + 1,N + 1),齐次相似性变化矩阵。仅当问题条件不完善时,矩阵才会包含NaN
值。
参考文献
- [1] “Least-squares estimation of transformation parameters between two point patterns”, Shinji Umeyama, PAMI 1991, :DOI:
10.1109/34.88573
μ
x
=
1
n
∑
i
=
1
n
x
i
(34)
μ
y
=
1
n
∑
i
=
1
n
y
i
(35)
σ
x
2
=
1
n
∑
i
=
1
n
∥
x
i
−
μ
x
∥
2
(36)
σ
y
2
=
1
n
∑
i
=
1
n
∥
y
i
−
μ
y
∥
2
(37)
Σ
x
y
=
1
n
∑
i
=
1
n
(
y
i
−
μ
y
)
(
x
i
−
μ
x
)
⊤
(38)
\begin{aligned} \mu_x &= \frac{1}{n}\sum_{i=1}^n x_i \qquad\qquad\qquad \text{(34)}\\ \mu_y &= \frac{1}{n}\sum_{i=1}^n y_i \qquad\qquad\qquad \text{(35)}\\ \sigma_x^2 &= \frac{1}{n}\sum_{i=1}^n \| x_i-\mu_x\|^2 \qquad\qquad \text{(36)}\\ \sigma_y^2 &= \frac{1}{n}\sum_{i=1}^n \| y_i-\mu_y\|^2 \qquad\qquad \text{(37)}\\ \Sigma_{xy} &= \frac{1}{n}\sum_{i=1}^n (y_i-\mu_y)(x_i-\mu_x)^\top \quad \text{(38)} \end{aligned}
μxμyσx2σy2Σxy=n1i=1∑nxi(34)=n1i=1∑nyi(35)=n1i=1∑n∥xi−μx∥2(36)=n1i=1∑n∥yi−μy∥2(37)=n1i=1∑n(yi−μy)(xi−μx)⊤(38)
Σ
x
y
\Sigma_{xy}
Σxy 是
X
X
X 和
Y
Y
Y 的协方差矩阵,
μ
x
\mu_x
μx 和
μ
y
\mu_y
μy 分别是
X
X
X 和
Y
Y
Y 的均值向量,
σ
x
2
\sigma_x^2
σx2 和
σ
y
2
\sigma_y^2
σy2 是
X
X
X 和
Y
Y
Y 的方差。
num
是点的数量,dim
为点的坐标维度。
num = src.shape[0]
dim = src.shape[1]
# Compute mean of src and dst.
src_mean = src.mean(axis=0)
dst_mean = dst.mean(axis=0)
# Subtract mean from src and dst.
src_demean = src - src_mean
dst_demean = dst - dst_mean
# Eq. (38).
A = dst_demean.T @ src_demean / num
对协方差矩阵 Σ x y \Sigma_{xy} Σxy 进行奇异值分解 U D V ⊤ UDV^\top UDV⊤,其中 D = d i a g ( d i ) , d 1 ≥ d 2 ≥ ⋯ ≥ d m ≥ 0 D=\mathrm{diag}(d_i), d_1 \ge d_2 \ge\cdots \ge d_m \ge 0 D=diag(di),d1≥d2≥⋯≥dm≥0。
S
=
{
I
if
d
e
t
(
Σ
x
y
)
≥
0
d
i
a
g
(
1
,
1
,
…
,
1
,
−
1
)
if
d
e
t
(
Σ
x
y
)
<
0
(39)
S = \begin{cases} I &\text{if } \mathrm{det}(\Sigma_{xy}) \ge 0 \\ \mathrm{diag}(1,1,\dots,1,-1) &\text{if } \mathrm{det}(\Sigma_{xy}) < 0 \end{cases}\quad \text{(39)}
S={Idiag(1,1,…,1,−1)if det(Σxy)≥0if det(Σxy)<0(39)
numpy.linalg.det 计算数组的行列式(determinant)。
numpy.linalg.svd 对数组进行奇异值分解。
S
对应分解的奇异值
d
i
a
g
(
d
i
)
\mathrm{diag}(d_i)
diag(di)。
d
为行向量,对应公式中的
S
S
S。
# Eq. (39).
d = np.ones((dim,), dtype=np.double)
if np.linalg.det(A) < 0:
d[dim - 1] = -1
T = np.eye(dim + 1, dtype=np.double)
U, S, V = np.linalg.svd(A)
numpy.linalg.matrix_rank 使用 SVD 方法返回数组的矩阵秩。
当
rank
(
Σ
x
y
)
=
m
\text{rank}{(\Sigma_{xy})}= m
rank(Σxy)=m 时,
S
=
{
I
if
d
e
t
(
U
)
d
e
t
(
V
)
=
1
d
i
a
g
(
1
,
1
,
…
,
1
,
−
1
)
if
d
e
t
(
U
)
d
e
t
(
V
)
=
−
1
(43)
S = \begin{cases} I &\text{if } \mathrm{det}(U)\mathrm{det}(V)=1 \\ \mathrm{diag}(1,1,\dots,1,-1) &\text{if } \mathrm{det}(U)\mathrm{det}(V)=-1 \end{cases} \quad \text{(43)}
S={Idiag(1,1,…,1,−1)if det(U)det(V)=1if det(U)det(V)=−1(43)
当
rank
(
Σ
x
y
)
>
m
\text{rank}{(\Sigma_{xy})}> m
rank(Σxy)>m 时,最变换参数为:
R
=
U
S
V
⊤
(40)
t
=
μ
y
−
c
R
μ
x
(41)
c
=
1
σ
x
2
t
r
(
D
S
)
(42)
\begin{aligned} R &= USV^\top \qquad \text{(40)}\\ t &= \mu_y - cR\mu_x \quad \text{(41)}\\ c &= \frac{1}{\sigma_x^2}\mathrm{tr}(DS) \quad \text{(42)} \end{aligned}
Rtc=USV⊤(40)=μy−cRμx(41)=σx21tr(DS)(42)
s
临时保存。
# Eq. (40) and (43).
rank = np.linalg.matrix_rank(A)
if rank == 0:
return np.nan * T
elif rank == dim - 1:
if np.linalg.det(U) * np.linalg.det(V) > 0:
T[:dim, :dim] = U @ V
else:
s = d[dim - 1]
d[dim - 1] = -1
T[:dim, :dim] = U @ np.diag(d) @ V
d[dim - 1] = s
else:
T[:dim, :dim] = U @ np.diag(d) @ V
- 首先计算尺度参数 c c c (公式42);
- 然后计算平移参数 t t t(公式41);
- 最后旋转参数 R R R 和尺度参数 c c c 融合到一起。
T
的形式为:
[
s
cos
(
θ
)
sin
(
θ
)
t
x
sin
(
θ
)
s
cos
(
θ
)
t
y
0
0
1
]
\begin{bmatrix} s \cos(\theta) & \sin(\theta) & t_x\\ \sin(\theta) & s\cos(\theta) & t_y\\ 0 & 0 & 1 \end{bmatrix}
scos(θ)sin(θ)0sin(θ)scos(θ)0txty1
if estimate_scale:
# Eq. (41) and (42).
scale = 1.0 / src_demean.var(axis=0).sum() * (S @ d)
else:
scale = 1.0
T[:dim, dim] = dst_mean - scale * (T[:dim, :dim] @ src_mean.T)
T[:dim, :dim] *= scale
return T
参考资料:
- What does the “at” (@) symbol do in Python?
- face alignment[Ordinary Procrustes Analysis]
- Similarity (geometry)
- How does dlib face aligment works? #1382
- 传统算法和深度学习的结合和实践,解读与优化 deepfake
- Umeyama算法
- similarity transform matrix in c++ is different from python #481
- C++ using
- 4.3 Planar Graphs
- Find All Cycles (Faces) In a Graph
- Chinese Whispers
- Chinese Whispers - an Efficient Graph Clustering Algorithm and its Application to Natural Language Processing Problems
- Applying Affine transform on an image using dlib
- dlib人脸关键点代码解析
- Get Face Landmarks
- dlib人脸对齐源码详解
- dlib 人脸对齐 基本原理
- 对mtcnn的人脸对齐的理解
- Review of similarity transformation and Singular Value Decomposition
- 图像的等距变换,相似变换,仿射变换,射影变换及其matlab实现
- skimage库的transform.SimilarityTransform()用法
- How to compute the similarity transformation matrix
- Average Face : OpenCV ( C++ / Python ) Tutorial
- SimilarityTransform
- Aligning Face Images
- Face Alignment
- face alignment algorithm on images
- dlib数据结构matrix
- Dlib源码解析之一 matrix和array2d和image_view
- Dlib Element Specific Operations
- maketform
- dlib.net/matrix_ex.cpp
- deepfakes/faceswap/lib/umeyama.py
- ethz-asl/maplab/test/end-to-end-common/python/end_to_end_common/umeyama.py
- CarloNicolini/ralign
- Singular Value Decomposition (SVD) tutorial