title: 鱼眼相机也能做双目测距?——双鱼眼深度估计探索
slug: fisheye-stereo-distance-demo鱼眼相机也能做双目测距?——双鱼眼深度估计探索
摘要
两个鱼眼相机虽然畸变大、不水平对齐、光轴不平行,但如果它们有固定基线和明显的重叠视野,能否构成一个非标准的双目立体对来做三角化测距?
答案是可行的。经过虚拟针孔展开→手动点选→solvePnP→stereoRectify→triangulation 的完整管线,在 0.2m 标定布格子上,baseline 估计值与实际物理测量值的误差约 3%,棋盘格距离平均误差约 3.4mm。本文记录完整的探索过程、实验结果和工程经验。
本文涉及的部分代码已开源在 GitHub:ruali-dev/fisheye-stereo-distance-demo(Apache 2.0)。仓库包含标定工具、深度探查器、示例图片和交互式浏览器 UI,可直接复现本文的工作
写在前面:为什么突然写自动驾驶感知?
这篇文章整理的是我在实习过程中一个突发奇想的小实验:尝试基于一对鱼眼相机图像做双目测距。这个工作本身并不复杂,实际探索大概花了一个下午,但由于这个月毕业答辩、材料提交、实习任务和其他杂事堆在一起,一直到月底才抽时间把它整理出来。
我最近的实习内容主要和自动驾驶感知相关。乍一看,这和我之前更关注的机器人操作、具身智能、LeRobot、VLA 这些方向有点跳跃。但实际上,自动驾驶并不是一个完全陌生的领域。大二时我参加过 CARSMOS 国际自动驾驶算法挑战赛,也是在那个阶段第一次接触到了自动驾驶这个方向。后来看到 ALOHA 这类工作之后,我逐渐把兴趣转向了机械臂操作和具身智能,于是很长一段时间没有继续深入自动驾驶。

大二时参加 CARSMOS 国际自动驾驶算法挑战赛获得三等奖。某种意义上,自动驾驶是我技术起步的入口之一。至于大二怎么在这种研究生比赛里打出三等奖,那就是另外一个故事了。
这次重新接触自动驾驶,更多是现实路径和工程经验共同作用的结果。一方面,考研结束之后,我需要找一段实习来积累真实项目经验;另一方面,对于双非本科背景、且临近毕业的学生来说,具身智能/机器人操作方向的实习机会并不好找。再加上毕业前每隔一段时间就会冒出来一件学校事务需要处理,最终这家研究院孵化的小公司提供了一个相对合适的实习机会。既然已经来了,就把这段经历当作一次补工程课的机会:看看真实项目里一些技术到底是怎么落地的。
我后续的主线大概率还是会继续放在机器人操作和具身智能方向上。毕竟现在已经拿到港中深MAIR的offer,接下来还有两年的研究生阶段可以继续积累,看来还能继续做具身和manipulation。这段实习之前是签到了7月初,但既然已经参与其中,就把它当作一次接触工业界、补工程经验的机会。
这篇文章有些不方便公开的东西隐去了,所以部分地方可能会显得有些断裂。虽然临近毕业各种事情都很忙,但这段时间确实做了不少东西,后续也会慢慢整理出来。
1. 双目测距的基本原理
1.1 视差
假设你双眼平视前方。你竖起一根手指放在鼻子前 30cm 处。闭上左眼用右眼看,手指在背景中的一个位置;闭上右眼用左眼看,手指"跳"到了另一个位置。这个"跳跃"的距离就是视差(disparity)。
两个相机的情况完全一样。同一个三维点 P,在左相机图像里落在像素 (u_L, v_L),在右相机图像里落在 (u_R, v_R)。如果两个相机是水平对齐的,那么垂直坐标 v 应该一样,水平坐标的差就是视差:
disparity = u_L - u_R
1.2 基线
基线(baseline) 是两个相机光心之间的距离。基线越大,同样的物体产生的视差越大,深度估计的灵敏度越高;但基线太大也有问题——两个相机看到的东西差异太大,匹配变得困难。
工程直觉:基线越大 → 能测更远但近处匹配更难;基线越小 → 近处精细但远处误差大。
1.3 三角化
已知左相机和右相机的光心位置以及它们的朝向,如果同时知道一个点在左右相机中的投影位置,从两个光心向各自的投影方向画射线,两条射线的交点就是这个点的 3D 坐标。这就是三角化(triangulation)。
1.4 深度-视差-基线的经典关系
在理想水平双目的情况下:
Z = f · B / d
其中:
- Z:深度(相机到物体的距离)
- f:焦距(像素)
- B:基线(米)
- d:视差(像素)
工程直觉:
- 深度和视差成反比。远处物体视差小,测不准。
- 基线越长,同样的物体视差越大,测得更准。
- 焦距越长,同样的视差对应的深度变化越大。

2. 普通双目 vs 鱼眼双目
2.1 标准双目相机
典型的标准双目相机(如 Intel RealSense、ZED):
- 两个相机光轴平行
- 水平对齐(极线水平)
- 针孔模型,畸变已校准
- 出厂已标定好
这种情况下做立体匹配很简单:对于左图中的一个像素,右图中对应的像素一定在同一行(或附近几行),顺着水平线搜就够了。
2.2 鱼眼双目
鱼眼双目完全不同:
- 畸变严重,原图不能直接用于匹配
下面两张图是实验用的两个鱼眼相机原始图像。可以看到,直的地板缝和标定布边缘在画面中严重弯曲——这就是鱼眼畸变的典型表现:


因此鱼眼双目需要一套预处理管线。
2.3 虚拟相机预处理再双目测距
虚拟针孔展开后用单组图片验证(本文的做法)
- 两个鱼眼各自做内参标定
- 分别生成虚拟针孔图(调 FOV/yaw/pitch/roll 让两者有重叠区)
- 在重叠区手动点选对应点
- 分别 solvePnP → 计算相对位姿 → stereoRectify → triangulate

3. 实验环境
3.1 相机布局
实验“因地制宜”的使用了安装在车辆同侧的两个鱼眼相机——一个偏前、一个偏后。两个相机之间存在约 1.2m 的固定物理距离(基线),对于 1~3m 范围内的近距离测距来说,这是一个合适的基线长度。
两个鱼眼相机的光轴并不平行——它们各自朝向车侧的不同角度。因此它们的原始图像不能直接用于双目匹配。需要先经过预处理,使两者产生可用于立体匹配的重叠视图。
3.2 虚拟相机展开与重叠区域
预处理的思路是虚拟相机展开:通过调整 FOV 和朝向(yaw/pitch/roll),从鱼眼的大视场中分别切出两个针孔视角的虚拟视图。调整的目标是让这两个虚拟视图之间有尽可能大且清晰的重叠区域。
最终找到了一组合适的参数,使两个虚拟视图之间存在一块清晰的重叠区域,拍到的是同一块棋盘格标定板。这组 pair 就是后续双目几何验证的输入。

下面是实际生成的虚拟针孔视图(左眼和右眼)。可以看到去畸变后原本弯曲的标定布变直了,两个视图共享同一块棋盘格区域:


4. 实验流程
4.1 整体管线
步骤 1: 输入两个相机的虚拟针孔图
步骤 2: 手动在左右图中点击同一批棋盘格角点
步骤 3: 根据虚拟相机 FOV 和分辨率计算虚拟相机内参 K
步骤 4: 假设虚拟图像为 pinhole 图像(畸变已通过虚拟相机展开消除)
步骤 5: 根据棋盘格真实尺寸构造 object_points(3D 坐标)
步骤 6: 左右分别 solvePnP → 推出右相机相对于左相机的 R, t
步骤 7: stereoRectify → 生成校正后的左右图和 Q 矩阵
步骤 8: triangulatePoints → 三角化所有角点
步骤 9: 评估重投影误差、baseline 估计、三角化距离误差、hold-out 误差
4.2 手动点选
OpenCV 交互式窗口:左图按行顺序点击棋盘格角点 → 右图同样顺序点击对应角点。
棋盘格选择:3 行 × 4 列 = 12 个内角点,正方形格子边长 0.2m。
点选质量直接影响后续精度。12 个点在两个不同视角中都能看清楚,手动选点的一致性较好。

4.3 solvePnP 和相对位姿
左右图分别用 cv2.solvePnP 求解各自相对于棋盘格坐标系的位姿(R_L, t_L)和(R_R, t_R)。
然后用位姿关系计算右相机相对于左相机的 R 和 t:
R = R_R @ R_L^T
t = t_R - R @ t_L
4.4 stereoRectify 的关键操作
cv2.stereoRectify 需要你指定 R 和 t 的方向。但是 solvePnP 算出的 R, t 可能是从"左→右"的方向,也可能是"从右→左"。因此需要同时尝试两个方向(forward 和 inverse),选择 epipolar y-error 更小的那个。
5. 实验结果
5.1 最优一次的结果
使用 12 个棋盘格点(3×4 网格,正方形格子边长 0.2m),所有点参与计算:
| 指标 | 数值 | 评价 |
|---|---|---|
| Baseline(估计) | 1.186 m | 与实测约 1.2m 误差约 3% |
| Reproj L | 0.48 px | 很好 |
| Reproj R | 0.53 px | 很好 |
| Triangulation mean distance error | 0.0034 m(3.4mm) | 相对误差 1.7% of square size |
各相邻点的距离误差:
pair (0,1): tri=0.206m gt=0.200m err=0.006m
pair (1,2): tri=0.199m gt=0.200m err=0.001m
pair (2,3): tri=0.206m gt=0.200m err=0.006m
...
Mean distance error: 0.0034 m
Relative error: 1.7%
5.2 Hold-out 验证
为了验证是否过拟合,把 12 个点按棋盘格交错方式拆分为 6 个训练点 + 6 个测试点:
| 指标 | Train | Test |
|---|---|---|
| Reproj L | 0.33 px | 1.22 px |
| Reproj R | 0.60 px | 1.03 px |
| Distance error mean | — | 0.0057 m |
Test 集的重投影误差略高于 Train 集(这是预期内的),但仍在可接受范围(~1px)。测试点距离误差均值 5.7mm。
这说明:
- 模型没有严重过拟合
- 虚拟双目 pair 的几何关系基本自洽
5.3 尺度修正:一个参数填错,所有结果差一个数量级
这里记录一次关键的调试经历,它很好地说明了"几何验证是标定中最重要的 sanity check"。
最初我设棋盘格边长 square_size=0.05m,运行整个管线后,估计出的 baseline 只有 0.294m。而这两个相机的实际光心距离是大约 1.2m(机械测量值)。差了 4 倍,显然有问题。
排查后发现原因很简单:棋盘格的实际格子边长是 0.2m,不是 0.05m。 之前的 0.05m 是我凭记忆写的,没有实际测量确认。
将 square_size 改为 0.2m 后重新跑:
| 参数修正前 (0.05m) | 参数修正后 (0.2m) | 实际测量值 |
|---|---|---|
| baseline ≈ 0.294m | baseline ≈ 1.186m | ≈ 1.2m |
baseline 估计值 1.186m 与实际值的误差约 3%,尺度基本对上了。这说明整个几何管线是自洽的——solvePnP + 相对位姿 + 三角化的链路在正确参数下能恢复出正确的物理尺度。
教训:在涉及物理尺度的几何计算中,一个参数填错会导致所有结果差一个数量级。每次拿到新数据后,先验证一个已知尺寸(比如棋盘格边长、两个标记点的实际距离),确认尺度正确后再分析其他指标。
5.4 3D 重建可视化
三角化得到的 12 个 3D 点大致形成一个 4×3 的平面网格,各点间距离接近 0.2m 的真值。3D 点位于相机前方约 1.5~2.1m 处——和棋盘格在虚拟相机坐标系下的位置一致。

实际运行结果如下。俯视图展示三角化后的 12 个点在 X-Z 平面上的分布,相邻点标注了距离——可以看到距离值都接近 0.2m(ground truth)。3D 散点图则展示了点在三维空间中的位置:


6. 存在的不足
6.1 Epipolar y-error 偏大
stereoRectify 后的 y-error:
| 统计量 | 数值 |
|---|---|
| mean | 14.13 px |
| median | 4.81 px |
| max | 43.35 px |
| std | 14.16 px |
median 4.8px 可以接受,但 mean 14px 和 max 43px 说明有几个点的匹配质量很差。这些 outliers 可能是手动点击误差导致的。
对工程的影响:如果后续做自动立体匹配,不能在严格的单行水平线上搜,需要在 y 方向保留 ±50px 左右的搜索带。
6.2 可能的原因
- 单组图片估计 R/t 不够稳定(只有一张图,没有多帧约束)
- 手动点选存在误差
- 虚拟相机 K 与实际 render 使用的 K 可能存有微小不一致
- 当前不是正式的 stereo calibration,只是单组图片 demo
7. 手动点选测距
7.1 基本思路
标定完成后(有了 stereo rectification 参数和 Q 矩阵),就可以对任意一对匹配点做三角化测距了。交互方式很简单:
用户在左右图上各点击一个对应点 → 程序三角化 → 输出 3D 坐标和距离
实际使用的交互界面是浏览器中的 split-view:左右图并排显示,用户在左图点击一个特征点,再在右图点击同一个物理点,底部即时显示三角化结果。
7.2 适合点击的特征点
适合手动选点的特征:
- 棋盘格角点、黑白边界
- 箱子角、墙角
- 线缆交叉点
- 地面明显纹理点
不适合的点:
- 纯灰地面、纯白墙面
- 模糊区域
- 反光区域
- 两个视图中外观差异很大的点(如镜面反射)
7.3 使用体验
手动测距工具把"鱼眼双目测距"从一个理论概念变成了可交互的 demo。在左右图上点击同一块棋盘格角点,按下回车立即看到三角化距离——这种即时反馈对于验证技术路线的可行性非常重要。

8. 开源仓库
本文涉及的工具代码已开源:ruali-dev/fisheye-stereo-distance-demo(Apache 2.0)。
8.1 仓库结构
fisheye-stereo-distance-demo/
├── launcher.py # 标定工具后端(端口 8765)
├── launcher_probe.py # 深度探查器后端(端口 8766)
├── configs/
│ └── stereo_pair.yaml # 配置文件(FOV、棋盘格尺寸等)
├── input/ # 左右虚拟针孔图像
├── output/ # 标定结果、精度报告、可视化图
├── tools/
│ ├── point_picker.html # 角点点选交互界面
│ └── depth_probe.html # 测距探查交互界面
├── src/ # 核心模块
│ ├── single_pair_extrinsic.py # solvePnP 相对位姿估计
│ ├── rectify_pair.py # stereoRectify 校正
│ ├── triangulate_points.py # 三角化
│ ├── holdout_validation.py # hold-out 验证
│ ├── manual_depth_probe.py # 手动测距
│ ├── semi_auto_depth_probe.py # 半自动测距
│ └── visualize.py # 可视化
└── assets/ # 示例图片
8.2 交互方式
工具通过浏览器 UI 操作,不需要命令行交互。标定流程和测距探查各有一个 HTML 界面:
- 标定工具(
point_picker.html):在左右图上点击对应棋盘格角点 → 一键运行完整管线 → 输出标定结果和精度报告。 - 深度探查器(
depth_probe.html):在左右图上各点击对应特征点 → 即时三角化 → 显示距离读数。
8.3 复现方法
git clone https://github.com/ruali-dev/fisheye-stereo-distance-demo.git
cd fisheye-stereo-distance-demo
# 安装依赖
pip install opencv-python numpy pyyaml
# 准备你自己的左右虚拟针孔图,放入 input/ 目录
# 编辑 configs/stereo_pair.yaml(FOV、棋盘格参数等)
# 启动标定工具
python launcher.py
# 浏览器打开 http://localhost:8765/tools/point_picker.html
# 标定完成后,启动测距探查器
python launcher_probe.py
# 浏览器打开 http://localhost:8766/tools/depth_probe.html
仓库 input/ 目录中已包含示例图片,可以直接体验完整流程。
9. 小结
- 鱼眼双目≠标准双目:光轴不平行、不对齐、畸变大——不能直接套用标准双目流程。需要虚拟相机展开做预处理。
- 还不是成熟方案:当前只是 single-pair demo。正式工程化需要多帧同步采集、正式 stereo calibration、epipolar y-error 改善和自动匹配。