
上个月帮朋友看一个机械臂抓取料框的项目需求写得很干脆识别框里散乱摆放的工件输出抓取位姿。选型会开场第一句话就是——上深度相机还是上双目相机这个问题我这两年被问过不下十次每次都得先花二十分钟把两条技术路线的边界讲清楚才能往下聊正事。因为太多人把「深度相机」当成一个具体的东西实际上它是一类输出结果的统称而「双目相机」说的是硬件形态是被包含在深度相机这个大筐里的。概念没理顺后面选型基本就是碰运气。我自己是从双目起手的后来陆续拆过结构光模组、试过 ToF 相机、也拿主动红外双目做过料框抓取中间踩过的坑足够写一本小册子。这篇就把这些经验铺开讲三条深度获取路线的原理和边界在哪双目相机的视差公式该怎么算精度账标定流程里那些文档不会写的细节以及一个特别容易被跳过、但对最终精度影响巨大的环节——双目相机标定里剔除不合格角点。适合刚入行做三维视觉的、要选型的硬件工程师、以及实验室里第一次搭双目平台的同学。看完至少能少走两个月弯路。1. 把深度相机和双目相机摆到一张桌子上比1.1 名字里藏着的分类陷阱先说清楚一件事深度相机是按输出定义的只要它最终能给你一张每个像素带距离值的深度图它就叫深度相机。双目相机是按硬件形态定义的指的是用两个相机从不同视角看同一个场景。这两者的关系是包含不是并列。市面上绝大多数双目相机出厂时并不带深度输出你得自己标定、自己跑立体匹配它才变成一台深度相机。麻烦的地方在于厂商的宣传口径。有些产品叫「双目结构光」有些叫「主动双目」有些叫「双目立体视觉相机」名字听着像一家人拆开看里面的东西差得远。我见过一款标着「双目」的模组拆开发现投影器是标配、两个红外相机只做辅助本质上是结构光方案披了件双目的外套。反过来也有人把散斑投影的双目当成结构光卖这两条路线的失效场景其实完全不同。所以拿到一个产品页我习惯先干一件事找它的失效条件章节。结构光怕强光ToF 怕多路径和反射率双目怕无纹理这些在规格书里通常藏在很不起眼的角落但恰恰决定了你的项目能不能落地。规格书上标的那句「0.2 米到 1.5 米工作范围」往往是在白墙、室内、无阳光的理想条件下测出来的。1.2 从输出形式倒推技术路线判断一台设备走的是哪条路线最直接的办法是看它给了你什么数据接口。只给深度图的多半是结构光或者 ToF同时给左右两张红外图加一张深度图的基本是双目类方案只给左右两张彩色图的那就是纯被动双目深度得你自己算。还有一个更快的判定方法看它有没有红外投影口。结构光和主动红外双目一定有发射窗口用手电筒照一下能看到暗红色的滤光片或者拿手机前置摄像头很多手机前置没有红外滤光片对着看一眼能看到投影器微微发亮。ToF 的发射口和接收口通常是分开的两个窗口结构光的发射和接收也分窗口靠这个很难区分还是得看数据接口。我一般建议新手先从形态判断入手再通过数据接口确认最后翻一下 SDK 里有没有投影器的开关控制。有投影器开关控制的基本可以确定是主动方案这种模组在室外强光下通常可以关掉投影走被动模式但代价是深度图会出现大片空洞。1.3 一张对比表先看全局下面这张表是我自己在选型会上常用的速查版本数字是几款常见同类产品的综合区间不是某一台的具体值实际以规格书为准。维度结构光飞行时间 ToF主动红外双目被动双目测距依据投影图案的形变或相位光往返时间或调制相位双目视差加散斑纹理纯双目视差近距离精度高亚毫米到毫米中毫米到厘米中高毫米级依赖纹理波动大典型有效距离0.2 到 1.5 米0.5 到 5 米0.3 到 3 米随基线延长室外强光表现差中中好纹理依赖无无弱强材质敏感度黑、镜面、透明失效反射率影响相位相对宽容无纹理直接失效帧率与运动中高中取决于算法多机干扰明显散斑串扰有一定概率较低无功耗中高中低成本中高中低这张表里最值得记住的一行是「多机干扰」。如果你的产线上要摆十几台相机同时工作结构光方案一定要考虑分时触发否则投影的散斑互相打架深度图会出现随机的大面积错误而且这种错误在调试时特别难复现。2. 深度相机的三条主流路线拆解2.1 结构光用已知图案的形变反推三维结构光的思路很直观。你拿一个投影器往场景里投一张已知的图案图案落到物体表面会随表面形状发生扭曲相机拍下扭曲后的图案通过图案和投影器的对应关系就能用三角测量算出每个点的深度。投影图案的种类很多最早期是格雷码加相移后来有正弦条纹现在消费级产品里最常见的是随机散斑因为散斑只需要一张图就能算出深度适合做高帧率。它的强项是近距离精度。因为投影图案的变形量在近距离下非常敏感稍远一点精度就迅速掉下来。我实测过的一款散斑结构光模组在 50 厘米处深度噪声能压到 1 毫米以内但到 1.5 米就退化到 1 厘米以上了。这个特性决定了它很难做远距离应用。失效场景也很明确。第一是强红外环境太阳光里的红外成分会把投影散斑淹没室外中午基本不能用这是硬伤不是调参能解决的。第二是黑色吸光材质散斑投上去反射不回来深度直接空洞。第三是镜面和透明体反射方向不确定算出来的深度是乱的。第四是多台设备同时工作散斑相互干扰需要做分时或者编码区分。投影器工作时别让眼睛正对发射口看尤其是给小孩和宠物演示的时候注意一下。这类模组的发射功率通常符合安全等级要求但近距离直视仍然不建议。2.2 飞行时间直接数光跑了个来回ToF 的逻辑是测量光从发射到返回的时间。连续波调制的方式更常见发射一束经过高频调制的光接收端测出返回光和发射光之间的相位差用相位差反推距离。因为相位是周期性重复的单一频率下存在距离模糊所以实际产品会用多个调制频率组合来解算真实距离这也是为什么 ToF 的标定参数里经常会看到一列频率。它的优势是整幅图像同步曝光帧率高对纹理完全没有依赖白色墙面和黑布都能出深度。中远距离的稳定性比结构光好很多1 到 3 米是它的舒适区。缺点是近距离精度不如结构光而且有一类特有的错误叫多路径干扰就是光打到墙角后反射了一圈才回到接收器算出来的距离比真实值大。这类错误在直角走廊、箱体内部特别容易出现表现是深度图上出现奇怪的斜面。还有一类错误叫飞点。物体边缘的像素里混进了前景和背景两种深度的回波最终解算出一个介于两者之间的值看起来深度图上物体轮廓外有一圈悬空的噪点。这在做抓取的时候很致命因为点云里会多出一堆不存在的点得靠边缘膨胀加深度一致性过滤来清掉。2.3 主动红外双目给双目装了把手电筒主动红外双目可以理解成给传统双目加了一个红外散斑投影器。它的深度依然靠视差算投影器的工作只是把纹理「画」到无纹理表面上让匹配算法有东西可以匹配。这个设计很巧妙既保留了双目在室外可以关掉投影走被动模式的灵活性又解决了室内白墙匹配不上的问题。它和结构光的本质区别在于结构光是靠图案本身携带的编码信息来解算深度投影器和相机之间需要严格标定主动红外双目是靠左右两个相机的视差来解算深度投影器只是个辅助光源不参与几何计算。所以你切换投影器开关深度的绝对尺度不会变只会影响有效像素的比例。这个特性带来的好处是投影器的标定漂移不影响精度维护成本低。坏处是它的精度上限受双目基线限制做不到结构光那种亚毫米级的近距离精度。我一般把它定位成「通用场景的万金油」室内外都能用价格适中SDK 生态成熟是入门三维视觉最不容易翻车的选择。2.4 参数与实测对比社区里这几年被反复提到的「大白深度相机」本质上就是入门价位的小体积深度模组大家在创客圈里习惯这么叫。这类模组的价格和体积都对个人开发者很友好适合做原型验证。但有个问题必须提醒不同批次的一致性、出厂标定的质量参差不齐如果你的应用需要毫米级测量千万别指望出厂参数直接可用拿到手第一件事就是自己做一次校正和验证。我整理了几类方案在我手上实测的感受这里的数字只是方向性参考不同产品差异很大近距离精细测量比如小零件高度检测选结构光但要把工作距离限制在它最舒服的那一档。中距离场景理解比如服务机器人避障选 ToF功耗换稳定性。室内外切换、抓取、体积测量选主动红外双目容错率高维护简单。纯室外阳光下的测量被动双目或者激光方案更稳。一个很实在的判据是如果你的场景里同时存在无纹理表面和强环境光那基本没有单一方案能完美覆盖需要做方案组合或者接受一部分无效像素。我在一个户外料堆体积测量的项目里最后就是用了双目加补光的方式靠场景本身的地面纹理撑住匹配投影器只在阴影区打开。3. 双目相机的核心视差、三角测量与精度账3.1 视差公式与它的物理含义双目测深度的核心公式只有一个距离等于焦距乘以基线再除以视差。写成公式是 Z f × B / d其中 f 是相机的等效焦距单位是像素B 是左右两个相机光心之间的距离也就是基线单位是毫米d 是同一个物点在左右图像上的横坐标差值也就是视差单位是像素。这个公式的物理含义很值得琢磨。基线越大同样的距离产生的视差越大测量越灵敏焦距越长视差也越大视差越小说明物体越远而且距离和视差是反比关系这条曲线在近处陡、远处平意味着远处的深度分辨率会急剧下降。举个数一台 1280 乘 720 的相机像元尺寸 3.45 微米镜头焦距 4 毫米那么等效焦距 f 等于 4 毫米除以 3.45 微米大约是 1159 像素。基线 60 毫米。一个距离 1 米的物点视差是 1159 乘 60 除以 1000约等于 69.5 像素。这就是为什么双目标定一定要做到亚像素精度因为一点点视差误差就会放大成明显的距离误差。3.2 基线、焦距、像素分辨率怎么配这三个参数不是随便定的互相之间有硬约束。先看最近可测距离它受最大视差搜索范围限制。如果算法把视差搜索范围限制在 0 到 128 像素那么最近可测距离就是 f 乘 B 除以 128代入刚才的数字是 1159 乘 60 除以 128约等于 543 毫米。也就是说物体离相机近了视差会超出搜索范围直接匹配失败深度图上出现大片空洞。反过来最远可测距离受最小可分辨视差限制。假设系统能稳定分辨 0.2 像素的视差那么理论最远距离是 1159 乘 60 除以 0.2约等于 347 米但这是纯理论值实际受标定误差、图像噪声、匹配算法鲁棒性影响通常有效距离是理论值的十分之一都不到。我手上这套 60 毫米基线的模组稳定出深度的距离大概到 8 米左右再远就开始大面积噪点了。基线也不是越大越好。基线变大近距离的遮挡区域会明显增大。所谓遮挡是指某个物点能被左相机看到但被右相机被前景挡住了反过来也一样。这部分区域在深度图上一定是空洞而且空洞宽度近似和基线成正比。基线从 60 毫米加到 120 毫米精度提升一倍但近处盲区和空洞也差不多翻倍。基线焦距搜索范围最近可测最远可靠范围1 米处视差60 mm1159 px0 到 128 px约 0.54 m约 6 到 8 m69.5 px60 mm1159 px0 到 256 px约 0.27 m约 6 到 8 m69.5 px120 mm1159 px0 到 256 px约 0.54 m约 12 m139 px120 mm2300 px0 到 256 px约 1.08 m约 20 m276 px这张表能看出一个反直觉的结论加大焦距和加大基线的效果类似但加大焦距会让视场变窄、最近可测距离变远而加大基线只影响最近可测距离和遮挡。所以我一般优先调基线实在不行再考虑长焦长焦最后才动。3.3 精度账要算在项目启动前深度误差的传播关系是深度误差等于距离的平方乘以视差误差再除以焦距和基线的乘积。写成式子就是 ΔZ Z² × Δd / (f × B)。这个平方项是最要命的距离翻倍同样的视差误差导致的深度误差变成四倍。拿一组数字算给你看。f 是 1159 像素B 是 60 毫米假设匹配算法能做到 0.2 像素的视差精度距离 Z视差 d深度误差0.5 m139.1 px约 0.72 mm1 m69.5 px约 2.9 mm2 m34.8 px约 11.5 mm4 m17.4 px约 46 mm8 m8.7 px约 184 mm看到最后一行就知道为什么远距离的三维重建必须上别的方案了。8 米处接近 20 厘米的深度误差做粗定位可以做精细抓取完全没戏。我见过一些项目在需求里写「要能测 10 米外的物体精度 1 厘米」这种需求用双目基本是不可能完成的得换成激光或者别的测距方式。所以每次立项我都会把这张表算一遍然后反过来问这个精度要求在目标距离上需要多少像素的视差精度如果算出来要求 0.05 像素那就要考虑是不是该换方案了因为亚像素匹配做到 0.05 像素非常困难尤其是纹理弱的表面。3.4 双目绕不开的纹理依赖视差匹配的本质是在左右图里找同一个物点靠的是局部图像特征的相似性。如果一块区域在图像里是纯白一片没有任何灰度变化匹配算法就找不到对应点深度就是空的。这是双目的天然限制所有改进算法都只是在缓解不能根除。缓解手段有几个方向。第一是加主动散斑投影也就是前面说的主动红外双目把纹理「造」出来。第二是加额外的结构光模块做辅助。第三是用深度学习匹配网络这类方法在弱纹理区域能靠上下文推断出一部分深度但代价是算力需求高而且推断出来的深度不一定可靠做测量要谨慎。如果场景里同时存在大面积的白色墙面和大面积的黑色地毯双目方案就需要认真评估了。我一般会建议客户先拍一组现场图用简易的立体匹配算法跑一遍看看空洞率空洞率超过三成就要考虑换路线或者加补光。4. 双目相机标定实操全流程4.1 靶标与硬件准备标定板的选择直接决定标定质量上限。棋盘格是最常用的OpenCV 支持完善角点提取稳定。ChArUco 板适合需要局部遮挡的场景因为它每个标记都能独立识别但代价是角点提取的精度在边缘不如棋盘格。圆点板对模糊和光照更鲁棒但圆心提取存在偏心误差需要额外的畸变校正入门阶段不推荐。材质上纸打印的板子只适合验证流程。真要出精度的建议直接买氧化铝或者陶瓷基底的标定板平整度好、不易变形。如果预算有限用激光打印后贴在浮法玻璃或者厚铝板上贴的时候用喷胶均匀压平别用双面胶双面胶的厚度不均匀。还有一件很容易被忽略的事打印机有缩放。我见过同一次打印出来的板子实测方格边长比标称值小了百分之二官方规格写 25 毫米实测 24.5 毫米。这百分之二的尺度误差会直接变成深度图的系统性比例偏差1 米处就差 2 厘米。所以拿到标定板第一件事是用卡尺量十个格子的总长度算出实际边长填进程序里。标定板的平整度要求通常在 0.05 毫米以内用手按一下能感觉到弯曲的板子基本不能用。我吃过这个亏一块亚克力板在阳光下晒了一下午变形后标出来的参数怎么调都不对。4.2 采集阶段的姿态与覆盖要求采集这步是整个标定里最花时间、也最考验耐心的。规则说起来简单让标定板在图像里尽可能覆盖多的位置和姿态但真做起来很容易偷懒。我的经验是分三轮采集。第一轮让标定板在图像中心区域绕两个倾斜轴各摆大约正负 20 度到 40 度同时绕自身法线转 0 度、45 度、90 度每一组拍一张大概 12 到 15 张。第二轮把标定板移到图像的四个角和四条边每个位置拍两三个姿态重点是把畸变最严重的边缘区域覆盖到这部分对畸变系数的估计贡献最大。第三轮拍几个极端姿态比如大角度倾斜、贴近相机、远离相机用来约束焦距和主点。双目还有额外要求。左右图像必须同时拍到完整的标定板不能一边清楚一边模糊。两台相机的曝光要固定最好用硬件同步触发软触发在运动场景下抖动能有几毫秒标定板上如果有运动就废了。如果标定板是静止的、相机也是静止的软同步问题不大但曝光必须手动锁定自动曝光会让左右图的亮度不一致直接影响匹配。模糊是另一个杀手。手抖、对焦不实、标定板移动中抓拍都会导致角点定位偏差。我一般会在采集时放大看角点位置如果角点周围有明显的拖影这张图就直接丢。宁可多拍二十张也不要留一张模糊的。4.3 标定代码骨架与参数输出下面是 OpenCV 的标定流程骨架我加了注释说明每一步在干什么。import cv2 import numpy as np import glob PATTERN (9, 6) # 内角点的列数、行数不含边框 SQUARE 24.6 # 实测方格边长单位 mm criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 40, 1e-4) # 构造标定板在自身坐标系下的三维点Z 恒为 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) * SQUARE obj_points, img_points_l, img_points_r [], [], [] left_files sorted(glob.glob(left/*.png)) right_files sorted(glob.glob(right/*.png)) for lf, rf in zip(left_files, right_files): img_l cv2.imread(lf, cv2.IMREAD_GRAYSCALE) img_r cv2.imread(rf, cv2.IMREAD_GRAYSCALE) flags cv2.CALIB_CB_ADAPTIVE_THRESH cv2.CALIB_CB_NORMALIZE_IMAGE ok_l, cor_l cv2.findChessboardCorners(img_l, PATTERN, flags) ok_r, cor_r cv2.findChessboardCorners(img_r, PATTERN, flags) if not (ok_l and ok_r): continue # 有一边没找全直接丢帧 cor_l cv2.cornerSubPix(img_l, cor_l, (11, 11), (-1, -1), criteria) cor_r cv2.cornerSubPix(img_r, cor_r, (11, 11), (-1, -1), criteria) obj_points.append(objp) img_points_l.append(cor_l) img_points_r.append(cor_r) size img_l.shape[::-1] # 单目标定取内参初值 _, K1, D1, _, _ cv2.calibrateCamera(obj_points, img_points_l, size, None, None) _, K2, D2, _, _ cv2.calibrateCamera(obj_points, img_points_r, size, None, None) # 立体标定固定内参只优化外参 ret, K1, D1, K2, D2, R, T, E, F cv2.stereoCalibrate( obj_points, img_points_l, img_points_r, K1, D1, K2, D2, size, criteriacriteria, flagscv2.CALIB_FIX_INTRINSIC ) print(stereo rms , ret) print(baseline mm , np.linalg.norm(T))这里有个选择值得说flags 用 CALIB_FIX_INTRINSIC 还是全优化。固定内参的好处是结果稳定、可复现缺点是单目标定的误差会被带进外参。全优化的好处是整体一致性更好缺点是容易过拟合尤其是标定图数量少的时候。我一般先用固定内参跑一遍看 RMS如果超过 0.5 像素再改成全优化对比一下如果全优化也没明显改善问题多半出在图像质量本身不是优化策略。标定完之后还要做极线校正把左右图像映射到共面且极线水平的位置这样立体匹配就只需要在水平方向搜索了。R1, R2, P1, P2, Q, roi1, roi2 cv2.stereoRectify( K1, D1, K2, D2, size, R, T, alpha0 ) map1x, map1y cv2.initUndistortRectifyMap(K1, D1, R1, P1, size, cv2.CV_32FC1) map2x, map2y cv2.initUndistortRectifyMap(K2, D2, R2, P2, size, cv2.CV_32FC1)alpha 参数控制校正后的视野范围取 0 会裁剪掉黑边视野变窄但有效像素利用率高取 1 保留全部像素但有黑边。我一般取 0然后在机械安装上留出视野余量。4.4 重投影误差怎么读cv2.stereoCalibrate 返回的那个 RMS 是整体重投影误差单位是像素。它统计的是三维标定点投影回图像后和实际检测到的角点之间的平均距离。这个值越小越好但也不是越小越对。我的判断标准是这样的低于 0.3 像素优秀可以放心用0.3 到 0.5 像素合格大多数应用够用0.5 到 1.0 像素勉强需要检查是不是有坏帧或者靶标问题超过 1.0 像素基本不能用必须重标。但只看整体 RMS 是不够的还得看分布。下面这段代码把每一帧的误差单独算出来能快速定位问题帧。def per_frame_error(obj_points, img_points, K, D, R, T): errs [] for op, ip in zip(obj_points, img_points): proj, _ cv2.projectPoints(op, R, T, K, D) errs.append(cv2.norm(ip, proj, cv2.NORM_L2) / len(proj)) return np.array(errs)理想情况下每帧误差应该在均值附近小幅波动。如果某一帧的误差是其他帧的三倍以上那这一帧大概率有问题可能是标定板边缘被截断可能是轻微模糊也可能是角点提取时被反光区域带偏了。我一般会把误差最大的两三帧画出来人工看一眼多数时候能一眼看出问题。还有一点RMS 低不代表深度一定准。RMS 反映的是重投影一致性如果标定板本身不平、或者棋盘格尺寸测量有系统偏差RMS 可能很漂亮但深度尺度是错的。所以我每次标定完都会做一个验证拿一个已知尺寸的物体比如一根校准过的量块放在工作距离上测一下它的尺寸误差在百分之一以内才算通过。5. 标定不合格角点的识别与剔除5.1 哪些角点属于不合格这里要先破除一个误区。很多人以为角点提取是确定性的找到就是找到了找不到就是找不到。实际上 findChessboardCorners 返回的是「我找到了 54 个角点」至于这 54 个点各自有多可信它基本不告诉你。cornerSubPix 在此基础上做亚像素细化但它也只是朝局部梯度最大的方向迭代如果初始位置偏了它会收敛到一个错误的局部极值上而且收敛结果看起来很合理。我在实际项目里遇到的不合格角点大致分三类。第一类是成像质量导致的定位偏差比如过曝区域的角点周围全是白的梯度信息几乎为零亚像素细化会随机漂移欠曝区域同理噪点会干扰梯度方向运动模糊的角点会拉长成一个椭圆细化结果偏向模糊方向。第二类是几何异常导致的偏差比如标定板被手指挡住一角、板子边缘超出图像、板子弯曲或反光导致某几行角点整体偏移。这类问题会表现为棋盘格的行列不再是一条直线用直线拟合残差能很好地量化。第三类是左右视图配合问题比如左右曝光差太多导致一边角点位置偏移、软触发不同步导致左右帧的内容不是同一时刻的、匹配时把一个点配到了相邻的点上。这类问题在单目里看不出来只有做立体约束才会暴露。第三类问题尤其隐蔽。我见过一次标定结果怎么调都不理想最后发现是左右相机曝光差了将近两档右图的角点在暗区里整体往亮的方向偏了半个像素单看右图的重投影误差只有 0.2 像素但立体标定的 RMS 一直卡在 0.9 像素下不来。5.2 三道筛选关卡的具体实现我的做法是给每个角点算三个指标任意一个超标就标记为不合格。第一个指标是对比度衡量角点周围的明暗对比是否足够。棋盘格角点有个很好的特性它周围的四个象限是两两对角的明暗分布相邻象限的灰度差应该很大对角象限的灰度应该接近。def corner_contrast(gray, pt, half5): 用四象限最小灰度差衡量角点锐度单位是灰度级 patch cv2.getRectSubPix(gray, (2 * half 1, 2 * half 1), (float(pt[0]), float(pt[1]))).astype(np.float32) q1 patch[0:half, 0:half].mean() q2 patch[0:half, half 1:].mean() q3 patch[half 1:, 0:half].mean() q4 patch[half 1:, half 1:].mean() # 四个相邻象限对都应该有明显差异取最小值最保守 return min(abs(q1 - q2), abs(q2 - q4), abs(q4 - q3), abs(q3 - q1))第二个指标是网格直线性残差。同一行的角点在理想情况下应该共线同一列也是。用最小二乘拟合出行方向和列方向的直线算每个点到直线的最大距离这个值能很好地反映局部畸变和遮挡。def grid_residual(corners, pattern): 返回每个角点在行列拟合直线上的最大偏差单位像素 pts corners.reshape(pattern[1], pattern[0], 2).astype(np.float32) res np.zeros((pattern[1], pattern[0]), np.float32) def dist_to_line(line_pts, query_pts): vx, vy, x0, y0 cv2.fitLine(line_pts, cv2.DIST_L2, 0, 0.01, 0.01).flatten() n np.hypot(vx, vy) vx, vy vx / n, vy / n return np.abs((query_pts[:, 0] - x0) * vy - (query_pts[:, 1] - y0) * vx) for r in range(pattern[1]): res[r] np.maximum(res[r], dist_to_line(pts[r], pts[r])) for c in range(pattern[0]): res[:, c] np.maximum(res[:, c], dist_to_line(pts[:, c], pts[:, c])) return res.reshape(-1)第三个指标是极线残差只在立体标定里用。用左右角点算一个基础矩阵然后把左图角点投影成右图上的极线看对应的右图角点离这条极线有多远。距离过大说明这对点要么是误匹配要么是时间不同步或者畸变没校正好。def epipolar_residual(F, pts_l, pts_r): 每对匹配点到极线的距离单位像素 lines cv2.computeCorrespondEpilines( pts_l.reshape(-1, 1, 2), 1, F).reshape(-1, 3) res [] for (a, b, c), (x, y) in zip(lines, pts_r.reshape(-1, 2)): res.append(abs(a * x b * y c) / np.hypot(a, b)) return np.array(res)三个指标算完合成一个布尔掩码def check_frame(gray_l, gray_r, cor_l, cor_r, pattern, FNone, contrast_th12.0, grid_th0.8, epi_th1.5): pts_l cor_l.reshape(-1, 2) pts_r cor_r.reshape(-1, 2) c_l np.array([corner_contrast(gray_l, p) for p in pts_l]) c_r np.array([corner_contrast(gray_r, p) for p in pts_r]) g_l grid_residual(cor_l, pattern) g_r grid_residual(cor_r, pattern) bad (c_l contrast_th) | (c_r contrast_th) \ | (g_l grid_th) | (g_r grid_th) if F is not None: e epipolar_residual(F, pts_l, pts_r) bad | (e epi_th) return bad, 1.0 - bad.mean()在主循环里这样用gray_l cv2.imread(lf, cv2.IMREAD_GRAYSCALE) ok, bad, ratio True, None, 1.0 F, _ cv2.findFundamentalMat(cor_l, cor_r, cv2.FM_RANSAC, 1.0, 0.999) bad, ratio check_frame(gray_l, gray_r, cor_l, cor_r, PATTERN, F) if ratio 0.85: # 保留率太低整帧丢弃 continue keep ~bad obj_points.append(objp[keep]) img_points_l.append(cor_l.reshape(-1, 2)[keep].reshape(-1, 1, 2)) img_points_r.append(cor_r.reshape(-1, 2)[keep].reshape(-1, 1, 2))注意 OpenCV 的标定接口允许每一帧的点数不同所以直接删掉不合格点是安全的不需要补齐。5.3 剔除强度的取舍与效果验证阈值定在哪里是个需要实测的问题。定太松坏点留下来了标定精度上不去定太紧好点被误删样本量不足反而让参数估计不稳定。我在一套 60 毫米基线的红外双目模组上做过一轮阈值扫描记录如下可以作为调参起点。对比度阈值保留率立体 RMS1 米处深度噪声不剔除100%0.51 px约 8 mm897%0.34 px约 5.2 mm1292%0.24 px约 3.5 mm2078%0.26 px约 3.8 mm3055%0.45 px约 7.1 mm阈值从 12 抬到 20保留率掉到 78%RMS 反而略微变差说明这时候删掉的主要是正常点样本量不足导致的方差上升超过了坏点减少的收益。阈值 30 那档更明显RMS 直接退化到 0.45 像素因为很多帧的角点数已经少到不足以约束畸变系数了。所以我的建议是把对比度阈值设在 10 到 15 之间网格残差阈值设在 0.6 到 1.0 像素之间极线阈值设在 1.0 到 2.0 像素之间然后通过保留率来交叉验证。经验上剔除后整体保留率在 85% 到 95% 之间是比较健康的低于 80% 说明采集质量本身有问题应该回去重新采集而不是硬靠算法救。剔除后的效果验证不能只看 RMS还要看几个实际指标。一是基线的重复性也就是把同一套数据分成两半分别标定比较两次得到的基线长度差多少。我这边剔除前是正负 0.9 毫米剔除后压到正负 0.15 毫米。二是深度噪声用固定距离的平面做一百帧看深度的标准差。三是视差图的一致性看无纹理区域之外的像素是否还有大量孤立噪点。这三个指标一起看才能确认标定是真的改善了而不是 RMS 变好看了。6. 常见问题与排查速查表6.1 深度图异常问题定位深度图大面积空洞是最常见的问题成因按概率排下来大概是无纹理或低反射率表面、超出有效距离、视差超出搜索范围、曝光不当。排查顺序我一般是从近到远先把物体放到半米处看看有没有深度如果有说明硬件和标定没问题是距离导致的如果没有换一张有丰富纹理的纸看看如果这时候有深度了说明是纹理问题。深度图上物体边缘有一圈悬空的噪点也就是飞点这多半是匹配窗口跨越了深度不连续区域导致的。解决办法有三个方向加边缘膨胀操作把前景往外扩几个像素再做深度过滤用左右一致性检查也就是把左图的视差投射到右图再反投回来不一致的点标为无效用子像素插值加置信度过滤把置信度低的点直接丢。深度图上出现周期性的条纹或者波纹通常是投影图案和相机采样频率之间的干涉学名叫莫尔条纹。调整投影图案的周期、加轻微散焦让图案变模糊一点或者做多频投影融合都能缓解。这类问题在结构光方案里比在双目方案里更常见。深度值整体偏大或偏小也就是尺度不对基本可以锁定是标定的问题。最常见的原因就是标定板方格的实际边长和程序里填的值不一致其次是基线在标定后被机械结构改变了。所以标定完我会在机构上打防松标记任何拆装之后都要重新验证。6.2 标定与匹配类问题定位标定 RMS 一直降不下来按下面这个顺序查先看有没有明显模糊的帧全部人工过一遍再确认左右曝光是否一致把两张图的亮度均值打印出来对比再检查标定板的姿态覆盖是不是所有帧都集中在图像中心最后确认方格边长是否实测过打印机缩放是最容易被忽略的一项。立体匹配出来的视差图整体偏移表现为点云比实际物体厚或者薄一层这通常是极线校正没做好或者左右图的时间戳对不齐。如果是静止场景时间戳问题不大如果有运动一定要上硬件同步。双目匹配在某个区域总是匹配失败而左右图看起来纹理都正常这时候要检查那块区域是不是落在视差搜索范围之外。把视差图的可视化打开看看失败区域对应的视差值是不是贴着搜索范围的上限或者下限。是的话就要调整范围或者接受这块区域无效。还有一个容易被忽略的问题镜头畸变模型选错。普通镜头用五参数模型够了广角镜头需要八参数鱼眼镜头必须用专门的鱼眼模型。用错模型的典型表现是图像边缘的角点拟合不上重投影误差在图像中心很小、在角落很大呈现明显的径向分布。这时候换个模型重标往往能立竿见影。6.3 速查表现象最可能的原因快速验证方法处理方向深度图大面积空洞无纹理或超出距离换带纹理的纸在 0.5 m 处测加散斑投影或缩短距离边缘悬空噪点匹配窗口跨深度不连续放大看边缘点云一致性检查加边缘膨胀深度周期性条纹投影与采样干涉变换投影周期观察多频融合或轻微散焦深度整体比例偏差标定板尺寸填错量块实测尺寸对比重测方格边长重标RMS 卡在 0.5 px 以上曝光不一致或模糊帧打印每帧亮度与误差锁曝光剔除坏帧边缘误差大中心正常畸变模型不匹配看残差是否径向分布换八参数或鱼眼模型匹配区域整体偏移极线校正或同步问题静止与运动场景对比重做校正或加硬触发同一场景重复测不一致温度漂移或机构松动打表测基线变化加温补或结构加固7. 选型建议与踩坑体会7.1 按场景对照选型把前面这些拼起来选型其实可以简化成几个问题。工作距离在 1 米以内、要求亚毫米精度、环境光可控直接上结构光这是它的绝对主场。工作距离在 1 到 3 米、需要高帧率、场景里有大量无纹理表面ToF 更合适代价是功耗和成本。要在室内外切换、预算有限、希望一套设备吃多个场景主动红外双目是最省心的选择。如果场景是纯室外、阳光充足、表面有自然纹理被动双目成本最低。但要注意室外场景的深度图质量受天气影响极大晴天和阴天的效果能差一倍做产品要把这个波动算进指标里。我在一个户外项目里就吃过亏实验室调好的参数到了现场中午完全不能用最后靠加装遮光罩和调整工作时间窗口解决的。还有一个维度是维护成本。结构光的投影器和相机之间的相对位置必须保持稳定一旦拆装就要重标双目只需要保证两个相机之间的相对位置投影器坏了不影响几何精度换一个就能用ToF 是一体化模组基本免维护但坏了要整换。产线上批量部署的话这个差异会显著影响长期成本。7.2 几个我不想再踩的坑第一个坑是盲信出厂标定。几乎所有低价模组的出厂标定都只能算「能用」做演示没问题做测量差得远。我现在拿到任何一台新设备第一件事都是用自己的标定板跑一遍和出厂参数对比如果基线差超过 0.5 毫米或者焦距差超过百分之一就以自己标的为准。第二个坑是标定时忘了锁曝光。自动曝光在标定过程中会根据标定板的白色区域调整增益导致每张图的亮度都不一样角点提取的系统偏差也跟着变。锁曝光之后我的 RMS 从 0.6 降到 0.3就是这么直接。第三个坑是忽略温度漂移。铝制结构的支架在温差十几度的环境里基线能有零点几毫米的变化对应到 1 米处就是几毫米的深度偏差。高精度应用要么做温补要么用低膨胀材料要么就接受这个误差并把工作温度范围写进规格。第四个坑是只盯着 RMS 不看实际效果。RMS 是重投影一致性指标它不检查绝对尺度。我见过 RMS 0.18 像素但深度尺度错了百分之三的标定结果原因是标定板尺寸填错了。所以务必用已知尺寸的量块做端到端验证这一步花十分钟能省掉后面几天的排查。第五个坑是把角点剔除当成万能药。剔除确实有用能把我手上这套模组的 RMS 从 0.5 降到 0.24但它的作用是「把已经采好的数据里的坏点挑出来」如果采集阶段本身就拍糊了、曝光乱了、姿态覆盖不全再好的剔除算法也救不回来。采集质量决定上限算法只能逼近这个上限。最后分享一个我一直在用的小习惯给每次标定建一个目录把原始图、标定板实测尺寸、曝光参数、标定脚本、输出参数、验证量块的测量结果全部存进去目录名带上日期和版本号。三维视觉的项目里标定参数是会随硬件状态漂移的出问题的时候能翻出上一次的完整记录来对比排查效率能高好几倍。这个习惯看起来笨但真到关键时刻它比任何调试技巧都管用。