几何变换是指将一幅图像映射到另 一幅图像内的操作。OpenCV提供了多个与映射有关的函数。
根据OpenCV函数的不同,映射关系可以划分为缩放、翻转、仿射变换、透视、重映射等。
在OpenCV中,使用cv2.resize()实现对图像缩放:
dst = cv2.resize(src, dsize, [, fx [, fy [, interpolation]]])
在cv2.resize()函数中,目标图像的大小可以通过"参数dsize"或者"参数fx和fy"二者之一来确定,具体情况如下:
通过参数dsize指定:
如果指定参数dsize的值,则无论是否指定了参数fx和fy的值,都由参数dsize来决定目标图像的大小。
此时需要注意的是,dsize内第1个参数对应缩放后图像的宽度(width,即列数cols,与参数fx相关),第2个参数对应缩放后图像的高度(height,即行数rows,与参数fy相关)。
指定参数dsize的值时,x方向的缩放大小(参数fx)为:
同时,y方向的缩放大小(参数fy)为:
通过参数fx和fy指定:
如果参数dsize的值是None,那么目标图像的大小通过参数fx和fy来决定。此时,目标图像的大小为:dsize=Size(round(fx * src.cols),round(fy * src.rows))
插值 :
插值是指在对图像进行几何处理时,给无法直接通过映射得到值的像素点赋值。例如,将图像放大为原来的2倍,必然会多出一些无法被直接映射值的像素点,对于这些像素点,插值方式决定了如何确定它们的值。除此以外,还会存在一些非整数的映射值,例如,反向映射可能会把目标图像中的像素点值映射到原始图像中的非整数值对应的位置上,当然原始图像内是不可能存在这样的非整数位置的,即目标图像上的该像素点不能对应到原始图像的某个具体位置上,此时也要对这些像素点进行插值处理,以完成映射。
注意:
函数cv2.resize()能实现对原始图像的缩放功能,需要注意的是,开始运算前,操作前的目标图像dst自身的大小、类型与最终得到的目标图像dst是没有任何关系的。目标图像dst的最终大小和类型是通过src、dsize、fx、fy指定的。如果想让原始图像调整为和目标图像一样大,则必须通过上述属性指定。
当缩小图像时,使用区域插值方式(INTER_AREA)能够得到最好的效果;当放大图像时,使用三次样条插值(INTER_CUBIC)方式和双线性插值(INTER_LINEAR)方式都能够取得较好的效果。三次样条插值方式速度较慢,双线性插值方式速度相对较快且效果并不逊色。
例:
import cv2
img = cv2.imread('../lena.bmp')
rows, cols = img.shape[:2]
size = (int(cols * 0.9), int(rows * 0.5))
rst = cv2.resize(img, size)
print('img.shape=', img.shape)
print('rst.shape=', rst.shape)
cv2.imshow('img', img)
cv2.imshow('rst', rst)
cv2.waitKey()
cv2.destroyAllWindows()
# 输出结果
img.shape= (512, 512, 3)
rst.shape= (256, 460, 3)
在OpenCV中,图像的翻转采用函数cv2.flip()实现,该函数可以实现图像在水平方向翻转,垂直方向翻转,两个方向同时翻转。
dst = cv2.flip( src, filpCode )
该函数中,目标像素点与原始像素点的关系可表述为:
d s t i j = { s r c s r c . r o w s − i − 1 , j , f i l c C o d e = 0 s r c i , s r c . c o l s − j − 1 , f i l c C o d e > 0 s r c s r c . r o w s − i − 1 , s r c . c o l s − j − 1 , f i l c C o d e < 0 dst_{ij} = \begin{cases} src_{src.rows-i-1, \quad j}, \quad filcCode = 0 \\ src_{i, \quad src.cols-j-1}, \quad filcCode > 0 \\ src_{src.rows-i-1, \quad src.cols-j-1}, \quad filcCode < 0 \end{cases} dstij=⎩⎪⎨⎪⎧srcsrc.rows−i−1,j,filcCode=0srci,src.cols−j−1,filcCode>0srcsrc.rows−i−1,src.cols−j−1,filcCode<0
其中,dst是目标像素点,src是原始像素点。
例:
import cv2
img = cv2.imread('../lena.bmp')
x = cv2.flip(img, 0)
y = cv2.flip(img, 1)
xy = cv2.flip(img, -1)
cv2.imshow('img', img)
cv2.imshow('x', x)
cv2.imshow('y', y)
cv2.imshow('xy', xy)
cv2.waitKey()
cv2.destroyAllWindows()
仿射变换是指图像可以通过一系列的几何变换来实现平移、旋转等多种操作。该变换能够保持图像的平直性和平行性。平直性是指图像经过仿射变换后,直线仍然是直线;平行线是指图像在完成仿射变换后,平行线仍然是平行线。
在OpenCV中的实现仿射函数为cv2.warpAffine(),其通过一个变换矩阵(映射矩阵)M实现变换,具体为: d s t ( x , y ) = s r c ( M 11 x + M 12 y + M 13 , M 13 x + M 22 y + M 23 ) dst(x, y) = src(M_{11}x + M_{12}y + M_{13}, M_{13}x + M_{22}y + M_{23} ) dst(x,y)=src(M11x+M12y+M13,M13x+M22y+M23)
如图所示,可以通过一个变换矩阵M,将原始图像O变换为仿射图像R
因此,可以采用仿射函数cv2.warpAffine()实现对图像的平移、旋转等操作:
通过以上分析可知,在OpenCV中使用函数cv2.warpAffine()实现仿射变换,忽略其可选参数后的语法格式为:
其通过转换矩阵M将原始图像src转换为目标图像dst:
因此,进行何种形式的仿射变换完全取决于转换矩阵M。下面分别介绍通过不同的转换矩阵M实现的不同的仿射变换。
通过转换矩阵M实现将原始图像src转换为目标矩阵dst:
将原始图像src向右侧移动100个像素,向下方移动200个像素其对应的关系为:
将上述表达式不成完整,即:
因此可以初定对饮的转换矩阵M中的各个元素值为:
转换矩阵M:
import cv2
import numpy as np
img = cv2.imread('../lena.bmp')
h, w = img.shape[:2]
x = 100
y = 200
M = np.float32([[1, 0, x], [0, 1, y]])
move = cv2.warpAffine(img, M, (w, h))
cv2.imshow('img', img)
cv2.imshow('move', move)
cv2.waitKey()
cv2.destroyAllWindows()
在OpenCV中使用函数cv2.warpAffine()对图像进行旋转时,可以通过函数cv2.getRotationMatrix2D()获取转换矩阵。该函数的语法是:
利用函数cv2.getRotationMatrix2D()可以直接生成要使用的转换矩阵M。例如,想要以图像中心为原点,逆时针旋转45度,并将目标图像缩小为原始图像的0.6倍,则在调用函数cv2.getRotationMatrix2D()生成转换矩阵M时的语句为:
例如:根据上述要求进行图像旋转
import cv2
lena = cv2.imread('../lena.bmp')
h, w = lena.shape[:2]
M = cv2.getRotationMatrix2D((h / 2, w / 2), 45, 0.6)
rotate = cv2.warpAffine(lena, M, (w, h))
cv2.imshow('lena', lena)
cv2.imshow('rotate', rotate)
cv2.waitKey()
cv2.destroyAllWindows()
前面讲了平移和旋转两种仿射变换都比较简单,对于更多复杂的仿射变换,OpenCV提供了函数cv2.getAffineTransFORM()来生成仿射函数cv2.warpAffine()所需要的转换矩阵M。该函数的语法格式是:
式中:
在该函数中,其参数值src和dst是包含三个二维数组(x, y)点的数组。上述参数通过函数cv2.getAffineTransform()定义了两个平行四边形。src和dst中的三个点分别对应平行四边形左上角、右上角、左下角三个点。函数cv2.getAffineTransform()对所指定的点完成映射后,将所有其他点的映射关系按照指定点的关系计算确定。函数cv2.warpAffine()以函数cv2.getAffineTransform()获取的转换矩阵M为参数,将src中的点仿射到dst中。
例:
import cv2
import numpy as np
img = cv2.imread('../lena.bmp')
rows, cols, ch = img.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)
dst = cv2.warpAffine(img, M, (cols, rows))
cv2.imshow('img', img)
cv2.imshow('dst', dst)
cv2.waitKey()
cv2.destroyAllWindows()
仿射可以将矩形映射为任意的平行四边形,透视变换则可以将矩形映射为任意的四边形。
透视变换的函数是cv2.warpPerspective(),具体语法是:
与仿射变换一样,同样使用一个函数来生成函数cv2.warpPerspective()所使用的转换矩阵。该函数是cv2.getPerspectiveTransform(),语法格式为:
需要注意的是,src 参数和 dst 参数是包含四个点的数组。实际使用中,我们可以根据需要控制src中的四个点映射到dst中的四个点。
例:
import cv2
import numpy as np
img = cv2.imread('../demo.bmp')
rows, cols = img.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)
dst = cv2.warpPerspective(img, M, (cols, rows))
cv2.imshow('img', img)
cv2.imshow('dst', dst)
cv2.waitKey()
cv2.destroyAllWindows()
把一幅图像内的像素点映射到另一幅图像的指定位置,这个过程称为重映射。OpenCV中有多种重映射的的方式,但是我们有时希望使用自定义的方式来完成重映射。
OpenCV内重映射函数cv2.remap()提供了更方便、更自由的映射方式,其语法格式如下:
dst = cv2.remap( src, map1, map2, interpolation [, borderMode [, borderValue]])
**重映射通过修改像素点的位置得到一幅新图像。在构建新图像时,需要确定新图像中每个像素点在原始图像中的位置。因此,映射函数的作用是查找新图像像素在原始图像内的位置。该过程是将新图像像素映射到原始图像的过程,因此被称为反向映射。**在函数 cv2.remap()中,参数map1和参数map2用来说明反向映射,map1针对的是坐标x,map2针对的是坐标y。
需要说明的是,map1和map2的值都是浮点数。因此,目标图像可以映射回一个非整数的值,这意味着目标图像可以“反向映射”到原始图像中两个像素点之间的位置(当然,该位置是不存在像素值的)。这时,可以采用不同的方法实现插值,函数中的interpolation参数可以控制插值方式。正是由于参数map1和参数map2的值是浮点数,所以通过函数cv2.remamp()所能实现的映射关系变得更加随意,可以通过自定义映射参数实现不同形式的映射。
需要注意的是,函数cv2.remap()中参数map1指代的是像素点所在位置的列号,参数map2指代的是像素点所在位置的行号。**例如,我们想将目标图像(映射结果图像)中某个点A映射为原始图像内处于第0行第3列上的像素点B,那么需要将A点所对应的参数map1对应位置上的值设为3,参数map2对应位置上的值设为0。**所以,通常情况下,我们将map1写为mapx,并且将map2写成mapy,以方便理解。
同样,如果想将目标图像(映射结果图像)中所有像素点都映射为原始图像内处于第0行第3列上的像素点B,那么需要将参数map1内的值均设为3,将参数map2内的值均设为0。
例:
import cv2
import numpy as np
img = np.random.randint(0, 256, size=[4, 5], dtype=np.uint8)
# rows, cols = img.shape
mapx = np.ones(img.shape, np.float32) * 3
mapy = np.ones(img.shape, np.float32) * 0
rst = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)
print('img=\n', img)
print('mapx=\n', mapx)
print('mapy=\n', mapy)
print('rst=\n', rst)
# 输出结果
img=
[[196 22 15 82 189]
[228 208 215 88 113]
[159 6 157 101 174]
[181 95 194 206 111]]
mapx=
[[3. 3. 3. 3. 3.]
[3. 3. 3. 3. 3.]
[3. 3. 3. 3. 3.]
[3. 3. 3. 3. 3.]]
mapy=
[[0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0.]]
rst=
[[82 82 82 82 82]
[82 82 82 82 82]
[82 82 82 82 82]
[82 82 82 82 82]]
为了更好地了解重映射函数cv2.remap()的使用方法,下面介绍如何通过该函数实现图像的复制。在映射时,将参数做如下处理:
将map1的值设定为对饮位置上的x轴的坐标轴。
例:
import cv2
import numpy as np
img = cv2.imread('../lena.bmp')
rows, cols = img.shape[:2]
mapx = np.zeros(img.shape[:2], np.float32)
mapy = np.zeros(img.shape[:2], np.float32)
for i in range(rows):
for j in range(cols):
mapx.itemset((i, j), j)
mapy.itemset((i, j), i)
rst = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)
cv2.imshow('img', img)
cv2.imshow('rst', rst)
cv2.waitKey()
cv2.destroyAllWindows()
映射过程中如果要绕x轴旋转:
反映在map1, map2上:
需要注意,OpenCV 中行号的下标是从0开始的,所以在对称关系中存在“当前行号+对称行号=总行数-1”的关系。据此,在绕着x轴翻转时,map2中当前行的行号调整为“总行数-1-当前行号”。
import cv2
import numpy as np
img = cv2.imread('../lena.bmp')
rows, cols = img.shape[:2]
mapx = np.zeros(img.shape[:2], np.float32)
mapy = np.zeros(img.shape[:2], np.float32)
for i in range(rows):
for j in range(cols):
mapx.itemset((i, j), j)
mapy.itemset((i, j), rows-1-i)
rst = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)
cv2.imshow('img', img)
cv2.imshow('rst', rst)
cv2.waitKey()
cv2.destroyAllWindows()
映射过程中如果要绕y轴旋转:
反映在map1, map2上:
需要注意,OpenCV 中行号的下标是从0开始的,所以在对称关系中存在“当前列号+对称列号=总列数-1”的关系。据此,在绕着y轴翻转时,map1中当前列的列号调整为“总列数-1-当前列号”。
import cv2
import numpy as np
img = cv2.imread('../lena.bmp')
rows, cols = img.shape[:2]
mapx = np.zeros(img.shape[:2], np.float32)
mapy = np.zeros(img.shape[:2], np.float32)
for i in range(rows):
for j in range(cols):
mapx.itemset((i, j), cols - j -1)
mapy.itemset((i, j), i)
rst = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)
cv2.imshow('img', img)
cv2.imshow('rst', rst)
cv2.waitKey()
cv2.destroyAllWindows()
映射过程中如果要绕x、y轴旋转:
反映在map1, map2上:
import cv2
import numpy as np
img = cv2.imread('../lena.bmp')
rows, cols = img.shape[:2]
mapx = np.zeros(img.shape[:2], np.float32)
mapy = np.zeros(img.shape[:2], np.float32)
for i in range(rows):
for j in range(cols):
mapx.itemset((i, j), cols - j - 1)
mapy.itemset((i, j), rows - i - 1)
rst = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)
cv2.imshow('img', img)
cv2.imshow('rst', rst)
cv2.waitKey()
cv2.destroyAllWindows()
如果想让图像的x轴、y轴互换,意味着在映射过程中,对于任意一点,都需要将其x轴、y轴坐标互换。反映在mapx和mapy上:
需要注意的是,如果行数和列数不一致,上述运算可能存在值无法映射的情况。默认情况下,无法完成映射的值会被处理为0。
import cv2
import numpy as np
img = cv2.imread('../lena.bmp')
rows, cols = img.shape[:2]
mapx = np.zeros(img.shape[:2], np.float32)
mapy = np.zeros(img.shape[:2], np.float32)
for i in range(rows):
for j in range(cols):
mapx.itemset((i, j), i)
mapy.itemset((i, j), j)
rst = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)
cv2.imshow('img', img)
cv2.imshow('rst', rst)
cv2.waitKey()
cv2.destroyAllWindows()
前面的映射都是直接完成整数映射,处理起来比较方便。在处理更复杂的问题时,就需要对行、列值进行比较复杂的运算来实现。
例如:使用cv2.remap()缩小图像。
缩小图像后,可以将图像固定在围绕其中心的某个区域。例如,将其x轴、y轴设置为:
为了处理方便,我们让不在上述区域的点都取(0,0)坐标点的值。
import cv2
import numpy as np
img = cv2.imread('../11111.png')
print(img.shape)
rows, cols = img.shape[:2]
mapx = np.zeros(img.shape[:2], np.float32)
mapy = np.zeros(img.shape[:2], np.float32)
for i in range(rows):
for j in range(cols):
if 0.25 * cols < j < 0.75 * cols and 0.25 * rows < i < 0.75 * rows:
mapx.itemset((i, j), 2 * (j - cols * 0.25) + 0.5)
mapy.itemset((i, j), 2 * (i - rows * 0.25) + 0.5)
else:
mapx.itemset((i, j), 0)
mapy.itemset((i, j), 0)
rst = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)
cv2.imshow('img', img)
cv2.imshow('rst', rst)
cv2.waitKey()
cv2.destroyAllWindows()