一、函数原型(常用)
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_32F
或CV_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_REFLECT
、BORDER_REFLECT_101
:反射 -
BORDER_WRAP
:环绕 -
BORDER_TRANSPARENT
:不绘制(保留 dst 原值) -
BORDER_ISOLATED
:不考虑外部像素(与复制不同)
-
-
borderValue
:当BORDER_CONSTANT
时使用的常量(例如Scalar(0,0,0)
)。
四、插值方法选取建议
-
缩小(downsample) :优先使用
INTER_AREA
(抗混叠好)。 -
放大(upsample) :使用
INTER_CUBIC
或INTER_LANCZOS4
获得更好视觉效果。 -
实时/速度优先 :使用
INTER_LINEAR
(速度快、效果合理)。 -
二值图或掩码 :使用
INTER_NEAREST
(避免插值造成非 0/1 值)。
详细说明
-
当尺寸不一致(缩放)时
-
目标像素对应的源坐标几乎总是非整数位置,所以需要插值。
-
例如:把 480×480 缩放到 48×48,目标像素会映射到源图的 10×10 区域,需要插值来估算值。
-
-
当尺寸一致,但有旋转 / 平移 / 剪切时
-
即使输出尺寸和输入尺寸一样,目标像素也可能映射到源图像的浮点坐标,仍然要插值。
-
例如:平移 0.5 个像素,旋转 30°,这些变换都会让采样点落在"像素之间",需要插值。
-
-
当变换刚好是整数平移且不缩放、不旋转时
-
每个目标像素会对应到源图像的整数坐标,不需要插值。
-
在这种情况下,
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());
六、注意细节与陷阱
-
M 的类型 :用
float
或double
都可以,但使用时需匹配Mat
类型,通常CV_64F
更稳妥(精度高)。 -
WARP_INVERSE_MAP :如果你已经有映射
dst→src
(例如来自某些算法),设置该 flag 可以避免内部求逆。 -
像素坐标原点/像素中心:OpenCV 用像素中心坐标,即像素 (i,j) 对应坐标 (x=j, y=i)。若非常讲究数值精度要注意这一点。
-
数据类型与插值 :如果 input 为
CV_16U
或CV_32F
,warpAffine 在内部会进行浮点插值并再转换为目标类型(可能会有截断)。 -
掩码与透明区域 :若想把变换后的透明区域保持原样可用
BORDER_TRANSPARENT
,或者自己先用掩码处理再合并。 -
ROI 与 非连续内存 :若
src
是子矩阵(ROI),它可能不是连续的,warpAffine 仍然能工作但性能可能略差。 -
仿射 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_x
和 map_y
(每个 dst 像素对应 src 坐标),然后选择插值与边界策略。warpAffine
是 remap
的特例(矩阵产生的规则映射)。
十、总结速查表
-
用途:对图像做仿射变换(旋转/缩放/剪切/平移)
-
关键矩阵 :2×3 (
CV_32F
/CV_64F
);用getRotationMatrix2D
、getAffineTransform
或自构造 -
常用 flags :
INTER_LINEAR
(多数场景),INTER_AREA
(缩小),INTER_CUBIC
(放大) -
边界 :
BORDER_CONSTANT
+borderValue
、BORDER_REPLICATE
、BORDER_REFLECT
等 -
提示 :若 M 已经是 dst→src,使用
WARP_INVERSE_MAP
;想保留整个旋转图像需调整输出尺寸与平移分量。