仿射变换是指图像可以通过一系列的几何变换来实现平移、旋转等多种操作。该变换能够保持图像的平直性和平行性。平直性是指图像经过仿射变换后,直线仍然是直线;平行性是指图像在完成仿射变换后,平行线仍然是平行线。
在OpenCV
中仿射函数为cv2.warpAffine()
,它通过一个变换矩阵(映射矩阵)M
实现变换,具体为:dst(𝑥, 𝑦) = src(𝑀11𝑥 + 𝑀12𝑦 + 𝑀13, 𝑀21𝑥 + 𝑀22𝑦 + 𝑀23)。如下图所示,通过一个变换矩阵M
将原始图像O
变换为仿射图像R
。
图1 仿射图像R
=变换矩阵M
× 原始图像O
利用仿射函数cv2.warpAffine()
可以实现对图像的旋转,其函数的语法格式如下:dst = cv2.warpAffine( src, M, dsize[, flags[, borderMode[, borderValue]]] )
参数解析:
dst
:仿射变换后输出的图像,该图像的类型与原始图像的类型相同,由dsize
决定它的的实际大小。
src
:要进行仿射变换的原始图像。
M
:一个 2×3 的变换矩阵。使用不同的变换矩阵可以实现不同的仿射变换。
dsize
:输出图像的尺寸大小。
flags
:插值方法,默认为INTER_LINEAR
。当该值为WARP_INVERSE_MAP
时,意味着M
是逆变换类型,实现从目标图像dst
到原始图像src
的逆变换。具体可选值可见下表。
类型 | 说明 |
---|---|
cv2.INTER_NEAREST | 最临近插值 |
cv2.INTER_LINEAR | 双线性插值(默认方式) |
cv2.INTER_CUBIC | 三次样条插值。首先对源图像附近的 4×4 近邻区域进行三次样条拟合,然后将目标像素对应的三次样条值作为目标图像对应像素点的值 |
cv2.INTER_AREA | 区域插值,根据当前像素点周边区域的像素实现当前像素点的采样。该方法类似最临近插值方式 |
cv2.INTER_LANCZOS4 | 一种使用 8×8 近邻的 Lanczos 插值方法 |
cv2.INTER_LINEAR_EXACT | 位精确双线性插值 |
cv2.INTER_MAX | 差值编码掩码 |
cv2.WARP_FILL_OUTLIERS | 标志,填补目标图像中的所有像素。如果它们中的一些对应源图像中的奇异点(离群值),则将它们设置为零 |
cv2.WARP_INVERSE_MAP | 标志,逆变换。例如,极坐标变换: 如果 flag 未被设置,则进行转换:dst(∅, 𝜌) = src(𝑥, 𝑦);如果 flag 被设置,则进行转换:dst(x, y) = src(∅, ρ) |
borderMode
:边类型, 默认为BORDER_CONSTANT
。当 该值为BORDER_TRANSPARENT
时,意味着目标图像内的值不做改变,这些值对应原始图像内的异常值。
borderValue
:边界值,默认为 0。
由此可见,通过转换矩阵M
将原始图像src
转换为目标图像dst
:dst(𝑥, 𝑦) = src(𝑀11𝑥 + 𝑀12𝑦 + 𝑀13, 𝑀21𝑥 + 𝑀22𝑦 + 𝑀23)
因此所进行仿射变换种类完全取决于转换矩阵M
。1. 平移
通过转换矩阵
M
可以实现将原始图像src
转换为目标图像dst
: dst(𝑥, 𝑦) = src(𝑀11𝑥 + 𝑀12𝑦 + 𝑀13, 𝑀21𝑥 + 𝑀22𝑦 + 𝑀23)
将原始图像src
向右侧移动150个像素、向下方移动250个像素,其对应关系为:dst (x, y) = src (x + 100, y + 200)
。完整的表达式为dst (x, y) = src (1·x + 0·y + 100, 0·x + 1·y + 200)
根据上述表达式可以确定对应的转换矩阵 M 中各个元素的值为:
元素 | 值 |
---|---|
𝑀11 | 1 |
𝑀12 | 0 |
𝑀13 | 150 |
𝑀21 | 0 |
𝑀22 | 1 |
𝑀23 | 250 |
将上述值代入转换矩阵
M
,得到:
[ 1 0 150 0 1 200 ] \begin{bmatrix} 1&0&150\\ 0&1&200\\ \end{bmatrix} [1001150200]程序示例一
利用仿射函数
cv2.warpAffine()
实现对图像的平移操作。
import cv2
import numpy as np
lena = cv2.imread("./lena.jpg") # 读取原图
height, width = lena.shape[:2] # 获取图像的高度和宽度
x = 120 #向右侧移动 120 个像素
y = 200 #向下方移动 200 个像素
M = np.float32([[1, 0, x], [0, 1, y]]) #转换矩阵M
Panned_lena = cv2.warpAffine(lena, M, (width, height))
cv2.imshow("原始图像", lena)
cv2.imshow("平移图像", Panned_lena)
cv2.waitKey()
cv2.destroyAllWindows()
程序运行结果如下:
图2 原始图像
图3 平移图像2. 旋转
在使用函数
cv2.warpAffine()
对图像进行旋转时,可以通过函数cv2.getRotationMatrix2D()
获取转换矩阵。该函数的语法格式为:retval=cv2.getRotationMatrix2D(center, angle, scale)
参数解析:
center
:旋转中心点。
angle
:旋转角度,正数表示逆时针旋转,负数表示顺时针旋转。
scale
:变换尺度(缩放大小)。 利用函cv2.getRotationMatrix2D()
可以直接生成要使用的转换矩阵M
。例如,想要以图像中心为圆点,逆时针旋转 60°,并将目标图像缩小为原始图像的 0.7 倍,则在调用函数cv2.getRotationMatrix2D()
生成转换矩阵 M 时所使用的语句为:M=cv2.getRotationMatrix2D((height/2,width/2),60,0.7)
程序示例二
通过函数
cv2.warpAffine()
实现图像的旋转。
import cv2
lena = cv2.imread("./lena.jpg") # 读取原图
height, width = lena.shape[:2] # 获取图像的高度和宽度
# 以图像中心为圆点,逆时针旋转 60°,并将目标图像缩小为原始图像的 0.7 倍
M = cv2.getRotationMatrix2D((width/2, height/2), 60, 0.7) #生成转换矩阵M
rotate_lena = cv2.warpAffine(lena, M, (width, height))
cv2.imshow("原始图像", lena)
cv2.imshow("旋转图像", rotate_lena)
cv2.waitKey()
cv2.destroyAllWindows()
程序运行结果如下:
图4 原始图像
图5 旋转图像3. 更复杂的仿射变换
对于更复杂仿射变换,
OpenCV
提供了函数cv2.getAffineTransform()
来生成仿射函数cv2.warpAffine()
所使用的转换矩阵M
。该函数的语法格式为:retval=cv2.getAffineTransform(src, dst)
参数解析:
src
:输入图像的三个点坐标。
dst
:输出图像的三个点坐标。
在该函数中,参数src
和dst
是包含三个二维数组(x, y)
点的数组。上述参数通过函数cv2.getAffineTransform()
定义了两个平行四边形。src
和dst
中的三个点分别对应平行四边形的左上角、右上角、左下角三个点。函数cv2.warpAffine()
以函数cv2.getAffineTransform()
获取的转换矩阵M
为参数,将src
中的点仿射到dst
中。函数cv2.getAffineTransform()
对所指定的点完成映射后,将所有其他点的映射关系按照指定点的关系计算确定。程序示例三
设计程序完成图像仿射变换。
import cv2
import numpy as np
lena=cv2.imread('./lena.jpg')
rows,cols,ch=lena.shape #获取图像的行数、列数和色彩通道数
p1=np.float32([[0,0],[cols-1,0],[0,rows-1]])
p2=np.float32([[0,rows*0.33],[cols*0.85,rows*0.25],[cols*0.15,rows*0.7]])
M=cv2.getAffineTransform(p1,p2) #转换矩阵 M
dst=cv2.warpAffine(lena,M,(cols,rows)) # 将所有其他点的映射关系按照指定点的关系计算确定
cv2.imshow("原始图像",lena)
cv2.imshow("仿射变换图像",dst)
cv2.waitKey()
cv2.destroyAllWindows()
程序运行结果如下:
图5 原始图像
图6 仿射变换图像4.透视
仿射变换可以将矩形映射为任意平行四边形,而透视变换可以将矩形映射为任意四边形。通过函数
cv2.warpPerspective()
可以实现透视变换,其语法为dst = cv2.warpPerspective( src, M, dsize[, flags[, borderMode[, borderValue]]] )
参数解析:
dst
:透视处理后的输出图像,与原始图像具有相同的类型。dsize
决定输出图像的实际大小。
src
:要进行透视变换的图像。
M
:一个 3×3 的变换矩阵。
dsize
:输出图像的尺寸大小。
flags
:插值方法,默认为INTER_LINEAR
。当该值为WARP_INVERSE_MAP
时,意味着M
是逆变换类型,能实现从目标图像dst
到原始图像src
的逆变换,具体可选值参见下表。
类型 | 说明 |
---|---|
cv2.INTER_NEAREST | 最临近插值 |
cv2.INTER_LINEAR | 双线性插值(默认方式) |
cv2.INTER_CUBIC | 三次样条插值。首先对源图像附近的 4×4 近邻区域进行三次样条拟合,然后将目标像素对应的三次样条值作为目标图像对应像素点的值 |
cv2.INTER_AREA | 区域插值,根据当前像素点周边区域的像素实现当前像素点的采样。该方法类似最临近插值方式 |
cv2.INTER_LANCZOS4 | 一种使用 8×8 近邻的 Lanczos 插值方法 |
cv2.INTER_LINEAR_EXACT | 位精确双线性插值 |
cv2.INTER_MAX | 差值编码掩码 |
cv2.WARP_FILL_OUTLIERS | 标志,填补目标图像中的所有像素。如果它们中的一些对应源图像中的奇异点(离群值),则将它们设置为零 |
cv2.WARP_INVERSE_MAP | 标志,逆变换。例如,极坐标变换: 如果 flag 未被设置,则进行转换:dst(∅, 𝜌) = src(𝑥, 𝑦);如果 flag 被设置,则进行转换:dst(x, y) = src(∅, ρ) |
borderMode
:边类型, 默认为BORDER_CONSTANT
。 当 该值为BORDER_TRANSPARENT
时,意味着目标图像内的值不做改变,这些值对应原始图像内的异常值。
borderValue
:边界值,默认为 0。
与仿射变换函数一样,同样可以使用一个函数来生成函数cv2.warpPerspective()
所使用的转换矩
阵。该函数是cv2.getPerspectiveTransform()
,其语法格式为:retval = cv2.getPerspectiveTransform( src, dst )
参数解析:
src
:输入图像的四个顶点的坐标。
dst
:输出图像的四个顶点的坐标。
与仿射变换函数cv2.getAffineTransform()
不同的。需要注意的是,src
参数和dst
参数是包含四个点的数组,实际使用中,可以根据需要控制src
中的四个点映射到dst
中的四个点。程序示例四
设计程序,完成图像透视。
import cv2
import numpy as np
lena=cv2.imread('./lena.jpg') #导入原图
rows,cols=lena.shape[:2] #获取图像的行数和列数
pts1 = np.float32([[150,50],[400,50],[60,450],[310,450]])
pts2 = np.float32([[50,50],[rows-50,50],[50,cols-50],[rows-50,cols-50]])
M=cv2.getPerspectiveTransform(pts1,pts2) #转换矩阵M
dst=cv2.warpPerspective(lena,M,(cols,rows))
cv2.imshow("lena",lena)
cv2.imshow("dst",dst)
cv2.waitKey()
cv2.destroyAllWindows()
在指定原始图像中的平行四边形顶点
pts1
,指定目标图像中矩形的四个顶点pts2
,使用M=cv2.getPerspectiveTransform(pts1,pts2)
生成转换矩阵M
。接下来,使用语句dst=cv2.warpPerspective(img,M,(cols,rows))
完成从平行四边形到矩形的转换。程序运行结果如下:
图7 原始图像
图8 透视变换图像5. 重映射
把一幅图像中的某个位置的像素点放置到另一幅图像内的指定位置的过程为图像的重映射。OpenCV提供的重映射函数
cv2.remap()
可以实现自定义的方式的重映射。其语法格式为dst = cv2.remap( src, map1, map2, interpolation[, borderMode[, borderValue]] )
参数解析:
dst
:目标图像,与src
有相同的大小和类型。
src
:原始图像。
map1
:该参数有两种可能的值:①表示(x,y)
点的一个映射;②表示CV_16SC2
,CV_32FC1
,CV_32FC2
类型(x,y)
点的x
值。
map2
:该参数有两种可能的值:①当map1
表示(x,y)
时,该值为空。②当map1
表示(x,y)
点的x
值时,该值是CV_16UC1
,CV_32FC1
类型(x,y)
点的y
值。
Interpolation
:插值方式,这里不支持INTER_AREA
方法,具体值参见下表。
类型 | 说明 |
---|---|
cv2.INTER_NEAREST | 最临近插值 |
cv2.INTER_LINEAR | 双线性插值(默认方式) |
cv2.INTER_CUBIC | 三次样条插值。首先对源图像附近的 4×4 近邻区域进行三次样条拟合,然后将目标像素对应的三次样条值作为目标图像对应像素点的值 |
cv2.INTER_LANCZOS4 | 一种使用 8×8 近邻的 Lanczos 插值方法 |
cv2.INTER_LINEAR_EXACT | 位精确双线性插值 |
cv2.INTER_MAX | 差值编码掩码 |
cv2.WARP_FILL_OUTLIERS | 标志,填补目标图像中的所有像素。如果它们中的一些对应源图像中的奇异点(离群值),则将它们设置为零 |
cv2.WARP_INVERSE_MAP | 标志,逆变换。例如,极坐标变换: 如果 flag 未被设置,则进行转换:dst(∅, 𝜌) = src(𝑥, 𝑦);如果 flag 被设置,则进行转换:dst(x, y) = src(∅, ρ) |
borderMode
:边界模式。当该值为BORDER_TRANSPARENT
时,表示目标图像内的对应源图像内奇异点的像素不会被修改。
borderValue
:边界值,默认为 0。
重映射是通过修改像素点的位置得到一幅新图像。在构建新图像时,需要确定新图像中每个像素点在原始图像中的位置
。因此,映射函数的作用是查找新图像像素在原始图像内的位置
。
重映射是将新图像像素映射到原始图像的过程,因此被称为反向映射。在函数cv2.remap()
中,参数map1
和参数map2
用来说明反向映射,map1
针对的是坐标x
,map2
针对的是坐标y
。
因为参数map1
和参数map2
的值是浮点数,所以目标图像可以映射回一个非整数的值,这意味着目标图像可以“反向映射”到原始图像中两个像素点之间的位置
(当然,该位置是不存在像素值的)。而且由于参数map1
和参数map2
的值是浮点数,所以通过函数cv2.remamp()
实现的映射关系变得更加随意,可以通过自定义映射参数实现不同形式的映射。5.1 复制
在映射时,通过将
map1
和map2
的值分别设定为对应位置上的x
轴坐标和y
轴坐标值,可以让函数cv2.remap()
实现图像复制。程序示例五
使用函数
cv2.remap()
完成对图像的复制。
import cv2
import numpy as np
lena = cv2.imread("./lena.jpg")
rows, cols = lena.shape[:2]
map1 = np.zeros(lena.shape[:2], np.float32) #设置x轴方向的坐标
map2 = np.zeros(lena.shape[:2], np.float32) #设置y轴方向的坐标
for i in range(rows):
for j in range(cols):
map1.itemset((i, j), j)
map2.itemset((i, j), i)
rst = cv2.remap(lena, map1, map2, cv2.INTER_LINEAR)
cv2.imshow("原始图像", lena)
cv2.imshow("复制图像", rst)
cv2.waitKey()
cv2.destroyAllWindows()
程序运行结果如下
图9 原始图像
图10 复制图像5.2 绕x轴旋转
如果想要图像绕x轴翻转,则在映射过程中需要满足:①
x坐标轴的值保持不变
;②y坐标轴的值以x轴为对称轴进行交换。
反映在map1
和map2
上就是map1
的值保持不变,map2
的值调整为“总行数-1-当前行号”。
需要注意,OpenCV
中行号的下标是从 0 开始的,所以在对称关系中存在“当前行号+对称行号=总行数-1”的关系。因此在绕着x
轴翻转时,map2
中当前行的行号调整为“总行数-1-当前行号”。程序示例六
使用函数
cv2.remap()
实现对图像绕x
轴翻转。
import cv2
import numpy as np
lena=cv2.imread("./lena.jpg")
rows,cols=lena.shape[:2]
map1 = np.zeros(lena.shape[:2],np.float32)
map2 = np.zeros(lena.shape[:2],np.float32)
for i in range(rows):
for j in range(cols):
map1.itemset((i,j),j)
map2.itemset((i,j),rows-1-i)
rst=cv2.remap(lena,map1,map2,cv2.INTER_LINEAR)
cv2.imshow("原始图像",lena)
cv2.imshow("绕x轴旋转图像",rst)
cv2.waitKey()
cv2.destroyAllWindows()
程序运行结果如下
图11 原始图像
图12 绕x轴旋转图像5.3 绕y轴旋转
如果想要图像绕y轴翻转,则在映射过程中需要满足:①
y坐标轴的值保持不变
;②x坐标轴的值以y轴为对称轴进行交换。
反映在map1
和map2
上就是map2
的值保持不变,map1
的值调整为“总行数-1-当前列号”。
需要注意,OpenCV
中列号的下标是从 0 开始的,所以在对称关系中存在“当前列号+对称列号=总列数-1”的关系。因此在绕着y
轴翻转时,map2
中当前行的行号调整为“总列数-1-当前列号”。程序示例七
使用函数
cv2.remap()
实现对图像绕y
轴翻转。
import cv2
import numpy as np
lena=cv2.imread("./lena.jpg")
rows,cols=lena.shape[:2]
map1 = np.zeros(lena.shape[:2],np.float32)
map2 = np.zeros(lena.shape[:2],np.float32)
for i in range(rows):
for j in range(cols):
map1.itemset((i,j),cols-1-j)
map2.itemset((i,j),i)
rst=cv2.remap(lena,map1,map2,cv2.INTER_LINEAR)
cv2.imshow("原始图像",lena)
cv2.imshow("绕y轴旋转图像",rst)
cv2.waitKey()
cv2.destroyAllWindows()
图13 原始图像
图14 绕y轴旋转图像5.4 绕x轴、y轴翻转
如果想让图像绕着
x
轴、y
轴翻转,意味着在映射过程中:①x 坐标轴的值以 y 轴为对称轴进行交换
;②y 坐标轴的值以 x 轴为对称轴进行交换
。
反映在map1
和map2
上:map1
的值调整为“总列数-1-当前列号”;map2
的值调整为“总行数-1-当前行号”。程序示例八
使用函数
cv2.remap()
实现图像绕x
轴、y
轴翻转。
import cv2
import numpy as np
lena=cv2.imread("./lena.jpg")
rows,cols=lena.shape[:2]
map1 = np.zeros(lena.shape[:2],np.float32)
map2 = np.zeros(lena.shape[:2],np.float32)
for i in range(rows):
for j in range(cols):
map1.itemset((i,j),cols-j-1)
map2.itemset((i,j),rows-1-i)
rst=cv2.remap(lena,map1,map2,cv2.INTER_LINEAR)
cv2.imshow("原始图像",lena)
cv2.imshow("绕x轴、y轴旋转图像",rst)
cv2.waitKey()
cv2.destroyAllWindows()
图15 原始图像
图16 绕x轴、y轴旋转图像5.5 x轴、y轴互换
如果想让图像的
x
轴、y
轴互换,意味着在映射过程中,对于任意一点,都需要将其x
轴、y
轴坐标互换。反映在 map1 和 map2 上就是:①map1
的值调整为所在行的行号;②map2
的值调整为所在列的列号。
需要注意的是,如果行数和列数不一致,上述运算可能存在值无法映射的情况。默认情况下,无法完成映射的值会被处理为 0
。程序示例九
使用函数
cv2.remap()
实现图像绕x
轴、y
轴互换。
import cv2
import numpy as np
lena=cv2.imread("./lena.jpg")
rows,cols=lena.shape[:2]
map1 = np.zeros(lena.shape[:2],np.float32)
map2 = np.zeros(lena.shape[:2],np.float32)
for i in range(rows):
for j in range(cols):
map1.itemset((i,j),i)
map2.itemset((i,j),j)
rst=cv2.remap(lena,map1,map2,cv2.INTER_LINEAR)
cv2.imshow("原始图像",lena)
cv2.imshow("x轴、y轴互换图像",rst)
cv2.waitKey()
cv2.destroyAllWindows()
图17 原始图像
图18 x轴、y轴互换图像