首页
/ OpenCV 仿射变换实战:warpAffine 与 getRotationMatrix2D 的图像几何变换指南

OpenCV 仿射变换实战:warpAffine 与 getRotationMatrix2D 的图像几何变换指南

2026-09-06 19:15:21作者:邓越浪Henry

本文是 OpenCV(Open Source Computer Vision Library)官方教程《Affine Transformations》的完整中文导读与实战深化版。文章围绕仿射变换的数学原理、cv::warpAffinecv::getRotationMatrix2D 两个核心 API,以及官方示例 Geometric_Transforms_Demo.cpp 的逐步拆解展开,覆盖 C++、Java、Python 三种语言写法。读完本文,你将掌握:如何用两组三个对应点求解 2×3 仿射矩阵,如何对图像做任意角度绕中心旋转并叠加缩放,以及底层 API 各参数的真实语义与边界行为。

1. 本教程要达成的目标

按照官方教程约定,通过本教程你将学会两件事:

  • 使用 OpenCV 的 cv::warpAffine 完成图像的仿射重映射(remapping);
  • 使用 cv::getRotationMatrix2D 构造以图像中心为旋转中心的 2×3 旋转矩阵。

配套的示例程序会依次完成以下动作(对应 C++ 示例 的主流程,Python/Java 版本行为一致):

  1. 读取一张输入图像;
  2. 依据源图像中的三个点与目标图像中的三个点的对应关系求取仿射变换矩阵,并通过 warpAffine 施加到图像上;
  3. 以变换后图像的中心为圆心、指定角度与缩放因子构造旋转矩阵,再对变换结果施加旋转;
  4. 持续显示图像窗口,直到用户按下任意键退出。

需要特别说明:官方示例使用负角度旋转。在 OpenCV 的坐标约定中,图像坐标系 y 轴向下,因此正角度表示逆时针(counter-clockwise)旋转,负角度表示顺时针旋转,示例中的 -50.0 度即顺时针旋转 50 度。

2. 仿射变换理论:线性变换 + 平移

2.1 什么是仿射变换?

仿射变换(Affine Transformation)本质上是两类基本操作的复合:

  1. 矩阵乘法(线性变换)——对应旋转与缩放;
  2. 向量加法(平移)

因此,一次仿射变换可用来表达图像之间的旋转、平移、缩放,以及它们的任意组合。从几何上讲,仿射变换描述的正是两幅图像之间的对应(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 底层实现(详见 warpAffineimgproc.hpp 源码注释)所采用的标准形式。

2.3 仿射矩阵从哪里来?

仿射变换是两幅图像之间的"关系",获取这个关系大致有两条路径:

  1. 已知 (X) 与 (T):即已知源点与目标点的对应关系,任务是根据对应点反解出矩阵 (M);
  2. 已知 (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 类中,而公开类 GeometricTransformsDemomain() 首先执行 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.colssrc.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_NEARESTINTER_CUBIC;若叠加设置 WARP_INVERSE_MAP,则表示传入的 M 本身就是 dst→src 的逆变换,函数不再求逆;
  • borderMode:越界像素的外推方式(参考 BorderTypes),默认 BORDER_CONSTANT;若设为 BORDER_TRANSPARENT,则目标图像中对应源图"越界区域"的像素保持原值不被修改,适合需要叠加透视/仿射贴图的场景;
  • borderValueBORDER_CONSTANT 模式下的填充值,默认 0(即黑色);
  • hint:实现层面的优化开关,例如设置 ALGO_HINT_APPROX 可在硬件支持时以 FP16 精度加速线性插值计算;
  • 约束warpAffine 不支持原地(in-place)操作,srcdst 不能是同一幅图;
  • 同类 API 参考:warpPerspective(透视变换,需要 3×3 矩阵与四组点)、resizeremapgetRectSubPixtransform,它们共享同一套反向映射与插值框架。

第 4 步:确定旋转参数

要对图像做旋转,需要明确三件事:

  1. 旋转中心:示例取 warp_dst 的正中心 Point(warp_dst.cols/2, warp_dst.rows/2)
  2. 旋转角度:单位为度。在 OpenCV 中正角度逆时针、负角度顺时针(y 轴向下坐标系所致),示例取 -50.0
  3. 可选缩放因子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. 要点回顾与延伸阅读

若想继续深入本主题,建议按官方教程脉络依次阅读相邻教程:warp_affine 的前置教程是 remap(像素级重映射),后续是直方图均衡化;更进阶的透视几何可参考 cv::warpPerspective 及其示例 samples/cpp/snippets/warpPerspective_demo.cpp。仿射变换是图像配准、数据增强、OCR 矫正、模板匹配等众多应用的基础几何工具,熟练掌握本教程的"三点定矩阵 + warpAffine 施加"模式即可在实际项目中灵活复用。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.74 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
595
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.63 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
518
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
389