OpenCV 图像几何变换实战:缩放、平移、旋转、仿射与透视变换的矩阵原理与代码实现
本文以 OpenCV Python 官方教程《Geometric Transformations of Images》为主体,系统讲解图像几何变换的两大核心函数 cv.warpAffine 与 cv.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_width、fy = 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)*scale、beta = 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。
两个实践要点:
- 中心点计算要严谨:用浮点除法
(cols-1)/2.0而非整数除法,否则中心会偏移半个像素,旋转后出现不对称的缺口; - 旋转后画布裁剪问题:非 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 点不共线 |
想验证本文结论或直接阅读底层实现,可以沿以下路径深入当前仓库:
- 旋转矩阵实现:modules/geometry/src/geometry.cpp#L718-L731,公式与教程完全一致;
- API 声明与文档注释:modules/geometry/include/opencv2/geometry/2d.hpp 中
getRotationMatrix2D、getAffineTransform、getPerspectiveTransform、invertAffineTransform的完整公式推导; - warp 函数正确性测试:modules/imgproc/test/test_imgwarp.cpp 与 modules/imgproc/test/test_imgwarp_strict.cpp,其中包含
getRotationMatrix2D构造矩阵后逐像素比对 warpAffine/warpPerspective 输出的严格测试; - 教程原文:doc/py_tutorials/py_imgproc/py_geometric_transformations/py_geometric_transformations.markdown,本文所有代码示例均出自该文档并已保留其完整逻辑。
小结
OpenCV 的图像几何变换体系非常收敛:所有操作都归结为"构造变换矩阵 → 调用 warpAffine / warpPerspective"两步。缩放由 cv.resize 特化完成;平移、旋转、仿射共享 2x3 矩阵通道,其中旋转矩阵由 getRotationMatrix2D 按"旋转中心不动"的几何约束生成(源码印证见 geometry.cpp);透视变换则升维到 3x3 单应矩阵,由 4 对点唯一确定。掌握"点数—自由度—共线约束"这条数学主线,再配合本文给出的可直接运行的 Python 代码,即可覆盖绝大多数工程场景中的图像对齐、文档校正与画面重定位需求。进一步阅读推荐教程原文末尾列出的参考书:Richard Szeliski 的 Computer Vision: Algorithms and Applications。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00



