几何变换的原理大多都是相似,只是变换矩阵不同,因此,我们以最常用的平移和旋转为例进行学习。在深度学习领域,我们常用平移、旋转、镜像等操作进行数据增广;在传统CV领域,由于某些拍摄角度的问题,我们需要对图像进行矫正处理,而几何变换正是这个处理过程的基础,因此了解和学习几何变换也是有必要的。
这次我们带着几个问题进行,以旋转为例:
先看第一个问题,变换的形式。与OpencV不同的是这里采取冈萨雷斯的《数字图像处理_第三版》的变换矩阵方式,关于OpenCV的策略可以看它的官方文档。根据冈萨雷斯书中的描述,仿射变换的一般形式如下:
[ x y 1 ] = [ v w 1 ] T = [ v w 1 ] [ t 11 t 12 0 t 21 t 22 0 t 31 t 32 1 ] \left[\begin{array}{lll} x & y & 1 \end{array}\right]=\left[\begin{array}{lll} v & w & 1 \end{array}\right] \mathbf{T}=\left[\begin{array}{lll} v & w & 1 \end{array}\right]\left[\begin{array}{lll} t_{11} & t_{12} & 0 \\ t_{21} & t_{22} & 0 \\ t_{31} & t_{32} & 1 \end{array}\right] [xy1]=[vw1]T=[vw1]⎣⎡t11t21t31t12t22t32001⎦⎤
式中的T就是变换矩阵,其中 (v,w)为原坐标,(x,y) 为变换后的坐标,不同的变换对应不同的矩阵,这里也贴出来,一些常见的变换矩阵及作用如下表:
也就是说,我们根据自己的目的选择不同变换矩阵就可以了。
再看第二个问题,变换中心,对于缩放、平移可以以图像坐标原点(图像左上角为原点)为中心变换,这不用坐标系变换,直接按照一般形式计算即可。而对于旋转和偏移,一般是以图像中心为原点,那么这就涉及坐标系转换了。
我们都知道,图像坐标的原点在图像左上角,水平向右为 X 轴,垂直向下为 Y 轴。数学课本中常见的坐标系是以图像中心为原点,水平向右为 X 轴,垂直向上为 Y 轴,称为笛卡尔坐标系。看下图:
因此,对于旋转和偏移,就需要3步(3次变换):
先看下图:
在图像中我们的坐标系通常是AB和AC方向的,原点为A,而笛卡尔直角坐标系是DE和DF方向的,原点为D。
令图像表示为M×N的矩阵,对于点A而言,两坐标系中的坐标分别是(0,0)和(-N/2,M/2),则图像某像素点(x’,y’)转换为笛卡尔坐标(x,y)转换关系为,x为列,y为行:
x = x ′ − N / 2 y = − y ′ + M / 2 \begin{aligned} &x=x^{\prime}-N / 2\\ &y=-y^{\prime}+M / 2 \end{aligned} x=x′−N/2y=−y′+M/2
逆变换为:
x ′ = x + N / 2 y ′ = − y + M / 2 \begin{aligned} &\mathrm{x}^{\prime}=\mathrm{x}+\mathrm{N} / 2\\ &y^{\prime}=-y+M / 2 \end{aligned} x′=x+N/2y′=−y+M/2于是,根据前面说的3个步骤(3次变换),旋转(顺时针旋转)的变换形式就为,3次变换就有3个矩阵:
( x , y , 1 ) = ( x , y , 1 ) T (x, y, 1)=(x, y, 1) T (x,y,1)=(x,y,1)T
= ( x , y , 1 ) [ 1 0 0 0 − 1 0 − 0.5 ∗ N 0.5 ∗ M 1 ] [ cos ( θ ) − sin ( θ ) 0 sin ( θ ) cos ( θ ) 0 0 0 1 ] [ 1 0 0 0 − 1 0 0. 5 ∗ N 0. 5 ∗ M 1 ] =(x, y, 1)\left[\begin{array}{ccc} 1 & 0 & 0 \\ 0 & -1 & 0 \\ -0.5 * N & 0.5 * M & 1 \end{array}\right]\left[\begin{array}{ccc} \cos (\theta) & -\sin (\theta) & 0 \\ \sin (\theta) & \cos (\theta) & 0 \\ 0 & 0 & 1 \end{array}\right]\left[\begin{array}{ccc} 1 & 0 & 0 \\ 0 & -1 & 0 \\ 0.5^{*} N & 0.5^{*} M & 1 \end{array}\right] =(x,y,1)⎣⎡10−0.5∗N0−10.5∗M001⎦⎤⎣⎡cos(θ)sin(θ)0−sin(θ)cos(θ)0001⎦⎤⎣⎡100.5∗N0−10.5∗M001⎦⎤
看第3个问题,在冈萨雷斯的《数字图像处理_第三版》中说的很清楚,前向映射就是根据原图用变换公式直接算出输出图像相应像素的空间位置。那么这会导致一个问题:可能会有多个像素坐标映射到输出图像的同一位置,也可能输出图像的某些位置完全没有相应的输入图像像素与它匹配,也就是没有被映射到,造成有规律的空洞(黑色的蜂窝状)。更好的一种方式是采用 反向映射(Inverse Mapping):扫描输出图像的位置(x,y),通过
[ v , w , 1 ] = [ x , y , 1 ] ∗ T − 1 [v, w, 1]=[x, y, 1] * T^{-1} [v,w,1]=[x,y,1]∗T−1(为T的逆矩阵)计算输入图像对应的位置 (v,w),通过插值方法决定输出图像该位置的灰度值。
第4个问题,采用反向映射后,需通过插值方法决定输出图像该位置的值,因此需要选择插值算法。通常有最近邻插值、双线性插值,双三次插值等,OpencV默认采用双线性插值,我们也就采用双线性插值。
OpenCV仿射变换相关的函数一般涉及到warpAffine和getRotationMatrix2D这两个:
void cv::warpAffine(InputArray src,
OutputArray dst,
InputArray M,
Size dsize,
int flags = INTER_LINEAR,
int borderMode = BORDER_CONSTANT,
const Scalar &borderValue = Scalar())
参数:
第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可。
第二个参数,OutputArray类型的dst,函数调用后的运算结果存在这里,需和源图片有一样的尺寸和类型。
第三个参数,InputArray类型的M,2×3的变换矩阵。
第四个参数,Size类型的dsize,表示输出图像的尺寸。
第五个参数,int类型的flags,插值方法的标识符。此参数有默认值INTER_LINEAR(线性插值),可选的插值方式如下:
INTER_NEAREST - 最近邻插值
INTER_LINEAR - 线性插值(默认值)
INTER_AREA - 区域插值
INTER_CUBIC –三次样条插值
INTER_LANCZOS4 -Lanczos插值
CV_WARP_FILL_OUTLIERS - 填充所有输出图像的象素。如果部分象素落在输入图像的边界外,那么它们的值设定为 fillval.
CV_WARP_INVERSE_MAP –表示M为输出图像到输入图像的反变换,即 。因此可以直接用来做象素插值。否则, warpAffine函数从M矩阵得到反变换。
第六个参数,int类型的borderMode,边界像素模式,默认值为BORDER_CONSTANT。
第七个参数,const Scalar&类型的borderValue,在恒定的边界情况下取的值,默认值为Scalar(),即0。
2、getRotationMatrix2D函数详解
cv::Mat getRotationMatrix2D(Point2f center, double angle, double scale)
参数:
第一个参数,Point2f类型的center,表示源图像的旋转中心。
第二个参数,double类型的angle,旋转角度。角度为正值表示向逆时针旋转(坐标原点是左上角)。
第三个参数,double类型的scale,缩放系数。
cv::Mat src = cv::imread("lenna.jpg");
cv::Mat dst;
cv::Size dst_sz = src.size();
//定义平移矩阵
cv::Mat t_mat =cv::Mat::zeros(2, 3, CV_32FC1);
t_mat.at<float>(0, 0) = 1;
t_mat.at<float>(0, 2) = 20; //水平平移量
t_mat.at<float>(1, 1) = 1;
t_mat.at<float>(1, 2) = 10; //竖直平移量
//根据平移矩阵进行仿射变换
cv::warpAffine(src, dst, t_mat, dst_sz);
//显示平移效果
cv::imshow("image", src);
cv::imshow("result", dst);
cv::waitKey(0);
return 0;
cv::Mat src = cv::imread("sample.jpg");
cv::Mat dst;
//旋转角度
double angle = 45;
cv::Size src_sz = src.size();
cv::Size dst_sz(src_sz.height, src_sz.width);
int len = std::max(src.cols, src.rows);
//指定旋转中心(图像中点)
cv::Point2f center(len / 2., len / 2.);
//获取旋转矩阵(2x3矩阵)
cv::Mat rot_mat = cv::getRotationMatrix2D(center, angle, 1.0);
//根据旋转矩阵进行仿射变换
cv::warpAffine(src, dst, rot_mat, dst_sz);
//显示旋转效果
cv::imshow("image", src);
cv::imshow("result", dst);
cv::waitKey(0);
return 0;
import cv2
import numpy as np
import matplotlib.pyplot as plt
img = cv2.imread('sample.jpg')
H = np.float32([[1,0,100],[0,1,50]])
rows,cols = img.shape[:2]
res = cv2.warpAffine(img,H,(rows,cols)) #需要图像、变换矩阵、变换后的大小
plt.subplot(121)
plt.imshow(img)
plt.subplot(122)
plt.imshow(res)
import cv2
import matplotlib.pyplot as plt
img = cv2.imread('sample.jpg')
rows,cols = img.shape[:2]
#第一个参数旋转中心,第二个参数旋转角度,第三个参数:缩放比例
M = cv2.getRotationMatrix2D((cols/2,rows/2),45,1)
#第三个参数:变换后的图像大小
res = cv2.warpAffine(img,M,(rows,cols))
plt.subplot(121)
plt.imshow(img)
plt.subplot(122)
plt.imshow(res)
opencv文档: https://docs.opencv.org/3.1.0/da/d54/group__imgproc__transform.html#ga0203d9ee5fcd28d40dbc4a1ea4451983
博客:https://blog.csdn.net/weixin_40647819/article/details/87912122
https://www.jianshu.com/p/18cd12e776e1
https://blog.csdn.net/whuhan2013/article/details/53814026
python版本:https://blog.csdn.net/g11d111/article/details/79978582
https://www.kancloud.cn/aollo/aolloopencv/264331 http://www.woshicver.com/FifthSection/4_2_%E5%9B%BE%E5%83%8F%E5%87%A0%E4%BD%95%E5%8F%98%E6%8D%A2/
学习教程:
https://github.com/datawhalechina
关于Datawhale:
Datawhale是一个专注于数据科学与AI领域的开源组织,汇集了众多领域院校和知名企业的优秀学习者,聚合了一群有开源精神和探索精神的团队成员。Datawhale以“for the learner,和学习者一起成长”为愿景,鼓励真实地展现自我、开放包容、互信互助、敢于试错和勇于担当。同时Datawhale 用开源的理念去探索开源内容、开源学习和开源方案,赋能人才培养,助力人才成长,建立起人与人,人与知识,人与企业和人与未来的联结。