OpenCV: cv::warpAffine()逆仿射变换详解

一、函数原型(常用)

cpp 复制代码
void cv::warpAffine(
    InputArray src,
    OutputArray dst,
    InputArray M,           // 2x3 仿射矩阵
    Size dsize,             // 输出图像尺寸 (width, height)
    int flags = INTER_LINEAR | WARP_INVERSE_MAP,
    int borderMode = BORDER_CONSTANT,
    const Scalar& borderValue = Scalar()
);

二、基本概念与数学公式

warpAffine 执行的是二维仿射变换(平移/缩放/旋转/剪切/组合)。仿射矩阵 M 是一个 2×3 矩阵:

标准坐标变换(M源坐标 映射到 目标坐标):

cpp 复制代码
[x_dst]   [m00 m01 m02] [x_src]
[y_dst] = [m10 m11 m12] [y_src]
[ 1   ]   [  0   0   1 ] [  1  ]

warpAffine 在内部采用的是 逆映射(inverse mapping) 的方式来进行插值:对每个目标像素 (x_dst,y_dst),计算对应的源坐标 (x_src,y_src),然后对源图像做插值取值。
注意 flags 的 WARP_INVERSE_MAP

  • 如果设置了 WARP_INVERSE_MAP,则传入的 M 被认为是从目标坐标到源坐标 的矩阵(dst→src),即直接用 M 映射 (x_dst,y_dst)(x_src,y_src)

  • 如果没有设置该标志,M 被视为 src→dst,你应传 src→dst 的矩阵;OpenCV 会在内部对 M 做逆运算(或等价处理)以得到 dst→src 的映射。

通俗:如果你手里已经有了把目标映射到源的矩阵(dst→src),在调用时加上 WARP_INVERSE_MAP,避免内部再求逆,节省一点开销并避免数值误差。

三、参数解释

  • src:输入图像(任意通道数、通常为 CV_8U、CV_16U、CV_32F 等)。

  • dst:输出图像(大小为 dsize,类型与 src 一致;若 dsize 为 (0,0) 通常不允许,需显式给定)。

  • M:2×3 仿射矩阵(CV_32FCV_64F),用 cv::Mat 构造,例如 Mat M = (Mat_<double>(2,3) << ...);

  • dsize:目标图像尺寸 Size(width, height)。注意:若你旋转图像并想保留完整内容,需计算合适的 dsize

  • flags:插值方式 + 可选 WARP_INVERSE_MAP。常见插值:

    • INTER_NEAREST:最近邻(最快,但质量最差)

    • INTER_LINEAR:双线性(默认,速度/质量平衡)

    • INTER_CUBIC:双三次(更慢更平滑)

    • INTER_AREA:像素区域重采样(通常用于缩小)

    • INTER_LANCZOS4:高质量(上采样时)

      可将插值与 WARP_INVERSE_MAP 用按位或组合:INTER_LINEAR | WARP_INVERSE_MAP

  • borderMode:当源坐标落在图像范围外时如何取值:

    • BORDER_CONSTANT:常数填充(使用 borderValue

    • BORDER_REPLICATE:复制边界最近像素

    • BORDER_REFLECTBORDER_REFLECT_101:反射

    • BORDER_WRAP:环绕

    • BORDER_TRANSPARENT:不绘制(保留 dst 原值)

    • BORDER_ISOLATED:不考虑外部像素(与复制不同)

  • borderValue:当 BORDER_CONSTANT 时使用的常量(例如 Scalar(0,0,0))。

四、插值方法选取建议

  • 缩小(downsample) :优先使用 INTER_AREA(抗混叠好)。

  • 放大(upsample) :使用 INTER_CUBICINTER_LANCZOS4 获得更好视觉效果。

  • 实时/速度优先 :使用 INTER_LINEAR(速度快、效果合理)。

  • 二值图或掩码 :使用 INTER_NEAREST(避免插值造成非 0/1 值)。

详细说明

  1. 当尺寸不一致(缩放)时

    • 目标像素对应的源坐标几乎总是非整数位置,所以需要插值。

    • 例如:把 480×480 缩放到 48×48,目标像素会映射到源图的 10×10 区域,需要插值来估算值。

  2. 当尺寸一致,但有旋转 / 平移 / 剪切时

    • 即使输出尺寸和输入尺寸一样,目标像素也可能映射到源图像的浮点坐标,仍然要插值。

    • 例如:平移 0.5 个像素,旋转 30°,这些变换都会让采样点落在"像素之间",需要插值。

  3. 当变换刚好是整数平移且不缩放、不旋转时

    • 每个目标像素会对应到源图像的整数坐标,不需要插值。

    • 在这种情况下,INTER_NEAREST 实际上就是直接复制像素,效果和插值无差别。

如果你旋转 45°,目标图的某个像素可能对应到源图的 (x_src=1.3, y_src=2.7),那么:

  • INTER_NEAREST:直接取离它最近的整数像素,比如 (1,3)

  • INTER_LINEAR:取周围 4 个像素,按距离双线性加权

  • INTER_CUBIC:取周围 16 个像素,做更平滑的插值

五、常见用例与代码示例

1) 旋转(绕图像中心)

cpp 复制代码
cv::Mat src = cv::imread("img.jpg");
cv::Point2f center(src.cols/2.0f, src.rows/2.0f);
double angle = 30.0, scale = 1.0;
cv::Mat M = cv::getRotationMatrix2D(center, angle, scale);
cv::Mat dst;
cv::warpAffine(src, dst, M, src.size(), cv::INTER_LINEAR, cv::BORDER_CONSTANT, cv::Scalar(0,0,0));

2) 由 3 个点构造仿射(例如把三角形 A映射到三角形 B)

cpp 复制代码
std::vector<cv::Point2f> srcTri = { {0,0}, {100,0}, {0,100} };
std::vector<cv::Point2f> dstTri = { {10,20}, {120,30}, {20,200} };
cv::Mat M = cv::getAffineTransform(srcTri, dstTri);
cv::warpAffine(src, dst, M, dstSize);

3) 平移 + 缩放 + 剪切(手写矩阵)

平移 (tx,ty) 与缩放 (sx,sy)

cpp 复制代码
cv::Mat M = (cv::Mat_<double>(2,3) << sx, 0, tx,
                                      0, sy, ty);
cv::warpAffine(src, dst, M, cv::Size(newW,newH));

剪切(水平 shear):

cpp 复制代码
double shx = 0.3; // x' = x + shx*y
cv::Mat M = (cv::Mat_<double>(2,3) << 1, shx, 0,
                                      0,   1, 0);

4) 保证旋转后完整图像不被裁剪(compute bounding)

cpp 复制代码
cv::Mat M = cv::getRotationMatrix2D(center, angle, 1.0);

// 计算旋转后四角的新坐标,求 bounding box
std::vector<cv::Point2f> corners = { {0,0}, {w,0}, {w,h}, {0,h} };
std::vector<cv::Point2f> cornersTrans(4);
cv::transform(corners, cornersTrans, M);
cv::Rect bbox = cv::boundingRect(cornersTrans);

// 为避免裁剪,平移矩阵 M 的平移分量
M.at<double>(0,2) += -bbox.x;
M.at<double>(1,2) += -bbox.y;
cv::warpAffine(src, dst, M, bbox.size());

六、注意细节与陷阱

  1. M 的类型 :用 floatdouble 都可以,但使用时需匹配 Mat 类型,通常 CV_64F 更稳妥(精度高)。

  2. WARP_INVERSE_MAP :如果你已经有映射 dst→src(例如来自某些算法),设置该 flag 可以避免内部求逆。

  3. 像素坐标原点/像素中心:OpenCV 用像素中心坐标,即像素 (i,j) 对应坐标 (x=j, y=i)。若非常讲究数值精度要注意这一点。

  4. 数据类型与插值 :如果 input 为 CV_16UCV_32F,warpAffine 在内部会进行浮点插值并再转换为目标类型(可能会有截断)。

  5. 掩码与透明区域 :若想把变换后的透明区域保持原样可用 BORDER_TRANSPARENT,或者自己先用掩码处理再合并。

  6. ROI 与 非连续内存 :若 src 是子矩阵(ROI),它可能不是连续的,warpAffine 仍然能工作但性能可能略差。

  7. 仿射 vs 透视 :若需要做透视变换(非线性),请用 warpPerspective()(3×3 矩阵)

七、性能与优化建议

  • 使用合适的插值,INTER_LINEAR 在大多数场景性价比最好;缩小图像使用 INTER_AREA

  • 如果对速度敏感(GPU 可用),用 CUDA 版本:cv::cuda::warpAffine(需要 OpenCV 的 CUDA 模块)。

  • 若需要对大量小块重复调用,尽量复用(预分配)目标 Mat,并预计算矩阵 M

  • M 已经是 dst→src,设置 WARP_INVERSE_MAP 可节省一次矩阵求逆。

  • 如果做大量整数平移,考虑用 copyTo + ROI 操作替代通用 warpAffine

八、完整示例集(可直接拷贝运行)

cpp 复制代码
#include <opencv2/opencv.hpp>
using namespace cv;

// 旋转并保持不裁剪
Mat rotateNoCrop(const Mat& src, double angle) {
    Point2f center(src.cols/2.0f, src.rows/2.0f);
    Mat M = getRotationMatrix2D(center, angle, 1.0);

    // 计算新边界
    std::vector<Point2f> corners = { {0,0}, { (float)src.cols, 0 }, { (float)src.cols, (float)src.rows }, {0, (float)src.rows} };
    std::vector<Point2f> cornersT(4);
    transform(corners, cornersT, M);
    Rect bbox = boundingRect(cornersT);

    // 平移
    M.at<double>(0,2) -= bbox.x;
    M.at<double>(1,2) -= bbox.y;

    Mat dst;
    warpAffine(src, dst, M, bbox.size(), INTER_LINEAR, BORDER_CONSTANT, Scalar(0,0,0));
    return dst;
}

// affine from three points
Mat affineFrom3(const Mat& src, const std::vector<Point2f>& srcTri, const std::vector<Point2f>& dstTri) {
    Mat M = getAffineTransform(srcTri, dstTri);
    Mat dst;
    warpAffine(src, dst, M, Size(src.cols, src.rows), INTER_LINEAR);
    return dst;
}

int main() {
    Mat img = imread("lena.png");
    Mat r = rotateNoCrop(img, 37.0);
    imshow("rot", r);

    // example: shear
    Mat M = (Mat_<double>(2,3) << 1, 0.5, 0, 0, 1, 0);
    Mat shear;
    warpAffine(img, shear, M, Size(img.cols + 100, img.rows + 50), INTER_CUBIC, BORDER_REPLICATE);
    imshow("shear", shear);

    waitKey();
}

九、什么时候用 remap

若你的变换不是仿射或透视(例如光学畸变校正、复杂的非线性变形),remap() 更灵活:你可直接给出 map_xmap_y(每个 dst 像素对应 src 坐标),然后选择插值与边界策略。warpAffineremap 的特例(矩阵产生的规则映射)。


十、总结速查表

  • 用途:对图像做仿射变换(旋转/缩放/剪切/平移)

  • 关键矩阵 :2×3 (CV_32F/CV_64F);用 getRotationMatrix2DgetAffineTransform 或自构造

  • 常用 flagsINTER_LINEAR(多数场景),INTER_AREA(缩小),INTER_CUBIC(放大)

  • 边界BORDER_CONSTANT + borderValueBORDER_REPLICATEBORDER_REFLECT

  • 提示 :若 M 已经是 dst→src,使用 WARP_INVERSE_MAP;想保留整个旋转图像需调整输出尺寸与平移分量。

相关推荐
非优秀程序员3 小时前
开发人员如何使用在自己的系统中对接 Nano Banana 【完整教程】
人工智能
阿三08123 小时前
钉钉 AI 深度赋能制造业 LTC 全流程:以钉钉宜搭、Teambition 为例
人工智能·低代码·钉钉·teambition
摩羯座-185690305943 小时前
京东商品评论接口技术实现:从接口分析到数据挖掘全方案
人工智能·数据挖掘
格调UI成品3 小时前
智能制造新视角:工业4.0中,数字孪生如何优化产品全生命周期?
人工智能·工业4.0
机器学习之心3 小时前
PINN物理信息神经网络用于求解二阶常微分方程(ODE)的边值问题,Matlab实现
人工智能·神经网络·matlab·物理信息神经网络·二阶常微分方程
zandy10113 小时前
LLM与数据工程的融合:衡石Data Agent的语义层与Agent框架设计
大数据·人工智能·算法·ai·智能体
大千AI助手3 小时前
梯度消失问题:深度学习中的「记忆衰退」困境与解决方案
人工智能·深度学习·神经网络·梯度·梯度消失·链式法则·vanishing
研梦非凡3 小时前
CVPR 2025|无类别词汇的视觉-语言模型少样本学习
人工智能·深度学习·学习·语言模型·自然语言处理
seegaler3 小时前
WrenAI:开源革命,重塑商业智能未来
人工智能·microsoft·ai