第 10 章

图像的几何变换

本章共 5 个小节 · 缩放、翻转、仿射、透视、重映射
图像的几何变换会改变图像的大小、方向、外形或像素对应关系。相关数学并不简单, 但 OpenCV 已经把矩阵计算与插值流程封装到函数中;掌握函数参数、坐标顺序和输出尺寸, 就能完成缩放、翻转、仿射、透视和自定义重映射等效果。参考原书第 10-2 页。
10-1

图像缩放效果

cv2.resize() 用来重新设定图像尺寸。可以直接给出目标尺寸 dsize, 也可以把 dsize 设为 None,再用 fxfy 指定水平和垂直方向的缩放比例。

dst = cv2.resize(src, dsize, fx, fy, interpolation)
参数说明
src原始图像。
dsize目标尺寸,格式是 (width, height),不是 (height, width)
fx / fy水平方向与垂直方向的缩放比例。
interpolation缩放时的插值方法,影响边缘平滑度与速度。
插值常量用途
cv2.INTER_NEAREST最近邻插值,速度快,但放大时容易有块状边缘。
cv2.INTER_LINEAR双线性插值,默认方法,通常适合一般缩放。
cv2.INTER_CUBIC双三次插值,放大时边缘更平滑,但计算较慢。
cv2.INTER_AREA缩小时常用,能得到较稳定的重采样效果。
cv2.INTER_LANCZOS4Lanczos 插值,会在 x、y 方向参考更多邻近点。

程序实例 ch10_1.py:使用 dsize 指定新尺寸

src = cv2.imread("southpole.jpg") width = 300 height = 200 dsize = (width, height) dst = cv2.resize(src, dsize)

程序实例 ch10_2.py:使用 fx 与 fy 指定缩放比例

src = cv2.imread("southpole.jpg") dst = cv2.resize(src, None, fx=0.5, fy=1.1) print(f"src.shape = {src.shape}") print(f"dst.shape = {dst.shape}")
重点 shape 输出顺序是 (height, width, channels),而 dsize 输入顺序是 (width, height)
缩放执行结果
ch10_1.py 图像缩放结果
使用 dsize=(300, 200) 缩放图像,参考原书第 10-3 页。
10-2

图像翻转

cv2.flip() 根据 flipCode 翻转图像。垂直翻转可理解为沿 x 轴上下翻转, 水平翻转可理解为沿 y 轴左右翻转;如果 flipCode 为负数,则两个方向同时翻转。 参考原书第 10-410-6 页。

dst = cv2.flip(src, flipCode)
flipCode效果
0垂直翻转,上下颠倒。
正值,例如 1水平翻转,左右颠倒。
负值,例如 -1同时水平与垂直翻转。

程序实例 ch10_3.py:一次比较三种翻转

src = cv2.imread("python.jpg") dst1 = cv2.flip(src, 0) dst2 = cv2.flip(src, 1) dst3 = cv2.flip(src, -1)
翻转执行结果
ch10_3.py 垂直、水平和双向翻转结果
从左到右为原始图像、垂直翻转、水平翻转、水平与垂直翻转,参考原书第 10-6 页。
10-3

图像仿射

仿射变换是在二维平面上移动图像坐标。变换之后,直线仍是直线,平行线仍保持平行。 常见仿射效果包括平移、旋转和倾斜。OpenCV 使用 2 行 3 列的矩阵 M 描述仿射关系,再交给 cv2.warpAffine() 生成新图像。参考原书第 10-710-14 页。

dst = cv2.warpAffine(src, M, dsize, flags, borderMode, borderValue)
参数说明
M2x3 仿射矩阵,不同矩阵会得到不同的平移、旋转或倾斜结果。
dsize输出图像尺寸,格式仍是 (width, height)
flags插值方法。若包含 cv2.WARP_INVERSE_MAP,表示 M 是逆变换矩阵。
borderMode边界像素处理方式,默认是常量填充。
borderValue边界填充值,默认是 0,所以空白区域通常显示为黑色。

10-3-3 图像平移

平移就是把图像内容平行移动。书中示例把图像向右移动 x=50、向下移动 y=100, 因此矩阵的第三列就是水平和垂直位移量。

程序实例 ch10_4.py:平移 x=50、y=100

height, width = src.shape[0:2] M = np.float32([[1, 0, 50], [0, 1, 100]]) dsize = (width, height) dst = cv2.warpAffine(src, M, dsize)
平移执行结果
ch10_4.py 图像平移结果
右移 50 像素、下移 100 像素后,空出的区域以黑色填充,参考原书第 10-9 页。

10-3-4 图像旋转

旋转同样通过仿射矩阵完成。cv2.getRotationMatrix2D() 可根据旋转中心、角度和缩放比生成矩阵。 角度为正值时逆时针旋转,负值时顺时针旋转。

M = cv2.getRotationMatrix2D(center, angle, scale)

程序实例 ch10_5.py:逆时针与顺时针旋转 30 度

height, width = src.shape[0:2] dsize = (width, height) M = cv2.getRotationMatrix2D((width / 2, height / 2), 30, 1) dst1 = cv2.warpAffine(src, M, dsize) M = cv2.getRotationMatrix2D((width / 2, height / 2), -30, 1) dst2 = cv2.warpAffine(src, M, dsize)
旋转执行结果
ch10_5.py 图像旋转结果
原始图像、逆时针 30 度、顺时针 30 度的比较,参考原书第 10-10 页。

10-3-5 图像倾斜

倾斜可以看成把矩形映射成平行四边形。因为仿射会保持平行性,所以只要指定来源图像的 3 个点, 以及目标图像中对应的 3 个点,就能计算矩阵。OpenCV 使用 cv2.getAffineTransform() 根据这两组三点坐标产生 M

M = cv2.getAffineTransform(src_points, dst_points)

程序实例 ch10_6.py:目标图像向右上方倾斜

srcp = np.float32([[0, 0], [width - 1, 0], [0, height - 1]]) dstp = np.float32([[30, 0], [width - 1, 0], [0, height - 1]]) M = cv2.getAffineTransform(srcp, dstp) dst = cv2.warpAffine(src, M, (width, height))

程序实例 ch10_7.py:目标图像向左上方倾斜

srcp = np.float32([[0, 0], [width - 1, 0], [0, height - 1]]) dstp = np.float32([[0, 0], [width - 30, 30], [30, height - 1]]) M = cv2.getAffineTransform(srcp, dstp) dst = cv2.warpAffine(src, M, (width, height))

程序实例 ch10_8.py:倾斜时更改输出宽度

srcp = np.float32([[0, 0], [width - 1, 0], [0, height - 1]]) dstp = np.float32([[80, 0], [width - 1, 0], [0, height - 1]]) M = cv2.getAffineTransform(srcp, dstp) dst = cv2.warpAffine(src, M, (width + 80, height))

程序实例 ch10_9.py:使用 AB 点倾斜线重新设计 ch10_8.py

srcp = np.float32([[0, 0], [width - 1, 0], [0, height - 1]]) dstp = np.float32([[80, 0], [width - 1, 80], [0, height - 1]]) M = cv2.getAffineTransform(srcp, dstp) dst = cv2.warpAffine(src, M, (width + 80, height + 80))
倾斜执行结果
ch10_9.py 图像倾斜结果
改变目标图像的 A、B、C 三个对应点即可得到不同倾斜效果,参考原书第 10-14 页。
10-4

图像透视

透视变换可以把一个矩形映射成任意四边形,因此适合处理由拍摄角度造成的远近变化。 它不像仿射那样只需要 3 个对应点,而是需要来源图像和目标图像各 4 个点。 先用 cv2.getPerspectiveTransform() 取得透视矩阵,再用 cv2.warpPerspective() 产生目标图像。参考原书第 10-1410-16 页。

M = cv2.getPerspectiveTransform(src_points, dst_points)
dst = cv2.warpPerspective(src, M, dsize, flags, borderMode, borderValue)

程序实例 ch10_10.py:四点透视变换

srcp = np.float32([[0, 0], [width - 1, 0], [0, height - 1], [width - 1, height - 1]]) dstp = np.float32([[150, 0], [width - 150, 0], [0, height - 1], [width - 1, height - 1]]) M = cv2.getPerspectiveTransform(srcp, dstp) dst = cv2.warpPerspective(src, M, (width, height))
比较 仿射用 3 个点,保持平行线;透视用 4 个点,可以让平行线在视觉上收敛,形成近大远小的效果。
透视执行结果
ch10_10.py 透视变换结果
把目标图像上方收窄,得到由下方向前观察的透视效果,参考原书第 10-16 页。
10-5

重映射

cv2.remap() 是自定义映射函数。前面的仿射和透视主要由矩阵定义坐标关系; 重映射则直接给出每一个目标像素应该到来源图像的哪个坐标取值,因此可用于复制、翻转、扭曲、 局部变形和特殊缩放。参考原书第 10-17 页。

dst = cv2.remap(src, map1, map2, interpolation, borderMode, borderValue)
参数说明
map1 / mapx目标图像每个像素对应的来源 x 坐标,也就是 column 坐标。
map2 / mapy目标图像每个像素对应的来源 y 坐标,也就是 row 坐标。
interpolation插值方法,默认常用 cv2.INTER_LINEAR;重映射不支持 INTER_AREA
borderMode来源坐标超出边界时的像素处理方式。
borderValue边界填充值。

10-5-1 map1 与 map2

映射的核心是从目标图像回查来源图像:对目标图像的每个位置 (r, c)mapx[r, c] 保存来源图像的 x 坐标,mapy[r, c] 保存来源图像的 y 坐标。 如果所有目标像素都取来源图像的同一个坐标,就会得到一张填满同一像素值的图像。 参考原书第 10-18 页。

程序实例 ch10_10_1.py:所有目标像素都映射到同一来源坐标

# ch10_10_1.py import cv2 import numpy as np src = np.random.randint(0, 256, size=[3, 4], dtype=np.uint8) rows, cols = src.shape mapx = np.ones(src.shape, np.float32) * 3 mapy = np.ones(src.shape, np.float32) * 2 dst = cv2.remap(src, mapx, mapy, cv2.INTER_LINEAR) print(f"src =\n{src}") print(f"mapx =\n{mapx}") print(f"mapy =\n{mapy}") print(f"dst =\n{dst}")

10-5-2 图像复制

复制图像时,目标图像每个像素都取来源图像同一位置:mapx[r, c] = cmapy[r, c] = r。矩阵示例和图像示例分别对应原书第 10-1910-20 页。

程序实例 ch10_11.py:使用 remap() 执行矩阵复制

# ch10_11.py import cv2 import numpy as np src = np.random.randint(0, 256, size=[3, 5], dtype=np.uint8) rows, cols = src.shape mapx = np.zeros(src.shape, np.float32) mapy = np.zeros(src.shape, np.float32) for r in range(rows): for c in range(cols): mapx.itemset((r, c), c) mapy.itemset((r, c), r) dst = cv2.remap(src, mapx, mapy, cv2.INTER_LINEAR) print(f"src =\n{src}") print(f"mapx =\n{mapx}") print(f"mapy =\n{mapy}") print(f"dst =\n{dst}")

程序实例 ch10_12.py:使用 remap() 执行图像复制

# ch10_12.py import cv2 import numpy as np src = cv2.imread("huang.jpg") rows, cols = src.shape[:2] mapx = np.zeros(src.shape[:2], np.float32) mapy = np.zeros(src.shape[:2], np.float32) for r in range(rows): for c in range(cols): mapx.itemset((r, c), c) mapy.itemset((r, c), r) dst = cv2.remap(src, mapx, mapy, cv2.INTER_LINEAR) cv2.imshow("src", src) cv2.imshow("dst", dst) cv2.waitKey(0) cv2.destroyAllWindows()
重映射复制结果
ch10_12.py remap 图像复制结果
目标图像逐点回查来源图像相同坐标,结果就是完整复制,参考原书第 10-20 页。

10-5-3 与 10-5-4 翻转

用重映射做垂直翻转时,x 坐标不变,y 坐标改为 rows - 1 - r; 做水平翻转时,y 坐标不变,x 坐标改为 cols - 1 - c。 这与 cv2.flip() 的效果类似,但 remap() 显示了像素坐标如何被重新指定。

程序实例 ch10_13.py:使用 remap() 执行垂直翻转的矩阵实例

# ch10_13.py import cv2 import numpy as np src = np.random.randint(0, 256, size=[3, 5], dtype=np.uint8) rows, cols = src.shape mapx = np.zeros(src.shape, np.float32) mapy = np.zeros(src.shape, np.float32) for r in range(rows): for c in range(cols): mapx.itemset((r, c), c) mapy.itemset((r, c), rows - 1 - r) dst = cv2.remap(src, mapx, mapy, cv2.INTER_LINEAR) print(f"src =\n{src}") print(f"mapx =\n{mapx}") print(f"mapy =\n{mapy}") print(f"dst =\n{dst}")

程序实例 ch10_14.py:使用 remap() 执行图像垂直翻转

# ch10_14.py import cv2 import numpy as np src = cv2.imread("huang.jpg") rows, cols = src.shape[:2] mapx = np.zeros(src.shape[:2], np.float32) mapy = np.zeros(src.shape[:2], np.float32) for r in range(rows): for c in range(cols): mapx.itemset((r, c), c) mapy.itemset((r, c), rows - 1 - r) dst = cv2.remap(src, mapx, mapy, cv2.INTER_LINEAR) cv2.imshow("src", src) cv2.imshow("dst", dst) cv2.waitKey(0) cv2.destroyAllWindows()

程序实例 ch10_15.py:使用 remap() 执行水平翻转的矩阵实例

# ch10_15.py import cv2 import numpy as np src = np.random.randint(0, 256, size=[3, 5], dtype=np.uint8) rows, cols = src.shape mapx = np.zeros(src.shape, np.float32) mapy = np.zeros(src.shape, np.float32) for r in range(rows): for c in range(cols): mapx.itemset((r, c), cols - 1 - c) mapy.itemset((r, c), r) dst = cv2.remap(src, mapx, mapy, cv2.INTER_LINEAR) print(f"src =\n{src}") print(f"mapx =\n{mapx}") print(f"mapy =\n{mapy}") print(f"dst =\n{dst}")

程序实例 ch10_16.py:使用 remap() 执行图像水平翻转

# ch10_16.py import cv2 import numpy as np src = cv2.imread("huang.jpg") rows, cols = src.shape[:2] mapx = np.zeros(src.shape[:2], np.float32) mapy = np.zeros(src.shape[:2], np.float32) for r in range(rows): for c in range(cols): mapx.itemset((r, c), cols - 1 - c) mapy.itemset((r, c), r) dst = cv2.remap(src, mapx, mapy, cv2.INTER_LINEAR) cv2.imshow("src", src) cv2.imshow("dst", dst) cv2.waitKey(0) cv2.destroyAllWindows()
垂直翻转
ch10_14.py remap 垂直翻转结果
使用 mapy = rows - 1 - r 实现上下翻转,参考原书第 10-22 页。
水平翻转
ch10_16.py remap 水平翻转结果
使用 mapx = cols - 1 - c 实现左右翻转,参考原书第 10-23 页。

10-5-5 与 10-5-6 缩放和压缩

重映射也能完成缩放。书中先把目标图像中间区域映射到来源图像完整区域, 外围像素则统一映射到来源图像 (0, 0),因此外围显示为来源左上角像素的颜色。 垂直压缩则把 mapy 的来源 y 坐标放大,例如 mapy[r, c] = 2 * r, 让目标图像只采样来源图像的上半部分。

程序实例 ch10_17.py:把图像缩小到目标中央

# ch10_17.py import cv2 import numpy as np src = cv2.imread("tunnel.jpg") rows, cols = src.shape[:2] mapx = np.zeros(src.shape[:2], np.float32) mapy = np.zeros(src.shape[:2], np.float32) for r in range(rows): for c in range(cols): if 0.25 * rows < r < 0.75 * rows and 0.25 * cols < c < 0.75 * cols: mapx.itemset((r, c), 2 * (c - cols * 0.25)) mapy.itemset((r, c), 2 * (r - rows * 0.25)) else: mapx.itemset((r, c), 0) mapy.itemset((r, c), 0) dst = cv2.remap(src, mapx, mapy, cv2.INTER_LINEAR) cv2.imshow("src", src) cv2.imshow("dst", dst) cv2.waitKey(0) cv2.destroyAllWindows()
重映射缩小
ch10_17.py remap 缩小结果
中间区域显示缩小图像,外围取来源图像左上角像素,参考原书第 10-25 页。

程序实例 ch10_18.py:垂直压缩一半

# ch10_18.py import cv2 import numpy as np src = cv2.imread("tunnel.jpg") rows, cols = src.shape[:2] mapx = np.zeros(src.shape[:2], np.float32) mapy = np.zeros(src.shape[:2], np.float32) for r in range(rows): for c in range(cols): mapx.itemset((r, c), c) mapy.itemset((r, c), 2 * r) dst = cv2.remap(src, mapx, mapy, cv2.INTER_LINEAR) cv2.imshow("src", src) cv2.imshow("dst", dst) cv2.waitKey(0) cv2.destroyAllWindows()
垂直压缩
ch10_18.py remap 垂直压缩结果
把来源 y 坐标加倍后,目标图像呈现垂直方向压缩,参考原书第 10-26 页。
习题

1. 参考 ch10_4.py,把图像改为向上移动 100 像素、向左移动 50 像素。

2. 使用 remap() 执行水平与垂直同时翻转。

习题 2 执行结果
习题 2 remap 水平与垂直同时翻转结果
参考原书第 10-27 页。

3. 使用 remap()hung_square.jpg 逆时针旋转 90 度。

习题 3 执行结果
习题 3 remap 逆时针旋转 90 度结果
参考原书第 10-27 页。

习题说明参考原书第 10-2610-27 页。