双目视觉深度测量全流程:标定、立体匹配与三角测量实战
简介面向计算机视觉学习与开发者的双目立体视觉工程包聚焦OpenCV环境下的相机标定、立体匹配与三角测量适用于三维重建、深度估计等场景。压缩包共40个文件主体为21个C源码与5个头文件覆盖SIFT、Harris、Fast等特征检测以及模板匹配、颜色提取、霍夫直线与椭圆检测等算法另有7个Markdown说明文档、双目标定与图像采集工具并附赠PDF资料整体仅215KB。目录按功能拆分包含特征提取、图像处理、标定与重建等模块便于对照源码理解完整流程。已有84人浏览学习。读者可直接提取双目标定与图像采集相关源码搭建标定和匹配环境并基于三角测量示例生成三维点云适合希望从理论走向实践的多视几何入门者。1. 为什么说“标定立体匹配三角测量”是一套必须整体落地的深度测量方案做工业尺寸测量、零件抓取、定位导航的时候很多人第一反应是直接拿基于OpenCV的双目相机标定与立体匹配来试跑通一个 demo 就以为能交货。真正动手后才发现双目视觉的核心不是“调库”而是把相机标定、立体匹配、三角测量三段串成一个误差可控的链路。这套方法用 C 语言封装成跨平台的高精度三角测量系统从双目图像里提取深度信息、构建三维点云听起来不复杂但如果每段的边界条件没摸清楚出来的距离数据会忽近忽远根本没法用。我见过不少开发者困在“标定看着成功了点云却是一团糟”的状态里。这篇文章适合手里有一对 USB 相机或工业相机、准备做三维重建或测距的人。我会按双目标定、立体匹配、三角测量、点云构建的顺序展开最后用五个实际踩过的坑和一套验证方法收尾。你不需要有深厚的矩阵基础只需要能跑通 OpenCV 的 C 示例再把我的封装思路换成自己的工程结构。2. 双目标定拿到准确内外参是一切三维重建的地基双目标定的本质是把左右相机当成一个“会计算深度的整体”。单目标定只解决单个相机自身的投影关系双目标定额外求出了左右相机之间的旋转矩阵 R 和平移向量 T。后续做立体校正、立体匹配、三角测量全部依赖这一组 R 和 T。很多人在这一步只看了重投影误差就收工这是不够的——R 和 T 里的基线长度如果错了 1 毫米5 米外的深度误差会放大成几十毫米。2.1 针孔模型与畸变模型先把坐标系和数学关系讲清楚相机成像用针孔模型表达空间点经过外参转到相机坐标系再通过内参矩阵 K [[fx, 0, cx], [0, fy, cy], [0, 0, 1]] 投影到像素平面。fx、fy 一般不同cx、cy 是主点坐标。真实镜头会有径向畸变 k1, k2, k3 和切向畸变 p1, p2畸变公式在 OpenCV 文档里写得很清楚我不重复推导。你要记住的是标定就是反解出 K、畸变系数以及左右相机之间的 R 和 T。立体视觉里最常用的坐标系链是“世界—左相机—右相机”。通常把左相机坐标系当作测量基准右相机的 R 和 T 描述的是“右相机相对左相机的姿态”。三角测量代码里传入的投影矩阵 P1、P2就是由左相机内参、畸变校正后的内参以及 R、T 推导出来的。调试时如果左右目的主点 cx 差了几个像素不用奇怪这是镜头装配导致的标定会把这部分误差吸收进外参里。你需要做的是保证 R、T 的方向语义从头到尾一致否则后面很容易出现深度正负翻转。2.2 双目标定的 C 语言封装一个纯 C 头文件把细节藏住标题里强调“C 实现跨平台”这里先说明一个现实OpenCV 的 C 接口稳定好用老版本的纯 C 接口虽然还能找到但维护成本高。我的做法是用 C 写 OpenCV 核心逻辑再用 extern C 包装一层对外只暴露 C 函数。上层业务、灰度处理、点云管理都可以写纯 C跨平台编译时只要链接 OpenCV 和 C 标准库就行。这个结构我沿用很久了比硬抠老 C 接口省心得多。先用纯 C 头文件定义接口/* stereo_calib.h 双目标定C接口 */ #ifndef STEREO_CALIB_H #define STEREO_CALIB_H #ifdef __cplusplus extern C { #endif /* 从左右图像文件序列执行双目标定 * left/right_files: 图像路径数组count: 有效对数 * board_w/board_h: 内角点数量square_size: 棋盘格边长(mm) * 成功后R3x3旋转矩阵T3x1平移向量(单位mm) */ int stereo_calibrate_from_files(const char** left_files, const char** right_files, int count, int board_w, int board_h, double square_size, double R[9], double T[3]); #ifdef __cplusplus } #endif #endif这个头文件不依赖任何 OpenCV 头文件纯 C 模块可以直接包含它不会被 C 类型拖累。函数返回 0 表示成功非 0 表示有效图像对不足或标定失败。调用方只需要准备左右图像路径数组不需要关心 OpenCV 内部是怎么找角点、怎么解方程的。对应的 C 实现里关键逻辑是找角点和调 stereoCalibrate// stereo_calibrate.cpp #include stereo_calib.h #include opencv2/opencv.hpp #include vector int stereo_calibrate_from_files(const char** left_files, const char** right_files, int count, int board_w, int board_h, double square_size, double R[9], double T[3]) { cv::Size board(board_w, board_h); std::vectorcv::Point3f obj; for (int i 0; i board_h; i) for (int j 0; j board_w; j) obj.push_back(cv::Point3f(j * square_size, i * square_size, 0.f)); std::vectorstd::vectorcv::Point3f obj_pts; std::vectorstd::vectorcv::Point2f l_pts, r_pts; for (int i 0; i count; i) { cv::Mat l cv::imread(left_files[i], cv::IMREAD_GRAYSCALE); cv::Mat r cv::imread(right_files[i], cv::IMREAD_GRAYSCALE); std::vectorcv::Point2f lc, rc; bool ok_l cv::findChessboardCorners(l, board, lc); bool ok_r cv::findChessboardCorners(r, board, rc); if (!ok_l || !ok_r) continue; // 一对没检测全就跳过 cv::cornerSubPix(l, lc, cv::Size(5,5), cv::Size(-1,-1), cv::TermCriteria(cv::TermCriteria::EPScv::TermCriteria::COUNT, 30, 0.01)); cv::cornerSubPix(r, rc, cv::Size(5,5), cv::Size(-1,-1), cv::TermCriteria(cv::TermCriteria::EPScv::TermCriteria::COUNT, 30, 0.01)); obj_pts.push_back(obj); l_pts.push_back(lc); r_pts.push_back(rc); } cv::Mat lK, rK, lD, rD, Rm, Tm, E, F; double rms cv::stereoCalibrate(obj_pts, l_pts, r_pts, lK, lD, rK, rD, cv::Size(1280, 720), Rm, Tm, E, F, cv::CALIB_FIX_ASPECT_RATIO | cv::CALIB_ZERO_TANGENT_DIST); for (int i 0; i 9; i) R[i] Rm.atdouble(i / 3, i % 3); T[0] Tm.atdouble(0); T[1] Tm.atdouble(1); T[2] Tm.atdouble(2); return 0; }obj 是棋盘格上每个内角点的三维坐标单位毫米。注意循环顺序必须和 findChessboardCorners 返回的角点顺序一致否则标定能跑通但外参会错得离谱。stereoCalibrate 返回的 rms 是重投影均方根误差我没有在接口里返回它实际项目里建议单独打印出来后面会讲怎么用它判断标定质量。两个参数要重点说明。cv::CALIB_FIX_ASPECT_RATIO 表示 fx/fy 固定为输入图像的宽高比这适合工业相机如果你用的是畸变严重的广角镜头最好去掉这个标志。cv::CALIB_ZERO_TANGENT_DIST 假设切向畸变为 0适合廉价的 USB 镜头但对装歪的镜头不适用。怎么判断“装歪”看标定出的 p1、p2 值如果超过 0.05就不要用这个标志了。另外代码里 cv::Size(1280, 720) 是图像分辨率你必须改成相机实际分辨率写错会使 fx、fy 和主点全部错位。2.3 标定采图和质量判断多少张图、什么角度、误差多少才算合格双目标定不是拍两张图就能结束的。我的经验是至少采 20 对有效图像每对图像必须被左右相机完整看到。标定板要放在画面的九个方位左上、上中、右上、左中、中心、右中、左下、下中、右下再做两种倾角绕水平轴倾斜 15 度、绕垂直轴倾斜 15 度。如果只用平放的十几张图内参里最敏感的离群点无法被约束住重投影误差再好看也没有说服力。标定质量的三个硬指标我是这样把控的指标合格线说明重投影误差 rms小于 0.3 像素不是越低越好低于 0.05 可能过拟合标定出的基线长度误差小于 1mm用卡尺量相机光心间距做交叉验证有效图像对数量至少 20 对去除角点漏检和模糊图后统计基线长度的验证容易被忽略。你用卷尺量一下两个相机外壳中心的距离再和 T 向量的模长对比。如果差 3 毫米以上问题多半是标定板照片太少或者左右图像对应角点顺序错位。我会在采完图后先把每一对角点画出来人工扫一遍这一步花 10 分钟能省掉后面一整天的调参时间。3. 立体匹配从视差图到深度值的核心换算标定拿到内外参之后立体匹配要做的事情是找到左右图像上的同名点。同名点的水平像素差叫视差深度和视差成反比。效率排序上基于 SIFT、ORB 这类特征匹配天然适合稀疏点云但不适合逐像素深度BM 和 SGBM 算的是稠密视差图更适合让点云铺满物体表面。3.1 为什么说“支持多种特征检测与匹配算法”不等于任何场景都通用标题里写了“支持多种特征检测与匹配算法”这是模块设计上的说法。真正跑起来要区分用途稀疏三维点用 SIFT 或 ORB 寻找左右图特征点做描述子匹配再用 RANSAC 剔除误匹配。速度快但只在纹理丰富的区域有点。稠密深度图用 BM 或 SGBM 逐像素计算视差。低纹理区域和反光区域会出现空洞需要后处理。已知模板如果被测物在场景里固定用模板匹配定位再三角测量精度最高。我在流水线上做零件高度检测时最终永远是用 SGBM 算稠密视差。特征匹配更适合做视觉里程计或物体位姿粗定位因为它是稀疏的“提取深度信息并构建三维点云”这个目标至少需要一个稠密视差方案做底。模块里可以同时保留两种能力默认路径一定是 SGBM。3.2 用 SGBM 做稠密立体匹配一次把参数调到不闪火星先看核心代码// c_stereo_match.cpp —— 去畸变后计算视差 #include opencv2/opencv.hpp #include opencv2/calib3d.hpp cv::Mat compute_disparity(const cv::Mat left, const cv::Mat right, int num_disparities, int block_size) { cv::Ptrcv::StereoSGBM sgbm cv::StereoSGBM::create( 0, // minDisparity通常为0 num_disparities, // 视差搜索范围必须是16的倍数 block_size, // 块大小奇数默认11 8 * block_size * block_size, // P1 32 * block_size * block_size, // P2 1, // disp12MaxDiff左右一致性检查容忍差 63, // preFilterCap 10, // uniquenessRatio唯一性百分比 200, // speckleWindowSize剔除小连通域 32, // speckleRange连通域像素差容忍度 cv::StereoSGBM::MODE_SGBM); cv::Mat disp16; sgbm-compute(left, right, disp16); cv::Mat disp32f; disp16.convertTo(disp32f, CV_32F, 1.0 / 16.0); return disp32f; }代码注释里的参数就是我上一套双目系统的最终配置。SGBM 返回值是 CV_16S 类型每个像素的视差值要除以 16 才是真实像素单位这一步最容易翻车很多新手直接把 disp16 当成像素用结果深度全部缩小 16 倍。参数调整我有个“三步法”。第一步只调 numDisparities 和 blockSize先让它出一个能看清轮廓的图第二步开大 uniquenessRatio推荐 12 到 20去除重复纹理区域的误匹配第三步处理空洞和飞点speckleWindowSize 在 100 到 300 之间太大会把真正的细小物体抹掉。场景里是低纹理白墙时把 preFilterCap 降到 15 左右能明显减少大片黑色空洞。SGBM 的前置条件比参数本身更关键左右图像必须先经过 stereoRectify 的 remap让极线严格水平。不校正就喂给 SGBM视差图会有严重的横向错位算法完全无法通过参数救回来。下面这段是校正和 remap 的标准动作应该在标定后立刻生成映射表// rectify_map.cpp —— 生成左右目remap映射表 cv::Mat Rl, Rr, Pl, Pr, Q; cv::stereoRectify(lK, lD, rK, rD, cv::Size(1280,720), Rm, Tm, Rl, Rr, Pl, Pr, Q, cv::CALIB_ZERO_DISPARITY); cv::initUndistortRectifyMap(lK, lD, Rl, Pl, cv::Size(1280,720), CV_32FC1, lmapx, lmapy); cv::initUndistortRectifyMap(rK, rD, Rr, Pr, cv::Size(1280,720), CV_32FC1, rmapx, rmapy); cv::remap(left, leftRect, lmapx, lmapy, cv::INTER_LINEAR); cv::remap(right, rightRect, rmapx, rmapy, cv::INTER_LINEAR);这组代码里lK、lD、rK、rD、Rm、Tm 来自第 2 章的 stereoCalibrate 输出。lmapx、lmapy 要先声明为 cv::Mat代码里省略了声明。stereoRectify 的第五、第六个参数是原始左右内参和畸变后面 Pl、Pr 是新的投影矩阵Q 是重投影矩阵。cv::CALIB_ZERO_DISPARITY 会让主点被对齐使视差在无穷远点接近 0这个标志强烈建议保留后面的三角测量代码能省掉不少特殊判断。3.3 视差到深度焦距、基线、主点和“负视差”的换算关系立体校正后视差 d 与深度 Z 的关系是 Z f * B / d其中 f 是校正后内参的 fxB 是基线长度d 是视差像素。如果直接对校正前的图像做匹配还要考虑左右主点 cx 之差不过我们已经在 remap 阶段解决公式可以保持简洁。工程上必须处理三种无效视差d0 的像素、SGBM 里被 disp12MaxDiff 判为不可靠的像素、掩码标记为低置信度的像素。我习惯用一个 C 函数统一过滤/* depth_from_disparity.c —— 视差转深度无效值返回-1 */ float depth_from_disparity(float disparity, float focal_px, float baseline_mm, int use_median) { if (disparity 0.5f) return -1.0f; /* 负视差或0视差直接丢弃 */ float z focal_px * baseline_mm / disparity; return z; }focal_px 是标定得到的 fxbaseline_mm 是 T 向量的模长。单位要保持一致如果 baseline 用毫米深度就是毫米如果要米换算成米再除。这个函数看起来简单但容易踩坑的地方是 SGBM 的视差帧率达到 30fps 后个别像素抖动会造成深度毛刺。我实际用的是连续 5 帧取中位数后的视差再算深度而不是每一帧单独算。use_median 这个开关就是为这种场景留的后门。4. 三角测量与三维点云构建把像素坐标变成空间点视差图给出的是每个像素的深度标量三维点云还需要把左图像的像素坐标 (u,v) 和深度 Z 换算成相机坐标系下的 X、Y、Z。这一步叫重投影OpenCV 里有两个常见做法用 stereoRectify 产生的 Q 矩阵做 reprojectImageTo3D或者用 cv::triangulatePoints 逐点重建。我偏好后者因为它能直接返回四维齐次坐标配合特征点匹配时不需要额外转格式。4.1 三角测量原理两条视线为什么能确定一个三维点一个空间点 X 被左相机投影到像素点 x1被右相机投影到 x2。从光心出发x1 对应一条射线x2 对应另一条射线理想情况下两条射线在三维空间相交于 X。由于图像离散化和标定误差射线通常不严格相交所以最小二乘解成为标准做法。OpenCV 的 triangulatePoints 本质是在解一个超定方程组通过 SVD 找最小特征值对应的特征向量得到齐次坐标 [X, Y, Z, W]最后除以 W 就是非齐次坐标。这个原理决定了三角测量的误差特性深度越远两条射线的夹角越小Z 方向的误差按平方增长。所以任何双目标定系统的“高精度”都有距离边界。我手头的相机基线大约 120mm5 米范围内精度还能看10 米外就只适合粗定位。标定的意义就是把射线夹角的系统误差压到最小这也是前面为什么反复强调基线验证。4.2 用 cv::triangulatePoints 重建三维点两点输入与四点输出在 C 模块里我封装了一个批量重建函数。输入是校正后的左右投影矩阵 P1、P2以及两幅图像上已经配对好的同名点。triangulatePoints 要求输入是 2×N 的齐次坐标矩阵类型是浮点。下面这段代码展示了把 std::vector 转成 Mat 再调用的完整过程// triangulate.cpp —— 批量三角测量 #include opencv2/opencv.hpp #include opencv2/calib3d.hpp std::vectorcv::Point3f triangulate_points( const cv::Mat P1, const cv::Mat P2, const std::vectorcv::Point2f left_pts, const std::vectorcv::Point2f right_pts) { int n (int)left_pts.size(); cv::Mat L(2, n, CV_64F), R(2, n, CV_64F); for (int i 0; i n; i) { L.atdouble(0, i) left_pts[i].x; L.atdouble(1, i) left_pts[i].y; R.atdouble(0, i) right_pts[i].x; R.atdouble(1, i) right_pts[i].y; } cv::Mat X4(4, n, CV_64F); cv::triangulatePoints(P1, P2, L, R, X4); std::vectorcv::Point3f out; out.reserve(n); for (int i 0; i n; i) { double w X4.atdouble(3, i); if (std::abs(w) 1e-8) continue; // 无穷远点直接丢弃 out.push_back(cv::Point3f((float)(X4.atdouble(0,i)/w), (float)(X4.atdouble(1,i)/w), (float)(X4.atdouble(2,i)/w))); } return out; }P1、P2 来自 stereoRectify 的 Pl、Pr类型是 3×4。如果你使用特征点匹配left_pts 和 right_pts 就是左右图匹配点如果你使用 SGBM 视差可以用以下方式生成对左图像每个有效视差 d右图像素坐标 (u - d, v)然后同样喂给这个函数。第二种方式效率不高但胜在逻辑统一我给客户做演示时经常用它因为一台普通机器也能实时跑出几十个点的三维骨架。参数上你唯一要关心的是坐标顺序。triangulatePoints 内部使用 OpenCV 的像素坐标约定输入点不要提前归一化投影矩阵也不要自己缩放否则输出的坐标会整体错乱。它自己会处理齐次因子所以 w 接近 0 的点意味着空间点在无穷远或被平行射线锁定直接丢弃是安全的。4.3 点云去噪与坐标变换从相机坐标系到测量坐标系三角测量得到的点位于左相机坐标系。实际测量中用户希望点云在一个“世界坐标系”里比如工件底面是 Z0 平面。这时可以做一个刚体变换/* pointcloud_transform.c —— 对点云做旋转和平移 */ void transform_pointcloud(const float* src, float* dst, int n, const double R[9], const double T[3]) { for (int i 0; i n; i) { const float* p src 3*i; float* q dst 3*i; q[0] (float)(R[0]*p[0] R[1]*p[1] R[2]*p[2] T[0]); q[1] (float)(R[3]*p[0] R[4]*p[1] R[5]*p[2] T[1]); q[2] (float)(R[6]*p[0] R[7]*p[1] R[8]*p[2] T[2]); } }R 是 3×3 旋转矩阵T 是 3×1 平移向量。在标定过程中我通常先找一个带定位孔的标定板定义出目标坐标系再计算“用户坐标系到左相机坐标系”的变换矩阵应用到全部点云。这样导出到三维软件时坐标和 CAD 保持一致省去手动对齐。点云去噪的顺序我建议是先用直通滤波裁掉超出测量区域的范围再用体素降采样把点距统一到 0.5mm 或 1mm最后做一次基于邻域距离的离群点剔除。这三个步骤用 C 实现都简单唯一要注意的是降采样的体素大小要大于点云噪声水平否则等于没滤。如果不想自己写也可以先用 OpenCV 的 3D 模块读入 PLY再用 PCL 的思路手动实现空间哈希每格保留离中心最近的一个点。5. 避坑从标定板到点云这 5 个坑我替你先踩了这一章不谈理论全部是实际调试中遇到过的“现象→原因→解决”记录。每一条都能让你少折腾半天到一周。5.1 角点检测把方向标反了标定结果显示“成功”现象标定完成后所有参数看着正常重投影误差 0.2 像素但基线长度始终和实际差一倍以上或者重建出的物体深度呈“镜像”状。原因findChessboardCorners 返回的角点是按棋盘格从左到右、自上而下的网格顺序排列的但采集时标定板转了 90 度或者有人把左右图放反导致 obj 里依次排列的角点与实际图像角点错位。OpenCV 的标定算法只是最小化重投影误差它会在局部最优里给出一个“能自洽但物理上错误”的结果。解决在 cornerSubPix 之后加一个简单校验——把第一行角点连成直线确认斜率方向与 obj 一致或者把标定板放在一个已知朝向的支架上统一采集方向。更保险的做法是打印出每张图的角点并人工抽查尤其是左右相机各自独立采集时极易左右颠倒。5.2 视差图噪声大深度值测出来像“毛玻璃”现象SGBM 输出的视差图大面积是黑色空洞或雪花状噪点人形轮廓都看不清。原因大多数情况下不是 SGBM 参数问题而是喂进去的图像没有做极线校正。不开 remap 时SGBM 在未对齐的行上做搜索匹配代价自然很差。另一个常见原因是光照差异左右相机自动曝光不一致导致同一纹理在左右图上亮度相差过大。解决先调 stereoRectify 和 remap再在采图时把左右相机的曝光、增益、白平衡全部锁定成手动。SGBM 参数方面把 uniquenessRatio 从 10 调到 15blockSize 从 11 调到 15往往就能看到明显改善。如果还有大量空洞把 numDisparities 翻倍但注意计算量和近距离盲区会同时变大。5.3 三角测量出来的 Z 坐标正负不同模型前后颠倒现象同样的点云在某台机器上算出来 Z 是正换一台变成负或者离镜头越远的物体 Z 值越小。原因三角测量本身没有定义 Z 轴朝向。左相机坐标系默认 Z 轴指向物体但如果把右相机的投影矩阵 P2 错误地传成左相机的或者 R、T 方向反了重建出的深度就会翻转。SGBM 视差和特征匹配的左右顺序也会影响深度符号。解决拿到标定结果后先用一对已知距离的标定板点验证 Z 符号。在代码里约定视差必须用“左图减右图”的正值投影矩阵 P1、P2 必须严格来自同一个 R、T 序列。我习惯在标定头文件里加一行注释“T is the position of right camera in left camera frame”把语义锁死。5.4 特征匹配把左右图搞反点云出现大量离群点现象SIFT 或 ORB 匹配后生成的稀疏点云在物体轮廓外拉出很长的“飞线”离群点占一半以上。原因特征匹配时把左图特征和右图特征调换了顺序导致同名点的 x 坐标差值变成负值三角测量解出的空间点在相机背后。还可能是 RANSAC 阈值设得太宽把误匹配也当成内点。解决在 match 之前规定左图一定是 query右图一定是 train并在匹配结果里加一条校验只保留 kp_right.pt.x kp_left.pt.x 的匹配。双目校正后同名点只可能出现在右图左侧用一行 if 就能过滤掉大部分飞线。RANSAC 阈值我一般用重投影误差 1.5 像素不要为了内点数好看而放宽到 5 像素。5.5 从 Windows 移植到 Linux 后图像路径和内存对齐导致翻车现象同一份代码在 Windows 上跑得好好的到 Linux 上点云坐标乱掉或者程序崩溃在 cv::triangulatePoints 里。原因第一是图像路径分隔符写死成反斜杠Linux 不认第二是 OpenCV Mat 的 step 是带对齐的代码里如果直接用 ptr 假设每行紧密排列遇到宽度不是 4 倍数时会读错数据第三是 C 和 C 混编时如果 C 文件里把 Mat 对象当普通结构体返回析构会崩。解决路径统一用 / 或者在构建脚本里做平台判断遍历像素用 mat.at (y,x) 或 mat.ptr (y)不要用裸指针加宽度乘法对外 C 接口只传递 double 数组或 float 数组不要传 Mat 结构体。跨平台编译时我还会显式加 -stdc11 和 -fvisibilityhidden避免两个库之间的符号冲突。6. 精度验证与调试技巧用一把尺子、一张棋盘格和一块白墙把系统调到可信6.1 三步现场验证重投影误差、视差空洞率、深度实测偏差标定和匹配都跑通以后不要急着生成点云给客户看。我的验证流程固定成三步总共不超过半小时却能判断系统是“能用”还是“只能看”。验证项方法通过标准重投影误差重新拍 10 张棋盘格重算标定 rms小于 0.3 像素视差空洞率统计 SGBM 有效像素占全图比例大于 70%目标大于 90%深度实测偏差在 1m、2m、3m 处各放一个平面目标用点云拟合平面间距和实际距离比较3m 内误差小于 0.5%最后一项最直观用白墙当目标在 3 米处测量重建平面与相机的距离。如果偏差大于 1%先回头看基线长度是否正确再看视差单位有没有除以 16。这两个错误占了我遇到的七成以上“精度不对”问题。6.2 把点云导出为 PLY让重建结果能被任何三维软件读入点云构建完最终要导出。我习惯导出 PLY 格式因为它是 ASCII 文本方便调试也方便和外部软件对接。下面是一个精简导出函数/* export_ply.c —— 将float数组点云写成PLY文本 */ void export_ply(const char* path, const float* pts, int n) { FILE* f fopen(path, w); if (!f) return; fprintf(f, ply\nformat ascii 1.0\nelement vertex %d\n, n); fprintf(f, property float x\nproperty float y\nproperty float z\nend_header\n); for (int i 0; i n; i) fprintf(f, %.3f %.3f %.3f\n, pts[3*i], pts[3*i1], pts[3*i2]); fclose(f); }用法很简单把 triangulate_points 返回的 vector 转成连续 float 数组后调用这个函数。如果你还想保留颜色可以在 PLY 头里加 property uchar red/green/blue并把对应像素的 RGB 写进去。PLY 文件非常大时比如几十万点建议改成 binary_little_endian 写入速度更快。最后说说我的习惯每换一套双目相机我不会直接调算法而是先拍 10 对标定图只看基线长度和卡尺量的偏差。这个数字超过 0.2mm说明标定数据本身就有问题后面所有精调都不会稳定。希望帮到你。本文还有配套的精品资源点击获取