老读者都知道这个系列不太喜欢绕弯子,今天直接开讲。做视觉的人早晚都会碰一次双目立体——只要你想用两个普通摄像头恢复场景深度,就躲不开从相机标定到点云生成这条链路。这篇是这个系列的第三十期,我把整条流程从单目标定、双目标定、立体校正、SGBM 视差到最终点云重建完整拆一遍,最后补上几个我自己踩过多次的坑。
拍着胸脯说,这条链路本质不复杂,核心就是一个视差公式和几个矩阵。但真上手你会发现 80% 的时间都在处理细节:标定板拍多少张才够、RMS 怎么看、SGBM 参数为什么是这些值、点云为什么歪了。这篇文章面向两类人:一是刚入门打算复现整套流程的初学者,二是已经跑通 demo 但被质量调优折磨的项目开发。无论哪一类,我希望每段结论你都能直接用到自己的数据上。
1. 先看整条链路:从像素差到三维坐标到底经过哪几步
1.1 一个公式解释双目为什么能测距
双目立体视觉最底层的事,就是利用“同一物体在左右两张图上的位置差异”来推算距离。想象你闭上一只眼睛再换另一只,眼前物体会在背景上“跳”一下,这个位移就是视差。物体越近,跳得越明显;物体越远,跳得越不明显。
用公式表达就是:
Z = f × B / d- Z:物体到左相机光心的深度,单位与 B 一致,通常用毫米;
- f:相机以像素为单位的焦距,来自内参矩阵里的 fx;
- B:两台相机光心之间的直线距离,也就是基线;
- d:同一个点像素在左图 x 坐标减去右图 x 坐标之差。
举个例子:一个镜头标定后 fx = 800,两台相机基线 120 mm,如果某个点在左右图中横坐标差了 10 个像素,那它的深度就是 800 × 120 / 10 = 9600 mm,也就是 9.6 米。如果视差变成 100 像素,深度就是 960 mm。这个反比关系是整套系统的地基,后面每一步其实都在为精确计算 d 和 B 服务。
1.2 五步流程与各环节输出物
从原始的双目图像到最终点云,工程上一般拆成五个环节:
| 环节 | 输入 | 输出 | OpenCV 核心函数 |
|---|---|---|---|
| 1. 单目标定 | 棋盘格图像集 | 左右相机内参 K、畸变系数 | calibrateCamera |
| 2. 双目标定 | 左右图像点集 + 内参 | 旋转 R、平移 t、本质矩阵 E、基础矩阵 F | stereoCalibrate |
| 3. 立体校正 | 左右原图 + R/t | 校正后图像、投影矩阵 P1/P2、Q 矩阵 | stereoRectify / remap |
| 4. 视差计算 | 校正后的左右图 | 视差图 disparity | StereoSGBM |
| 5. 立体重建 | 视差图 + Q 矩阵 | 三维点云 | reprojectImageTo3D |
也就是说,你最终拿到的点云其实是每一步输出层层传递的结果。任何一个环节的误差都会在后面放大,所以“链路思维”特别重要——你不可能跳过标定直接调 SGBM,也不可能在校正不准的情况下得到干净的点云。
1.3 顺序为什么不能乱
很多人问能不能直接给两张图算视差,不做标定和校正。答案是可以试,但结果通常没法用。原因很简单:真实镜头有畸变,两台相机也不可能严格平行放置,直接匹配时同一个点在右图里的搜索范围是一个二维区域,计算量暴增,误匹配率也高得离谱。先标定获得内参,再用双目标定获得相对位姿,最后通过校正把图像“拉”到理想的双目平行模型上,匹配才变成纯粹的一维搜索。这就是为什么哪怕只为了测个距离,也别省掉标定的原因,它不是加分项,是及格线。
2. 张正友标定法实操:棋盘格、内参、畸变和 RMS 怎么读
2.1 为什么今天的相机标定都绕不开棋盘格
相机标定家族里常见的有三种:摄像头内参标定、手眼标定、传感器之间的外参标定。今天这期说的是第一种,但它是后面所有标定的基础。提到内参标定,几乎绕不开张正友标定法。它的核心贡献是告诉大家:用一块平面棋盘格就能完成标定,不需要昂贵且精度要求极高的三维标定块。
棋盘格的角点天然是精确的亚像素特征,而且整个棋盘格位于同一个平面上,这个几何约束让算法有足够的方程去解出相机内参。我们只需要在不同角度、不同距离拍几十张照片,找到角点,再交给 OpenCV 解算。整个过程不需要高精度机械装置,这也是它能成为事实标准的原因。
2.2 标定前必须想清楚的几个采集细节
我见过太多人在标定阶段就翻车,核心原因基本都是采集动作不对。几条实测有效的规则:
- 标定板要平整,最好是玻璃夹层板或贴在高刚性铝板上。软纸板一弯,角点坐标离谱,解出来的内参全是错的。
- 拍摄数量建议 15~25 张。少于 10 张时,外参自由度太高,容易过拟合。
- 每一张都要让棋盘格出现在画面不同位置,别每次都摆在正中央,否则畸变系数尤其是边缘部分的 k1、k2 根本约束不住。
- 倾斜角度要有跨度,最好覆盖 0~45 度的俯仰和左右旋转。只拍正面平视会让相机中心附近的约束过强,而边缘像差永远不触发。
- 拍摄距离最好接近实际工作距离。你要标定一台用来测 1~5 米障碍物的相机,就别拿 20 厘米距离的照片凑数。
- 光照要均匀,避免棋盘格表面反光。反光会让 findChessboardCorners 在同一张照片上删了又找,而且亚像素定位精度会下降。
- 固定好相机或标定板,拍摄时轻微移动即可,不要出现运动模糊。
这里有个很容易被忽略的点:棋盘格的物理边长必须用卡尺量准。很多厂家标称 24 mm,实际可能差零点几毫米。别小看这个误差,内参的尺度、后续基线长度、点云尺寸都会跟着错。
2.3 单目标定的最小可运行代码
Python 版本最直观,逻辑完整:
import cv2 import numpy as np pattern = (9, 6) # 内角点数量:横向9个,纵向6个 square_mm = 24.0 # 棋盘格每个格子的实际边长(毫米) objp = np.zeros((pattern[0] * pattern[1], 3), np.float32) objp[:, :2] = np.mgrid[0:pattern[0], 0:pattern[1]].T.reshape(-1, 2) objp *= square_mm obj_pts, img_pts = [], [] image_paths = [...] # 你的标定图片路径 for path in image_paths: img = cv2.imread(path) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners = cv2.findChessboardCorners(gray, pattern, None) if not ret: continue criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-3) corners = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) obj_pts.append(objp) img_pts.append(corners) rms, K, dist, rvecs, tvecs = cv2.calibrateCamera( obj_pts, img_pts, gray.shape[::-1], None, None) print("RMS:", rms) print("内参矩阵:\n", K) print("畸变系数:", dist.ravel())这里唯一要解释的黑盒是 cornerSubPix。findChessboardCorners 得到的是像素级整数角点,cornerSubPix 会在角点邻域内做亚像素迭代优化,精度能达到 0.01 像素级别。这一步直接影响后续标定质量,千万别省。
2.4 RMS、内参矩阵和畸变系数怎么判断好坏
calibrateCamera 返回的 rms 是全部角点的平均重投影误差,单位是像素。我的经验值如下:
- rms < 0.1:非常理想,内参可信;
- 0.1 ~ 0.3:可用,但要确认它是不是被某几张差图拉高的;
- 0.3 ~ 0.5:建议查一下拍摄集,剔除模糊角度奇怪的图;
0.5:基本可以认定标定失败,优先检查标定板平整度、采集数量、反光。
内参矩阵 K 形如:
[[fx, 0, cx], [0, fy, cy], [0, 0, 1]]fx、fy 表示焦距与像素尺寸的换算结果,cx、cy 是主点位置。如果 fx 和 fy 相差过大,说明像素不是标准正方形;如果 cx、cy 偏离画面中心太多,说明镜头光轴和传感器中心没对齐,这些都正常,关键是它们要稳定。
畸变系数 dist 包含径向畸变 k1、k2、k3 和切向畸变 p1、p2。一般 k1、k2 的量级通常在十的负二次方,如果出现零点几甚至更大,就要警惕是不是把畸变、内参和外参绕成一团了,这时候重新标定通常比强行收拾更省时间。
3. 双目标定:两台相机之间的 R 和 t,比任何参数都重要
3.1 单目标定完成之后,还缺什么
单目标定只解决“一台相机到底是怎么看世界的”,但立体视觉需要回答更关键的问题:左右相机之间的相对位姿是什么。换句话说,右相机在左相机坐标系里旋转了多少、平移了多少。
这个相对位姿就是双目标定要求解的东西。有了它,我们才知道基线 B 到底多长,也才能把两台相机校正成理想的双目平行模型。很多人问“我量了两个相机中心距离 120 毫米,能不能直接当作 B 填进去”,答案是不能。你量的是外壳间距,不是光学中心的距离,而且还包含安装角度误差,必须通过算法反算。
3.2 stereoCalibrate 到底解出了什么
双目标定的输入是同一时刻左右相机拍摄到的同一块棋盘格的角点。因为棋盘格上的 3D 坐标已知,左右图上的 2D 点也已知,相当于拿到了若干组“双目观测”,OpenCV 通过多视图几何关系解出两台相机之间的旋转矩阵 R 和平移向量 t。
代码核心部分:
rms_stereo, K1, d1, K2, d2, R, T, E, F = cv2.stereoCalibrate( obj_pts_all, # 所有帧的棋盘格3D点 img_pts_left_all, # 左相机对应2D角点 img_pts_right_all, # 右相机对应2D角点 K1, d1, K2, d2, # 单目标定的结果 gray.shape[::-1], # 图像尺寸 flags=cv2.CALIB_FIX_INTRINSIC) print("双目RMS:", rms_stereo) print("R:\n", R) print("T (mm):", T.ravel())注意这里我用了 CALIB_FIX_INTRINSIC,表示双目标定过程中只优化两台相机之间的相对位姿,不再动单目标定得到的内参和畸变。这个做法在排错时特别好用:如果最终结果不对,可以分别验证单目标定和双目标定,而不是一堆参数搅在一起。
帧匹配逻辑也要说清楚:左右两张图必须是同一时刻拍摄的,我建议保存图像时就按帧号对应。写代码时先分别对左右图片找角点,只有左右两边都成功的那一帧才加入 obj_pts_all、img_pts_left_all 和 img_pts_right_all。
3.3 基线的选择与标定结果的验收规则
基线 B 是双目标定的直接输出,也就是 T 向量的模长。基线的选择直接影响深度精度:
- 近距离测量(0.5~5 米),基线 5~12 厘米比较舒服;
- 远距离测量(10 米以上),基线最好到 30 厘米甚至更宽;
- 基线越长,近处共同视野越小,遮挡区域越大;
- 基线越短,视差变化越微弱,远距离深度误差迅速膨胀。
验收双目标定结果,我一般看三个指标:
- 双目 RMS 同样要小于 0.2 像素;
- 基线的数值和用卷尺量的光心距差距在 5% 以内,偏差过大要检查 square_mm 是否写错;
- R 矩阵应该非常接近单位阵。如果旋转量过大,说明两台相机的相对姿态估算紊乱,多半是标定板拍得不够或左右图像时间不同步。
4. 立体校正:把左右相机“掰正”到共面平行
4.1 极线约束:为什么匹配必须做校正
双目标定之后,我们其实已经知道左右相机之间的位姿,但真实安装中两台相机不可能做到光轴严格平行、成像平面共面。如果不校正,同一个点在左图位于第 300 行,在右图可能位于第 312 行,匹配时需要在二维区域内搜索,计算量和误匹配率都不可接受。
立体校正的目的就是把左右图像通过重采样变成“理想双目”的样子:两幅图像行对齐,任意一个空间点投影到左右图上的 y 坐标相同。这样匹配搜索就从二维直接降成一维水平搜索,这也叫极线约束。
OpenCV 的 stereoRectify 基于 Bouguet 算法,原理大致是先把双目标定得到的旋转矩阵分解成左右各一半,让两机光轴先变成平行,再构造一个把极点推向无穷远的校正旋转矩阵,使极线与图像扫描线完全水平。
4.2 校正矩阵和 remap 的完整代码
校正这一步的代码结构在 Python 里是固定的:
R1, R2, P1, P2, Q, roi1, roi2 = cv2.stereoRectify( K1, d1, K2, d2, gray.shape[::-1], R, T, alpha=0) map1_l, map2_l = cv2.initUndistortRectifyMap( K1, d1, R1, P1, gray.shape[::-1], cv2.CV_32FC1) map1_r, map2_r = cv2.initUndistortRectifyMap( K2, d2, R2, P2, gray.shape[::-1], cv2.CV_32FC1) rect_l = cv2.remap(img_l, map1_l, map2_l, cv2.INTER_LINEAR) rect_r = cv2.remap(img_r, map1_r, map2_r, cv2.INTER_LINEAR)这里有几个经验值得单独说:
- initUndistortRectifyMap 返回的两个矩阵,按顺序原样传给 remap 的第三、四参数。很多人在这里把 map1 和 map2 传反,结果图像翻转或者直接错乱。
- alpha 参数影响校正后图像的黑边处理。alpha=0 会裁剪掉几乎所有无效区域,适合后续匹配;alpha=1 保留全部像素但会出现大片空白。实际项目我一般用 alpha=0,再用 roi1/roi2 框出有效区域。
- remap 用双线性插值 INTER_LINEAR 就够了,不要在这里用太高阶的插值,耗时大且收益不明显。
4.3 用一条水平线验证校正结果
校正做得好不好,最直观的办法就是在两张校正图上叠画几条水平线,然后肉眼判断场景里的同一个物体是否落在同一高度。如果左图某个门把手在第 240 行,右图它也在第 240 行附近,说明校正基本成功。
更严格一点的做法是手工选取 5 个以上的特征点(棋盘格角点、纸箱边缘、表盘中心都行),统计左右校正图上的 y 坐标差。中心区域的偏差应该控制在 2~3 像素以内。如果偏差达到十个像素以上,回头检查双目标定阶段是不是有错帧、画面是不是没同步,不要急着调 SGBM。
5. SGBM 视差计算:参数原理和工程调优的十几个细节
5.1 视差匹配的本质:在一维搜索空间里找最像的块
校正完成后,真正核心的计算才开场。StereoSGBM 做的事情可以这样理解:对左图每个像素,在右图同一行上滑动一个小窗口,寻找颜色和梯度最接近的窗口中心位置,两者的横向差距就是视差 d。
SGBM 的 S 是 Semi-Global,半全局。它在逐像素匹配的基础上,增加了对相邻像素视差变化的平滑惩罚,能在弱纹理区域减少误匹配,同时又不像全局匹配那样计算量爆炸。它输出的是 CV_16S 格式的定点数,真实视差需要除以 16。
匹配是一个天生有歧义的过程:光滑墙面、重复纹理、遮挡边缘都没有稳定的匹配信号,所以 SGBM 质量的高低很大程度上取决于参数是否贴合你的场景和图像分辨率。
5.2 参数表与推荐值(这些数字别乱抄)
C++ 版本创建 SGBM 的代码很典型:
cv::Ptr<cv::StereoSGBM> sgbm = cv::StereoSGBM::create( 0, // minDisparity,最小视差 96, // numDisparities,必须为16的倍数 5, // blockSize,必须为奇数 8 * 1 * 5 * 5, // P1 32 * 1 * 5 * 5, // P2 1, // disp12MaxDiff 63, // preFilterCap 25, // uniquenessRatio 100, // speckleWindowSize 32, // speckleRange true); // mode: MODE_SGBM cv::Mat disparity; sgbm->compute(rect_l, rect_r, disparity); cv::Mat disp8; cv::normalize(disparity, disp8, 0, 255, cv::NORM_MINMAX, CV_8U);参数含义和参考值整理成表:
| 参数 | 作用 | 我的常用值 |
|---|---|---|
| minDisparity | 从哪个视差值开始搜索 | 0 |
| numDisparities | 搜索范围,必须是 16 的倍数 | 64~128 |
| blockSize | 匹配窗口边长,奇数 | 5 或 7 |
| P1、P2 | 视差平滑惩罚 | P1=8×blockSize²,P2=4×P1 |
| disp12MaxDiff | 左右一致性检查容忍差 | 1~3 |
| preFilterCap | 预滤波截断值 | 63 |
| uniquenessRatio | 最佳匹配的独特程度,越高越严格 | 10~30 |
| speckleWindowSize | 散斑滤波窗口大小 | 50~200 |
| speckleRange | 散斑内允许的视差波动 | 16~32 |
这里最容易被新手改坏的参数是 blockSize 和 P2。blockSize 太小会让视差图全是椒盐噪声,太大则会把边缘磨平,我通常先给 5,分辨率高或纹理弱时试 7 甚至 9。P2 是平滑惩罚强度,太大整个视差图会糊成一整块,太小则弱纹理区域到处都是洞。
需要特别提醒:numDisparities 决定了最近的能测深度,但它不是越大越好。搜索范围变大后,弱纹理处的误匹配机会也变多。实际项目里先根据最近目标距离估算最大视差:假设最近距离 0.5 米,fx=800,B=120mm,最大视差就是 800 × 120 / 500 = 192 像素,numDisparities 取 208 或者 192 的倍数向上取整。
5.3 从视差图到有效掩码:除以 16 和无效像素
SGBM 输出的 disparity 是 CV_16S,内部用 1/16 像素作为视差精度档位,所以真实视差值要除以 16。如果你直接拿原始值做 reprojectImageTo3D,OpenCV 会在内部替你做除法,但如果自己写公式算深度,一定要记得除以 16。
无效匹配会被置成最小负数或者固定负值。生成掩码最稳妥的方法:
valid_mask = disparity > 0视差小于等于 0 的像素在物理上不可信,要么是没匹配上,要么匹配到了不可信的低置信度位置。
5.4 WLS 后处理与弱纹理区域的补洞思路
SGBM 输出经常带有空洞和噪点,特别是在墙面、天空、反光面上。普通中值滤波可以去掉一部分椒盐噪声,但对大面积空洞无能为力。想要更平滑的结果,推荐 OpenCV contrib 模块里的 WLS 视差滤波。它会把彩色图像的边缘信息引进来引导视差平滑,边缘保持效果比单纯滤波好很多。
import cv2.ximgproc as ximgproc left_matcher = cv2.StereoSGBM_create(...) right_matcher = ximgproc.createRightMatcher(left_matcher) wls = ximgproc.createDisparityWLSFilter(left_matcher) wls.setLambda(8000.0) wls.setSigmaColor(1.5) left_disp = left_matcher.compute(rect_l, rect_r) right_disp = right_matcher.compute(rect_r, rect_l) filtered_disp = wls.filter(left_disp, rect_l, None, right_disp)需要单独安装 opencv-contrib-python。WLS 对弱纹理区域能补出连贯的平面,但不能拯救完全相同的纹理,因为它本质上还是靠“图像边缘约束视差连续”来工作。
6. Q 矩阵与点云生成:重投影公式必须吃透
6.1 从视差到三维坐标的两种等价写法
校正完成后的理想双目模型,深度和视差的关系在前面已经写过。真正把每个像素变成三维点,可以有两种理解方式。
第一种是直接按公式算。对于校正后图像中的像素 (u, v),已知视差 d:
Z = f × B / d X = (u - cx) × Z / f Y = (v - cy) × Z / f这里的 f 是校正后左相机的像素焦距 fx,cx、cy 是校正后左相机的主点。这种方式直观,适合调试。
第二种是使用 OpenCV 在 stereoRectify 阶段输出的 Q 矩阵。它把 (u, v, d, 1) 一次映射成四维齐次坐标 (X, Y, Z, W),再除以 W 就得到三维点。Q 矩阵的形式大致是:
Q = [[1, 0, 0, -cx], [0, 1, 0, -cy], [0, 0, 0, f], [0, 0, -1/B, (cx - cx')/B]]其中 cx、cx' 分别是左右校正图主点的横坐标。如果左右主点相同,Q[3][3] 为 0,深度公式就退化成 Z = f × B / d。推导过程不复杂,但工程上我们直接用 reprojectImageTo3D 就够了,关键是要理解 Q 矩阵在替我们干这件事。
6.2 reprojectImageTo3D 的坑和正确用法
C++ 调用:
cv::Mat xyz; cv::reprojectImageTo3D(disparity, xyz, Q, true, CV_32F);Python 调用:
xyz = cv2.reprojectImageTo3D(disparity, Q, handleMissingValues=True, ddepth=cv2.CV_32F)几个坑提前说:
- disparity 如果是 CV_16S,函数会自动除以 16。如果你已经手动除以了 16 变成浮点,就不要再传原始格式,否则深度会整体错 16 倍。
- handleMissingValues=True 时,无效像素会被赋予该函数内部约定的巨大值 10000,并非 0。点云处理时建议用
xyz[..., 2] > 0或者xyz[..., 2] < 10000过滤,别直接把所有点都写进文件。 - xyz 是 H×W×3 的三通道矩阵,每个通道分别存 X、Y、Z,单位与标定时棋盘格的物理单位一致。你 square_mm 填了毫米,这里就是毫米。
6.3 手动验证一组坐标,把公式写进脑子
永远不要相信一段没验证过的重建代码,哪怕 API 是官方文档里抄的。我最常用的验证方法是:从视差图里取一个明显物体上的点,手算它的三维坐标,再和代码结果对比。
延续前面的例子:图像分辨率 640×480,fx=800,主点 (320, 240),基线 120 mm。取某个像素 (u=400, v=300),视差 d=50 像素,则:
Z = 800 × 120 / 50 = 1920 mm X = (400 - 320) × 1920 / 800 = 192 mm Y = (300 - 240) × 1920 / 800 = 144 mm如果代码输出 xyz[300, 400] 和 (192, 144, 1920) 对得上,链路才算真的通了。这个习惯能帮你排查一半以上的“点云全乱”问题。
6.4 点云的保存、可视化和后处理
得到 xyz 矩阵后,直接生成点云的简易方法:
import numpy as np points = xyz.reshape(-1, 3) valid = np.isfinite(points).all(axis=1) & (points[:, 2] > 0) & (points[:, 2] < 10000) points = points[valid]保存为最朴素的 PLY 文件:
def save_ply(path, points): with open(path, "w") as f: f.write("ply\n") f.write("format ascii 1.0\n") f.write("element vertex %d\n" % len(points)) f.write("property float x\n") f.write("property float y\n") f.write("property float z\n") f.write("end_header\n") for p in points: f.write("%.3f %.3f %.3f\n" % (p[0], p[1], p[2]))可视化我只推荐两种方式:轻量查看用 CloudCompare,它是处理点云的瑞士军刀,能直接打开 PLY,还能做配准、测量、剖面;程序内处理用 Open3D,可以做统计滤波、体素降采样、ICP 配准,配合点云可视化窗口调试非常方便。PCL 的 C++ 版本我也常用,但部署成本高于 Open3D,具体看项目环境选型。
7. 常见问题排查:为什么你的点云歪了、有洞、不达标
7.1 点云整体倾斜或尺度不对
如果你重建出来的点云地面是斜的,或者物体长宽比明显不对,第一反应不要怀疑 SGBM,先查标定物理尺寸。最常见的原因就是 square_mm 和真实棋盘格边长不一致。比如标称 24 mm 的棋盘格,你填了 24 cm,那所有深度、所有坐标都会放大 10 倍,地面自然就“塌”了。
第二常见的原因是双目标定没有固定内参。当内参、畸变、R、t 全部同时优化时,算法会把误差摊到各个参数上,看起来 RMS 还行,但校正矩阵已经严重失真。回到 CALIB_FIX_INTRINSIC 的流程,问题通常会消失。
第三常见原因是双目图像不是严格同步拍摄。如果左右相机采集有几十毫秒的延迟,物体一旦运动,视差里就会混入运动误差,点云边缘会出现撕裂感。
7.2 深度空洞和边缘飞点
空洞的主要来源有三个:遮挡、弱纹理和反光。遮挡是无解的几何事实,只能通过多帧融合或后续补洞;弱纹理可以靠 WLS 后处理改善;反光则需要调整相机曝光角度,或者把场景里的高光尽量压掉。
边缘飞点是另一种常见现象。物体轮廓附近,SGBM 的匹配窗口横跨前景和背景两个深度平面,很容易选到错误窗口中心,造成前景“膨胀”或背景“下陷”。缓解办法:blockSize 尽量小,disp12MaxDiff 开起来,点云生成后用统计滤波去掉离群点。Open3D 的 remove_statistical_outlier 默认参数在多数场景下都够用,一般设置 nb_neighbors=20, std_ratio=1.0 效果就不错。
7.3 坐标系方向与可视化差异
OpenCV 输出的是右撇子相机坐标系:原点在左相机光心,X 向右,Y 向下,Z 沿光轴向前。很多点云工具默认把 Z 当“地面向上”,一打开就会看到点云“躺”着。这不是重建错了,只是坐标系约定不同。
习惯的做法是重建完成后手动做一次旋转变换:想把 Y 改为向上,就让 X 不变、Y 取反、Z 和 Y 交换,再拼成 4×4 变换矩阵。如果项目要和激光雷达点云对齐,同理需要额外做一次手眼标定,用外参把两套坐标系统一到基点下。
7.4 用卷尺验证整条链路的推荐做法
链路到底行不行,最后一定要做一次物理验证。我的做法是在场景里放几个已知距离的目标,最好是不同远近的纸箱或人像立牌。程序读目标区域点云的中位深度,和卷尺实际距离对比。
误差在 5% 以内,这套系统就可以进入正常开发;误差超过 10%,优先排查标定,而不是继续调 SGBM。这里有个规律你要有预期:双目测距误差随距离按平方增长,2 米处误差 2 厘米的系统,到 10 米处可能误差到半米以上。所以别指望一台短基线相机既做近处高精度,又能覆盖几十米的探测范围,场景需求决定了基线长度和相机焦距的取舍。
我个人的习惯是验证完再把所有参数归档一次,图像分辨率、棋盘格尺寸、标定 RMS、SGBM 参数、验证距离和误差。过一个月后回来调试,看到这份记录,比自己重新摸索快得多。双目立体这条链路,参数互相影响又多又深,一套可复现的调试日志,比任何高端技巧都更值钱。