OpenCV 仿射变换实战:warpAffine 与 getRotationMatrix2D 的图像几何变换指南
本文是 OpenCV(Open Source Computer Vision Library)官方教程《Affine Transformations》的完整中文导读与实战深化版。文章围绕仿射变换的数学原理、cv::warpAffine 与 cv::getRotationMatrix2D 两个核心 API,以及官方示例 Geometric_Transforms_Demo.cpp 的逐步拆解展开,覆盖 C++、Java、Python 三种语言写法。读完本文,你将掌握:如何用两组三个对应点求解 2×3 仿射矩阵,如何对图像做任意角度绕中心旋转并叠加缩放,以及底层 API 各参数的真实语义与边界行为。
1. 本教程要达成的目标
按照官方教程约定,通过本教程你将学会两件事:
- 使用 OpenCV 的
cv::warpAffine完成图像的仿射重映射(remapping); - 使用
cv::getRotationMatrix2D构造以图像中心为旋转中心的 2×3 旋转矩阵。
配套的示例程序会依次完成以下动作(对应 C++ 示例 的主流程,Python/Java 版本行为一致):
- 读取一张输入图像;
- 依据源图像中的三个点与目标图像中的三个点的对应关系求取仿射变换矩阵,并通过
warpAffine施加到图像上; - 以变换后图像的中心为圆心、指定角度与缩放因子构造旋转矩阵,再对变换结果施加旋转;
- 持续显示图像窗口,直到用户按下任意键退出。
需要特别说明:官方示例使用负角度旋转。在 OpenCV 的坐标约定中,图像坐标系 y 轴向下,因此正角度表示逆时针(counter-clockwise)旋转,负角度表示顺时针旋转,示例中的 -50.0 度即顺时针旋转 50 度。
2. 仿射变换理论:线性变换 + 平移
2.1 什么是仿射变换?
仿射变换(Affine Transformation)本质上是两类基本操作的复合:
- 矩阵乘法(线性变换)——对应旋转与缩放;
- 向量加法(平移)。
因此,一次仿射变换可用来表达图像之间的旋转、平移、缩放,以及它们的任意组合。从几何上讲,仿射变换描述的正是两幅图像之间的对应(relation):它把直线映射为直线、平行线映射为平行线,并保持共线点之间的比例不变。
2.2 2×3 矩阵表示
仿射变换通常用一个 2×3 矩阵表示。令线性部分为 2×2 矩阵 (A),平移部分为 2×1 向量 (B):
[ A = \begin{bmatrix} a_{00} & a_{01} \ a_{10} & a_{11} \end{bmatrix}{2\times2},\qquad B = \begin{bmatrix} b{00} \ b_{10} \end{bmatrix}_{2\times1} ]
二者拼接即得完整的仿射矩阵:
[ M = [A;; B] = \begin{bmatrix} a_{00} & a_{01} & b_{00} \ a_{10} & a_{11} & b_{10} \end{bmatrix}_{2\times3} ]
要把一个二维向量 (X = \begin{bmatrix}x \ y\end{bmatrix}) 变换为 (T),可使用如下两种等价写法:
[ T = A\cdot\begin{bmatrix}x \ y\end{bmatrix} + B \qquad\text{或}\qquad T = M\cdot[x,;y,;1]^{T} ]
展开后即:
[ T = \begin{bmatrix} a_{00}x + a_{01}y + b_{00} \ a_{10}x + a_{11}y + b_{10} \end{bmatrix} ]
其中齐次坐标写法 (M\cdot[x,;y,;1]^{T}) 是 OpenCV 底层实现(详见 warpAffine 的 imgproc.hpp 源码注释)所采用的标准形式。
2.3 仿射矩阵从哪里来?
仿射变换是两幅图像之间的"关系",获取这个关系大致有两条路径:
- 已知 (X) 与 (T):即已知源点与目标点的对应关系,任务是根据对应点反解出矩阵 (M);
- 已知 (M) 与 (X):直接计算 (T = M\cdot X)。此时 (M) 的来源既可以是显式给定的 2×3 矩阵,也可以是由若干点之间的几何对应关系推导而来。
本教程正是第二条路径的典型实践:仿射矩阵的自由度决定了它只需三对不共线的对应点即可完全确定。如下示意图所示,图像 1 中的 1、2、3 三个点构成一个三角形,映射到图像 2 后仍是三角形,但位置、大小与朝向发生了明显改变:
只要用这三组点(可按需任意选取)求出仿射变换矩阵,就可以把这个关系推广应用到整幅图像的每一个像素上。
3. 完整示例代码:三种语言对照
下面分别给出官方教程的 C++、Java、Python 版本,三者逻辑完全一致:先做三点仿射,再做绕中心旋转。可直接作为可运行的完整程序参考。
3.1 C++ 版本
完整源码见 samples/cpp/tutorial_code/ImgTrans/Geometric_Transforms_Demo.cpp:
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/geometry.hpp"
#include "opencv2/imgproc.hpp"
#include <iostream>
using namespace cv;
using namespace std;
int main( int argc, char** argv )
{
CommandLineParser parser( argc, argv, "{@input | lena.jpg | input image}" );
Mat src = imread( samples::findFile( parser.get<String>( "@input" ) ) );
if( src.empty() )
{
cout << "Could not open or find the image!\n" << endl;
cout << "Usage: " << argv[0] << " <Input image>" << endl;
return -1;
}
// 三组源点(原图左上角三点形成的三角形)
Point2f srcTri[3];
srcTri[0] = Point2f( 0.f, 0.f );
srcTri[1] = Point2f( src.cols - 1.f, 0.f );
srcTri[2] = Point2f( 0.f, src.rows - 1.f );
// 三组目标点(映射后的三角形顶点,坐标按图像尺寸比例设定)
Point2f dstTri[3];
dstTri[0] = Point2f( 0.f, src.rows*0.33f );
dstTri[1] = Point2f( src.cols*0.85f, src.rows*0.25f );
dstTri[2] = Point2f( src.cols*0.15f, src.rows*0.7f );
Mat warp_mat = getAffineTransform( srcTri, dstTri ); // 求 2x3 仿射矩阵
Mat warp_dst = Mat::zeros( src.rows, src.cols, src.type() );
warpAffine( src, warp_dst, warp_mat, warp_dst.size() ); // 施加仿射变换
// 以图像中心为圆心:角度 -50 度(顺时针),缩放 0.6
Point center = Point( warp_dst.cols/2, warp_dst.rows/2 );
double angle = -50.0;
double scale = 0.6;
Mat rot_mat = getRotationMatrix2D( center, angle, scale ); // 求旋转矩阵
Mat warp_rotate_dst;
warpAffine( warp_dst, warp_rotate_dst, rot_mat, warp_dst.size() ); // 施加旋转
imshow( "Source image", src );
imshow( "Warp", warp_dst );
imshow( "Warp + Rotate", warp_rotate_dst );
waitKey();
return 0;
}
3.2 Python 版本
完整源码见 samples/python/tutorial_code/ImgTrans/warp_affine/Geometric_Transforms_Demo.py:
import cv2 as cv
import numpy as np
import argparse
parser = argparse.ArgumentParser(description='Code for Affine Transformations tutorial.')
parser.add_argument('--input', help='Path to input image.', default='lena.jpg')
args = parser.parse_args()
src = cv.imread(cv.samples.findFile(args.input))
if src is None:
print('Could not open or find the image:', args.input)
exit(0)
# 三组源点与目标点,需为 float32
srcTri = np.array( [[0, 0], [src.shape[1] - 1, 0], [0, src.shape[0] - 1]] ).astype(np.float32)
dstTri = np.array( [[0, src.shape[1]*0.33], [src.shape[1]*0.85, src.shape[0]*0.25], [src.shape[1]*0.15, src.shape[0]*0.7]] ).astype(np.float32)
warp_mat = cv.getAffineTransform(srcTri, dstTri)
warp_dst = cv.warpAffine(src, warp_mat, (src.shape[1], src.shape[0]))
center = (warp_dst.shape[1]//2, warp_dst.shape[0]//2)
angle = -50
scale = 0.6
rot_mat = cv.getRotationMatrix2D(center, angle, scale)
warp_rotate_dst = cv.warpAffine(warp_dst, rot_mat, (warp_dst.shape[1], warp_dst.shape[0]))
cv.imshow('Source image', src)
cv.imshow('Warp', warp_dst)
cv.imshow('Warp + Rotate', warp_rotate_dst)
cv.waitKey()
3.3 Java 版本
完整源码见 samples/java/tutorial_code/ImgTrans/warp_affine/GeometricTransformsDemo.java,核心流程如下:
// 读取图像
String filename = args.length > 0 ? args[0] : "../data/lena.jpg";
Mat src = Imgcodecs.imread(filename);
// 三组源点与目标点
Point[] srcTri = new Point[3];
srcTri[0] = new Point( 0, 0 );
srcTri[1] = new Point( src.cols() - 1, 0 );
srcTri[2] = new Point( 0, src.rows() - 1 );
Point[] dstTri = new Point[3];
dstTri[0] = new Point( 0, src.rows()*0.33 );
dstTri[1] = new Point( src.cols()*0.85, src.rows()*0.25 );
dstTri[2] = new Point( src.cols()*0.15, src.rows()*0.7 );
// 求仿射矩阵并施加变换
Mat warpMat = Imgproc.getAffineTransform( new MatOfPoint2f(srcTri), new MatOfPoint2f(dstTri) );
Mat warpDst = Mat.zeros( src.rows(), src.cols(), src.type() );
Imgproc.warpAffine( src, warpDst, warpMat, warpDst.size() );
// 绕图像中心旋转:-50 度,缩放 0.6
Point center = new Point(warpDst.cols() / 2, warpDst.rows() / 2);
Mat rotMat = Imgproc.getRotationMatrix2D( center, -50.0, 0.6 );
Mat warpRotateDst = new Mat();
Imgproc.warpAffine( warpDst, warpRotateDst, rotMat, warpDst.size() );
HighGui.imshow( "Source image", src );
HighGui.imshow( "Warp", warpDst );
HighGui.imshow( "Warp + Rotate", warpRotateDst );
HighGui.waitKey(0);
System.exit(0);
注意 Java 版本中 run() 被封装在 GeometricTransforms 类中,而公开类 GeometricTransformsDemo 的 main() 首先执行 System.loadLibrary(Core.NATIVE_LIBRARY_NAME) 加载 OpenCV 原生库,再调用 run(args)。
4. 分步讲解:从三点到旋转的完整链路
第 1 步:加载输入图像
- C++:使用
CommandLineParser解析第一个位置参数@input(默认lena.jpg),并通过samples::findFile在 OpenCV 自带样本数据目录中定位图片,再以imread读取; - Python:通过
argparse的--input参数(默认lena.jpg)读取; - Java:直接取命令行首参(默认
../data/lena.jpg)。
无论哪种语言,读入后都应检查矩阵是否为空(src.empty() / src is None),为空则提示用法并退出,这是防止后续 API 崩溃的必要防护。
第 2 步:设定三对对应点并求仿射矩阵
三点仿射的核心在于把三角形整体搬移。示例中源三角形取图像左上角:
srcTri[0] = Point2f( 0.f, 0.f ); // 左上角
srcTri[1] = Point2f( src.cols - 1.f, 0.f ); // 右上角
srcTri[2] = Point2f( 0.f, src.rows - 1.f ); // 左下角
目标三角形把这三个顶点"摊开"到图像的不同比例位置:
dstTri[0] = Point2f( 0.f, src.rows*0.33f );
dstTri[1] = Point2f( src.cols*0.85f, src.rows*0.25f );
dstTri[2] = Point2f( src.cols*0.15f, src.rows*0.7f );
两点注意:
- Python 版本要求点数组显式
astype(np.float32),否则cv.getAffineTransform会因数据类型不符合float32而报错; - 目标坐标按
src.cols与src.rows的比例计算,因此这套映射对任意尺寸的输入图像都适用——这正是"把三点对应关系推广到全图"的思想。
随后调用:
Mat warp_mat = getAffineTransform( srcTri, dstTri );
cv::getAffineTransform 返回一个 2×3 矩阵(示例中记作 warp_mat)。它内部通过解线性方程组,恰好拟合这三对点的对应关系,其结果与理论小节中推导的 (M = [A;;B]) 完全同构。
你完全可以在图上把这几组点画出来对比观察:映射后三点所成三角形的尺寸与朝向都发生了改变,直观印证了仿射矩阵的作用。
第 3 步:施加仿射变换
Mat warp_dst = Mat::zeros( src.rows, src.cols, src.type() );
warpAffine( src, warp_dst, warp_mat, warp_dst.size() );
cv::warpAffine 的参数含义:
| 参数 | 含义 |
|---|---|
src |
输入图像 |
warp_dst |
输出图像 |
warp_mat |
2×3 仿射变换矩阵 |
warp_dst.size() |
输出图像的目标尺寸(此处与输入同尺寸) |
先以 Mat::zeros 用与源图相同的行列数与类型分配一块全零的输出缓冲区,保证变换过程中未覆盖区域的像素为黑色背景。执行到这里,第一张"歪斜"的变换图就诞生了。
从源码看 warpAffine 的完整签名与可选参数
根据 imgproc.hpp 的函数声明,warpAffine 实际还有若干可选参数,理解它们有助于在生产代码中用好这个函数:
CV_EXPORTS_W void warpAffine( InputArray src, OutputArray dst,
InputArray M, Size dsize,
int flags = INTER_LINEAR,
int borderMode = BORDER_CONSTANT,
const Scalar& borderValue = Scalar(),
AlgorithmHint hint = cv::ALGO_HINT_DEFAULT);
- 变换公式:dst 像素坐标由
M前向映射给出(源码注释 中的公式);当未设置WARP_INVERSE_MAP标志时,OpenCV 会先用invertAffineTransform求逆,再按逆变换做反向映射采样,以规避空洞; flags:插值方法的组合(参考InterpolationFlags),常用INTER_LINEAR(双线性插值,默认)、INTER_NEAREST、INTER_CUBIC;若叠加设置WARP_INVERSE_MAP,则表示传入的M本身就是 dst→src 的逆变换,函数不再求逆;borderMode:越界像素的外推方式(参考BorderTypes),默认BORDER_CONSTANT;若设为BORDER_TRANSPARENT,则目标图像中对应源图"越界区域"的像素保持原值不被修改,适合需要叠加透视/仿射贴图的场景;borderValue:BORDER_CONSTANT模式下的填充值,默认 0(即黑色);hint:实现层面的优化开关,例如设置ALGO_HINT_APPROX可在硬件支持时以 FP16 精度加速线性插值计算;- 约束:
warpAffine不支持原地(in-place)操作,src与dst不能是同一幅图; - 同类 API 参考:
warpPerspective(透视变换,需要 3×3 矩阵与四组点)、resize、remap、getRectSubPix、transform,它们共享同一套反向映射与插值框架。
第 4 步:确定旋转参数
要对图像做旋转,需要明确三件事:
- 旋转中心:示例取
warp_dst的正中心Point(warp_dst.cols/2, warp_dst.rows/2); - 旋转角度:单位为度。在 OpenCV 中正角度逆时针、负角度顺时针(y 轴向下坐标系所致),示例取
-50.0; - 可选缩放因子:
0.6,表示旋转的同时把图像缩小到原来的 60%。
Point center = Point( warp_dst.cols/2, warp_dst.rows/2 );
double angle = -50.0;
double scale = 0.6;
第 5 步:生成旋转矩阵
Mat rot_mat = getRotationMatrix2D( center, angle, scale );
cv::getRotationMatrix2D 按给定中心、角度与缩放因子返回一个 2×3 旋转-缩放矩阵(示例记作 rot_mat)。值得强调的是,旋转本身也是仿射变换的一种,因此它返回的依然是 2×3 矩阵,可以无缝交给 warpAffine 使用。
第 6 步:施加旋转并显示结果
Mat warp_rotate_dst;
warpAffine( warp_dst, warp_rotate_dst, rot_mat, warp_dst.size() );
与仿射步骤相同,只是把 warp_mat 换成 rot_mat,即可得到"先仿射、再旋转并缩放"的最终图像。
展示环节创建三个窗口并各自 imshow:
imshow( "Source image", src );
imshow( "Warp", warp_dst );
imshow( "Warp + Rotate", warp_rotate_dst );
waitKey(); // 等待用户按键退出
其中 waitKey()(C++ 无参形式,Java 为 waitKey(0),Python 为 cv.waitKey())阻塞等待任意键盘输入后程序结束。
5. 运行效果与结果解读
编译运行上述代码并传入一张图片作为参数。若输入为下图的风景原图:
施加第一步的三点仿射变换后,原图内容被"压扁倾斜"到输出画布的局部区域,其余部分保持为黑色背景:
最后再叠加一次负角度(顺时针)旋转与 0.6 倍缩放,得到最终的旋转效果图——与上一张相比,内容在整体缩小的同时发生了明显旋转:
三个窗口将分别展示"源图 / 仿射结果 / 仿射+旋转结果",便于直观对比每一步的几何效果。
6. 要点回顾与延伸阅读
- 仿射变换 = 线性变换(旋转、缩放)+ 平移,可用一个 2×3 矩阵 (M=[A;;B]) 完整表达;
- 三对不共线的对应点即可唯一确定仿射矩阵,对应 API 为
cv::getAffineTransform(两点对应则用cv::getPerspectiveTransform对应透视场景); cv::warpAffine是执行仿射重映射的核心入口,默认采用反向映射 + 双线性插值 + 常量边界填充,且支持WARP_INVERSE_MAP、INTER_*、BORDER_*等控制项,不支持原地操作;- 旋转同样只是特殊的仿射变换,用
cv::getRotationMatrix2D(center, angle, scale)生成矩阵后再交给warpAffine即可,注意 OpenCV 约定正角度为逆时针; - 三种语言版本源码分别存放于 samples/cpp/tutorial_code/ImgTrans/Geometric_Transforms_Demo.cpp、samples/python/tutorial_code/ImgTrans/warp_affine/Geometric_Transforms_Demo.py 与 samples/java/tutorial_code/ImgTrans/warp_affine/GeometricTransformsDemo.java。
若想继续深入本主题,建议按官方教程脉络依次阅读相邻教程:warp_affine 的前置教程是 remap(像素级重映射),后续是直方图均衡化;更进阶的透视几何可参考 cv::warpPerspective 及其示例 samples/cpp/snippets/warpPerspective_demo.cpp。仿射变换是图像配准、数据增强、OCR 矫正、模板匹配等众多应用的基础几何工具,熟练掌握本教程的"三点定矩阵 + warpAffine 施加"模式即可在实际项目中灵活复用。
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 StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00



