上篇 OpenCV 之 图像几何变换 介绍了等距、相似和仿射变换,本篇侧重投影变换的平面单应性、OpenCV相关函数、应用实例等。
1 投影变换
投影变换 (Projective Transformation),是仿射变换的一般化,二者区别如下:
1.1 平面单应性
假定平面 与 之间,存在映射,使得 内任意点 ,满足下式:
当 非奇异时, 与 之间的映射即为 2D 投影变换,也称 平面单应性, 则为 单应性矩阵
例如:在相机标定中,如果选用 平面标定板,则 物平面 和 像平面 之间,就是一种典型的 平面单应性 映射
1.2 单应性矩阵
有 9 个未知数,但实际只有 8 个自由度 (DoF),其归一化有两种方法:
法一,令 ;法二,加单位向量限制
下面接着法一,继续推导公式:
整理得:
一组对应特征点 -> 可构造 2 个方程,要求解 8 个未知数 (归一化后的),则需要 8 个方程,4 组对应特征点
因此,求 可转化为求解 ,参见 OpenCV 中 getPerspectiveTransform() 的源码实现
2 OpenCV 函数
2.1 投影变换矩阵
a) 四组对应特征点:已知四组对应特征点坐标,带入 getPerspectiveTransform() 函数中,可求解 src 投影到 dst 的单应性矩阵
1 2 3 4 5 | Mat getPerspectiveTransform ( const Point2f src[], // 原图像的四角顶点坐标 const Point2f dst[], // 目标图像的四角顶点坐标 int solveMethod = DECOMP_LU // solve() 的解法 ) |
该函数的源代码实现如下:构造8组方程,转化为 的问题,调用 solve() 函数来求解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | Mat getPerspectiveTransform( const Point2f src[], const Point2f dst[], int solveMethod) { Mat M(3, 3, CV_64F), X(8, 1, CV_64F, M.ptr()); double a[8][8], b[8]; Mat A(8, 8, CV_64F, a), B(8, 1, CV_64F, b); for ( int i = 0; i < 4; ++i ) { a[i][0] = a[i+4][3] = src[i].x; a[i][1] = a[i+4][4] = src[i].y; a[i][2] = a[i+4][5] = 1; a[i][3] = a[i][4] = a[i][5] = a[i+4][0] = a[i+4][1] = a[i+4][2] = 0; a[i][6] = -src[i].x*dst[i].x; a[i][7] = -src[i].y*dst[i].x; a[i+4][6] = -src[i].x*dst[i].y; a[i+4][7] = -src[i].y*dst[i].y; b[i] = dst[i].x; b[i+4] = dst[i].y; } solve(A, B, X, solveMethod); M.ptr< double >()[8] = 1.; return M; } |
b) 多组对应特征点:对于两个平面之间的投影变换,求得对应的多组特征点后,带入 findHomography() 函数,便可得到 srcPoints 投影到 dstPoints 的
1 2 3 4 5 6 7 8 9 | Mat findHomography ( InputArray srcPoints, // 原始平面特征点坐标,类型是 CV_32FC2 或 vector<Point2f> InputArray dstPoints, // 目标平面特征点坐标,类型是 CV_32FC2 或 vector<Point2f> int method = 0, // 0--最小二乘法; RANSAC--基于ransac的方法 double ransacReprojThreshold = 3, // 最大允许反投影误差 OutputArray mask = noArray(), // const int maxIters = 2000, // 最多迭代次数 const double confidence = 0.995 // 置信水平 ) |
2.2 投影变换图像
已知单应性矩阵 ,将任意图像代入 warpPerspective() 中,便可得到经过 2D投影变换 的目标图像
1 2 3 4 5 6 7 8 9 | void warpPerspective ( InputArray src, // 输入图像 OutputArray dst, // 输出图象(大小 dsize,类型同 src) InputArray M, // 3x3 单应性矩阵 Size dsize, // 输出图像的大小 int flags = INTER_LINEAR, // 插值方法 int borderMode = BORDER_CONSTANT, // const Scalar& borderValue = Scalar() // ) |
3 代码示例
3.1 透视校正
像平面 image1 和 image2 是相机在不同位置对同一物平面所成的像,这三个平面任选两个都互有 平面单应性
以相机标定为例,当人拿着棋盘格旋转不同角度时,可利用任意两个棋盘格间的单应性,将旋转不同角度的棋盘格,转换为统一角度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | #include "opencv2/imgproc.hpp" #include "opencv2/highgui.hpp" #include "opencv2/calib3d.hpp" using namespace cv; Size kPatternSize = Size(9, 6); int main() { // 1) read image Mat src = imread( "chessboard1.jpg" ); Mat dst = imread( "chessboard2.jpg" ); if (src.empty() || dst.empty()) return -1; // 2) find chessboard corners std::vector<Point2f> corners1, corners2; bool found1 = findChessboardCorners(src, kPatternSize, corners1); bool found2 = findChessboardCorners(dst, kPatternSize, corners2); if (!found1 || !found2) return -1; // 3) estimate H matrix Mat H = findHomography(corners1, corners2, RANSAC); // 4) Mat src_warp; warpPerspective(src, src_warp, H, src.size()); // 5) imshow( "src" , src); imshow( "dst" , dst); imshow( "src_warp" , src_warp); waitKey(); } |
结果如下:
棋盘格倾斜 棋盘格正对 倾斜校正为正对
3.2 图像拼接
当相机围绕其投影轴,只做旋转运动时,所有的像素点可等效视为在一个无穷远的平面上,则单应性矩阵可由旋转变换 和 相机标定矩阵 来表示
因此,如果已知相机的标定矩阵,以及旋转变换前后的位姿,可利用平面单应性,将旋转变换前后的两幅图像拼接起来
用 Blender 软件,获取相机只做旋转变换时的视图1和视图2,在已知相机标定矩阵和旋转矩阵的情况下,可计算出两个视图之间的单应性矩阵,从而完成拼接。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | #include "opencv2/imgproc.hpp" #include "opencv2/highgui.hpp" #include "opencv2/calib3d.hpp" using namespace cv; int main() { // 1) read image Mat img1 = imread( "view1.jpg" ); Mat img2 = imread( "view2.jpg" ); if (img1.empty() || img2.empty()) return -1; // 2) camera pose from Blender at location 1 Mat c1Mo = (Mat_< double >(4, 4) << 0.9659258723258972, 0.2588190734386444, 0.0, 1.5529145002365112, 0.08852133899927139, -0.3303661346435547, -0.9396926164627075, -0.10281121730804443, -0.24321036040782928, 0.9076734185218811, -0.342020183801651, 6.130080699920654, 0, 0, 0, 1); // camera pose from Blender at location 2 Mat c2Mo = (Mat_< double >(4, 4) << 0.9659258723258972, -0.2588190734386444, 0.0, -1.5529145002365112, -0.08852133899927139, -0.3303661346435547, -0.9396926164627075, -0.10281121730804443, 0.24321036040782928, 0.9076734185218811, -0.342020183801651, 6.130080699920654, 0, 0, 0, 1); // 3) camera intrinsic parameters Mat cameraMatrix = (Mat_< double >(3, 3) << 700.0, 0.0, 320.0, 0.0, 700.0, 240.0, 0, 0, 1); // 4) extract rotation Mat R1 = c1Mo(Range(0, 3), Range(0, 3)); Mat R2 = c2Mo(Range(0, 3), Range(0, 3)); // 5) compute rotation displacement: c1Mo * oMc2 Mat R_2to1 = R1 * R2.t(); // 6) homography Mat H = cameraMatrix * R_2to1 * cameraMatrix.inv(); H /= H.at< double >(2, 2); // 7) warp Mat img_stitch; warpPerspective(img2, img_stitch, H, Size(img2.cols * 2, img2.rows)); // 8) stitch Mat half = img_stitch(Rect(0, 0, img1.cols, img1.rows)); img1.copyTo(half); imshow( "Panorama stitching" , img_stitch); waitKey(); } |
输出结果如下:
视图1 视图2 拼接后的视图
参考资料:
OpenCV Tutorials / feature2d module / Basic concepts of the homography explained with code
Lecture 16: Planar Homographies, Robert Collins
Affine and Projective Transformations
原文链接: http://www.cnblogs.com/xinxue/
专注于机器视觉、OpenCV、C++ 编程
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人