图像的旋转
- 1 单点旋转
- 2. 图片旋转(cv2.getRotationMatrix2D)
- 3. 插值方法
- 3.1 最近邻插值(cv2.INTER_NEAREST)
- 3.2 双线性插值(cv2.INTER_LINEAR)
- 3.3 像素区域插值(cv2.INTER_AREA)
- 3.4 双三次插值(cv2.INTER_CUBIC)
- 3.5 Lanczos插值(cv2.INTER_LANCZOS4)
- 3.6 小结
- 4. 边缘填充方式(cv2.warpAffine的属性borderMode)
- 4.1 边界复制(BODER_REPLICATE)
- 4.2 边界反射(BORDER_REFLECT)
- 4.3 边界反射101(BORDER_REFLECT_101)
- 4.4 边界常数(BORDER_CONSTANT)
- 4.5 边界包裹(BORDER_WRAP)
图像旋转是指图像以某一点为旋转中心,将图像中的所有像素点都围绕该点旋转一定的角度,并且旋转后的像素点组成的图像与原图像相同。
1 单点旋转
首先我们以最简单的一个点的旋转为例子,且以最简单的情况举例,令旋转中心为坐标系中心O(0,0),假设有一点 P 0 ( x 0 , y 0 ) P_{0}(x_{0},y_{0}) P0(x0,y0), P 0 P_{0} P0离旋转中心O的距离为r, O P 0 OP_{0} OP0与坐标轴x轴的夹角为 α \alpha α, P 0 P_{0} P0绕O顺时针旋转 θ \theta θ角后对应的点为 P ( x , y ) P(x,y) P(x,y),如下图所示:
单点旋转的原理
2. 图片旋转(cv2.getRotationMatrix2D)
明白了单个点的旋转过程之后,其实图像旋转也很好理解,就是将图像里的每个像素点都带入仿射变换矩阵里,从而得到旋转后的新坐标。在OpenCV中,要得到仿射变换矩阵可以使用cv2.getRotationMatrix2D(),通过这个函数即可直接获取到上面的旋转矩阵,
格式如下:
cv2.RotationMatrix2D(Center,Angle,Scale)
该函数需要接收的参数为:
- Center:表示旋转的中心点,是一个二维的坐标点(x,y)
- Angle:表示旋转的角度
- Scale:表示缩放比例,可以通过该参数调整图像相对于原始图像的大小变化
因此,在本实验中只需要在组件中填好图片要旋转的角度与缩放的比例即可。
代码如下:
'''图片的旋转'''
img=cv2.imread(r"../15day4.10/src/1.jpg")
# 求出图片的高宽和颜色
h,w,c=img.shape
# 给出一个旋转角度
angle=45
# 给出缩放倍数
sacle=1
# 以图片的中心作为旋转中心的仿射变换矩阵
m=cv2.getRotationMatrix2D((h/2,w/2),angle=angle,scale=sacle)
img_rotate=cv2.warpAffine(img,m,(w,h))
cv2.imshow("img",img)
cv2.imshow("img_rotate",img_rotate)
cv2.waitKey(0)
但是这里会有一个问题:
- 由于三角函数的值是小数,那么其乘积也会是小数,虽然OpenCV中会对其进行取整操作,但是像素点旋转之后的取整结果也有可能重合,这样就会导致可能会在旋转的过程中丢失一部分原始的像素信息。
- 并且如果使用了scale参数进行图像的缩放的话,当图像放大时,比如一个10*10的图像放大成20*20,图像由100个像素点变成400个像素点,那么多余的300个像素点是怎么来的?而当图像缩小时,比如一个20*20的图像缩小为10*10的图像,需要丢掉300个像素点,那到底要怎么丢才能保证图像还能是一个正常的图像?
- 因此我们需要一种方法来帮我们计算旋转后的图像中每一个像素点所对应的像素值,从而保证图像的完整性,这种方法就叫做插值法。
3. 插值方法
在图像处理和计算机图形学中,插值(Interpolation)是一种通过已知数据点之间的推断或估计来获取新数据点的方法。它在图像处理中常用于处理图像的放大、缩小、旋转、变形等操作,以及处理图像中的像素值。
图像插值算法是为了解决图像缩放或者旋转等操作时,由于像素之间的间隔不一致而导致的信息丢失和图像质量下降的问题。当我们对图像进行缩放或旋转等操作时,需要在新的像素位置上计算出对应的像素值,而插值算法的作用就是根据已知的像素值来推测未知位置的像素值。本实验提供了五种常见的插值算法,下面一一介绍。
3.1 最近邻插值(cv2.INTER_NEAREST)
最近邻插值的原理
代码如下:
'''最近邻插值法'''
img=cv2.imread(r"../15day4.10/src/1.jpg")
# 求出图片的高宽和颜色
h,w,c=img.shape
# 给出一个旋转角度
angle=45
# 给出缩放倍数
sacle=1.5
#给出缩放后的图像的窗口大小
frame=(2*w,2*h)
# 以图片的中心作为旋转中心的仿射变换矩阵
m=cv2.getRotationMatrix2D((h/2,w/2),0,sacle)
img_rotate=cv2.warpAffine(img,m,frame,flags=cv2.INTER_NEAREST)
cv2.imshow("img",img)
cv2.imshow("img_rotate",img_rotate)
cv2.waitKey(0)
3.2 双线性插值(cv2.INTER_LINEAR)
双线性插值的原理
代码如下:
'''双线性插值'''
img=cv2.imread(r"../15day4.10/src/1.jpg")
# 求出图片的高宽和颜色
h,w,c=img.shape
# 给出一个旋转角度
angle=45
# 给出缩放倍数
sacle=1.5
#给出缩放后的图像的窗口大小
frame=(2*w,2*h)
# 以图片的中心作为旋转中心的仿射变换矩阵
m=cv2.getRotationMatrix2D((h/2,w/2),0,sacle)
img_rotate=cv2.warpAffine(img,m,frame,flags=cv2.INTER_LINEAR)
cv2.imshow("img",img)
cv2.imshow("img_rotate",img_rotate)
cv2.waitKey(0)
注意:
- 双线性插值和最近邻插值的区别就,当放大倍数为小数时,最近邻插值(cv2.INTER_NEAREST)是对结果下取整,而双线性插值(cv2.INTER_LINEAR)根据距离哪个像素点近就将取大的权值
3.3 像素区域插值(cv2.INTER_AREA)
像素区域插值主要分两种情况,缩小图像和放大图像的工作原理并不相同。
当使用像素区域插值方法进行缩小图像时,它就会变成一个均值滤波器(滤波器其实就是一个核,这里只做简单了解,后面实验中会介绍),其工作原理可以理解为对一个区域内(均值滤波器/核)的像素值取平均值。
当使用像素区域插值方法进行放大图像时,如果图像放大的比例是整数倍,那么其工作原理与最近邻插值类似;如果放大的比例不是整数倍,那么就会调用双线性插值进行放大。
其中目标像素点与原图像的像素点的对应公式如下所示:
s
r
c
X
=
d
s
t
X
∗
s
r
c
W
i
d
t
h
d
s
t
W
i
d
t
h
s r c X=d s t X*{\frac{s r c W i d t h}{d s t W i d t h}}
srcX=dstX∗dstWidthsrcWidth
s
r
c
Y
=
d
s
t
Y
∗
s
r
c
H
e
i
g
h
t
d
s
t
H
e
i
g
h
t
s r c Y=d s t Y*{\frac{s r c H e i g h t}{d s t H e i g h t}}
srcY=dstY∗dstHeightsrcHeight
代码如下:
'''像素区域插值'''
img=cv2.imread(r"../15day4.10/src/1.jpg")
# 求出图片的高宽和颜色
h,w,c=img.shape
# 给出一个旋转角度
angle=45
# 给出缩放倍数
sacle=1.5
#给出缩放后的图像的窗口大小
frame=(2*w,2*h)
# 以图片的中心作为旋转中心的仿射变换矩阵
m=cv2.getRotationMatrix2D((h/2,w/2),0,sacle)
img_rotate=cv2.warpAffine(img,m,frame,flags=cv2.INTER_AREA)
cv2.imshow("img",img)
cv2.imshow("img_rotate",img_rotate)
cv2.waitKey(0)
像素区域插值(cv2.INTER_AREA)就是最近邻插值和双线性插值的结合
3.4 双三次插值(cv2.INTER_CUBIC)
与双线性插值法相同,该方法也是通过映射,在映射点的邻域内通过加权来得到放大图像中的像素值。不同的是,双三次插值法需要原图像中近邻的16个点来加权。
目标像素点与原图像的像素点的对应公式如下所示:
s
r
c
X
=
d
s
t
X
∗
s
r
c
W
i
d
t
h
d
s
t
W
i
d
t
h
s r c X=d s t X*{\frac{s r c W i d t h}{d s t W i d t h}}
srcX=dstX∗dstWidthsrcWidth
s r c Y = d s t Y ∗ s r c H e i g h t d s t H e i g h t s r c Y=d s t Y*{\frac{s r c H e i g h t}{d s t H e i g h t}} srcY=dstY∗dstHeightsrcHeight
下面我们举例说明,假设原图像A大小为m*n,缩放后的目标图像B的大小为M*N。其中A的每一个像素点是已知的,B是未知的,我们想要求出目标图像B中每一个像素点(X,Y)的值,必须先找出像素(X,Y)在原图像A中对应的像素(x,y),再根据原图像A距离像素(x,y)最近的16个像素点作为计算目标图像B(X,Y)处像素值的参数,利用BiCubic基函数求出16个像素点的权重,图B像素(x,y)的值就等于16个像素点的加权叠加。
假如下图中的P点就是目标图像B在(X,Y)处根据上述公式计算出的对应于原图像A中的位置,P的坐标位置会出现小数部分,所以我们假设P点的坐标为(x+u,y+v),其中x、y表示整数部分,u、v表示小数部分,那么我们就可以得到其周围的最近的16个像素的位置,我们用a(i,j)(i,j=0,1,2,3)来表示,如下图所示。
然后给出BiCubic函数:
其中,a一般取-0.5或-0.75。
我们要做的就是将上面的16个点的坐标带入函数中,获取16像素所对应的权重
W
(
x
)
W(x)
W(x)。然而BiCubic函数是一维的,所以我们需要将像素点的行与列分开计算,比如a00这个点,我们需要将x=0带入BiCubic函数中,计算a00点对于P点的x方向的权重,然后将y=0带入BiCubic函数中,计算a00点对于P点的y方向的权重,其他像素点也是这样的计算过程,最终我们就可以得到P所对应的目标图像B在(X,Y)处的像素值为:
B
(
X
,
Y
)
=
∑
i
=
0
3
∑
j
=
0
3
a
i
j
×
W
(
i
)
×
W
(
j
)
B(X,Y)=\sum_{i=0}^{3}\sum_{j=0}^{3}a_{i j}\times W_{(i)}\times W_{(j)}
B(X,Y)=i=0∑3j=0∑3aij×W(i)×W(j)
依此办法我们就可以得到目标图像中所有的像素点的像素值。
代码如下:
'''双三次插值'''
img=cv2.imread(r"../15day4.10/src/1.jpg")
# 求出图片的高宽和颜色
h,w,c=img.shape
# 给出一个旋转角度
angle=45
# 给出缩放倍数
sacle=1.5
#给出缩放后的图像的窗口大小
frame=(2*w,2*h)
# 以图片的中心作为旋转中心的仿射变换矩阵
m=cv2.getRotationMatrix2D((h/2,w/2),0,sacle)
img_rotate=cv2.warpAffine(img,m,frame,flags=cv2.INTER_CUBIC)
cv2.imshow("img",img)
cv2.imshow("img_rotate",img_rotate)
cv2.waitKey(0)
双三次插值(cv2.INTER_CUBIC)比双线性插值(cv2.INTER_LINEAR)对图片处理更精细,但是执行效率比较低
3.5 Lanczos插值(cv2.INTER_LANCZOS4)
Lanczos插值方法与双三次插值的思想是一样的,不同的就是其需要的原图像周围的像素点的范围变成了8*8,并且不再使用BiCubic函数来计算权重,而是换了一个公式计算权重。
首先还是目标像素点与原图像的像素点的对应公式如下所示:
s
r
c
X
=
d
s
t
X
∗
s
r
c
W
i
d
t
h
d
s
t
W
i
d
t
h
s r c X=d s t X*{\frac{s r c W i d t h}{d s t W i d t h}}
srcX=dstX∗dstWidthsrcWidth
s r c Y = d s t Y ∗ s r c H e i g h t d s t H e i g h t s r c Y=d s t Y*{\frac{s r c H e i g h t}{d s t H e i g h t}} srcY=dstY∗dstHeightsrcHeight
下面我们举例说明,假设原图像A大小为m*n,缩放后的目标图像B的大小为M*N。其中A的每一个像素点是已知的,B是未知的,我们想要求出目标图像B中每一个像素点(X,Y)的值,必须先找出像素(X,Y)在原图像A中对应的像素(x,y),再根据原图像A距离像素(x,y)最近的64个像素点作为计算目标图像B(X,Y)处像素值的参数,利用权重函数求出64个像素点的权重,图B像素(x,y)的值就等于64个像素点的加权叠加。
假如下图中的P点就是目标图像B在(X,Y)处根据上述公式计算出的对应于原图像A中的位置,P的坐标位置会出现小数部分,所以我们假设P点的坐标为(x+u,y+v),其中x、y表示整数部分,u、v表示小数部分,那么我们就可以得到其周围的最近的64个像素的位置,我们用a(i,j)(i,j=0,1,2,3,4,5,6,7)来表示,如下图所示。
然后给出权重公式:
其中a通常取2或者3,当a=2时,该算法适用于图像缩小。a=3时,该算法适用于图像放大。
与双三次插值一样,这里也需要将像素点分行和列分别带入计算权重值,其他像素点也是这样的计算过程,最终我们就可以得到P所对应的目标图像B在(X,Y)处的像素值为:
S
(
x
,
y
)
=
∑
i
=
[
x
]
−
a
+
1
[
x
]
+
a
∑
j
=
[
y
]
−
a
+
1
[
y
]
+
a
s
i
j
L
(
x
−
i
)
L
(
y
−
j
)
S(x,y)=\sum_{i=[x]-a+1}^{[x]+a}\sum_{j=[y]-a+1}^{[y]+a}s_{i j}L(x-i)L(y-j)
S(x,y)=i=[x]−a+1∑[x]+aj=[y]−a+1∑[y]+asijL(x−i)L(y−j)
其中
[
x
]
[x]
[x]、
[
y
]
[y]
[y]表示对坐标值向下取整,通过该方法就可以计算出新的图像中所有的像素点的像素值。
代码如下:
'''lanczos'''
img=cv2.imread(r"../15day4.10/src/1.jpg")
# 求出图片的高宽和颜色
h,w,c=img.shape
# 给出一个旋转角度
angle=45
# 给出缩放倍数
sacle=1.5
#给出缩放后的图像的窗口大小
frame=(2*w,2*h)
# 以图片的中心作为旋转中心的仿射变换矩阵
m=cv2.getRotationMatrix2D((h/2,w/2),0,sacle)
img_rotate=cv2.warpAffine(img,m,frame,flags=cv2.INTER_LANCZOS4)
cv2.imshow("img",img)
cv2.imshow("img_rotate",img_rotate)
cv2.waitKey(0)
3.6 小结
最近邻插值的计算速度最快,但是可能会导致图像出现锯齿状边缘和失真,效果较差。双线性插值的计算速度慢一点,但效果有了大幅度的提高,适用于大多数场景。双三次插值、Lanczos插值的计算速度都很慢,但是效果都很好。
在OpenCV中,关于插值方法默认选择的都是双线性插值,且一般情况下双线性插值已经能满足大部分需求。
4. 边缘填充方式(cv2.warpAffine的属性borderMode)
可以看到,左图在逆时针旋转45度之后原图的四个顶点在右图中已经看不到了,同时,右图的四个顶点区域其实是什么都没有的,因此我们需要对空出来的区域进行一个填充。右图就是对空出来的区域进行了像素值为(0,0,0)的填充,也就是黑色像素值的填充。除此之外,后续的一些图像处理方式也会用到边缘填充,这里介绍五个常用的边缘填充方法。
4.1 边界复制(BODER_REPLICATE)
边界复制会将边界处的像素值进行复制,然后作为边界填充的像素值,如下图所示,可以看到四周的像素值都一样。
代码如下:
'''边界复制'''
img=cv2.imread(r"../15day4.10/src/face.png")
# 求出图片的高宽和颜色
h,w,c=img.shape
# 给出一个旋转角度
angle=45
# 给出缩放倍数
sacle=1
#给出缩放后的图像的窗口大小
frame=(2*w,2*h)
# 以图片的中心作为旋转中心的仿射变换矩阵
m=cv2.getRotationMatrix2D((h/2,w/2),angle,sacle)
img_rotate=cv2.warpAffine(img,m,frame,borderMode=cv2.BORDER_REPLICATE)
cv2.imshow("img",img)
cv2.imshow("img_rotate",img_rotate)
cv2.waitKey(0)
4.2 边界反射(BORDER_REFLECT)
如下图所示,会根据原图的边缘进行反射。
代码如下:
'''边界反射'''
img=cv2.imread(r"../15day4.10/src/face.png")
# 求出图片的高宽和颜色
h,w,c=img.shape
# 给出一个旋转角度
angle=45
# 给出缩放倍数
sacle=1
#给出缩放后的图像的窗口大小
frame=(2*w,2*h)
# 以图片的中心作为旋转中心的仿射变换矩阵
m=cv2.getRotationMatrix2D((h/2,w/2),angle,sacle)
img_rotate=cv2.warpAffine(img,m,frame,borderMode=cv2.BORDER_REFLECT)
cv2.imshow("img",img)
cv2.imshow("img_rotate",img_rotate)
cv2.waitKey(0)
边界反射是将原图沿着原图的边界镜像复制填充整个边框
4.3 边界反射101(BORDER_REFLECT_101)
与边界反射不同的是,不再反射边缘的像素点,如下图所示。
代码如下:
'''边界反射_101'''
img=cv2.imread(r"../15day4.10/src/face.png")
# 求出图片的高宽和颜色
h,w,c=img.shape
# 给出一个旋转角度
angle=45
# 给出缩放倍数
sacle=1
#给出缩放后的图像的窗口大小
frame=(2*w,2*h)
# 以图片的中心作为旋转中心的仿射变换矩阵
m=cv2.getRotationMatrix2D((h/2,w/2),angle,sacle)
img_rotate=cv2.warpAffine(img,m,frame,borderMode=cv2.BORDER_REFLECT_101)
cv2.imshow("img",img)
cv2.imshow("img_rotate",img_rotate)
cv2.waitKey(0)
边界反射101是将原图以原图的边界为轴镜像复制填充整个边框
4.4 边界常数(BORDER_CONSTANT)
当选择边界常数时,还要指定常数值是多少,默认的填充常数值为0,如下图所示。
img2=cv2.warpAffine(img,M,(shape[1],shape[0]),flags=cv2.INTER_LINEAR,borderMode=cv2.BORDER_CONSTANT,borderValue=100)
代码如下:
'''边界常数'''
img=cv2.imread(r"../15day4.10/src/face.png")
# 求出图片的高宽和颜色
h,w,c=img.shape
# 给出一个旋转角度
angle=45
# 给出缩放倍数
sacle=1
#给出缩放后的图像的窗口大小
frame=(2*w,2*h)
# 以图片的中心作为旋转中心的仿射变换矩阵
m=cv2.getRotationMatrix2D((h/2,w/2),angle,sacle)
#边界常数(borderMode=cv2.BORDER_CONSTANT)的边界值(borderVlue)为0则是黑色,如果就是一个值的话是将bgr这三个值都赋值为这个值
img_rotate=cv2.warpAffine(img,m,frame,borderMode=cv2.BORDER_CONSTANT,borderValue=(0,0,255))
cv2.imshow("img",img)
cv2.imshow("img_rotate",img_rotate)
cv2.waitKey(0)
边界常数(borderMode=cv2.BORDER_CONSTANT)的边界值(borderVlue)为0则是黑色,如果就是一个值的话是将bgr这三个值都赋值为这个值
4.5 边界包裹(BORDER_WRAP)
如下图所示。
代码如下:
'''边界包裹'''
img=cv2.imread(r"../15day4.10/src/face.png")
# 求出图片的高宽和颜色
h,w,c=img.shape
# 给出一个旋转角度
angle=45
# 给出缩放倍数
sacle=1
#给出缩放后的图像的窗口大小
frame=(2*w,2*h)
# 以图片的中心作为旋转中心的仿射变换矩阵
m=cv2.getRotationMatrix2D((h/2,w/2),angle,sacle)
img_rotate=cv2.warpAffine(img,m,frame,borderMode=cv2.BORDER_WRAP)
cv2.imshow("img",img)
cv2.imshow("img_rotate",img_rotate)
cv2.waitKey(0)
边界包裹(cv2.BORDER_WRAP)的含义就是将图片平铺填充窗口