尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于MFC与OpenCV的摄像机标定与立体匹配程序实战

基于MFC与OpenCV的摄像机标定与立体匹配程序实战 1. 项目背景与整体设计思路1.1 为什么要自研这个测试程序做机器视觉的同行应该都有体会OpenCV官方自带的那些示例程序——无论是calibrate.cpp还是stereo_match.cpp——功能虽然完整但用起来总是不够顺手。默认的控制台窗口只能看输出文字图像显示要么是弹窗要么是写死路径调参数要改源码重新编译标定过程也不直观。特别是做双目立体匹配的时候中间每一步的中间结果都看不到出了偏差很难定位是标定矩阵的问题还是极线校正的问题还是匹配算法参数的问题。所以2014年我动手写了这套基于MFC和OpenCV的摄像机定标与立体匹配测试程序。说白了就是把标定和立体匹配从OpenCV的示例代码里搬出来封装到一个带界面的MFC程序里让每一步都有图形反馈参数可以实时调。这套程序解决的核心问题有三个标定过程可视化棋盘角点检测结果、重投影误差分布、内外参矩阵都能直观看到不再是一堆黑底白字的控制台输出。立体匹配中间结果可见极线校正前后的图像对比、视差图、深度图每一步都能暂停查看方便定位问题到底出在哪一步。参数可调不重编匹配窗口大小、视差范围、标定板尺寸等核心参数在界面上直接改不需要改代码重新编译。适合谁参考如果你是正在学OpenCV双目标定和立体匹配的学生或者刚接手双目视觉项目、需要快速搭建一个验证环境的工程师这套程序的设计思路和关键代码实现值得花时间看完。我自己后来在多个项目里复用和改造了这套框架核心架构一直没怎么变过。1.2 MFC加OpenCV的选型考量为什么用MFC而不是Qt、WinForms或者纯OpenCV的高层GUI2014年那个时间节点MFC在工业视觉领域还是绝对的主流。工业相机厂商的SDK比如大恒、Basler、映美精给的示例基本都是MFC工程。用MFC做壳子对接相机SDK最省事坑最少。另外标定和匹配这种工具类程序用户习惯还是Windows桌面程序MFC编译出来单文件exe拷到工控机上就能跑不需要装额外的运行时静态编译的前提下。OpenCV方面当时已经出到2.4.x版本C接口相对稳定。2.4版本对MFC的支持其实不错IplImage到CImage的转换、cv::Mat到CImage的转换网上代码一大把。选OpenCV 2.4.x还有一个原因它的StereoBM和StereoSGBM接口在2.4.9之后已经比较成熟不需要像3.0版本那样引入contrib模块编译配置简单很多。这套程序的整体架构按照功能模块拆解下来是三层结构界面层MFC对话框负责参数输入、按钮相应、图像结果显示。算法层封装了标定、立体匹配、畸变矫正等OpenCV算法调用对外提供清晰的函数接口。数据层标定图片的管理、标定结果的保存与加载、匹配参数的配置读写。这个分层的好处显而易见——算法层不依赖MFC可以单独拎出来做单元测试也可以换成Qt界面或者控制台界面直接复用。我之前在另一个项目里就是把算法层原封不动搬到了Linux下用纯C调用代码基本没改。2. 摄像机定标的核心细节与实操要点2.1 标定原理回顾与参数理解先捋一下摄像机定标的基本原理这部分是老生常谈但必须讲清楚因为后面所有代码逻辑都是建立在它之上的。摄像机成像模型是针孔模型世界坐标系下的点经过刚体变换旋转R和平移T到相机坐标系再经过透视投影到图像平面最后加上透镜畸变的影响得到像素坐标。用矩阵表达就是s * [u, v, 1]^T A * [R | T] * [X, Y, Z, 1]^T其中A是内参矩阵包含焦距fx、fy主点cx、cy和倾斜因子一般置为0A [fx 0 cx 0 fy cy 0 0 1]畸变模型在OpenCV里用的是Brown-Conrady模型包含径向畸变k1、k2、k3和切向畸变p1、p2x_distorted x * (1 k1*r^2 k2*r^4 k3*r^6) 2*p1*x*y p2*(r^2 2*x^2) y_distorted y * (1 k1*r^2 k2*r^4 k3*r^6) p1*(r^2 2*y^2) 2*p2*x*y标定要做的事情就是拍摄多张已知几何形状棋盘格的图片提取角点然后通过最小化重投影误差的方式求解这些参数。立体标定则在单目标定的基础上进一步求解两台相机之间的相对位姿关系。两张图像上的对应点需要满足极线约束这个约束由本质矩阵E或基础矩阵F描述立体标定最终得到的就是右相机相对左相机的旋转矩阵R和平移向量T以及本征矩阵E和基础矩阵F。这些参数有什么实际意义内参和畸变系数用于单张图像的去畸变和三维重建时的相机建模双目的R和T用于极线校正让左右图像的对应点落在同一水平线上这是立体匹配得以高效进行的前提。2.2 标定板准备与图像采集要点标定板我用的是10x7的棋盘格格边长30mm打印出来贴在硬纸板上。这里有一个经常被忽略的细节OpenCV的findChessboardCorners传入的角点数是内角点数而不是格数。10x7的棋盘格对应内角点就是9x6。搞反了的话函数直接检测失败或者检测到一堆乱七八糟的点。采集图像的时候我一般遵循这几条经验图像数量不少于15张我习惯拍到20到25张。太少了参数解不稳定尤其是畸变系数。棋盘格要充满视野的不同区域中间、四角、上下左右边缘都要覆盖到。因为畸变在图像边缘最明显如果棋盘总在画面中央边缘区域的畸变参数就约束不足。棋盘格平面和相机光轴之间要有一定倾角不要每次都正对着拍。倾斜的角度能提供更多的深度信息有助于求解焦距和位姿。光照要均匀棋盘格表面不要有反光和阴影。这一点在工业现场特别头疼我后来干脆用背光板加漫射罩把环境光问题彻底解决。在采集界面里我加了一个实时检测的反馈功能。每抓一帧立刻调用findChessboardCorners检测如果检测成功就在图像上把角点画出来并在状态栏显示当前帧检测成功已采集XX张。这样操作人员不需要等全部拍完才发现某几张不合格边拍边筛效率高很多。采集完的图像统一存成BMP或者JPG文件名按序号递增。另外程序里会记录每张图像采集时的相机编号因为左右目必须成对采集。实际操作中我采用的做法是左右相机同时触发采集保证两张图是同一时刻、同一个棋盘姿态下拍摄的。对于测试程序来说即使做不到硬触发同步至少也要保证棋盘在采集过程中是静止的否则后续标定结果会有明显误差。2.3 角点检测与亚像素细化findChessboardCorners的原理基于黑白格交叉点的检测核心算法是先用自适应阈值分割找到候选角点区域再用四边形轮廓筛选确定棋盘格布局。它返回的角点坐标初始精度在像素级要进一步提升精度必须做亚像素细化。亚像素细化用的是cornerSubPix它基于角点邻域的灰度梯度信息通过迭代最小化误差来求取亚像素精度的角点坐标。这个函数有几个关键参数需要调winSize搜索窗口半宽我习惯设为Size(5, 5)对30mm边长的棋盘格、分辨率为1280x960的图像来说绰绰有余。zeroZone死区半宽防止奇异性设为Size(-1, -1)表示不使用死区。criteria迭代终止条件最大迭代次数和最小精度我习惯设为TermCriteria(CV_TERMCRIT_EPS CV_TERMCRIT_ITER, 30, 0.001)。角点提取的质量直接影响标定精度这一步别偷懒。cornerSubPix理论上精度能达到0.1像素但如果棋盘图像模糊、光照不均细化结果也不会太好。所以我多了一步在采集界面里实时计算当前帧的清晰度评价指标灰度方差或者LapLacian响应低于阈值就提示图像模糊请重新拍摄。2.4 单目标定与双目标定的代码实现说完原理和准备看核心代码。单目标定用的是calibrateCamera参数如下// 输入每张图像的内角点坐标亚像素精度和对应的世界坐标 // objectPoints世界坐标系下的棋盘角点坐标z值全为0 // imagePoints图像中的角点坐标 // imageSize图像的尺寸必须是原图尺寸 std::vectorstd::vectorcv::Point3f objectPoints; std::vectorstd::vectorcv::Point2f imagePoints; cv::Size imageSize(1280, 960); cv::Mat cameraMatrix, distCoeffs; std::vectorcv::Mat rvecs, tvecs; double rms cv::calibrateCamera(objectPoints, imagePoints, imageSize, cameraMatrix, distCoeffs, rvecs, tvecs);注意几点第一objectPoints的生成要严格按照棋盘格内角点的行列顺序。我的写法是for (int i 0; i patternHeight; i) { for (int j 0; j patternWidth; j) { objectPoints.push_back(Point3f(j * squareSize, i * squareSize, 0)); } }这里patternHeight和patternWidth分别是8和7还是9和6取决于你的棋盘格规格别搞混。第二calibrateCamera默认使用CALIB_USE_LU标志做内参初始化。如果你知道图像的传感器尺寸和镜头焦距可以用CALIB_USE_INTRINSIC_GUESS传入初始猜测值收敛更快也更稳。第三返回的rms是重投影均方根误差单位是像素。它是最直观的标定质量指标之一一般小于0.5像素就算可以接受。如果大于1像素说明角点检测精度不够或者标定板不平整甚至图像数量太少。双目标定在单目标定的基础上调用stereoCalibratecv::Mat R, T, E, F; double rms cv::stereoCalibrate(objectPoints, imagePointsLeft, imagePointsRight, cameraMatrixL, distCoeffsL, cameraMatrixR, distCoeffsR, imageSize, R, T, E, F, cv::CALIB_FIX_INTRINSIC, cv::TermCriteria(cv::TermCriteria::COUNT cv::TermCriteria::EPS, 100, 1e-5));这里我用了CALIB_FIX_INTRINSIC标志意思是左右相机的内参和畸变系数固定为单目标定的结果只优化双目之间的R和T。这样做的理由是单目标定各自已经收敛得不错双目标定再全局优化的话自由度太多容易过拟合反而可能把内参带偏。2.5 标定结果的分析与判断标定完成不是看程序跑完没报错就完事了。我习惯跑完标定后做以下几件事第一打印内参矩阵和畸变系数用肉眼判断合理性。fx、fy通常在图像尺寸的0.8到1.5倍之间对于水平视角约60度的镜头和1280宽的图像fx大概在1000左右。如果算出来fx是几千甚至几万说明标定过程出了问题。第二看重投影误差分布。OpenCV的calibrateCamera只返回总RMS但我想知道每张图各自的误差因为某张图角点检测质量差的话会拉低整体精度。做法是自己写一个小函数遍历每张图的角点用projectPoints投影世界坐标到图像平面和检测到的角点坐标求欧氏距离。for (size_t i 0; i objectPoints.size(); i) { std::vectorcv::Point2f projectedPoints; cv::projectPoints(objectPoints[i], rvecs[i], tvecs[i], cameraMatrix, distCoeffs, projectedPoints); double err cv::norm(imagePoints[i], projectedPoints, cv::NORM_L2) / projectedPoints.size(); printf(Image %d reprojection error: %.3f px\n, i, err); }如果某张图的误差明显大于其他图比如超过1.5像素而其他都小于0.3直接删掉这张图重新标定。这个边标定边筛选的做法比一次标完再返工省时间得多。第三检查双目标定得到的基线长度。如果棋盘格尺寸是30mm标定时两张图像中的棋盘大小接近那么基线长度T的模长应该在设备和棋盘尺度相当的范围内。离谱的T值基本说明标定数据有问题。3. 立体匹配的核心细节与实现要点3.1 极线校正与立体匹配算法选型双目标定得到R和T之后下一步是极线校正。OpenCV提供了两种校正方法stereoRectify加initUndistortRectifyMap。stereoRectify的输出是左右相机的校正旋转矩阵R1、R2和新的投影矩阵P1、P2。核心思想是将左右图像重新投影到一个公共平面上使得对应点的行号对齐即极线变成水平线。cv::Mat R1, R2, P1, P2, Q; cv::stereoRectify(cameraMatrixL, distCoeffsL, cameraMatrixR, distCoeffsR, imageSize, R, T, R1, R2, P1, P2, Q, cv::CALIB_ZERO_DISPARITY, 0);Q矩阵非常重要它是一个4x4的将视差图转换为三维坐标的投影矩阵后面生成深度图全靠它。校正之后用initUndistortRectifyMap和remap把原始图像变换到校正后的图像cv::Mat map1L, map2L, map1R, map2R; cv::initUndistortRectifyMap(cameraMatrixL, distCoeffsL, R1, P1, imageSize, CV_32FC1, map1L, map2L); cv::remap(imageL, rectifiedL, map1L, map2L, cv::INTER_LINEAR);这里有一个踩过的坑initUndistortRectifyMap的m1type参数之前用CV_16SC2导致remap速度很快但图像边缘偶尔出现锯齿后来统一改成CV_32FC1精度上去了速度差异在测试程序里完全可以接受。立体匹配算法方面OpenCV 2.4时代主流是StereoBM块匹配和StereoSGBM半全局块匹配。StereoBM速度快但视差图噪声大、边缘毛刺多适合对实时性要求高、精度要求低的场景。StereoSGBM基于Semi-Global Matching算法引入了多方向扫描线代价聚合效果好很多但速度大概慢三到五倍。我的程序里两种算法都做了封装界面上可以切换。默认用StereoSGBM参数做了几轮调优后固定了一套比较稳的初始值cv::StereoSGBM sgbm; sgbm.minDisparity 0; sgbm.numDisparities 64; sgbm.SADWindowSize 9; sgbm.P1 8 * 3 * sgbm.SADWindowSize * sgbm.SADWindowSize; sgbm.P2 32 * 3 * sgbm.SADWindowSize * sgbm.SADWindowSize; sgbm.uniquenessRatio 10; sgbm.lambda 0; sgbm.speckleWindowSize 100; sgbm.speckleRange 32;3.2 视差图到三维坐标的转换原理StereoSGBM输出的视差图默认是CV_16S类型每个像素的值是实际视差的16倍所以读取的时候要除以16才能得到真正的像素视差值。有了视差图和stereoRectify得到的Q矩阵就可以直接算三维坐标[X, Y, Z, W]^T Q * [u, v, disparity, 1]^T实际坐标就是X/W、Y/W、Z/W。这里有一个非常常见的坑如果图片是彩色图使用stereoRectify前必须把左右图像转成灰度图。彩色图直接用StereoBM也能跑但OpenCV内部会先做灰度转换性能没区别代码却更容易出错。另一个问题是视差图的无效值。左图中被右图遮挡的区域、低纹理区域、边缘区域匹配算法产生的视差值往往不可靠。StereoSGBM会把这些地方设为一个特殊值通常是负值或者minDisparity - 1转三维坐标时不能直接套公式要先做有效性判断if (disparity 0 disparity maxValidDisparity) { // 有效视差计算三维坐标 }3.3 深度图计算的几个隐藏细节计算深度图时除了视差图上文提到的有效性判断还有几个细节值得一说。第一Q矩阵中的T值单位是毫米因为标定时棋盘格尺寸用的毫米所以算出来的三维坐标单位也是毫米。如果你的棋盘格尺寸用的米或者厘米记得整个流程保持一致不然出来的深度值会差几个数量级。第二StereoBM运行前建议做一次preFilterType参数设置。默认的StereoBM会做亮度归一化预处理能提升光照不均匀情况下的匹配稳定性。我遇到过这样的情况左右相机自动增益不同导致整体亮度有差异匹配结果很差把StereoBM::create里的preFilterType设为CV_STEREO_BM_NORMALIZED_RESPONSE之后效果立刻改善。第三uniquenessRatio参数的作用是确保左右一致性。它表示当最佳匹配的代价比次佳匹配小多少百分比时才认为是有效匹配。值越大匹配越严格视差图上孔洞越多但误匹配越少。我一般设10到15如果发现视差图出现大量细碎噪声优先调大这个值。3.4 匹配参数的调优与界面交互设计这套程序在界面上把立体匹配参数全部暴露出来了。参数调节区用了MFC的CSliderCtrl和CEdit组合滑块拖动改参数编辑框同步更新数值点击重新匹配按钮后就跑一遍完整流程视差图和深度图立刻刷新。实践证明这种交互方式对调试特别友好。调试双目算法的时候参数之间的影响是耦合的调大SADWindowSize视差图更平滑但边缘细节丢失调大numDisparities近处物体的视差能算出来但远处物体的区分度变差同时计算量变大。靠肉眼判断效果边调边看比对着参数表猜要高效得多。参数调整过程中有一个细节界面上显示的是实际的奇数值但SADWindowSize必须是奇数numDisparities必须是16的整数倍。我在滑块回调里做了约束确保任意拖动都不会产生非法参数组合。这些看似不起眼的处理实际用起来会省掉很多参数非法、程序崩溃的烦恼。4. MFC工程整合OpenCV的实战记录4.1 环境配置与链接配置要点先说环境版本避免各位前辈踩我当年的坑。程序基于Visual Studio 2010和OpenCV 2.4.9编写MFC使用动态库方式。如果你用的是VS2015以上的版本OpenCV官方预编译包从3.0开始就不再提供VC12以下的版本需要自己用CMake编译或者用别人编译好的包。不过这套代码的核心逻辑不依赖MFC和OpenCV的特定版本迁移到OpenCV 3.x 4.x只需要改少量接口调用。MFC工程的配置有几个容易出问题的地方逐个说明。第一字符集必须统一。VS2010默认工程字符集是Unicode而OpenCV的cv::String和文件操作接口默认是ANSI字节流。我在工程属性里把字符集改成使用多字节字符集省去了所有字符串转换的麻烦。如果你必须用Unicode在调用OpenCV的文件读取、命令行参数接口时记得用CT2A或者WideCharToMultiByte做转换。第二附加包含目录和库目录要填对。OpenCV安装目录下include目录对应头文件x86或x64目录下vc10/vc12文件夹里的lib和dll对应不同版本的运行库。2010版对应vc10如果用vc12的库链接VS2010编译会直接报错lib不兼容。第三链接方式选择。Debug模式链接带d后缀的库文件如opencv_core249d.libRelease模式链接不带d的。混用的话Linker不会报错但运行时会出莫名其妙的崩溃而且特别难查。我在程序里用预编译宏做了控制Debug和Release的lib列表写在不同分支里。4.2 图像显示与CImage的转换MFC里显示图像最常见的方式是CImage加CDC::StretchBlt或者CStatic控件。但OpenCV的cv::Mat和CImage之间的互转需要自己写转换函数。void MatToCImage(const cv::Mat mat, CImage cimg) { int channels mat.channels(); int depth mat.depth(); if (channels 1) { cimg.Create(mat.cols, mat.rows, 8); for (int y 0; y mat.rows; y) { memcpy(cimg.GetPixelAddress(0, y), mat.ptruchar(y), mat.cols); } } else if (channels 3) { cimg.Create(mat.cols, mat.rows, 24); for (int y 0; y mat.rows; y) { const uchar* src mat.ptruchar(y); uchar* dst (uchar*)cimg.GetPixelAddress(0, y); for (int x 0; x mat.cols; x) { dst[x * 3] src[x * 3 2]; // B dst[x * 3 1] src[x * 3 1]; // G dst[x * 3 2] src[x * 3]; // R } } } }这里有个特别注意的点CImage的像素存储顺序是BGR与OpenCV的cv::Mat默认布局一致但每个GetPixelAddress访问时需要注意行对齐问题。默认的CImage创建在内存中可能带有补位字节每行字节数补齐到4的倍数所以复制整行时不能直接memcpy整行到目标地址要逐行复制。上面的代码已经做了逐行处理但确认下Create函数第三个参数设置为0时是否使用默认的4字节对齐这个细节在不同版本的MFC下行为一致。显示的时候也不是直接把CImageDraw到控件区就完事。图像分辨率大于控件大小的时候要先缩放。我在显示函数里加了一个比例计算保持宽高比缩放后居中显示另外加了一个1:1原始分辨率的切换按钮放大局部细节时特别有用。4.3 多线程与界面的响应性设计标定和立体匹配都是耗时操作。立体匹配的StereoSGBM在1280x960的分辨率下图可能需要200到400毫秒标定图像多一点也要好几秒。如果放在UI线程里执行窗口会直接卡死用户还以为是程序崩溃了。我的做法是单独开一个工作线程UI线程只负责收发消息。具体流程是用户点击开始标定UI线程创建工作线程。工作线程执行标定执行过程中通过PostMessage往UI线程发消息消息中包含当前进度、状态文字或中间结果图像。UI线程响应消息刷新进度条和图像显示。MFC的AfxBeginThread创建的工作线程和UI线程之间通信我用的是自定义的消息映射。#define WM_UPDATE_STATUS (WM_USER 100) #define WM_UPDATE_IMAGE (WM_USER 101) BEGIN_MESSAGE_MAP(CCalibDlg, CDialogEx) ON_MESSAGE(WM_UPDATE_STATUS, CCalibDlg::OnUpdateStatus) ON_MESSAGE(WM_UPDATE_IMAGE, CCalibDlg::OnUpdateImage) END_MESSAGE_MAP()发送端在线程函数里简单调用::PostMessage(GetSafeHwnd(), WM_UPDATE_STATUS, 0, (LPARAM)strPtr)即可。这里有个坑PostMessage是把消息投递到UI线程的消息队列然后返回不会阻塞发送线程但接收端处理消息时要注意LPARAM指向的内存是否已经被工作线程释放。我习惯用new分配一个字符串对象接收端处理完再delete避免悬垂指针。还有一种做法是直接在工作线程里操作OpenCV的waitKey等窗口显示函数这确实能绕过MFC控件更新的繁琐逻辑但会导致画面刷新不稳定尤其在窗口拖动或最小化时容易崩溃。我测试下来老老实实用消息机制最稳。4.4 标定结果文件的序列化标定结果需要保存下来供后续程序使用。我设计了一个简单的配置结构体用CFile和CArchive做序列化struct CameraCalibResult { cv::Mat cameraMatrixL, cameraMatrixR; cv::Mat distCoeffsL, distCoeffsR; cv::Mat R, T, E, F; double rms; double baseline; cv::Size imageSize; };序列化时先写一个文件头标识版本号和数据类型再写矩阵数据。OpenCV的Mat可以直接写Mat::data的内容读取时先拿行列数和类型重建Mat再填充数据。这里提醒一个问题cv::Mat的data是按行连续存储的但通道数不同时内存布局完全不同。序列化时必须同时保存类型标识符CV_32FC1还是CV_64FC1等反序列化时用create方法重建Mat否则内存布局对不上读出来的矩阵就是乱的。保存文件时我把内参矩阵精确到小数点后6位、畸变系数精确到小数点后9位输出到文件另外附带生成一份纯文本版本方便用记事本直接查看或者导入到Matlab做进一步分析。基线和Q矩阵也一并保存这样离线调试视差转三维坐标时不需要重新跑标定。5. 常见问题与排查技巧实录5.1 角点检测失败的典型场景findChessboardCorners返回false是最常见的问题我总结了四个典型场景和应对方法棋盘格太小角点在图像里少于10个像素换成大棋盘格或者相机靠近棋盘。棋盘格倾斜过大透视畸变导致棋盘格在图像里看起来不是矩形减少倾角保持在30度以内。背景复杂有类似棋盘格的纹理干扰把棋盘格贴在不反光的黑色或深色底板上放大角点检测的搜索范围无效后就干脆换纯色背景。光照不均棋盘格部分过曝或过暗调整光源或者使用CALIB_CB_ADAPTIVE_THRESH CALIB_CB_NORMALIZE_IMAGE组合标志增强图像。这些标志位很有用cv::findChessboardCorners(image, boardSize, corners, cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_NORMALIZE_IMAGE | cv::CALIB_CB_FAST_CHECK);加了CALIB_CB_FAST_CHECK能快速过滤掉明显不含棋盘格的图像处理大批量数据时能省不少时间。5.2 极线校正效果的验证方法极线校正做得对不对不能只靠肉眼看图。我的快速验证方法是在校正后的左图上点一个特征点然后看这个点在右图的同一行附近能不能找到对应点。批量验证的做法是写一个小函数在左图上画几条水平线和右图并排对比看交叉点是否都在同一水平线上。更精确的做法是用findFundamentalMat计算基础矩阵统计点到极线的平均距离一般小于1像素就算校正合格。实测中我发现如果两幅图像之间存在垂直方向的偏差即不满足极线约束StereoSGBM匹配出来的视差图会出现大片的横向条纹噪声。排查到最后基本就是两个原因要么标定板的某几张图像拍歪了导致R、T不准要么校正时用的stereoRectify的alpha参数没调好导致图像被裁剪出垂直偏移。alpha设为0时图像会尽量保留全部像素但可能引入变形alpha设为1时图像会严格满足极线约束但边缘会被裁剪。我在测试程序里把alpha做成可调滑块方便对比效果。5.3 视差图出现大片黑色区域的排查清单视差图上大片黑色区域无效视差意味着匹配算法认为这些区域不可靠。常见原因有这么几类低纹理区域白色墙壁、空白桌面。这类区域任何匹配算法都没辙唯一的办法是引入结构光等主动照明把纹理投影到场景里。遮挡区域左图能看到但右图被前面的物体挡住。这种区域在真值上也应该是无效视差属于正常现象。重复纹理区域纹理周期性重复导致匹配二义性。调大uniquenessRatio可以缓解一部分。光照差异过大左右图亮度不一致导致匹配代价异常。检查左右相机曝光参数是否一致或者开启StereoBM的预处理归一化。调试阶段可以在界面上叠加显示原始左图和视差图的半透明混合效果快速定位哪些区域的视差不合理。这个功能我用得非常频繁比来回切换窗口高效多了。5.4 程序崩溃的常见原因与内存管理经验MFC和OpenCV混编崩溃问题十有八九出在内存管理上。最典型的几个Mat的生命周期问题。OpenCV的Mat是引用计数管理的浅拷贝在函数里创建的Mat返回后引用计数是共享的如果原数据被释放而另一处还在使用就会崩溃。避免方法是确保图像数据源比如采集回调里的临时缓冲区在Mat拷贝走之前不释放。CImage的Create失败。图像分辨率太大或者系统GDI资源耗尽时会Create失败返回false而代码里紧接着调用GetPixelAddress就会崩溃。稳妥的做法是每次绘图前检查CImage::IsNull。多线程里访问同一个Mat。工作线程在remapUI线程同时在Draw同一块内存看似没读写冲突其实很容易出问题。我的做法是在消息里传cv::Mat的深拷贝clone一份再PostMessage保证UI线程访问的数据在工作线程释放后依然有效。虽然费一点内存但稳定得多。6. 程序测试与效果评估实录6.1 实验环境与实际测试结果我自己测试用的环境两个USB相机分辨率1280x960镜头焦距约4mm基线约100mm。棋盘格标定板10x7格长30mm粘贴在铝板表面保证平整。采集25对图像标定结果左目重投影误差RMS约0.18像素右目约0.21像素双目RMS约0.24像素基线与实测钢尺量出的100.5mm基本吻合。这个精度对于一般视觉测量和定位项目已经够用了。立体匹配测试用的场景是一个桌面上的小盒子距离相机约500mm。用StereoSGBM默认参数跑视差图整体连续盒子轮廓清晰边缘有些许毛刺但经过中值滤波后效果不错。盒子上表面中心的深度大概在490到510mm之间波动考虑到USB相机没有硬件同步、物体在轻微晃动这个结果在可接受范围内。如果用StereoBM跑相同场景速度提升明显但视差图噪声大了不少。做一个直观对比指标StereoBMStereoSGBM耗时1280x960numDisparities64约45ms约260ms视差图质量噪声明显边缘粗平滑边缘较锐低纹理区域大量无效区域有改善但仍存在对光照不均的容忍度较差需预处理相对较好6.2 距离测量精度的验证与误差分析为了验证三维重建的准确性我在不同距离上放了一块靶标用程序测量靶标到相机的距离和激光测距仪的结果做比对。结果发现在400到1500mm的范围内程序测距值整体偏差基本在2%以内距离越近精度越高到了2000mm以上偏差开始明显增大。误差来源主要有几个一是标定板打印和粘贴的平整度误差二是棋盘格角点的亚像素精度限制三是视差图在物体边缘的低置信度区域影响。四是USB相机没有硬件同步两帧图像之间如果物体运动立体匹配会产生系统性误差。对于这个测试程序来说这个精度已经达到了设计预期。7. 程序后续扩展与个人使用心得7.1 可以做的功能扩展这套程序跑通后我在实际项目中陆续加了几个扩展读者可以参考把标定结果导出成YAML格式方便与其他工具链衔接。OpenCV的FileStorage天生支持YAML改起来很快。增加图像校正前后对比的动画播放功能。用MFC的SetTimer做帧切换能直观看到校正前后的几何变化。把深度图转成伪彩色显示。基于简单的高低位映射视差从近到远由红变蓝视觉效果直观方便快速判断深度关系。OpenCV里有applyColorMap可以直接用我早期是自己写的查表映射。如果要移植到OpenCV 3.x/4.x主要改动点有三个StereoSGBM构造函数改成了create工厂方法findChessboardCorners加入了CALIB_CB_EXHAUSTIVE和CALIB_CB_ACCURACY等新标志用于提升精度stereoRectify的返回值从int变成了void但行为一致。这些改动都是机械性的花半天时间就能完成迁移。7.2 复盘几点踩坑的经验写这个程序的过程里踩过的坑挑几个印象最深的分享第一个是OpenCV的stereoRectify在校正前必须保证左右图像是同一相机拍摄的同一时刻场景。我之前测试时用了一个相机先后拍两张图当双目图用结果极线校正完全失效匹配结果惨不忍睹。后来才知道双目标定的stereoCalibrate输入要求左右图像是严格配对采集的。第二个是棋盘格角点检测的世界坐标顺序必须和findChessboardCorners返回的顺序完全一致。OpenCV返回值是按照棋盘格的扫描顺序排列的先从左到右、再从上到下。我当时写objectPoints生成代码时顺序写反了结果标定出来的矩阵完全错误重投影误差巨大排查了很久才发现是顺序问题。第三个是MFC对话框程序退出时的内存释放。OpenCV的Mat本身是引用计数管理但MFC的CImage必须显式调用Destroy释放GDI资源。程序频繁切换图像显示时如果CImage创建后没及时销毁GDI句柄会耗尽导致绘图黑屏甚至系统崩溃。我在显示函数里统一先cimg.Destroy()再cimg.Create()并且封装了一个Cleanup函数在对话框销毁时调用。7.3 关于这套程序框架的一些个人体会这套程序从最初当学习笔记用的小工具后来变成我多个项目里反复使用的算法验证平台核心原因是可视化和可调参这两件事做对了。做视觉算法的工程师都会有这种感受光看算法在控制台跑出数值很难判断对不对眼见为实的图像反馈能让人一下子看出问题出在哪一步。如果你正在做类似的工具我的建议是不要追求大而全先把标定过程做扎实把重投影误差、极线校正效果、视差图质量这三个关键节点的可视化做好再考虑扩展功能。一次踩透一个环节比面包屑一样铺一堆功能有用得多。我在这个测试程序上花了大半个月但这套框架在后续的两三个实际项目中直接复用甚至只是换个接口就能跑节省下来的时间远超当时投入的成本。
返回列表