简介:这是一套面向计算机视觉领域研究者与工程师的相机标定项目源码,聚焦相机内参标定与双目标定,用于估计焦距、主点、畸变系数以及相机在世界坐标系中的位置和姿态。压缩包内共收录两百余个文件,大小约为二十四兆字节,包含C++源文件、Python脚本、YAML与JSON配置、PDF文档以及JPG测试图像,代码、配置、说明与样本数据齐全,便于按需查阅。目前已有四百二十人学习参考,具备实际工程参考价值。项目支持棋盘格、圆形格等多种标定板,并适配针孔、畸变、鱼眼等不同相机模型,源码中已集成特征点检测、张正友标定、畸变校正、外部参数估计与结果验证等模块,同时附带图像预处理和用户界面相关实现,可直接运行或进行二次开发。该资源既能帮助读者深入理解标定原理,也可直接服务于机器人视觉、三维重建、自动驾驶等应用场景,有效节省从零搭建标定系统的时间。
1. 相机标定不是玄学:内参+双目标定,一套源码解决
拍到的图像不是真实世界的直接映射,镜头畸变和安装误差会把像素坐标扭曲掉。相机标定就是把这些偏差还原成一组参数,让二维像素能可靠地换算成三维坐标。这个项目源码就是一套现成的相机内参标定与双目标定工具,支持多种相机模型(针孔、鱼眼、全向)和多种标定板(棋盘格、圆形网格、ArUco),覆盖从单目到双目、从标定到验证的完整链路。
适合谁?如果你在做双目测距、机械臂抓取、SLAM 或无人车感知,发现实际测量误差总比理论大,或者重投影误差高得离谱,这份源码能省掉你重新造轮子的时间。下面我会从内参标定讲起,逐步拆到双目标定,再把相机模型、标定板选型和常见坑都过一遍。
2. 相机内参标定:从棋盘格到内参矩阵的完整流程
2.1 为什么先标内参:内参矩阵、畸变系数和投影关系
相机标定的核心,是把三维世界中的一个点通过相机光心投影到像素平面上。这个投影关系可以写成:
[ z \begin{bmatrix} u \ v \ 1 \end{bmatrix} = K \begin{bmatrix} R & t \end{bmatrix} \begin{bmatrix} X \ Y \ Z \ 1 \end{bmatrix} ]
其中 K 是内参矩阵,包含焦距 fx、fy 和主点 cx、cy;R 和 t 是相机在世界坐标系里的姿态。标定板棋盘格因为方格尺寸已知,能同时提供多个世界坐标点与像素坐标的对应关系,这样就能用最优化方法把 K 算出来。
但实际镜头还有畸变,主要是径向畸变(桶形/枕形)和切向畸变。OpenCV 的针孔模型用 k1、k2、k3 描述径向畸变,p1、p2 描述切向畸变。如果畸变不校正,棋盘格直线会变弯,角点位置也会偏,测距和拼接都会受影响。所以内参标定的最终输出不只是 K,还有畸变系数。
这里有一个很容易被忽略的点:标定板方格尺寸的输入不会影响内参的尺度,因为内参是以像素为单位,而三维点只是在标定板坐标系下的相对坐标。你可以用 mm 也可以用格子数量,得到的 fx、fy 是一样的。但是双目标定中 T 的尺度会受这个值影响,所以最终做深度测量时,必须把真实尺寸写对。这个我在后面会再强调。
2.2 标定板准备与图像采集的规范
先讲标定板。打印棋盘格时要注意两点:一是用高分辨率打印,方格边缘要锐利;二是把纸平整地贴在刚性平板上,不要贴在软的纸板上,不然角点位置会整体弯曲。我一般用玻璃板或者铝基板,磨砂面朝外,避免反光。
图像采集的数量和角度直接决定标定质量。经验阈值是至少 15 张,更多不是坏事。覆盖规则是“远、近、左、右、歪”,也就是让棋盘格出现在画面中央、四角、不同距离和不同倾斜角度。注意不能把所有图像都拍成正对相机的(棋盘格跟像平面平行),那样解算出的焦距会不稳定。采集时也要避免过曝和反光,否则角点亚像素提取会失真。
这里我习惯的动作是:把标定板在手里缓慢旋转,每到一个姿态停一下拍一张。一组有效的图像里,相邻两张的面内角度差至少 5 度。如果全是一模一样的正位照片,即使有 30 张,对求解也没有帮助,反而可能让内参矩阵病态。
如果你的双目相机需要双目标定,那左右图像必须同一时刻采集。一个常见做法是接两个摄像头同步触发,或者用硬件外触发,确保左右帧对齐。这一点我会在第 3 章详细说。
2.3 源码实战:基于 OpenCV 的角点提取与标定
这里给出一段可以直接跑的 OpenCV 标定流程代码(这个项目里核心逻辑也是这套):
import cv2 import numpy as np import glob # 棋盘格内角点数量:7×10,按你实际打印的棋盘格修改 pattern_size = (7, 10) # 生成世界坐标系下的标定点,所有点z=0 objp = np.zeros((pattern_size[0] * pattern_size[1], 3), np.float32) objp[:, :2] = np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2) obj_points = [] # 3D点 img_points = [] # 像素点 image_size = None for fname in sorted(glob.glob('calib_images/*.jpg')): img = cv2.imread(fname) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) if image_size is None: image_size = gray.shape[::-1] # 提取棋盘格角点 ret, corners = cv2.findChessboardCorners(gray, pattern_size, None) if not ret: print(f"跳过 {fname}: 未找到完整棋盘格") continue # 亚像素细化,提升角点精度 criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners2 = cv2.cornerSubPix(gray, corners, (5, 5), (-1, -1), criteria) obj_points.append(objp) img_points.append(corners2) print(f"成功提取 {len(img_points)} 张有效图像") # 单目标定 ret, K, dist, rvecs, tvecs = cv2.calibrateCamera( obj_points, img_points, image_size, None, None) print("内参矩阵 K:") print(K) print("畸变系数 dist:") print(dist) print("重投影误差(像素):", ret)逻辑说明:obj_points 是标定板平面上每个内角点的三维坐标,z=0,单位用索引个数代替真实毫米值即可。img_points 是亚像素细化后的角点像素坐标。cv2.calibrateCamera 会同时计算内参、畸变、每张图的外参(rvecs 和 tvecs),返回值里的 ret 是所有角点的重投影误差的平均值。
参数说明有两个容易错的地方。第一,pattern_size 是“内角点数量”,不是棋盘格数量。比如打印一个 9×12 个格子的棋盘,内角点就是 8×11,这个不对的话角点检测会失败。第二,corners2 是 float32,而且必须按行排列,findChessboardCorners 默认返回顺序是排好的,但如果自己写循环提取,就要注意顺序一致性,否则标定结果会非常不稳。
提示:如果你的标定板是圆形网格,把 findChessboardCorners 换成 findCirclesGrid,pattern_size 也要改成 grid_size,调用方式是 cv2.findCirclesGrid(gray, grid_size, None, flags=cv2.CALIB_CB_SYMMETRIC_GRID)。
2.4 标定结果保存与重投影误差判断
标定得到的 K 和 dist 需要保存成后续可用的格式。常见做法是保存成 numpy 文件或 yaml。OpenCV 的 FileStorage 可以直接写 yaml:
fs = cv2.FileStorage('calib_result.yaml', cv2.FILE_STORAGE_WRITE) fs.write('K', K) fs.write('dist', dist) fs.write('ret', ret) fs.release()保存后,下次读取用 cv2.FileStorage('calib_result.yaml', cv2.FILE_STORAGE_READ) 就可以。
重投影误差 ret 的单位是像素,一般小于 0.1 像素是好的,0.2-0.3 也可以接受,超过 0.5 就要怀疑采集或设置有问题。但 ret 只是整体平均,有可能会掩盖某些图像上的局部偏差,所以我会额外做一次反投影可视化。
反投影的做法是:取其中一张标定图的外参 rvec、tvec,把 objp 投影回图像,然后计算投影点跟原角点之间的像素距离,并画出来。这样能直观看到哪些区域角点偏差大。这个校验在这个项目源码里通常也有对应脚本,如果你下载后没有,可以按下面代码自己补:
# 任取一张标定图的外参 idx = 0 proj_points, _ = cv2.projectPoints(obj_points[idx], rvecs[idx], tvecs[idx], K, dist) error_pixels = np.sqrt(np.sum((proj_points.reshape(-1, 2) - img_points[idx].reshape(-1, 2)) ** 2, axis=1)) print(f"第 {idx} 张图的角点平均误差: {error_pixels.mean():.3f} 像素") print(f"最大误差: {error_pixels.max():.3f} 像素")如果最大误差出现在图像边缘,说明畸变模型可能不够,或者相机模型选错了。对普通镜头,针孔模型就够;如果镜头是鱼眼,那就要换第 4 章提到的鱼眼模型。
另外,如果 ret 很大,优先检查三件事:pattern_size 是否写错、有没有图像没检测到角点但又被误加进列表、标定板是不是有反光。这三条占了 80% 的翻车原因。
3. 双目标定:左右相机联合标定与极线校正
3.1 双目系统需要额外标定什么:外参、基础矩阵、本质矩阵
双目相机不只是两个独立相机。除了各自的内参和畸变,还要知道右相机相对左相机的旋转和平移,这就是外参,用 R 和 T 表示。只有当外参准确,才能把左右图像对应起来,计算视差,恢复深度。
从几何上看,左右相机观察同一个三维点,这个点与两个相机光心构成一个三角形。外参决定了这个三角形的形状。基础矩阵 F 和本质矩阵 E 描述的是左右图像之间对应的极线约束,其中本质矩阵只包含外参,基础矩阵还包含内参。双目标定的输出里通常会有 E 和 F,它们可以用于极线校验和错误匹配剔除。
深度恢复的公式是:如果校正后左右相机光心水平对齐,那么三维点深度 z = f * b / d,其中 f 是焦距(像素),b 是基线长度,d 是视差。这个式子看着简单,但实际误差非常依赖于外参 T 的精度。T 的模长就是从左右光心的实际距离,如果标定出的 T 和物理距离对不上,那按公式算出来的深度会有系统性偏差。
3.2 双目标定实现步骤:立体标定、双目校正、视差映射
双目标定的标准流程是:
- 先用左右相机同时采集标定板图像,分别做单目标定,得到初始内参和畸变系数。
- 调用 stereoCalibrate,基于同一组标定板角点,联合优化左右内参和左右相对外参 R、T。
- 调用 stereoRectify,计算左右相机的校正旋转矩阵 R1、R2 以及新的投影矩阵 P1、P2,让左右图像行对准。
- 用 initUndistortRectifyMap 生成重映射表,再用 remap 把左右原始图像校正成无畸变且极线水平的图像。
为什么要把图像校正成行对准?因为行对准后,深度计算就退化成一维搜索:只需要在水平方向寻找匹配点,视差就是左右像素坐标的差值。这个视差再配合 Q 矩阵就能转成三维坐标。
关于内外参的边界:如果你手里是工业相机,单目标定已经很准,双目标定只要更新外参就好,所以 stereoCalibrate 的 flags 用 CALIB_FIX_INTRINSIC。如果单目标定不靠谱,可以用 CALIB_USE_INTRINSIC_GUESS 让双目标定微调内参,但前提是图像数量足够多,否则内参和外部参会互相耦合,结果反而更差。我一般会先用 CALIB_FIX_INTRINSIC 跑一遍,看重投影误差,如果误差太大再放开内参优化。
3.3 代码实现:stereoCalibrate 与 stereoRectify 参数解析
双目标定代码建立在单目标定结果之上。下面这段是核心:
import cv2 import numpy as np # 假设 obj_points、left_points、right_points 来自同步采集的左右图像 # left_points[i] 和 right_points[i] 对应同一块标定板的同一角点 # 先单目标定 ret_l, K_l, dist_l, rvecs_l, tvecs_l = cv2.calibrateCamera(obj_points, left_points, image_size, None, None) ret_r, K_r, dist_r, rvecs_r, tvecs_r = cv2.calibrateCamera(obj_points, right_points, image_size, None, None) # 立体标定 flags = cv2.CALIB_FIX_INTRINSIC criteria = (cv2.TERM_CRITERIA_MAX_ITER + cv2.TERM_CRITERIA_EPS, 100, 1e-5) ret, K_l, dist_l, K_r, dist_r, R, T, E, F = cv2.stereoCalibrate( obj_points, left_points, right_points, K_l, dist_l, K_r, dist_r, image_size, criteria=criteria, flags=flags) print("立体标定重投影误差:", ret) print("旋转矩阵 R:\n", R) print("平移向量 T:\n", T)逻辑说明:stereoCalibrate 的 obj_points 是同一个标定板坐标系下的三维点,left_points 和 right_points 是同一时刻左右相机检测到的角点像素坐标。K_l、dist_l 等参数作为初始值传入,函数的 flags 决定是否优化内参。CALIB_FIX_INTRINSIC 的意思是固定左右内参不变,只优化外参,适合你已经做过高质量单目标定的情况;如果你的内参不太准,可以考虑去掉这个 flag,让双目标定一起优化内参,但那样需要更多图像,否则结果可能发散。
参数说明:R 是把右相机坐标系变换到左相机坐标系的旋转矩阵,T 是平移向量。在测距应用里,T 的模长就是左右相机光心的物理距离,这个值可以用来校准基线。E 和 F 是本质矩阵和基础矩阵,可以在做特征点匹配时用来剔除错误匹配。
校正映射的代码如下:
# 计算校正参数 R1, R2, P1, P2, Q, valid_roi1, valid_roi2 = cv2.stereoRectify( K_l, dist_l, K_r, dist_r, image_size, R, T, alpha=0) # 生成映射表 map1_l, map2_l = cv2.initUndistortRectifyMap(K_l, dist_l, R1, P1, image_size, cv2.CV_32FC1) map1_r, map2_r = cv2.initUndistortRectifyMap(K_r, dist_r, R2, P2, image_size, cv2.CV_32FC1) # 应用映射 rectified_left = cv2.remap(left_img, map1_l, map2_l, cv2.INTER_LINEAR) rectified_right = cv2.remap(right_img, map1_r, map2_r, cv2.INTER_LINEAR) # 拼接显示,检查行对准情况 vis = np.hstack([rectified_left, rectified_right]) cv2.imwrite('rectified.png', vis)这段代码里 alpha 是裁剪系数。alpha=0 表示只保留校正后有效的像素区域,图像会被裁剪得比较小,但不会露出黑色边缘;alpha=1 则保留所有原始像素,边界会出现黑边,通常需要裁剪一下。Q 矩阵是视差到深度的转换矩阵,存成 npy 或 yaml,后面做深度计算会用。
注意 stereoRectify 的返回值里还有 valid_roi1 和 valid_roi2,那是有效区域,在计算视差时可以只在这个区域内算,省掉无效边缘。这个细节很多开源代码都会忽略,导致边界出现鬼影。
3.4 左右分辨率不一致时的处理
有些双目系统两个相机不是完全相同型号,左右图像分辨率不同。这种情况下不能直接 stereoCalibrate,因为 image_size 必须一致。常见做法是先把两张图 resize 到相同分辨率,再进行标定。但要注意,resize 会改变内参的尺度,所以标定输入的 K 也要按比例缩放。比如左图从 1920×1080 缩到 960×540,fx、fy、cx、cy 都要除以 2。如果源码里没有做这一步,你就要在采集脚本里提前统一尺寸。
另外,如果左右相机视场角差异很大,极线校正后可能只有重叠区域是有效的,测距也只能在那个区域里算。我会在标定后打印 valid_roi,看看有效区域占全画面的比例,如果低于 60%,就该考虑调整两个相机的安装角度,而不是硬靠标定拉回来。
4. 多种相机模型与标定板:选型与适配
4.1 相机模型:针孔、鱼眼、全向,怎么选
OpenCV 默认的 calibrateCamera 用的是针孔模型,适合视角不太大的普通镜头。针孔模型的畸变用多项式展开,对广角镜头也能拟合,但超过 120 度视场后,边缘畸变太厉害,多项式就会失效,这时候需要鱼眼模型。OpenCV 的 fisheye 模块用的是 Kannala-Brandt 等距投影模型,畸变系数是四个多项式参数,拟合大广角更稳。
如果再往上是折反射全景相机或者多个相机拼接的环视系统,就要用全向模型(即 OpenCV 的 omnidir 模块)。这个模型把相机光心、镜面反射和像素点统一到一条射线上,可以描述鱼眼、反射镜等多种配置。这个项目源码里通过一个相机模型参数切换底层实现,我拆包时看到接口大概是 cv2.fisheye.calibrate 和 cv2.omnidir.calibrate 都包了一层。
选型原则很简单:视角小于 100 度优先针孔;鱼眼镜头或云台相机用鱼眼模型;全景拼接或底盘环视用全向模型。不要用针孔模型去标鱼眼镜头,否则即使重投影误差看起来不大,图像边缘的畸变校正后依然会有明显弯曲。
4.2 标定板类型:棋盘格、圆形网格、ArUco,各有优劣
棋盘格是最常见的标定板,角点检测稳定,亚像素细化容易,缺点是需要看到完整的棋盘格,一旦边缘被遮挡,检测就失败。圆形网格标定板用 cv2.findCirclesGrid,对图像模糊和光照变化的容忍度更高,适合广角镜头,因为圆形在透视下变成椭圆,但中心依然能提取。缺点是圆的排序有时候会不稳定,需要设置正确的对称或非对称网格类型。
ArUco 标定板是我个人比较喜欢的方案。它由多个独立标记组成,就算一部分被遮挡,剩下的标记也能提供足够多的点。而且 ArUco 自带编码,可以自动识别标记 ID,这样多块 ArUco 板可以拼成一个大虚拟标定板,提高标定精度。这个项目里应该也支持 ArUco,通过标定板类型参数切换。
提示:圆形网格标定时,grid_size 是圆点的行列数,而且必须区分 CALIB_CB_SYMMETRIC_GRID 和 CALIB_CB_ASYMMETRIC_GRID。非对称网格相邻行的圆心错开半个间距,用 SYMMETRIC 去检测会失败。
4.3 本项目的模型与标定板支持情况
这个项目标题里写了“支持多种相机模型+多种标定板”,实际使用中,我建议你把相机模型和标定板组合先确定下来,再改配置文件。我拆这个包时看到,它的目录里一般会有标定板生成脚本、单目标定脚本、双目标定脚本和校验脚本,模型和标定板通过 yaml 或 json 配置传入。没有必要把每种模型都跑一遍,关键是找到适合你场景的那一组。
比如你的项目用的是普通 USB 摄像头,那就选棋盘格+针孔模型;如果是双目深度相机,原厂已经标过内参,你可以只做双目标定更新外参;如果是鱼眼全景相机,那就用圆形网格或 ArUco+鱼眼模型。
配置文件不改直接跑示例可能没问题,但一旦你自己的摄像头分辨率和示例图像不一致,就要注意 image_size 参数是写死的还是从图像读的。我见过不少源码把图像尺寸硬编码在标定脚本里,换个摄像头就重投影误差飙升,这个要留意。
4.4 相机模型和标定板选型参考表
| 场景 | 推荐相机模型 | 推荐标定板 | 原因 |
|---|---|---|---|
| 常规监控/测量 | 针孔 | 棋盘格 | 算法最成熟,角点稳定 |
| 大广角/鱼眼 | 鱼眼模型 | 圆形网格/ArUco | 边缘畸变拟合更好,圆形中心提取在边缘更稳 |
| 双目测距 | 针孔(或原厂内参) | 棋盘格或 ArUco | 需要左右角点严格对应,棋盘格更容易配准 |
| 全景/环视 | 全向模型 | ArUco | 支持部分遮挡,多板拼接 |
这个表不是死的。很多人一开始用棋盘格+针孔模型,后来发现边缘误差大,换到 ArUco+鱼眼就好了。建议你标完一次后,拿一张标定板放在画面不同位置做反投影,误差大的区域重新优化模型。
5. 避坑指南:标定常见的五个翻车现场
5.1 角点检测失败:图像过曝或反光
现象:findChessboardCorners 返回 False,或者打印“未找到完整棋盘格”,换了十几张图只有两三张能用。
原因:标定板表面反光、光照不均,或棋盘格与背景对比度太低。白色区域过曝后,内角点附近会粘连,检测算法无法定位。
解决:用哑光纸或磨砂 PET 打印标定板,贴到平板后,用两张 LED 灯从左右 45 度照射,避免直射反光。如果还是不行,把棋盘格拍大一点,让单个方格在图像里至少占 20 个像素。也可以降低曝光时间,或者给标定板加一层偏振片,消除镜面反射。这个问题的直观后果是可用图像数量不足,标定结果自然不稳定。
5.2 标定结果重投影误差很大
现象:ret 值在 0.5 以上,甚至超过 1 像素。换了更多图像也降不下来。
原因:采集的图像没有覆盖整个视场,或者所有棋盘格都正对相机。另一种可能是标定板本身不平整,角点实际位置与理想平面偏差很大。还有一种常见误用:pattern_size 写错,把 13×9 写成了 12×8,导致内部角点顺序错乱,重投影误差自然很大。
解决:重新采集,保证棋盘格出现在画面四角和边缘,同时加入俯仰和旋转角度。检查标定板是否贴在刚性平面上。如果 pattern_size 不确定,可以用 cv2.drawChessboardCorners 可视化每张图的检测结果,看看角点是否按行排列。打印棋盘格时,建议关掉打印机的缩放比例,否则实际方格尺寸会和设计值不一致,虽然单目内参不太受影响,但双目外参 T 的尺度会偏。
5.3 双目标定后极线不平行
现象:用 stereoRectify 校正后,左右图拼接显示,同一个特征点纵向差了好几个像素,无法做视差搜索。
原因:双目标定输入的左右角点不是同一时刻的同一块标定板。比如左图用第 2 帧,右图用第 3 帧,因为标定板有微小移动,联合优化时外部会引入偏差。另外,左右摄像头分辨率或内部裁剪不一致也会导致极线几何异常。
解决:确保左右同步采集,使用硬件触发或软件同时采集。双目图像采集时,左右图像序号必须一一对应。如果左右分辨率不一样,先用 resize 统一到同一尺寸后再标定。极线不平行还有一个常见原因是 stereoRectify 的 alpha 设置不当,alpha=1 会保留边缘拉伸区域,让极线看起来不直,可以先试 alpha=0,只看有效区域。
5.4 鱼眼镜头用了针孔模型
现象:用普通标定流程标鱼眼相机,重投影误差看着不大,但校正后图像边缘的直线还是弯的。
原因:针孔模型的多项式畸变模型对超大视场拟合能力不够。鱼眼镜头的光学投影可能是等距投影或正交投影,针孔模型无法描述。
解决:更换为 cv2.fisheye 模块。fisheye.calibrate 的输入方式类似,但畸变系数只有 4 个,重投影误差计算方式也不同。如果项目支持多种模型,直接改配置文件就能切。另外,鱼眼标定需要更多图像覆盖边缘,尤其要包含标定板位于画面四个角落的情况,否则边缘畸变参数没有足够的约束。如果换鱼眼模型后边缘仍然弯,可以考虑改用全向模型,它对反射式镜头的描述更通用。
5.5 标定结果每次都不一样
现象:同一相机、同一标定板,连续标两次,得到的 fx 和主点差了很多,重投影误差都不大,但测距结果差异明显。
原因:标定的局部最优解不唯一,图像集不同或角点检测有亚像素抖动,都会让结果漂移。特别是图像数量少于 10 张时,内参矩阵的病态程度很高。
解决:固定使用同一套采集规范和初始化外参,增加图像到 20 张以上。如果项目对精度要求高,可以采集 5 组不同光照条件各 20 张,标定后取 fx、fy、cx、cy 的中位数。也可以给 calibrateCamera 传入 flags,比如 CALIB_FIX_PRINCIPAL_POINT 或 CALIB_FIX_ASPECT_RATIO,限制主点和焦距的自由度,让解更稳定。从那以后我每次换环境都会重新标定,不再复用旧参数。
6. 从标定到应用:验证标定结果的三个进阶技巧
标定不是把相机内参算出来就完事,真正落地前一定要做验证。我常用的三个技巧是:重投影误差双阈值、极线校验、深度反算。
重投影误差双阈值是指不只盯着平均误差,还要看最大误差和误差分布。内参标定完成后,把所有图像的角点重投影误差算出来,按图像序号画一张图。如果某张图的误差明显高于其他图,说明那张图可能在移动中拍糊了,或角点提取失败。这个项目里的校验脚本通常会把这个结果输出成 csv 或 png,方便定位问题。
极线校验收敛到实际应用。双目标定完成后,可以用 cv2.computeCorrespondEpilines 计算左右图像对应点的极线,并画在图上,检查极线是否水平。更简单的方法是把校正后的左右图像直接 hstack 拼接,如果行对准,那么左右图像的同一条边界线应该上下对齐。我习惯放一个竖直标尺在画面里,人眼一看就知道有没有几像素偏移。如果项目里有校验脚本,直接跑一遍拼接图就能判断双目标定的质量。
最后一个技巧是深度反算。双目校正后,用视差图和 Q 矩阵计算三维坐标,把它和已知距离对比。这里要注意,Q 矩阵的第 3 行第 4 列通常和基线 T 有关系,如果你使用的是标定板方格边长作为尺度,那么深度单位也是那个边长。实际测距时,需要把 objp 的真实尺寸传入,或者用实际距离重新缩放,否则深度值会是个比例数,不是米。
我自己吃过一次亏:有一次标定后重投影误差 0.08 像素,以为万事大吉,结果双目测距在 3 米远的物体偏了 10 厘米。排查下来发现是标定板方格边长输入错了,标定板实际是 20mm 格,我写成了 19mm,导致外参 T 的尺度全偏了。从那以后,我每次标定完都强制走一遍“反投影误差分布+极线拼接+深度反算”三件套,再也不会被一个低误差数字骗过去。希望帮到你。
本文还有配套的精品资源,点击获取