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;想保留整个旋转图像需调整输出尺寸与平移分量。

相关推荐
奔跑中的小象1 天前
统信UOS + 天数AI卡部署SGLang服务手册
人工智能·uos·sglang·天数智芯
DevSecOps选型指南1 天前
中国版Mythos,为何是悬镜安全灵脉CodeAI?
人工智能·安全
Shockang1 天前
LangGraph 状态机实战
人工智能
刹那芳华19921 天前
循环神经网络的从零开始实现(RNN)
人工智能·rnn·深度学习
阿童木写作1 天前
跨境图片翻译工具多合一,批量图片视频字幕翻译加智能抠图
人工智能·python·音视频·语音识别
科技之门1 天前
AI3D从建模到贴图、绑骨和动画的完整流程怎么做?V2Fun完整工作流指南
人工智能·3d·贴图
宇宙第一小趴菜1 天前
二、机器学习的应用领域和发展史
人工智能·机器学习
前端开发江鸟1 天前
我写过 MCP Server,却一直以为 MCP 只有 Tool
人工智能
天国梦1 天前
自习室智能化升级避坑指南:天学网AI智习室方案实测与选型建议
大数据·人工智能
阿里云云原生1 天前
可用性从 99.9% 跃升至 99.995%:畅捷通如何用 AI 重塑运维底座?
运维·网络·人工智能