ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

双目视觉测量源码实战:从Matlab标定到OpenCV三维重建与变形量计算

双目视觉测量源码实战:从Matlab标定到OpenCV三维重建与变形量计算

简介:本资源是一套面向毕业设计、课程设计与项目开发的双目视觉测量完整源码,围绕相机标定、立体校正、三维坐标重建与工件变形量计算展开,适合具备一定OpenCV与C++基础、希望深入理解双目视觉测量流程的本科生与开发者。项目通过图像预处理提取工件测量点,借助Matlab工具箱完成相机标定,再由OpenCV读取标定结果并结合立体校正与立体匹配算法重建物体三维坐标,最终求得工件变形量,整体流程覆盖从标定到测量的关键环节。压缩包共33个文件,约2.01MB,包含cpp与h源码、sln与vcxproj工程文件、xml与txt标定及结果数据、jpg与obj图像模型、md说明文档以及exe可执行文件等,结构清晰,便于按模块查阅与二次开发。目前已有306人学习下载,源码经过严格测试,可作为双目测量类课题的参考实现,帮助读者快速搭建实验环境、理解标定与重建的衔接方式,并在此基础上延伸出新的测量或检测功能。

1. 双目视觉测量这套源码,到底能不能直接跑通

去年帮一个做机械检测的朋友看工件变形量测量方案,他手里有一份双目视觉测量.zip,解压出来是图像预处理及特征点提取.sln、立体标定结果.txt、README.md和几张标定板照片。他问我的第一句话是:这东西能不能直接编译跑通,还是只能当参考。我花了一个下午把工程翻了一遍,结论是:能跑,但前提是你得先搞清楚它的数据流——Matlab 负责标定出内外参,OpenCV 负责立体校正和匹配,C++ 工程负责把三维坐标算出来再比对变形量。三个环节的接口对不上,就会出现"标定结果读进去但重投影误差爆炸"这种玄学问题。

这套资源适合三类人:正在做双目测量方向毕设的学生、需要快速搭一套工件变形检测原型的工程师、以及想拿一个完整 C++ 视觉工程练手的人。它不是一个封装好的 SDK,而是一条从标定到重建的完整链路,中间每一步你都能改参数、看中间结果。下面我按实际拆解顺序,把标定、校正、匹配、变形量计算这几块讲透,顺带把几个容易翻车的地方标出来。

2. 相机标定与立体校正:Matlab 出参、OpenCV 吃参的衔接

2.1 为什么标定用 Matlab、校正用 OpenCV

这套工程最值得说的一点,是它没有硬凑一个工具干所有事。相机标定本质是解一个非线性最小二乘问题,Matlab 的 Camera Calibrator 工具箱把角点检测、畸变模型选择、重投影误差可视化都做成了交互界面,你拖几张标定板照片进去就能看到每张图的平均重投影误差。而立体校正和后续的极线对齐,OpenCV 的stereoRectify和initUndistortRectifyMap是工业界验证过无数遍的实现,C++ 调用效率也高。

常见做法是:Matlab 标完导出stereoParams,手动把内参矩阵、畸变系数、旋转平移向量抄到 C++ 的配置文件里。这里第一个坑就来了——Matlab 的畸变系数顺序和 OpenCV 不一样。Matlab 导出的是RadialDistortion和TangentialDistortion分开的,OpenCV 要的是k1 k2 p1 p2 k3排成一列。顺序搞反,校正后的图会像被拧过的毛巾。

2.2 从标定结果到校正映射的完整步骤

先看 Matlab 侧导出后你手里应该有什么。打开立体标定结果.txt,正常会看到左右相机的内参矩阵、畸变向量、以及 stereoParams 里的旋转矩阵 R 和平移向量 T。下面这段是我一般会写的解析代码,把文本里的参数读进 OpenCV 的 Mat:

// 读取Matlab导出的标定参数,按OpenCV顺序重排畸变系数 #include <opencv2/opencv.hpp> #include <fstream> #include <iostream> bool loadCalibParams(const std::string& path, cv::Mat& K1, cv::Mat& D1, cv::Mat& K2, cv::Mat& D2, cv::Mat& R, cv::Mat& T, cv::Size& imgSize) { std::ifstream fin(path); if (!fin.is_open()) { std::cerr << "标定文件打不开,检查路径: " << path << std::endl; return false; } // 按文件实际排版读取,这里假设每行一个矩阵元素 // K1/K2 为 3x3,D1/D2 为 1x5,R 为 3x3,T 为 3x1 K1 = cv::Mat(3, 3, CV_64F); K2 = cv::Mat(3, 3, CV_64F); D1 = cv::Mat(1, 5, CV_64F); D2 = cv::Mat(1, 5, CV_64F); R = cv::Mat(3, 3, CV_64F); T = cv::Mat(3, 1, CV_64F); for (int i = 0; i < 9; ++i) fin >> K1.at<double>(i / 3, i % 3); for (int i = 0; i < 5; ++i) fin >> D1.at<double>(0, i); // k1 k2 p1 p2 k3 for (int i = 0; i < 9; ++i) fin >> K2.at<double>(i / 3, i % 3); for (int i = 0; i < 5; ++i) fin >> D2.at<double>(0, i); for (int i = 0; i < 9; ++i) fin >> R.at<double>(i / 3, i % 3); for (int i = 0; i < 3; ++i) fin >> T.at<double>(i, 0); imgSize = cv::Size(1280, 960); // 改成你标定时的实际分辨率 return true; }

逻辑说明:这段代码把文本参数按 OpenCV 期望的维度读进来,关键是畸变向量固定为 5 元素。参数说明:K1/K2是左右相机内参,D1/D2是畸变系数,R/T是右相机相对左相机的旋转和平移。imgSize必须和你标定时用的图像尺寸一致,否则stereoRectify算出来的映射表会错位。

拿到参数后,立体校正就三行核心调用:

cv::Mat R1, R2, P1, P2, Q; cv::stereoRectify(K1, D1, K2, D2, imgSize, R, T, R1, R2, P1, P2, Q, cv::CALIB_ZERO_DISPARITY, 0, imgSize); cv::Mat map1x, map1y, map2x, map2y; cv::initUndistortRectifyMap(K1, D1, R1, P1, imgSize, CV_32FC1, map1x, map1y); cv::initUndistortRectifyMap(K2, D2, R2, P2, imgSize, CV_32FC1, map2x, map2y);

Q矩阵是后面reprojectImageTo3D用的重投影矩阵,别丢。CALIB_ZERO_DISPARITY表示让左右校正图的主点对齐,做测量一般都要开。校正完你可以把左右图叠一半看极线是否水平——如果极线还是斜的,八成是 R/T 读反了或者畸变顺序错了。

2.3 标定板拍摄的硬性要求

工程里那几张1616582384(1).jpg之类的图就是标定板照片。我一般要求至少 15 对,棋盘格要覆盖画面四个角和中心,倾斜角度在 15 到 45 度之间。全是正对镜头的图,标出来的畸变系数基本是摆设。Matlab 标定完看Reprojection Errors,单张超过 0.5 像素的图直接删掉重拍,别心疼。

3. 图像预处理与特征点提取:从 .sln 工程看测量点怎么定

3.1 工程结构里藏着的处理链路

图像预处理及特征点提取.sln是这套资源的 C++ 主工程。用 VS 打开后,源码里一般会包含灰度化、滤波、边缘检测、轮廓筛选这几步。工件变形量测量的核心是找到工件上预先标记的测量点,常见做法是在工件表面贴圆形标记或者用激光打点,然后通过圆心检测或角点检测拿到像素坐标。

我翻这类工程的习惯是先看main里的调用顺序,再看每个函数的输入输出。典型链路是:cvtColor转灰度 →GaussianBlur去噪 →threshold或Canny出边缘 →findContours找轮廓 → 按面积和圆度筛选 →minEnclosingCircle或moments算圆心。下面这段是我会用来替换或补充的圆心提取代码:

// 提取圆形标记的亚像素圆心,用于后续三角测量 std::vector<cv::Point2f> extractMarkers(const cv::Mat& gray) { cv::Mat bin; // 自适应阈值应对工件表面反光不均 cv::adaptiveThreshold(gray, bin, 255, cv::ADAPTIVE_THRESH_GAUSSIAN_C, cv::THRESH_BINARY_INV, 21, 5); std::vector<std::vector<cv::Point>> contours; cv::findContours(bin, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); std::vector<cv::Point2f> centers; for (const auto& c : contours) { double area = cv::contourArea(c); if (area < 50 || area > 5000) continue; // 按实际标记大小调 float radius; cv::Point2f center; cv::minEnclosingCircle(c, center, radius); // 圆度校验:面积与包围圆面积比接近1才是圆 double circularity = area / (CV_PI * radius * radius); if (circularity < 0.75) continue; // 亚像素精化 cv::cornerSubPix(gray, std::vector<cv::Point2f>{center}, cv::Size(5, 5), cv::Size(-1, -1), cv::TermCriteria(cv::TermCriteria::EPS + cv::TermCriteria::MAX_ITER, 30, 0.01)); centers.push_back(center); } return centers; }

逻辑说明:先二值化把标记从背景里分出来,再用面积和圆度两道过滤去掉噪声轮廓,最后用cornerSubPix把圆心精度推到亚像素级。参数说明:adaptiveThreshold的 blockSize 取 21 是经验值,工件表面反光强就调大;面积范围 50 到 5000 要根据你标记的实际像素大小改,标定阶段可以先打印出来看看。

3.2 左右图特征点必须配对

提取出圆心只是第一步,双目测量的前提是同一个物理点在左右图里都被找到并且配对。常见做法是用极线约束:校正之后,左图某点的匹配点一定在右图同一行附近。所以配对时不用全图搜,只在同一行上下几个像素内找最近邻就行。这一步如果配错,后面三维坐标会飞到一个莫名其妙的位置,而且从数值上看不出来,只有画点云才会发现。

我一般会在配对后加一个左右一致性检查:左图点 A 匹配到右图点 B,再从 B 反向匹配回左图,如果回到的不是 A,就丢掉这对。这个检查能干掉大部分误匹配,代价只是多跑一遍匹配。

3.3 预处理参数怎么调

工程里如果用的是固定阈值,换个工件基本就废了。我的习惯是把阈值、滤波核大小、面积范围都提到配置文件或者命令行参数里,调的时候不用重新编译。README.md里如果写了推荐参数,先按那个跑一遍,然后拿你自己的图对比中间输出——把二值图、轮廓图都存下来看,比盯着最终结果猜要快得多。

4. 三维坐标重建与变形量计算:Q 矩阵和基准怎么定

4.1 从视差到三维坐标

校正后的左右图,同一个点在两图中的横坐标差就是视差 d。OpenCV 的reprojectImageTo3D用 Q 矩阵把视差图转成三维点云,公式本质是:

[X, Y, Z, W]^T = Q * [x, y, d, 1]^T 三维坐标 = (X/W, Y/W, Z/W)

Q 矩阵在stereoRectify里已经算好了,直接拿来用。但这里有个容易被忽略的点:reprojectImageTo3D输出的坐标单位跟 T 的单位一致。Matlab 标定时 T 一般是毫米,所以点云单位也是毫米。如果你发现 Z 值大得离谱,先检查 T 是不是被当成了米。

// 视差图转三维点云 cv::Mat disp, points3D; // disp 由立体匹配得到,CV_16S 格式,实际视差要除以16 cv::Mat dispFloat; disp.convertTo(dispFloat, CV_32F, 1.0 / 16.0); cv::reprojectImageTo3D(dispFloat, points3D, Q, true); // 取某个测量点的三维坐标 cv::Point2f leftPt = centersLeft[i]; cv::Vec3f pt3d = points3D.at<cv::Vec3f>(cvRound(leftPt.y), cvRound(leftPt.x)); std::cout << "测量点三维坐标: " << pt3d << std::endl;

逻辑说明:disp如果是StereoBM或StereoSGBM输出的 16 位视差图,要除以 16 才是真实视差。reprojectImageTo3D的第四个参数handleMissingValues设为 true 会把无效视差对应的点设成很大的值,取点前最好判断一下 Z 是否在合理范围。参数说明:points3D是CV_32FC3,每个像素存一个三维坐标,直接按左图像素坐标索引。

4.2 变形量计算的基准问题

工件变形量 = 变形后坐标 - 基准坐标。基准怎么定是这套工程里最需要你自己想清楚的地方。常见两种做法:一是第一次测量作为基准,后续每次跟它比;二是用工件上几个不变形区域的特征点拟合一个参考坐标系,把测量点变换到这个坐标系下再比。

第二种更稳,因为工件每次装夹位置会有微小变化。我一般会选工件上三个不在同一直线的固定点建一个局部坐标系,把所有测量点都转到这个坐标系下,这样装夹误差就被消掉了。工程里如果没做这一步,你测出来的变形量里会混进装夹偏移,数据会很难看。

4.3 立体匹配参数对测量精度的影响

StereoBM和StereoSGBM的参数直接决定视差图质量。numDisparities必须是 16 的倍数,blockSize取奇数。工件表面纹理少的话,BM 基本出不来视差,得换 SGBM 或者给工件表面投散斑。我一般先用 SGBM 跑,P1取8*channels*blockSize^2,P2取32*channels*blockSize^2,uniquenessRatio设 10 到 15 之间,能压掉不少误匹配。

视差图出来后在imshow里看一眼,好的视差图应该是平滑渐变、边缘清晰。如果大片黑洞,要么是纹理不够,要么是numDisparities设小了。这个参数得根据你相机基线和工作距离估:基线越长、距离越近,视差范围越大。

5. 避坑与排查:这套工程最容易翻车的五个地方

5.1 标定结果读进去但重投影误差巨大

现象:校正后的图极线不水平,三维点云整体扭曲。原因:Matlab 畸变系数顺序和 OpenCV 不一致,或者 R/T 转置了。解决:打印 D1 向量确认是k1 k2 p1 p2 k3顺序;R 应该是右相机相对左相机的旋转,如果点云镜像了,把 R 转置、T 取反试试。

5.2 编译报错找不到 OpenCV 头文件

现象:.sln打开后一堆红色波浪线,提示opencv2/opencv.hpp找不到。原因:工程属性里的包含目录和库目录还是原作者机器的路径。解决:在 VS 里右键工程 → 属性 → VC++ 目录,把包含目录改成你自己的 OpenCVinclude路径,库目录改成lib路径,链接器输入里确认opencv_world4xx.lib的版本号和你装的一致。

5.3 视差图全黑或全是噪点

现象:reprojectImageTo3D出来的点云几乎为空。原因:numDisparities太小覆盖不到实际视差范围,或者工件表面没有纹理。解决:先估算视差范围,numDisparities设成比最大视差大一点;纹理不足就投散斑或者换 SGBM 并调大speckleWindowSize。

5.4 测量点三维坐标跳变

现象:同一个点连续两帧算出来的坐标差好几毫米。原因:特征点提取不稳定,亚像素圆心在边缘处漂移。解决:检查标记是否完整落在视野内、有没有过曝;cornerSubPix的窗口别开太大,5x5 一般够用;配对时加左右一致性检查。

5.5 变形量数值合理但方向反了

现象:工件明明受压,算出来却是拉伸。原因:基准坐标系定义时 Z 轴方向搞反了,或者左右相机顺序颠倒。解决:用已知尺寸的标定物验证一下——量一个已知长度的物体,看三维坐标算出来的距离对不对。这一步我每次换镜头或调基线后都会做一遍,算是后悔药。

6. 进阶:把测量精度再压一压的几个实操技巧

如果你已经把整条链路跑通,想让变形量测量精度从"能用"到"敢写进报告",下面几个点值得花时间。

第一,标定阶段用更大尺寸的标定板并增加拍摄距离的多样性。标定板占画面比例太小,角点检测精度上不去,内参就不准。我一般让标定板占画面三分之一到二分之一,拍 20 对以上,覆盖近中远三个距离。

第二,立体匹配前给工件投散斑。激光散斑或者投影仪投随机点阵都行,目的是给表面加纹理。这一步对金属工件尤其关键,金属表面反光加无纹理,BM 和 SGBM 都抓瞎。投了散斑之后视差图的填充率能从 40% 提到 90% 以上。

第三,测量点用同心圆标记而不是实心圆。同心圆在亚像素提取时可以用椭圆拟合,圆心定位精度比minEnclosingCircle高一个量级。下面这段是椭圆拟合取圆心的写法:

// 同心圆标记的亚像素圆心提取 cv::RotatedRect ellipse = cv::fitEllipse(contour); cv::Point2f center = ellipse.center; // 椭圆长短轴比接近1说明是正圆,否则丢弃 float ratio = ellipse.size.width / ellipse.size.height; if (ratio < 0.9 || ratio > 1.1) continue;

第四,变形量计算前做一次温度补偿。如果测量环境温度变化超过 5 度,工件本身的热胀冷缩会混进变形量里。简单做法是记录环境温度,按材料线膨胀系数折算一个修正量减掉。这个在实验室里容易被忽略,但数据评审时经常被问。

第五,把 Q 矩阵和标定参数存成文件,每次测量加载同一套。我见过有人每次重新标定,结果两次测量的基准不一致,变形量数据完全没法比。从那以后我每次改完硬件都强制走一遍"标定 → 存参 → 用已知尺寸验证 → 再测量"的流程,少一步都不行。

这套双目视觉测量.zip的价值在于它把标定、校正、匹配、重建这条链路完整串起来了,你可以在此基础上换自己的标记、调自己的参数、接自己的工件。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进