前言
在前面做换脸的博客中提到了使用仿射变换和透视变换将两张不同的人脸基于关键点进行对齐,保证一张人脸贴到另一张人脸时,大小完全一致;所以有必要理解一下这两个概念的区别,由于以实用性为目的,所以所有的图像算法都会以opencv
为例,去探索其用法。
国际惯例,参考博客:
opencv
中的warpAffine
opencv
中的warpPerspective
【opencv实践】仿射变换和透视变换
仿射变换与透视变换区别
图像处理的仿射变换与透视变换
Affine and Projective Transformations
OpenCV Transformationmatrix: affine vs. perspective warping
点乘即投影向量
【TensorFlow-windows】扩展层之STN
理论
在做数据增强的时候,图像里面有很多几何变换,比如旋转、平移、缩放、拉伸等,但是他们的本质还是通过某个矩阵,将图像每个像素点的坐标变换到另一个新的位置。这种通过某个矩阵将图像进行变换的方法通常称为线性变换,也就是说利用了向量加法和标量乘法。
【注】本文的变换仅仅针对二维矩阵,非针对三维矩阵的变换。
仿射变换其实是透视变换的一种特殊形式,他俩都可以用下面这个矩阵表示
M=[a1a2b1a3a4b2c1c21]M=\begin{bmatrix} a_1&a_2&b_1\\ a_3&a_4&b_2\\ c_1&c_2&1 \end{bmatrix} M=⎣⎡a1a3c1a2a4c2b1b21⎦⎤
用此矩阵将(x,y)(x,y)(x,y)变换到新的坐标点就是
[x′y′1]=[a1a2b1a3a4b2c1c21][xy1]\begin{bmatrix} x'\\ y'\\ 1 \end{bmatrix} =\begin{bmatrix} a_1&a_2&b_1\\ a_3&a_4&b_2\\ c_1&c_2&1 \end{bmatrix}\begin{bmatrix} x\\ y\\ 1 \end{bmatrix} ⎣⎡x′y′1⎦⎤=⎣⎡a1a3c1a2a4c2b1b21⎦⎤⎣⎡xy1⎦⎤
学过线性代数就知道,这里面
- [a1a2a3a4]\begin{bmatrix} a_1&a_2\\ a_3&a_4 \end{bmatrix}[a1a3a2a4]用来处理旋转和缩放,比如有个点是[2,3][2,3][2,3],经过[2002]\begin{bmatrix} 2&0\\ 0&2 \end{bmatrix}[2002]矩阵变换后就成了[4,6][4,6][4,6]即被放大了四倍,而经过[0110]\begin{bmatrix} 0&1\\ 1&0 \end{bmatrix}[0110]就变成了[3,2][3,2][3,2]即从原来的[2,3][2,3][2,3]坐标点旋转到了[3,2][3,2][3,2]坐标点。
- [b1b2]\begin{bmatrix} b_1\\ b_2 \end{bmatrix}[b1b2]这个就显而易见是平移
- [c1,c2][c_1,c_2][c1,c2]是投影向量,因为点乘就是c1x+c2yc_1x+c_2yc1x+c2y,刚好代表一个向量在另一个向量的投影
投影变换(projective transformation
)展示的是当观察者视角变化以后,观察体的变化情况,通常用于产生透视畸变(perspective distortion
),有时候称为透视变换(perspective transformation
)
仿射变换(affine transformation
)用于缩放(scaling
)、拉伸(skew
)、旋转(rotation
)
注意的点:
- 两个变换都是将直线投影到直线
- 两条平行直线通过仿射变换后依旧是两条平行的直线
- 两条平行直线通过透视变换后可以是两条相交的直线
从数学上来讲,它俩的区别在变换矩阵的最后一行[c1,c2][c_1,c_2][c1,c2]的值上,仿射变换是0值,而透视变换通常不是。所以这一点也能说明仿射变换是透视变换的子集。
但是有一个要求,变换矩阵一定不能是奇异矩阵,因为奇异矩阵会导致AX=bAX=bAX=b有无穷解或者无解,也就是说会出现多个点变换到同一个点的情况。
变换公式
根据OpenCV
中所述:
仿射变换的变换公式为:
dst(x,y)=src(M11x+M12y+M13,M21x+M22y+M23)\texttt{dst} (x,y) = \texttt{src} ( \texttt{M} _{11} x + \texttt{M} _{12} y + \texttt{M} _{13}, \texttt{M} _{21} x + \texttt{M} _{22} y + \texttt{M} _{23}) dst(x,y)=src(M11x+M12y+M13,M21x+M22y+M23)
透视变换变换公式为:
dst(x,y)=src(M11x+M12y+M13M31x+M32y+M33,M21x+M22y+M23M31x+M32y+M33)\texttt{dst} (x,y) = \texttt{src} \left ( \frac{M_{11} x + M_{12} y + M_{13}}{M_{31} x + M_{32} y + M_{33}} , \frac{M_{21} x + M_{22} y + M_{23}}{M_{31} x + M_{32} y + M_{33}} \right )dst(x,y)=src(M31x+M32y+M33M11x+M12y+M13,M31x+M32y+M33M21x+M22y+M23)
代码实践
使用opencv
测试效果
仿射变换
使用warpAffine
函数,将图片旋转45度,同时向右平移300像素,向下平移100像素
#仿射变换
degree=np.deg2rad(45)
M1=np.array([[np.cos(degree),-np.sin(degree),300],[np.sin(degree),np.cos(degree),100]
])
dst1 = cv2.warpAffine(img,M1,(img.shape[1]*2,img.shape[0]*2))plt.figure(figsize=(8,8))
plt.subplot(121)
plt.imshow(img)
plt.subplot(122)
plt.imshow(dst1)
透视变换
使用warpPerspective
函数
如果将透视变换使用上面的仿射变换矩阵,补齐第三行,可以得到和仿射变换一样的结果
#透视变换
degree=np.deg2rad(45)
M2=np.array([[np.cos(degree),-np.sin(degree),300],[np.sin(degree),np.cos(degree),100],[0,0,1]
])
dst2 = cv2.warpPerspective(img,M2,(img.shape[1]*2,img.shape[0]*2))plt.figure(figsize=(8,8))
plt.subplot(121)
plt.imshow(img)
plt.subplot(122)
plt.imshow(dst2)
一旦稍微改变投影向量,也就是第三行的值,就会发生很大的变化
#透视变换
degree=np.deg2rad(45)
M2=np.array([[np.cos(degree),-np.sin(degree),300],[np.sin(degree),np.cos(degree),100],[0,-0.0015,1]
])
dst2 = cv2.warpPerspective(img,M2,(img.shape[1]*2,img.shape[0]*2))plt.figure(figsize=(8,8))
plt.subplot(121)
plt.imshow(img)
plt.subplot(122)
plt.imshow(dst2)
所以我们通常能够通过仿射变换矩阵思考出变换后的样子,但是透视变换却很难预测出变换后的样子。
理论扩展
上面说过仿射变换是特殊的透视变换,后者变换矩阵的第3行c1,c2c_1,c_2c1,c2为0的时候就变成了前者。
为了让变换可控,我们可以预先构建某些点来规定变换矩阵的映射是什么样的,依据变换矩阵能看出参数量:透视变换的矩阵为8个参数,仿射变换矩阵为6个参数。
根据线性代数,如果需要
- 求解仿射变换矩阵:6个未知数需要6个方程,即需要3组对应点
- 求解透视变换矩阵:8个未知数需要8个方程,即需要4组对应点
所以比如想把原图变成平行四边形时,可以平行四边形上的三个点求解仿射变换:
#获取仿射变换矩阵
src_pts = np.float32([[0,0],[0,1],[1,1]])
dst_pts = np.float32([[0,0],[1,1],[2,1]])
M = cv2.getAffineTransform(src_pts,dst_pts)
dst1 = cv2.warpAffine(img,M,(img.shape[1]*2,img.shape[0]*2))plt.figure(figsize=(8,8))
plt.subplot(121)
plt.imshow(img)
plt.subplot(122)
plt.imshow(dst1)
想把原图变成直角梯形时,可以使用直角梯形上的四个点求解透视变换
#获取透视变换矩阵
src_pts = np.float32([[0,0],[0,300],[400,300],[400,0]])
dst_pts = np.float32([[0,0],[0,300],[200,300],[400,0]])
M = cv2.getPerspectiveTransform(src_pts,dst_pts)
dst2 = cv2.warpPerspective(img,M,(img.shape[1]*2,img.shape[0]*2))plt.figure(figsize=(8,8))
plt.subplot(121)
plt.imshow(img)
plt.subplot(122)
plt.imshow(dst2)
总结
其实就是对图像处理的一些基本知识补充,在之前写过的换脸博客1和博客2中有用到相关理论。
博客和公众号致力于图像、机器学习、运动捕捉方向的理论和代码实践,注重基础和实践,有兴趣可关注一波,代码通常公布在公众号中的github
网址