资讯动态

双目立体标定与立体校正:从极线行差到可交付深度

发布时间:2026/10/24 3:00:55 来源:尧图企业网站定制
简介面向计算机视觉入门与双目测量实践者的立体标定与校正工程包基于 VS2013 与 OpenCV 3.0围绕左右相机拍摄的棋盘格标定图像完成双目标定、立体校正为后续立体匹配与三维重建准备可直接复用的基础工程。压缩包共 251 个文件约 29.64MB以 140 个 hpp 与 55 个 h 头文件构成 OpenCV 依赖与接口主体另含 20 张 png 标定与校正结果图、4 个 lib 与 3 个 dll 运行库、2 个 yml 参数文件以及解决方案、项目配置和 1 个 cpp 主程序等工程文件便于对照检查标定参数与校正效果。已有 1742 人学习下载说明该主题在课程实验与项目起步阶段需求稳定。读者可从中获得完整的双目标定流程样例、相机参数与校正映射的保存形式以及从图像采集到极线校正的工程组织方式适合用来搭建实验环境、排查标定误差并延伸到立体匹配环节。1. 一套 12 像素行差的模组把“双目”打回原形某开发者把一套自研双目模组递过来只说了句“视差图看着像雪花”。两张 1280×720 的图摊开同一颗螺丝在左图 y412、右图 y424极线上差了 12 个像素——这套双目立体视觉系统连最基本的行对齐都没做到往后算出来的深度全是错的。双目立体标定与立体校正干的就是这件事先解出两个相机的内参、畸变和它们之间的旋转平移再用 Bouguet 把两幅图重投影到同一成像平面让同名点只差水平方向的一个视差。它适合已经在做双目测距、被测距精度折磨过的从业者把行差从十几个像素压到 1 个像素以内深度才谈得上可信。2. 采集环节的硬指标把误差挡在成像之前标定精度上限在按下快门那一刻就定了。后面stereoCalibrate再怎么调 flag也只能在既有的角点坐标上做最小二乘救不回来被纸张翘曲、镜头呼吸、卷帘快门扭曲过的像素。这一章把工装、采集、单目预标定三段拆开每一段都给出可直接抄的判据。2.1 标定板选型平整度比角点数更要命棋盘格在这个链路上仍然是最稳的选择理由不是“大家都用”而是角点作为两条直线的交点本身具备亚像素级的可定位性而圆点板的圆心在透视投影下会产生偏心误差ellipse eccentricity标定板法线与光轴夹角超过 30° 后这个偏差会明显放大需要额外补偿。棋盘格的代价是它极度依赖板面平整纸基贴在 3mm 铝板上、用喷胶压平比拿一张 A4 纸直接手持强一个数量级。我见过最典型的翻车是拿亚克力夹着打印纸——反光和双折射一起上角点亚像素坐标能抖 0.3 个像素。方格边长必须实测。打印机有 0.5%~1% 的缩放直接按文件上的 25.0mm 填进去fx就会系统性偏 1%深度同比例偏 1%在 3 米处就是 30mm 的固定误差。做法是用卡尺量 10 个格距取平均再除以 10。板子的内角点数也别贪多9×6 到 11×8 覆盖主流视场就够板子太小、画面里只占一小块角点噪声在整体残差里的权重反而被放大。板型典型内角点推荐工作距离适用场景注意点纸基棋盘格贴铝板9×6 / 11×80.3~2 m常规双目模组、量产抽检粘贴必须压平边缘不能翘印刷菲林棋盘格11×80.2~1 m小视场、近距表面易划伤需背光或漫射打光陶瓷棋盘格9×60.2~3 m高精度、需长期复用重夹具要刚性非对称圆点板4×110.3~2 m大畸变广角镜头需偏心补偿角度受限选型还有个常被忽略的约束标定板在图像里的占比。经验阈值是板子短边至少覆盖画面短边的 1/3低于这个值畸变模型的边缘约束基本没被激励起来k1、k2会往奇怪的方向跑表现为“中心区重投影误差很小边角爆到 1 个像素以上”。2.2 采集策略15 对起步姿态要“绕三轴”数量上单目 15~25 对、双目 15~30 对是稳妥区间低于 10 对时R、T的重复性会肉眼可见地变差换个随机种子重跑一遍基线能差 2mm。真正决定质量的是姿态覆盖标定板法线与光轴夹角要有 0°、±20°、±40° 三档绕 X、Y、Z 三个轴都得有转动并且板子要出现在画面的四角与中心。全部拍成正对画面的平移等于只给了fx、fy、cx、cy约束畸变和主点偏移完全没被激励。同步问题分两种。静态场景下相机不动、标定板不动左右相机分两次拍同一位置是可以接受的但只要画面里有运动就必须硬件触发同步或者至少用同一路信号同时触发两个相机。卷帘快门更麻烦运动物体会产生行间曝光差左右图对同一块区域的成像时刻不同角点坐标本身就带偏差这时候谈 0.2 像素的重投影误差毫无意义。采集阶段我一般强制锁死这几项手动曝光、手动增益、手动白平衡、手动对焦对焦环用胶带缠住。自动对焦是双目标定的头号玄学来源——标定完镜头自己跑了一次内参全废。存储一律用 PNGJPEG 的块效应会在角点邻域产生振铃亚像素定位直接受影响。下面这段是抓图后的预筛模糊帧和角点漏检帧在进标定之前就该扔掉别指望calibrateCamera帮你兜底。import cv2 import numpy as np import glob PATTERN (9, 6) # 内角点列数、行数必须是板子上内部的交点 BLUR_TH 60.0 # Laplacian 方差阈值按实际曝光调太低会误杀 def quality_gate(path): img cv2.imread(path, cv2.IMREAD_GRAYSCALE) if img is None: return False, read_fail, 0.0 score cv2.Laplacian(img, cv2.CV_64F).var() if score BLUR_TH: return False, blur, score ok, _ cv2.findChessboardCornersSB(img, PATTERN) if not ok: ok, _ cv2.findChessboardCorners( img, PATTERN, flagscv2.CALIB_CB_ADAPTIVE_THRESH | cv2.CALIB_CB_NORMALIZE_IMAGE) return ok, (ok if ok else no_corner), score for p in sorted(glob.glob(raw/left/*.png)): ok, reason, s quality_gate(p) print(f{p}\t{reason}\t{s:.1f})Laplacian方差是最便宜的清晰度代理阈值 60 是经验值白天室内补光下大多数正常帧在 150~400 之间运动模糊帧往往掉到 30 以下。findChessboardCornersSB是 OpenCV 4.1 之后引入的扇区化检测器自带亚像素细化比老接口稳代价是对低对比度图像略敏感所以这里留了findChessboardCorners作为兜底。返回值里的reason一定要打印出来直接筛出一批空目录却不知道为什么是最容易浪费半天的情形。2.3 先跑单目把每组的内参和 RMS 摊开看双目之前必须先把单目做干净。顺序是单目标定拿到K1/D1、K2/D2再进stereoCalibrate并且用CALIB_FIX_INTRINSIC锁住只优化R、T。反过来让双目重投影误差去同时优化 10 个内参 6 个外参 畸变在 20 对图像的规模下自由度明显不够结果就是“误差很好看实际用起来飘”。import cv2 import numpy as np import glob PATTERN (9, 6) SQUARE 24.83 # 实测格距 mm不是打印文件上的标称值 criteria_sub (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-3) 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 def collect(folder): objpoints, imgpoints, size [], [], None for p in sorted(glob.glob(folder)): img cv2.imread(p, cv2.IMREAD_GRAYSCALE) if img is None: continue size img.shape[::-1] # (w, h)必须全序列一致 ok, c cv2.findChessboardCornersSB(img, PATTERN) if not ok: continue c cv2.cornerSubPix(img, c, (11, 11), (-1, -1), criteria_sub) objpoints.append(objp.copy()) imgpoints.append(c.reshape(-1, 1, 2)) return objpoints, imgpoints, size def calibrate_one(folder, tag): objp_list, imgp_list, size collect(folder) print(f[{tag}] frames{len(objp_list)} size{size}) rms, K, D, rvecs, tvecs cv2.calibrateCamera( objp_list, imgp_list, size, None, None, flagscv2.CALIB_FIX_K3, # 普通镜头先关 k3避免过拟合 criteria(cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 100, 1e-6)) print(f[{tag}] rms{rms:.4f}px fx{K[0,0]:.2f} fy{K[1,1]:.2f} fcx{K[0,2]:.2f} cy{K[1,2]:.2f}) print(f[{tag}] dist{D.ravel()}) return K, D, rms, size K1, D1, r1, size1 calibrate_one(raw/left/*.png, left) K2, D2, r2, size2 calibrate_one(raw/right/*.png, right) assert size1 size2, 左右图分辨率不一致后面 stereoRectify 会直接报错几个参数值得掰开说。SQUARE用实测值这一步省不了。CALIB_FIX_K3的意思是固定三阶径向畸变系数为 0普通工业镜头 5 参数模型足够硬解 8 参数会让边角噪声被k3吸收表现为“标定 RMS 更小但校正后边缘更歪”。cornerSubPix的窗口(11,11)对应棋盘格在图像里的实际像素尺度格距在图像上大约 60~120 像素时用 11 合适如果板子拍得很近、格距超过 200 像素窗口要放大到 15 或 21否则亚像素迭代会收敛到错误的局部极小。RMS 的判读不能只看一个数字。0.1~0.2 像素属于优秀0.2~0.5 可用超过 0.6 就该回去查采集。更关键的是分布把rvecs、tvecs加上每个角点的残差打出来如果整体 RMS 是 0.25 但某一对图像单帧误差 1.8那这一对基本可以判定角点顺序错乱或板子动了删掉重拍比调参划算。3. 双目标定的自由度与 flagstereoCalibrate该怎么喂单目给的是每个相机“自己看世界”的方式双目要的是两个相机之间的相对位姿。这一步的输入除了左右角点还有已经锁定的内参输出的R、T才是后面立体校正的直接原料。基线T的量纲由SQUARE决定用 mm 就全程 mm别中途换成米。3.1 从 556 个自由度说起stereoCalibrate优化的目标函数是所有棋盘格角点在左右图像上的重投影误差之和。待优化量包括左相机内参 4 个fx、fy、cx、cy、右相机内参 4 个、畸变系数若干以及 6 个外参——R三个旋转角、T三个平移分量。用CALIB_FIX_INTRINSIC后内参和畸变被冻结只剩下 6 个外参在动这才是 20 对图像撑得住的自由度规模。很多教程跳过E、F直接讲校正其实这两个矩阵是判断“标定有没有做歪”的好工具。E [T]_x RF K2^-T E K1^-1本质是极线约束的代数形式左图上的点x1在右图上一定落在极线F x1上。立体校正做的事情就是把这条极线强行掰成水平线。如果F算出来的极线斜率和图像里实测的同名点连线斜率对不上说明R、T已经不可信了此时无论stereoRectify传什么alpha都救不回来。外参还有一个隐含前提R、T描述的是“右相机坐标系相对左相机坐标系”的变换方向搞反的后果是基线符号错、视差全负。判断方法很直接——np.linalg.norm(T)应该等于你实测的两个镜头光心间距量一下模组机械尺寸对一对差超过 5% 就别往下走了。3.2 flag 与 criteria三组常用组合flags 组合前提适用情形风险CALIB_FIX_INTRINSIC已单独标定左右默认首选最稳单目内参本身有偏时会传导CALIB_USE_INTRINSIC_GUESS给了合理初值内参未知但可信初值初值差 20% 以上容易发散CALIB_SAME_FOCAL_LENGTH同型号同批次镜头量产快速标定实际焦距有差异时会引入偏差CALIB_FIX_PRINCIPAL_POINT主点接近图像中心图像已做过去畸变主点真偏了会锁死错误criteria我一般不吝啬迭代次数TERM_CRITERIA_EPS TERM_CRITERIA_MAX_ITER、100 次、1e-6。LM 迭代在这个规模下几十毫秒就收敛给足次数没有代价。import cv2 import numpy as np criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 100, 1e-6) retval, K1, D1, K2, D2, R, T, E, F cv2.stereoCalibrate( objp_list, # 与单目共用的同一份物点 imgp_list_l, imgp_list_r, # 左右角点顺序必须一一对应 K1, D1, K2, D2, image_size, flagscv2.CALIB_FIX_INTRINSIC, criteriacriteria) baseline float(np.linalg.norm(T)) print(fstereo rms{retval:.4f}px baseline{baseline:.2f}mm) print(T , T.ravel()) print(R , R)逻辑上这个调用做两件事先用E的分解给R、T一个初值再用光束法平差把所有角点残差压到最小。retval是整体 RMS判据和单目一致但要注意双目的 RMS 天然比单目略高因为它是两份残差合在一起统计的——单目各自 0.15双目 0.25~0.3 属于正常。T打出来之后立刻和卡尺量的机械基线对一下这是最省事的一致性检查。3.3 逐对误差别让一对烂图毁掉整个 RMS整体 RMS 是个平均值一对误差 3 像素的图混在 20 对里只把整体抬高 0.15很容易被忽略但它对R、T的拉扯是实打实的。所以要写一版逐对重投影误差把可疑帧揪出来。def per_view_error(objp_list, imgp_list, K, D, rvecs, tvecs): errs [] for i, (op, ip) in enumerate(zip(objp_list, imgp_list)): proj, _ cv2.projectPoints(op, rvecs[i], tvecs[i], K, D) e np.linalg.norm(ip.reshape(-1, 2) - proj.reshape(-1, 2), axis1) errs.append((i, float(e.mean()), float(e.max()))) return errs for tag, errs in ((L, per_view_error(objp_list, imgp_list_l, K1, D1, rvecs1, tvecs1)), (R, per_view_error(objp_list, imgp_list_r, K2, D2, rvecs2, tvecs2))): bad [e for e in errs if e[1] 0.5] # 单帧均值超 0.5px 视为可疑 print(tag, suspect frames:, bad)projectPoints用的是标定出来的外参把物点投回图像残差就是“模型与观测的差”。阈值 0.5 像素是我常用的分界线超过就人工看一眼原图。常见成因有三类角点顺序不一致findChessboardCorners对旋转 180° 的板子会换起始角点、板子在两次拍摄之间被碰了、图像边缘畸变过大导致cornerSubPix收敛到错误的角点。第一类最隐蔽删掉重拍是最快的解法别想着写代码自动纠正角点顺序——那是个更深的坑。4. 立体校正把两幅图掰到同一成像平面标定给的是“两个相机在哪”校正要的是“把两幅图重投影成什么样”。OpenCV 的stereoRectify实现的是 Bouguet 算法核心思路是把两个相机之间的旋转R拆成各转一半让左右光轴平行同时用新的投影矩阵P1、P2把两幅图的行对齐。4.1R1、R2、P1、P2、Q各自的职责R1、R2是两个虚拟相机的旋转矩阵作用在原相机坐标系上把光轴转到共同方向。Bouguet 的做法是对R做旋转矩阵开方通过Rodrigues转成轴角再取半左右各承担一半这样两幅图的畸变重采样量接近避免一侧被拉伸得过狠。P1、P2是校正后的虚拟相机内参矩阵形式是K_new [R | t]。关键点是它强制两个虚拟相机的主点相同CALIB_ZERO_DISPARITY时这是极线严格水平的代数保证——如果两个虚拟相机的cy不一样同名点就会带上固定的行偏移前面所有努力白费。P1的[0,0]元素就是校正后的等效焦距后面算深度要用它不能用原始K1[0,0]。Q是视差到三维的映射矩阵reprojectImageTo3D直接吃它。它的第 4 列和第 3 行藏着基线信息所以一旦Q生成后改了图像尺寸或裁剪了 ROIQ必须同步重新算否则深度会整体缩放。alpha参数决定视场保留策略alpha0只保留两幅图的有效重叠区边缘黑边最少代价是丢掉一部分视场alpha1保留全部原始像素黑边最多中间值折中。做测距我一般用 0因为保留下来的每一个像素都是双目共视的多余视场对深度没贡献。4.2 映射表落盘CV_16SC2与一次生成、长期复用initUndistortRectifyMap生成的是查表运行时用remap查表比每帧重算快一到两个数量级。生产环境一定是标定一次、生成一次映射表、存盘、运行时直接加载。import cv2 import numpy as np # alpha0只保留共视区CALIB_ZERO_DISPARITY 保证极线严格水平 R1, R2, P1, P2, Q, roi1, roi2 cv2.stereoRectify( K1, D1, K2, D2, image_size, R, T, flagscv2.CALIB_ZERO_DISPARITY, alpha0.0, newImageSizeimage_size) f_rect float(P1[0, 0]) b_rect float(abs(P2[0, 3] / P2[0, 0])) print(frectified f{f_rect:.2f}px baseline{b_rect:.2f}mm) print(roi1, roi1, roi2, roi2) # CV_16SC2定点查表remap 时最快内存也只有浮点表的一半 map1x, map1y cv2.initUndistortRectifyMap( K1, D1, R1, P1, image_size, cv2.CV_16SC2) map2x, map2y cv2.initUndistortRectifyMap( K2, D2, R2, P2, image_size, cv2.CV_16SC2) np.savez(stereo_maps.npz, map1xmap1x, map1ymap1y, map2xmap2x, map2ymap2y, P1P1, P2P2, QQ, image_sizenp.array(image_size), f_rectf_rect, b_rectb_rect)CV_16SC2是双通道 16 位定点格式把坐标编码成整数 分数位remap时有专门的快速路径。代价是精度只有 1/32 像素——对绝大多数工业场景够用如果你的应用对亚像素重采样极其敏感可以改用CV_32FC1分两张浮点表速度慢一些但精度更高。存盘用npz而不是yml加载快、跨语言读取也方便我在工程里通常还会附一个meta.json记录标定日期、板子规格、RMS、温度方便追溯。运行时用法就两行import cv2 import numpy as np m np.load(stereo_maps.npz) left cv2.imread(run/left_0001.png, cv2.IMREAD_GRAYSCALE) right cv2.imread(run/right_0001.png, cv2.IMREAD_GRAYSCALE) left_rect cv2.remap(left, m[map1x], m[map1y], cv2.INTER_LINEAR) right_rect cv2.remap(right, m[map2x], m[map2y], cv2.INTER_LINEAR) # 关键两张图必须用同一个 y 区间裁剪否则行对齐被人为破坏 y0, y1 40, 680 x0, x1 80, 1200 left_roi left_rect[y0:y1, x0:x1] right_roi right_rect[y0:y1, x0:x1]这里那个裁剪注释不是废话。roi1和roi2是两幅图各自的有效区域如果分别按roi1、roi2裁两张图的y起点不同极线立刻错位前面 0.2 像素的标定精度全白给。正确做法是取两个 ROI 的并集或人工指定一组共用的(x0, x1, y0, y1)左右保持一致。4.3 三个量化验收指标行差、重叠区、视差范围校正完不能靠眼睛看“好像对齐了”。我固定检查三项。第一项是极线行差提特征点做匹配统计同名点的|y_L - y_R|分布中位数应该小于 0.5 像素95 分位小于 1 像素。第二项是有效重叠区roi1、roi2的交集面积低于原图 50% 就得回头调alpha或者检查视场重叠是否太小。第三项是视差范围算出d_max f_rect * b_rect / Z_min这个值决定了后面 SGBM 的numDisparities该设多少。import cv2 import numpy as np def epipolar_check(left_rect, right_rect, max_pts400): orb cv2.ORB_create(4000) k1, d1 orb.detectAndCompute(left_rect, None) k2, d2 orb.detectAndCompute(right_rect, None) bf cv2.BFMatcher(cv2.NORM_HAMMING, crossCheckTrue) matches sorted(bf.match(d1, d2), keylambda m: m.distance)[:max_pts] dy, dx [], [] for m in matches: p1 np.array(k1[m.queryIdx].pt) p2 np.array(k2[m.trainIdx].pt) dy.append(abs(p1[1] - p2[1])) dx.append(p1[0] - p2[0]) # 左减右正值才符合常规视差定义 dy np.array(dy); dx np.array(dx) print(f|dy| median{np.median(dy):.3f}px p95{np.percentile(dy, 95):.3f}px) print(fdisparity range[{dx.min():.1f}, {dx.max():.1f}]) return np.median(dy) med epipolar_check(left_roi, right_roi) assert med 1.0, 行差超标先查 ROI 裁剪与映射表分辨率是否匹配用 ORB 而不是 SIFT是因为它快且对灰度变化不敏感做对齐检查足够了。crossCheckTrue加上距离排序取前 400 个能过滤掉大部分误匹配。注意dx的方向x_L - x_R才是正的视差反了说明R、T方向搞错或者左右图传反了。这一步得到的视差范围还要再放大 20% 左右留余量作为numDisparities的输入且必须是 16 的整数倍。5. 避坑与排查五条把误差放大的血泪记录这一节里的每一条都在实际项目里出现过共同特征是“现象看起来像算法不行实际是流程里某个环节漏了一步”。按现象、原因、解决三段写方便对着排查。5.1 采集与标定阶段的三个翻车点角点顺序错乱某几对图像的单帧误差爆到 2 像素以上。现象是整体 RMS 看着还行但逐对打印出来有几对明显离群且这几对的角点可视化里起始角点位置和其他帧不一样。原因是findChessboardCorners对旋转接近 180° 的板子会返回不同的角点排列而代码里用的是同一份objp物点与像点就错位了。解决办法是采集时统一板子朝向或者在检测后按“左上角点离图像左上角最近”这条规则重排角点顺序更省事的做法是直接删掉这几帧20 对里少 2 对无伤大雅。标定板不平畸变系数反直觉地大。现象是k1解出来是正值或者量级明显超过同类镜头校正后画面中间被“吸”进去。原因是纸基没贴平板面弯曲被模型误判成镜头畸变。判断方法是看畸变校正后的直线是否还弯——拿一张网格图过一遍undistort直线不直就是标定本身有问题。解决靠工装铝板 喷胶 重物压 24 小时或者直接换陶瓷板。只标一次用半年某天开始深度整体偏 3%。现象是系统跑了几周都正常某次维护之后深度一致性变差。原因是镜头对焦环被碰过或者模组经历了一次明显的温度循环fx漂了。全消色差镜头对温度不敏感但普通工业镜头在 20℃ 温差下焦距变化足以让深度偏 1%~3%。解决办法是把标定做成可重入的产线工序或者在系统里加一个自检拿一个已知距离的靶标定期测一次偏差超过 1% 就触发重标。5.2 校正与运行时阶段的三个坑左右图各自按roi裁剪极线行差突然变成 3 像素。现象是标定环节检查一切正常一上运行时行差就超标。原因是roi1、roi2的y起点不同分别裁剪后两幅图的行索引不再对应同一物理水平线。解决是所有裁剪统一用一组共享的(x0, x1, y0, y1)宁可多留黑边也不要各自裁。分辨率变更后直接复用旧映射表remap不报错但结果全错。现象是换成 640×480 采集后视差图完全乱掉甚至remap也没崩。原因是initUndistortRectifyMap生成的表尺寸和输入图尺寸不匹配OpenCV 会按表的尺寸输出或者越界读取。解决是把image_size写进npz运行时代码里 assert 一下输入图尺寸和表尺寸一致。真要降分辨率运行内参必须同步缩放fx fx * s、cx (cx 0.5) * s - 0.5这个 0.5 的偏移是像素中心定义带来的漏掉就是半像素的系统误差。numDisparities不够近处物体视差被截断。现象是近处目标深度明显偏大且边缘有横向条纹。原因是 SGBM 的搜索范围numDisparities小于实际最大视差超过范围的点被赋成无效值或落到边界。解决是先按f_rect * b_rect / Z_min算出理论最大值再乘 1.2向上取整到 16 的倍数同时把minDisparity设为负值以覆盖校正后可能出现的小幅负视差。6. 闭环验收与参数固化把标定结果变成能交付的东西标定做完最后一步是闭环验证——不是看极线图好不好看而是拿真实尺度测一次。我通常放一块 200mm 量块或者一块已知边长的标定板在 1m、2m、3m 三个距离上依次算深度看绝对误差和重复性。判据是1m 处误差小于 1%3m 处小于 2%同一位置连续 30 帧的标准差小于 0.5%。达不到就回去查基线是否与机械尺寸一致、Q是否与裁剪后的图像匹配。import cv2 import numpy as np m np.load(stereo_maps.npz) Q, f_rect, b_rect m[Q], float(m[f_rect]), float(m[b_rect]) sgbm cv2.StereoSGBM_create( minDisparity-16, # 允许小幅负视差 numDisparities128, # 必须 16 的倍数按 f*B/Z_min*1.2 估 blockSize5, # 奇数越大越平滑、越丢细节 P18 * 3 * 5 * 5, # 常规取 8*cn*blockSize^2 P232 * 3 * 5 * 5, # 32*cn*blockSize^2控制视差跳变惩罚 disp12MaxDiff1, # 左右一致性检查滤掉遮挡区误匹配 uniquenessRatio10, speckleWindowSize100, speckleRange2, modecv2.STEREO_SGBM_MODE_SGBM_3WAY) disp sgbm.compute(left_roi, right_roi).astype(np.float32) / 16.0 disp[disp 0] np.nan # 无效视差置 NaN别当 0 用 points cv2.reprojectImageTo3D(np.nan_to_num(disp), Q) # 单位与标定一致mm z points[..., 2] z[~np.isfinite(z) | (z 0) | (z 10000)] np.nan print(fdepth median{np.nanmedian(z):.1f}mm fvalid_ratio{np.isfinite(z).mean():.2%}) # 交叉验证Z f * B / d两者应当一致 d_sample disp[disp.shape[0] // 2, disp.shape[1] // 2] print(fformula check: Z{f_rect * b_rect / d_sample:.1f}mm)SGBM输出的视差是定点数缩放因子 16忘了除就会出现“深度全是几十毫米”的经典现象。reprojectImageTo3D直接吃Q但它要求视差是浮点且无效值不能用 0 冒充——0 视差会被映射到无穷远污染整片点云所以先转NaN再喂。最后那行公式交叉验证很有用Q路径和f*B/d路径算出来的深度必须一致不一致就说明Q与当前裁剪区域不匹配这是 ROI 裁剪后最常见的隐性错误。固化环节我一般产出三样东西stereo_maps.npz映射表 P1、P2、Q、image_size、f_rect、b_rect、intrinsics.yml左右K、D、R、T、E、F方便别的语言读取、以及一份meta.json记录标定日期、板子实测格距、整体 RMS、逐对最大误差、环境温度、镜头型号和光圈值。别小看这份meta半年后系统出问题第一个要查的就是“当时标定的条件和现在差多少”。血泪经验是我曾经为了赶进度跳过了闭环验收直接用极线图“看着挺齐”就交付结果现场 2m 处深度系统性偏 4%回头查是标定板格距用了标称值 25.0 而不是实测 24.83差 0.7% 正好对上。从那以后我每次标定都强制走一遍“量块实测 公式交叉验证 分辨率 assert”这三步哪怕项目再小也不省。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑