
简介这份资源是一份基于OpenCV的双目视觉立体标定与校正代码文档面向已具备一定C与计算机视觉基础、正在做双目相机标定实验或三维重建项目的开发者和学生解决从棋盘格图像采集到参数估计、图像校正的完整流程实现问题。压缩包共1个文件为doc文档约294KB内容以源码分析为主线包含StereoCalib函数的实现讲解与关键代码片段。文档围绕cv::stereoCalibrate完成内外参估计借助cv::undistort去除镜头畸变并用cv::stereoRectify做立体校正同时说明了标定flag的取值含义、棋盘格角点数与方格尺寸等输入参数的设置方式以及标定结果写入文件的处理思路。读者可据此理解双目标定的整体链路将示例代码迁移到自己的相机型号与采集环境中减少在参数配置和接口调用上的试错成本。目前已有2595人学习适合作为双目视觉入门到实践的参考材料。1. 一对摄像头摆上支架之后先别急着开 SGBM两个摄像头拧上铝型材很多人第一反应是直接跑 SGBM 看视差图结果参数调了两天还是满屏麻点——问题八成不在匹配算法而在前面的双目视觉立体标定与校正没做干净。这份基于 OpenCV 的双目立体标定与校正代码干的就是这件事读一组棋盘格图跑cvStereoCalibrate拿到双目内参和外参再用cvStereoRectify或cvStereoRectifyUncalibrated算校正映射cvRemap之后左右图的行就对齐了顺手还能出一张 BM 视差图。它出自经典教材的配套示例C 接口写成代码不长但两套校正路线、标定质量自检、StereoBM 参数一个不缺。刚搭好硬件想先跑通基线的人和匹配调到怀疑标定的人都值得先把它啃一遍。2. 立体标定的数学模型内参、外参、本征矩阵和基础矩阵怎么串起来2.1 为什么左右各标一次再拼起来不行单目标定做的事是让一个相机知道自己镜头的焦距、主点和畸变双目要做的事是让两个相机知道彼此站着的位置差。看着只差一步坐标系却完全不同。左右相机各自标定时棋盘格在左视图坐标系里有一套 R1、t1在右视图坐标系里有一套 R2、t2它们描述的是「棋盘相对相机」而两次拍摄时棋盘可以摆在任意位置——这两个 R 之间没有任何可比性硬拼出来的基线是错的。正解是让同一块棋盘同时出现在左右视野里把「同一组三维物点、两套二维像点」一起丢进优化器让左右相机的内外参在同一个重投影误差下联合求解。这样输出的R和T才有物理意义T的模就是基线长度T的 x 分量水平双目时近似就是两台相机的光心间距。这个数字直接决定深度精度Z f·B/d基线 B 差 5%十米外的深度就偏 5%。所以squareSize这种看着不起眼的量纲参数后面会反复咬人。源码里的cvStereoCalibrate调用就是把这件事一次做完既输出 M1、D1、M2、D2也输出 R、T、E、F。E 是本征矩阵F 是基础矩阵两者关系是E T× · R、F M2⁻ᵀ · E · M1⁻¹。E 在归一化坐标下描述对极约束F 在像素坐标下描述后者才是能直接拿像素点验证的那个。2.2cvStereoCalibrate的十五个参数逐个拆这个函数参数多到反人类但只要按「输入点 → 输出内参 → 输出外参 → 优化控制」四段去读就不乱了。序号参数方向说明与常见取值1objectPoints入N×3 物点N 帧数 × 每帧角点数单位由 squareSize 决定2imagePoints1入左图角点与物点一一对应3imagePoints2入右图角点顺序必须与左图完全一致4npoints入每帧点数本代码全部填 nx*ny5、7M1、M2入/出3×3 内参可以给初值也可以给单位阵让优化器自己找6、8D1、D2入/出畸变系数这里只用了 5 个k1 k2 p1 p2 k39imageSize入像素尺寸只用于细化内参不影响外参10、11R、T出右相机相对左相机的旋转与平移12、13E、F出本征矩阵、基础矩阵14criteria入通常cvTermCriteria(CV_TERMCRIT_ITERCV_TERMCRIT_EPS, 100, 1e-5)15flags入约束项下一节细说源码里的调用长这样我补上了返回值判断// 立体标定一次求解左右内参 相对位姿 CvMat _objectPoints cvMat(1, N, CV_32FC3, objectPoints[0]); CvMat _imagePoints1 cvMat(1, N, CV_32FC2, points[0][0]); CvMat _imagePoints2 cvMat(1, N, CV_32FC2, points[1][0]); CvMat _npoints cvMat(1, npoints.size(), CV_32S, npoints[0]); cvSetIdentity(_M1); // 内参初值单位阵 cvSetIdentity(_M2); cvZero(_D1); // 畸变初值全零 cvZero(_D2); int ok cvStereoCalibrate( _objectPoints, _imagePoints1, _imagePoints2, _npoints, _M1, _D1, _M2, _D2, // 输出内参与畸变 imageSize, // 仅用于内参细化 _R, _T, _E, _F, // 输出相对位姿与 E、F cvTermCriteria(CV_TERMCRIT_ITER CV_TERMCRIT_EPS, 100, 1e-5), CV_CALIB_FIX_ASPECT_RATIO CV_CALIB_ZERO_TANGENT_DIST CV_CALIB_SAME_FOCAL_LENGTH); if (ok 0) { fprintf(stderr, stereo calibrate failed: check correspondences\n); }这里的逻辑顺序值得说清楚先给初值再求解最后判返回值。内参初值给单位阵不是偷懒而是让优化器从「一个焦距等于 1 的针孔模型」起步靠 LM 迭代收敛到真实值如果给了一组离谱的初值比如手填了 5000反而容易在错误的局部极小值停下。返回值 0 表示求解失败最常见的原因是有效棋盘帧数太少、或者所有角点近似共线此时不会抛异常只会静默给你一堆垃圾数字所以这个判断别省。参数里最容易被忽略的是imageSize。源码取的是最后一帧图像的尺寸如果这组图里混进了不同分辨率的照片这个值就是错的内参细化会偏。我一般会在读图的循环里加一句尺寸一致性检查不一致直接报错退出比后面查半天强。2.3 三个标定 flag 的取舍什么时候该放什么时候该收源码用的组合是CV_CALIB_FIX_ASPECT_RATIO CV_CALIB_ZERO_TANGENT_DIST CV_CALIB_SAME_FOCAL_LENGTH这三个不是随便挑的。CV_CALIB_FIX_ASPECT_RATIO把 fy/fx 锁成一个自由参数等于承认「像素是方的」。绝大多数工业相机和 USB 相机确实如此锁上能少一个自由度在小样本十几帧时明显更稳。但如果相机用了非方形像素或者做过裁剪缩放这条就得去掉否则标出来的焦距看上去很漂亮实际重投影误差会异常偏大。CV_CALIB_ZERO_TANGENT_DIST强制切向畸变 p1、p2 为 0。镜片装配良好的相机切向畸变确实接近零锁掉它能防止这两个参数在噪声里乱跑——这是最常见的翻车点之一不锁的时候某些数据集会解出 p1 0.03 这种量级然后你拿它去畸变边角直接被拽歪。但如果镜头明显歪比如拆机重装的模组锁死反而掩盖问题这时应该放开同时把帧数堆到 30 以上。CV_CALIB_SAME_FOCAL_LENGTH假设左右相机焦距完全相同。同一批次同一型号的模组通常成立但如果你手上是两只来源不同的镜头这条必须去掉否则优化器会强行让两边妥协误差全压到主点上。判断标准不玄学跑一遍看avg err把某个 flag 放开再跑一遍如果误差没降反而参数跳变得离谱说明这条约束对你的硬件是成立的留着。2.4 用极线误差自检那个avg err到底在算什么标定跑完不能直接信得验。源码里的做法很地道先把角点用cvUndistortPoints拉到去畸变空间再用cvComputeCorrespondEpilines分别算出左右图每个点对应的极线最后把点代进直线方程取绝对值累加。// 去畸变后再算极线误差避免畸变污染判断 cvUndistortPoints(_imagePoints1, _imagePoints1, _M1, _D1, 0, _M1); cvUndistortPoints(_imagePoints2, _imagePoints2, _M2, _D2, 0, _M2); cvComputeCorrespondEpilines(_imagePoints1, 1, _F, _L1); cvComputeCorrespondEpilines(_imagePoints2, 2, _F, _L2); double avgErr 0; for (int i 0; i N; i) { double err fabs(points[0][i].x * lines[1][i].x points[0][i].y * lines[1][i].y lines[1][i].z) fabs(points[1][i].x * lines[0][i].x points[1][i].y * lines[0][i].y lines[0][i].z); avgErr err; } printf(avg err %g\n, avgErr / (nframes * n));这段的关键在于「先去畸变」。畸变会让极线弯成曲线而F描述的是针孔模型下的直线约束不先去畸变误差里混的是畸变量而不是标定质量数字没有参考价值。第二个注意点是这个avg err是代数误差不是点到直线的像素距离——代码没有除以sqrt(a²b²)所以严格来说它偏大一点量纲也不完全是像素。作为相对指标够用同一批数据同一套参数误差从 0.6 降到 0.3 说明改进有效但要跟别人比绝对值就没意义了想比就自己补上归一化。我的经验阈值1280×720 的图avg err在 0.20.5 属于正常0.51.0 要看看是不是某几帧拖后腿超过 1.5 基本可以判断角点提取有错配或者棋盘不平。想定位是哪一帧把每帧的误差单独打印出来排序最差的那几帧拉出来看图十有八九能看见反光、运动模糊或者棋盘被遮挡。3. 角点提取与列表组织源码里那几个数组的来龙去脉3.1ch12_list.txt的格式约定与左右交替读取这份代码没有用目录遍历而是要求你提供一个纯文本列表一行一个图片路径左右图交替排列——第一行左、第二行右、第三行左、第四行右。源码里靠lr i % 2判断当前行属于哪一路#开头的行跳过行尾空白会被裁掉。手工维护这个文件很容易错位我一般用脚本生成顺便做一次「左右图能否配对」的检查import os left_dir, right_dir stereo/left, stereo/right out [] lefts sorted(f for f in os.listdir(left_dir) if f.lower().endswith((.png, .jpg, .bmp))) for name in lefts: lpath os.path.abspath(os.path.join(left_dir, name)) rpath os.path.abspath(os.path.join(right_dir, name)) if not os.path.exists(rpath): # 缺右图直接报出来别静默跳过 print(missing pair:, name) continue out.append(lpath) out.append(rpath) # 严格左、右交替 with open(ch12_list.txt, w, encodingutf-8) as f: f.write(\n.join(out) \n) print(pairs:, len(out) // 2)逻辑说明sorted保证左右两侧按同一顺序取配对靠同名文件先写左再写右这个顺序不能反源码的i % 2是死约定反了整个标定会算出一个横向基线的镜像值误差看着还不大装到设备上才发现左右颠倒。参数说明如果采集时左右图用了不同前缀比如L_001.png/R_001.png把生成 rpath 的那行换成相应的重命名规则即可。还有两个细节路径里不要带空格fgets加cvLoadImage这条链路处理空格路径虽说不至于崩但 Windows 下的反斜杠容易在转义上出问题统一用正斜杠最省事。3.2 角点检测、放大重试与亚像素精化cvFindChessboardCorners在低分辨率、大畸变或者边缘虚焦的图上经常返回 0 个角点。源码的处理是加了一个maxScale的重试机制先原图检测失败就放大再试。虽然这里maxScale被设成 1等于没开但机制是现成的把它改成 2 或 3 能救回不少边缘帧。// 角点检测 亚像素精化 int count 0, found 0; for (int s 1; s maxScale; s) { IplImage* timg img; if (s 1) { // 放大重试 timg cvCreateImage(cvSize(img-width * s, img-height * s), img-depth, img-nChannels); cvResize(img, timg, CV_INTER_CUBIC); } found cvFindChessboardCorners( timg, cvSize(nx, ny), temp[0], count, CV_CALIB_CB_ADAPTIVE_THRESH | CV_CALIB_CB_NORMALIZE_IMAGE); if (timg ! img) cvReleaseImage(timg); if (found || s maxScale) for (int j 0; j count; j) { // 坐标还原回原尺度 temp[j].x / s; temp[j].y / s; } if (found) break; } if (found) { // 亚像素精化窗口 11x11迭代 30 次或精度 0.01 cvFindCornerSubPix(img, temp[0], count, cvSize(11, 11), cvSize(-1, -1), cvTermCriteria(CV_TERMCRIT_ITER CV_TERMCRIT_EPS, 30, 0.01)); }关键点在于坐标还原放大 s 倍检测出来的角点坐标是放大图上的必须除以 s 才能在原图上用源码在循环里做了这件事。第二个关键点是cvFindChessboardCorners只给整像素粗定位cvFindCornerSubPix才把它精化到亚像素。源码注释里那句 Calibration will suffer without subpixel interpolation 不是客套话——不做亚像素重投影误差基本卡在 0.5 像素往上做了能压到 0.1 量级。参数上亚像素窗口cvSize(11, 11)对应 640×480 到 1280×720 这一档比较合适如果图是 4K 的棋盘格在画面里占比很小11 像素窗口可能连一个格子的边都跨不过去得放大到 15 或 21。终止条件30 次 / 0.01 像素是稳妥值迭代次数给太小比如 10在低对比度图上会提前停。3.3 三维物点怎么造i和j的顺序别搞反物点列表是「假装棋盘就摆在某个已知位置」造出来的理想坐标。源码这段只有五行但错一个下标就全废objectPoints.resize(nframes * n); for (int i 0; i ny; i) // 注意外层是 ny for (int j 0; j nx; j) // 内层是 nx objectPoints[i * nx j] cvPoint3D32f(i * squareSize, j * squareSize, 0); for (int i 1; i nframes; i) // 其余帧复制第一帧 copy(objectPoints.begin(), objectPoints.begin() n, objectPoints.begin() i * n);索引是i * nx j坐标却写成(i * squareSize, j * squareSize)——也就是行方向给了 x、列方向给了 y跟常见的「x 走列、y 走行」相反。这算 bug 吗在这个场景里不算棋盘是平面规则网格x、y 轴互换等价于整块板子绕 z 轴转 90°只要左右两路用的是同一套物点联合优化出来的 R、T 仍然正确只是坐标系的定义换了。真正致命的是另一件事物点的排列顺序必须和cvFindChessboardCorners返回角点的顺序一一对应。那个函数是按行扫描从左到右返回的所以物点也必须按「第 0 行所有列、第 1 行所有列」这样排。顺序一乱优化器会试图把棋盘扭曲成一个不存在的形状现象是avg err直接飙到几十但程序一声不吭地跑完了。squareSize在源码里是1.f意思是「一格 1 个单位」。这个设定对标定内参没影响内参是像素单位与物点尺度解耦但会让 T 变成「多少格」比如 T [-3.2, 0.05, 0.1] 表示基线约 3.2 格。后面要拿 T 算真实基线长度或者做尺度恢复就必须把它换回毫米。我的习惯是直接把squareSize设成实测格长比如 25.0 表示 25mm一次到位省得后面换算时把单位丢了。3.4active[]和npoints成功与失败帧的记录方式源码里有个容易被忽略的设计active[lr]记录每帧是否检测成功npoints记录每帧的点数。这是为「部分帧检测失败」准备的——理论上你可以在后面只挑成功的帧参与标定。但源码实际用的nframes active[0].size()只看了左路然后npoints.resize(nframes, n)全部填满n等于默认左右两路帧数和成功情况完全一致。这是个隐患第 5 章会展开。我一般会把这个逻辑改成显式配对只有左右同一帧都检测成功才把这一对推进物点列表否则整对丢弃。宁可少几帧也不要让左右点的对应关系错位。4. 两条校正路线Bouguet 和 Hartley 分别适合谁4.1 Bouguet有内参就走这条映射表一次算好反复用useUncalibrated 0走的是标定校正也就是 Bouguet 方法。核心是两个函数接力cvStereoRectify算出左右各一个 3×3 旋转矩阵 R1、R2 和两个 3×4 投影矩阵 P1、P2把两个成像平面「掰」到共面且行对齐cvInitUndistortRectifyMap把内参、畸变、R、P 揉成两张映射表 mx/my之后每帧只需cvRemap查表速度很快。CvMat _R1 cvMat(3, 3, CV_64F, R1); CvMat _R2 cvMat(3, 3, CV_64F, R2); CvMat _P1 cvMat(3, 4, CV_64F, P1); CvMat _P2 cvMat(3, 4, CV_64F, P2); // alpha0只保留有效像素flags 给 0 表示不强制零视差 cvStereoRectify(_M1, _M2, _D1, _D2, imageSize, _R, _T, _R1, _R2, _P1, _P2, 0, 0); // 判断是不是上下堆叠的双目 isVerticalStereo fabs(P2[1][3]) fabs(P2[0][3]); // 预计算映射表后续每帧复用 cvInitUndistortRectifyMap(_M1, _D1, _R1, _P1, mx1, my1); cvInitUndistortRectifyMap(_M2, _D2, _R2, _P2, mx2, my2);cvStereoRectify的第六个参数是alpha源码传 0含义是「裁剪掉所有黑边只留两图共有的有效区域」。传 1 则保留全部原始像素但会引入大片黑边做视差时黑边区域没有匹配值。做测量类应用我倾向 0做可视化倾向 1。flags传 0 而不是CV_CALIB_ZERO_DISPARITY意味着不强制把主点对齐到同一行好处是保留更多有效像素代价是最小视差可能不为零minDisparity得相应设成负值——源码里minDisparity -64就是在兜这个底。如果你希望视差从 0 开始、方便直接映射成深度就把 flag 加上同时把minDisparity改回 0。isVerticalStereo那句判断很巧P2的第 4 列是右相机光心在左相机坐标系下的投影若 y 分量比 x 分量大说明两台相机是上下装的。这个标志后面控制拼接显示方向和是否允许做 BM 匹配别删。4.2 Hartley没有内参也能救但要接受精度损失useUncalibrated 1 或 2走的是非标定校正。顾名思义不需要事先知道内参只靠左右匹配点算出的基础矩阵 F直接求两个单应 H1、H2 把极线掰平行。 2的区别仅仅是在内部重新调一次cvFindFundamentalMat 1则用cvStereoCalibrate输出的 F。double H1[3][3], H2[3][3], iM[3][3]; CvMat _H1 cvMat(3, 3, CV_64F, H1); CvMat _H2 cvMat(3, 3, CV_64F, H2); CvMat _iM cvMat(3, 3, CV_64F, iM); if (useUncalibrated 2) cvFindFundamentalMat(_imagePoints1, _imagePoints2, _F); cvStereoRectifyUncalibrated(_imagePoints1, _imagePoints2, _F, imageSize, _H1, _H2, 3); // H 是在像素坐标下的单应转成旋转矩阵形式 R M^-1 * H * M cvInvert(_M1, _iM); cvMatMul(_H1, _M1, _R1); cvMatMul(_iM, _R1, _R1); cvInvert(_M2, _iM); cvMatMul(_H2, _M2, _R2); cvMatMul(_iM, _R2, _R2);这段推导值得看懂H作用在像素坐标上而cvInitUndistortRectifyMap要的是作用在归一化坐标上的旋转矩阵所以要做一次相似变换R M⁻¹ · H · M。源码用两次cvMatMul实现中间那次cvMatMul(_H1, _M1, _R1)的结果直接覆盖进_R1再左乘M⁻¹顺序不能反反了矩阵就不是旋转了remap 出来的图会明显扭曲。cvStereoRectifyUncalibrated的最后一个参数3是极线距离阈值用于把匹配点分成内点和外点。点质量差的时候可以放宽到 5 甚至 8但放太宽会让 F 被少量错误匹配带偏。什么时候用 Hartley我的判断是手上只有一对相机、没有任何标定板、又必须先把视差跑起来做算法验证的时候。它能用但输出的不是真正的行对准而是「极线大致平行」垂直方向仍残留视差做 BM/SGBM 时匹配窗口跨行搜索会显著掉精度。真要测量还是老老实实回到 Bouguet。顺带说一句这份源码里的一个笔误Hartley 分支最后两行是cvInitUndistortRectifyMap(_M2, _D1, _R2, _M2, mx2, my2); // 注意这里传的是 _D1右相机用左相机的畸变系数去建映射表畸变差异大时右图会被拉歪。正确写法应该是_D2。这类错误在用同一型号相机的实验里几乎看不出来换镜头就暴露了。4.3 校正完还得看视差StereoBM 参数怎么定源码在 remap 之后直接接了一个 BM 匹配参数是写死的参数源码取值作用与调整方向preFilterSize41预滤波窗口越大越平滑但吃细节一般 521preFilterCap31预滤波输出截断控制在 31 附近即可SADWindowSize41匹配代价窗口越大越稳但边缘越糊41 偏大minDisparity-64最小视差允许负值以兼容未强制零视差numberOfDisparities128视差搜索范围必须是 16 的整数倍textureThreshold10纹理阈值低于此值的区域直接判无效uniquenessRatio15唯一性比率越大越严格515 常见41 的 SAD 窗口放到 1280×720 的图上偏大边缘会被啃掉一大圈我一般从 15 或 21 起步看噪点情况再往上加。numberOfDisparities是最容易设错的一个它决定了能测多近的物体d_max B·f / Z_min基线 60mm、焦距 700 像素、想测最近 0.5m就需要d_max 84取 96 或 128 都行取 16 就完全测不到近处。CvStereoBMState* BMState cvCreateStereoBMState(); BMState-preFilterSize 41; BMState-preFilterCap 31; BMState-SADWindowSize 41; BMState-minDisparity -64; BMState-numberOfDisparities 128; BMState-textureThreshold 10; BMState-uniquenessRatio 15; cvFindStereoCorrespondenceBM(img1r, img2r, disp, BMState); cvNormalize(disp, vdisp, 0, 256, CV_MINMAX); // 16S - 8U 便于显示 cvShowImage(disparity, vdisp); cvReleaseStereoBMState(BMState); // 别忘了释放cvFindStereoCorrespondenceBM的输入必须是已经 remap 过的灰度图顺序是左、右、输出视差。输出disp是 16 位有符号单位是像素的 1/16OpenCV 的定点表示直接当像素用会小 16 倍。源码用cvNormalize(..., 0, 256, CV_MINMAX)映射到 8 位只是为了显示如果后面要算深度得用原始disp除以 16 再乘1/16——这个换算关系是 BM 的一个经典坑很多人第一次看到视差图全是黑的就是这里没除。4.4 C 接口到 C 接口的对照这份代码是 2008 年前后的 C 接口写法现在新项目基本都是cv::命名空间。对照关系整理如下迁移时逐个替换即可。C 接口C 接口cvFindChessboardCornerscv::findChessboardCornerscvFindCornerSubPixcv::cornerSubPixcvStereoCalibratecv::stereoCalibratecvStereoRectifycv::stereoRectifycvStereoRectifyUncalibratedcv::stereoRectifyUncalibratedcvInitUndistortRectifyMapcv::initUndistortRectifyMapcvRemapcv::remapcvCreateStereoBMStatecvFindStereoCorrespondenceBMcv::StereoBM::createcomputeIplImage*cv::Mat迁移时有一处语义差异要注意C 接口的cvStereoCalibrate接收CvMat*矩阵维度靠cvMat(1, N, ...)显式声明C 接口接收std::vectorcv::Point3f和std::vectorcv::Point2f点集组织方式变成「每帧一个 vector」外层再套一个 vector。别直接把扁平数组塞进去维度对不上会抛断言。5. 避坑清单那些让avg err从 0.3 飙到 5 的细节5.1 现象左右检测成功数不同程序不报错但标定结果离谱现象很隐蔽——程序正常跑完avg err也能打出来但数字大得离谱或者T的模长跟实测基线差一大截。原因是源码用nframes active[0].size()只统计了左路的帧数npoints全部填n而points[1]的长度取决于右路实际检测成功多少帧。一旦右路少了几帧两边点集长度就不一致优化器会把错位的点当成对应点去拟合解出来的东西自然没法看。解决办法很直接在读图循环结束后对左右两路的active逐帧做与运算只保留两边都成功的那几帧重建objectPoints、points[0]、points[1]。宁可丢帧不能错位。5.2 现象列表文件里有一行路径写错整个标定直接中断if (!img) break;这句话的意思是「读不到图就跳出整个循环」而不是跳过这一帧。列表里只要有一张图被删了、路径有中文、或者大小写不对程序会在那一行悄无声息地结束后面所有帧都白采。改法是把break换成continue并打印警告同时在循环外用计数器记录实际读入的对数低于 10 对就拒绝标定。另外文件夹路径尽量用全英文虽然 OpenCV 在多数平台能处理中文路径但fgets读进来的字符串编码和cvLoadImage期望的编码未必一致与其赌不如避开。5.3 现象棋盘格检测总返回 0换了角度也不行先确认nx、ny填的是内角点数而不是方格数。一块 10×7 格的棋盘内角点是 9×6——源码里StereoCalib(ch12_list.txt, 9, 6, 1)就是 9 列 6 行内角点。填成 10 和 7 的话函数永远找不到。再确认行列顺序cvSize(nx, ny)里 nx 是列方向水平角点数。如果棋盘在画面里是竖着放的nx 和 ny 要交换。判断方法是把displayCorners打开加一个窗口把检测到的角点画出来一眼就知道是数量不对还是顺序不对。此外棋盘格必须留白边——四周没有白边的棋盘外圈角点检测会不稳定这是打印时最容易忽略的一条。5.4 现象标定能跑但 remap 之后左右图行对不齐先看squareSize。设成 1.f 的时候T 的单位是「格」如果你后面拿 T 去和实测基线比对会发现数字对不上从而误以为标定错了。真正的行对齐问题和这个无关但要先把量纲排除掉。真正的常见原因是畸变没去干净。Bouguet 路线里cvInitUndistortRectifyMap同时做了去畸变和旋转理论上一步到位但如果标定阶段用了CV_CALIB_ZERO_TANGENT_DIST而镜头实际有明显切向畸变去畸变就是不完整的残差会体现在行方向上。验证方法是拿几张标定集之外的图remap 之后在左右图上画同一水平线看同一物理点是否落在同一行——差超过 2 像素就得回头放开切向畸变重标。还有一个只在上下堆叠双目上出现的问题源码注释里写得很清楚垂直双目且用标定校正时图像不会被转置极线是竖直的而 BM 匹配只支持水平极线。这时要么把相机转 90 度装要么在校正后手动转置图像再匹配。5.5 现象视差图整片发黑或者全是噪点整片发黑八成是numberOfDisparities太小或者minDisparity设错真实视差落在搜索范围之外全部被判无效。把disp归一化前后的最大值打印出来看看如果最大值恰好等于numberOfDisparities说明搜索范围被撑满了得往上加。全是噪点则通常是纹理问题白墙、纯色桌面这种低纹理场景textureThreshold 10会把大片区域直接判无效这其实是正确行为不是 bug。真要在这类场景出图就得换 SGBM 或者加结构光/投影纹理BM 本身救不了。6. 把标定结果落盘并做一次闭环验证标定跑通只是第一步真正上线要把 M1、D1、M2、D2、R、T、R1、R2、P1、P2 全部存下来否则每次启动都重标一遍既慢又不可复现。用cv::FileStorage存成 YAML读写都方便// 保存一次写全避免上线时缺参数 cv::FileStorage fs(stereo_calib.yml, cv::FileStorage::WRITE); fs image_width imageSize.width; fs image_height imageSize.height; fs M1 cv::Mat(3, 3, CV_64F, M1) D1 cv::Mat(1, 5, CV_64F, D1); fs M2 cv::Mat(3, 3, CV_64F, M2) D2 cv::Mat(1, 5, CV_64F, D2); fs R cv::Mat(3, 3, CV_64F, R) T cv::Mat(3, 1, CV_64F, T); fs R1 cv::Mat(3, 3, CV_64F, R1) R2 cv::Mat(3, 3, CV_64F, R2); fs P1 cv::Mat(3, 4, CV_64F, P1) P2 cv::Mat(3, 4, CV_64F, P2); fs square_size_mm squareSize; // 量纲一起存防止后人猜 fs.release(); // 加载尺寸不匹配就直接拒绝别硬算 cv::FileStorage in(stereo_calib.yml, cv::FileStorage::READ); int w, h; in[image_width] w; in[image_height] h; if (w ! frame.cols || h ! frame.rows) { throw std::runtime_error(calib resolution mismatch); }存的时候把square_size_mm一起写进去是我踩过坑之后加的习惯半年前标的一套参数回头要用时死活想不起T的单位是格还是毫米只能重标。加载时校验分辨率同理同一套参数拿去处理不同分辨率的图initUndistortRectifyMap不会报错只是默默给你一张歪的图。闭环验证我一般做两件事。一是留出法采集时预留 5 对标定集之外的图用保存的参数跑一遍完整的 remap然后在左右图上叠加等间距水平线肉眼看同一特征点是否在同一行差 12 像素可以接受超过就得回头查。二是重投影拿标定集里的角点用 R、T、M、D 反投影回图像算 RMS 误差跟当初的avg err对得上说明参数落盘没写错位。还有个小技巧把numberOfDisparities做成可配置项按B·f/Z_min估出来再取 16 的整数倍比拍脑袋填 128 靠谱。做完这套同一套硬件换个场景重采一组图重标十几分钟就能走完。从那以后我每次标完双目都会强制走一遍「存盘 → 重加载 → 预留图 remap 画线」这三步哪怕当时急着出结果也不跳——跳过的那几次无一例外都在一周后以「视差错得莫名其妙」的形式找回来。希望帮到你。本文还有配套的精品资源点击获取