首页
/ OpenCV 图像几何变换实战:缩放、平移、旋转、仿射与透视变换的矩阵原理与代码实现

OpenCV 图像几何变换实战:缩放、平移、旋转、仿射与透视变换的矩阵原理与代码实现

2026-09-06 13:25:11作者:翟江哲Frasier

本文以 OpenCV Python 官方教程《Geometric Transformations of Images》为主体,系统讲解图像几何变换的两大核心函数 cv.warpAffinecv.warpPerspective,覆盖缩放、平移、旋转、仿射变换和透视变换五类操作的完整代码、变换矩阵推导与参数细节,并结合仓库中 getRotationMatrix2D 的 C++ 源码实现与 imgproc 模块的测试用例,帮助读者从"会调用"深入到"懂原理"。读完本文,你可以独立完成任意 2D 图像几何变换,并理解每个参数(输出尺寸、旋转中心、插值方法)在底层如何起作用。

平移变换结果 旋转变换结果 仿射变换结果 透视变换结果

上图为教程中四类变换(平移、旋转、仿射、透视)在官方示例图上的真实运行结果,分别对应该教程 原文 中四个代码段的输出。

两个入口函数:warpAffine 与 warpPerspective

OpenCV 用两个函数支撑几乎全部几何变换:

  • cv.warpAffine():接收一个 2x3 的仿射变换矩阵,完成平移、旋转、缩放、剪切等仿射操作;
  • cv.warpPerspective():接收一个 3x3 的透视变换矩阵,完成单应(homography)级别的透视操作。

因此掌握几何变换的关键在于两件事:一是会构造正确的变换矩阵,二是理解变换矩阵如何被 warp 函数应用到每个像素。矩阵构造由三类辅助函数完成:

辅助函数 输入 输出矩阵 配套 warp 函数
cv.getRotationMatrix2D(center, angle, scale) 旋转中心、角度、缩放系数 2x3 cv.warpAffine
cv.getAffineTransform(src, dst) 源图/目标图各 3 个不共线对应点 2x3 cv.warpAffine
cv.getPerspectiveTransform(src, dst) 源图/目标图各 4 个对应点(其中任意 3 点不共线) 3x3 cv.warpPerspective

这些函数在 C++ 端的声明与文档注释见 imgproc 头文件geometry 模块头文件

缩放(Scaling)

缩放本质上就是图像缩放,由 cv.resize() 完成。尺寸可以手动指定,也可以指定缩放因子 fx / fy,两种写法等价:

import numpy as np
import cv2 as cv

img = cv.imread('messi5.jpg')
assert img is not None, "file could not be read, check with os.path.exists()"

res = cv.resize(img, None, fx=2, fy=2, interpolation=cv.INTER_CUBIC)

# OR

height, width = img.shape[:2]
res = cv.resize(img, (2*width, 2*height), interpolation=cv.INTER_CUBIC)

插值方法的选择直接影响质量,官方推荐的搭配是:

  • 缩小图像:优先 cv.INTER_AREA(区域平均,抑制混叠);
  • 放大图像:cv.INTER_CUBIC(三次卷积,较慢但质量高)或 cv.INTER_LINEAR(双线性,默认);
  • 若显式传入 dsize 而未指定 fx/fy,缩放因子由 fx = dst_width / src_widthfy = dst_height / src_height 反推。

注意 resize 的目标尺寸参数顺序是 (width, height),即 (宽, 高),与 img.shape 返回的 (height, width) 相反,这是初学者最常踩的坑之一。

平移(Translation)

平移是物体位置的移动。若已知 (x, y) 方向的偏移量 (tx, ty),变换矩阵为:

M = [ 1  0  tx ]
    [ 0  1  ty ]

将其转为 np.float32 数组后传入 cv.warpAffine() 即可。以偏移 (100, 50) 为例:

import numpy as np
import cv2 as cv

img = cv.imread('messi5.jpg', cv.IMREAD_GRAYSCALE)
assert img is not None, "file could not be read, check with os.path.exists()"
rows, cols = img.shape

M = np.float32([[1, 0, 100], [0, 1, 50]])
dst = cv.warpAffine(img, M, (cols, rows))

cv.imshow('img', dst)
cv.waitKey(0)
cv.destroyAllWindows()

运行结果即前文所示的 translation.jpg:图像整体向右下移动,左上角露出背景。

官方警告(务必注意)cv.warpAffine() 的第三个参数是输出图像尺寸,形式为 (width, height)。牢记 width = 列数(cols),height = 行数(rows)。若误写为 (rows, cols),输出图像会被拉伸变形。

旋转(Rotation)

以角度 θ 旋转的基础 2x2 矩阵为:

M = [ cosθ   -sinθ ]
    [ sinθ    cosθ ]

但 OpenCV 提供的 cv.getRotationMatrix2D() 支持可调缩放任意旋转中心,其生成的 2x3 矩阵为:

M = [ α   β   (1-α)·center.x - β·center.y ]
    [ -β  α   β·center.x + (1-α)·center.y ]

其中  α = scale·cosθ,  β = scale·sinθ

这个公式的含义是"先绕中心旋转+缩放,再平移回原位":中心点 (center.x, center.y) 本身在变换下保持不动。结合仓库 C++ 源码可以逐行印证:geometry.cpp 中 getRotationMatrix2D_ 的实现 中,先把角度换算成弧度,再计算 alpha = cos(angle)*scalebeta = sin(angle)*scale,最后直接按上式填充 2x3 矩阵;参数语义在 2d.hpp 的 API 注释 中说明得很清楚:角度单位为度,正值表示逆时针(以图像左上角为坐标原点),scale 是各向同性缩放因子。

将图像绕其中心旋转 90°(不缩放)的完整代码:

img = cv.imread('messi5.jpg', cv.IMREAD_GRAYSCALE)
assert img is not None, "file could not be read, check with os.path.exists()"
rows, cols = img.shape

# cols-1 and rows-1 are the coordinate limits.
M = cv.getRotationMatrix2D(((cols-1)/2.0, (rows-1)/2.0), 90, 1)
dst = cv.warpAffine(img, M, (cols, rows))

这里旋转中心取 ((cols-1)/2.0, (rows-1)/2.0),即图像几何中心(像素坐标上限为 cols-1 与 rows-1)。运行结果见 rotation.jpg

两个实践要点:

  1. 中心点计算要严谨:用浮点除法 (cols-1)/2.0 而非整数除法,否则中心会偏移半个像素,旋转后出现不对称的缺口;
  2. 旋转后画布裁剪问题:非 90° 倍数的旋转会超出原画布,教程示例固定输出尺寸为 (cols, rows) 会裁掉四角。从 imgproc 的 warp 测试用例 可以看到官方测试同样以旋转矩阵构造输入并逐像素比对 warpAffine/warpPerspective 的输出,验证了"矩阵 + 输出尺寸"这一最小输入组合足以完整定义一次 warp 操作。若需保留全部像素,常规做法是把 M 的平移项再补偿一个由旋转角度和图像尺寸推导出的位移量(即 expand 思想的等价手动实现)。

仿射变换(Affine Transformation)

仿射变换的几何性质是:原图中所有平行线,变换后仍然平行。求解变换矩阵需要输入图像上的 3 个点及其在输出图像中的对应位置,cv.getAffineTransform 据此解算出一个 2x3 矩阵,再交给 cv.warpAffine 执行。

教程以 drawing.png 为例,选取的三对对应点用绿色标出:

img = cv.imread('drawing.png')
assert img is not None, "file could not be read, check with os.path.exists()"
rows, cols, ch = img.shape

pts1 = np.float32([[50, 50], [200, 50], [50, 200]])
pts2 = np.float32([[10, 100], [200, 50], [100, 250]])

M = cv.getAffineTransform(pts1, pts2)

dst = cv.warpAffine(img, M, (cols, rows))

plt.subplot(121), plt.imshow(img), plt.title('Input')
plt.subplot(122), plt.imshow(dst), plt.title('Output')
plt.show()

结果见 affine.jpg:三个控制点精确映射到目标位置,整幅图的直线结构保持平行关系。

从源码结构看,getAffineTransform 的本质是解一个线性方程组:每对点 (xi, yi) → (xi', yi') 提供两个方程,3 对点共 6 个方程恰好解出 2x3 矩阵的 6 个未知数,这正是"为什么必须 3 个点且不能共线"的数学原因(共线时方程组秩不足,矩阵不唯一)。C++ 端对应的数学形式在 2d.hpp 的 getAffineTransform 注释 中给出:[x'_i, y'_i]^T = map_matrix · [x_i, y_i, 1]^T

仿射变换的另一个实用方向是求逆cv.invertAffineTransform() 可以从 2x3 矩阵直接解出逆变换矩阵(见 2d.hpp 注释),适用于"已知正向映射、需要反向对齐"的场景。

透视变换(Perspective Transformation)

透视变换需要 3x3 矩阵,其几何性质是:直线变换后仍为直线,但平行性不再保持(平行线可以收敛到灭点)。求解矩阵需要 4 对对应点,其中任意 3 点不共线,由 cv.getPerspectiveTransform 解算,再用 cv.warpPerspective 应用。

经典应用是把倾斜拍摄的文档"摆正"。教程用 sudoku.png 演示将斜拍的九宫格拉直为 300x300 的正矩形:

img = cv.imread('sudoku.png')
assert img is not None, "file could not be read, check with os.path.exists()"
rows, cols, ch = img.shape

pts1 = np.float32([[56, 65], [368, 52], [28, 387], [389, 390]])
pts2 = np.float32([[0, 0], [300, 0], [0, 300], [300, 300]])

M = cv.getPerspectiveTransform(pts1, pts2)

dst = cv.warpPerspective(img, M, (300, 300))

plt.subplot(121), plt.imshow(img), plt.title('Input')
plt.subplot(122), plt.imshow(dst), plt.title('Output')
plt.show()

结果见 perspective.jpg:左侧斜拍原图中四点连线构成的平行四边形,被精确映射为右侧 300x300 的正方形。

为什么是 4 个点?从 2d.hpp 中 getPerspectiveTransform 的注释 可以看到,3x3 单应矩阵有 8 个自由度(整体尺度不敏感),每对点对应齐次坐标方程 [t_i·x'_i, t_i·y'_i, t_i] = map_matrix · [x_i, y_i, 1],每对点提供 2 个有效方程,4 对点恰好解出 8 个未知数——这也解释了"任意 3 点不共线"的约束:否则方程组退化、解不唯一。

参数速查与验证路径

把五个操作的要点汇总为一张速查表:

变换类型 矩阵构造 执行函数 矩阵形状 关键点约束
缩放 无需矩阵 cv.resize(dsize, fx, fy, interpolation) 缩小用 INTER_AREA,放大用 INTER_CUBIC/INTER_LINEAR
平移 手工构造 [[1,0,tx],[0,1,ty]] cv.warpAffine 2x3 输出尺寸为 (width, height)
旋转 cv.getRotationMatrix2D(center, angle, scale) cv.warpAffine 2x3 角度单位是度,正值逆时针
仿射 cv.getAffineTransform(pts1, pts2) cv.warpAffine 2x3 3 对对应点,不可共线
透视 cv.getPerspectiveTransform(pts1, pts2) cv.warpPerspective 3x3 4 对对应点,任意 3 点不共线

想验证本文结论或直接阅读底层实现,可以沿以下路径深入当前仓库:

小结

OpenCV 的图像几何变换体系非常收敛:所有操作都归结为"构造变换矩阵 → 调用 warpAffine / warpPerspective"两步。缩放由 cv.resize 特化完成;平移、旋转、仿射共享 2x3 矩阵通道,其中旋转矩阵由 getRotationMatrix2D 按"旋转中心不动"的几何约束生成(源码印证见 geometry.cpp);透视变换则升维到 3x3 单应矩阵,由 4 对点唯一确定。掌握"点数—自由度—共线约束"这条数学主线,再配合本文给出的可直接运行的 Python 代码,即可覆盖绝大多数工程场景中的图像对齐、文档校正与画面重定位需求。进一步阅读推荐教程原文末尾列出的参考书:Richard Szeliski 的 Computer Vision: Algorithms and Applications

登录后查看全文
热门项目推荐
相关项目推荐