从相机标定到YOLOv5单目测距:原理推导与工程实践全解析
1. 相机模型与单目测距基础
第一次接触单目测距时,我和大多数人一样充满疑惑:用单个摄像头真能测出距离吗?后来在停车场倒车时突然想通了——当我们看到后视镜里越来越近的障碍物,其实大脑就在做单目测距。这个生活场景揭示了单目测距的核心:通过物体在图像中的变化反推真实距离。
相机模型可以类比为我们的眼睛。想象你拿着一个纸筒看远处的路灯:纸筒代表镜头,视网膜就是图像传感器。当路灯距离变化时,它在纸筒另一端形成的像会变大变小。这个简单的凸透镜成像原理,就是针孔相机模型的物理基础。
在实际工程中,我们需要用数学语言描述这个过程。相机坐标系(Xc,Yc,Zc)的原点位于镜头光心,Z轴指向拍摄方向。假设地面有个路灯底座点Q,它在图像平面的投影Q'的坐标(y')与真实高度(H)存在关系:
y' = f * (Yc / Zc)其中f是焦距。当相机以俯角α安装时,通过三角函数推导可得距离公式:
distance = H / (tan(α + atan(y'/f)) * cos(α))这个公式揭示了单目测距的三大依赖:
- 相机高度H:测量误差会线性影响结果
- 俯角α:需要精确的水平校准
- 焦距f:必须通过标定获得准确值
2. 相机标定实战指南
去年给工厂做AGV导航系统时,我花了整整两天调试标定参数。当时用普通USB摄像头采集的测距数据波动超过20%,后来发现是忽略了镜头的桶形畸变。这个教训让我明白:标定质量直接决定测距精度。
推荐使用OpenCV的棋盘格标定法,具体操作流程:
- 准备标定板:打印10x7的棋盘格图案(每个方格2cm),贴在平整硬纸板上
- 多角度拍摄:在不同距离(0.5m-3m)、不同倾斜角度拍摄15-20张照片
- 运行标定脚本:
import cv2 import numpy as np # 准备对象点 (0,0,0), (1,0,0),...,(9,6,0) objp = np.zeros((6*9,3), np.float32) objp[:,:2] = np.mgrid[0:9,0:6].T.reshape(-1,2) # 检测角点 img = cv2.imread('calib.jpg') gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners = cv2.findChessboardCorners(gray, (9,6), None) # 标定计算 ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera( [objp], [corners], gray.shape[::-1], None, None)关键参数说明:
| 参数名 | 物理意义 | 典型值范围 |
|---|---|---|
| mtx[0,0] | x轴焦距(fx) | 500-2000像素 |
| mtx[1,1] | y轴焦距(fy) | 通常接近fx |
| dist[0] | 径向畸变k1 | -0.2~0.2 |
| tvecs[0][2] | 相机高度 | 根据安装位置 |
实测建议:
- 标定板要占图像1/3以上面积
- 使用
cv2.CALIB_FIX_ASPECT_RATIO保持fx/fy比例 - 标定后要用
cv2.undistort()验证矫正效果
3. YOLOv5与测距的工程融合
在最新版的YOLOv5 v7.0中,我们可以通过修改detect.py实现端到端测距。核心思路是:利用检测框底部中点作为测距参考点。但实际测试发现,直接使用原始框会导致10%以上的误差,特别是对远处的小物体。
经过多次实验,我总结出三点优化策略:
- 参考点修正:对行人等直立物体,将参考点上移5%框高
- 动态滤波:对连续5帧距离变化>15%的结果进行中值滤波
- 高度补偿:不同物体类别采用预设高度(轿车1.5m,卡车3m等)
关键代码实现:
def estimate_distance(det, cam_matrix, height=1.5): """ 单目测距核心函数 Args: det: YOLOv5检测结果 [x1,y1,x2,y2,conf,cls] cam_matrix: 相机内参矩阵 height: 物体预设高度 """ # 获取检测框底部中点 (u,v) u = (det[0] + det[2]) / 2 v = det[3] - 0.05 * (det[3] - det[1]) # 上移5% # 坐标转换 fx = cam_matrix[0,0] fy = cam_matrix[1,1] v0 = cam_matrix[1,2] # 角度计算 beta = np.arctan((v - v0) / fy) distance = height / np.tan(beta + np.radians(15)) # 假设俯角15度 return distance部署时的性能优化技巧:
- 使用
torch.jit.trace将模型转为TorchScript - 对640x480图像,在Jetson Nano上可达15FPS
- 开启
half=True使用FP16推理可提升30%速度
4. 误差分析与调优方案
上周测试时遇到个有趣现象:同一辆卡车在10米处测距误差仅8%,但在3米处误差却达到25%。经过反复验证,发现是相机俯角导致的比例失真问题。这引出了单目测距的一个重要结论:误差与距离成非线性关系。
常见误差来源及解决方案:
| 误差类型 | 影响程度 | 解决方法 |
|---|---|---|
| 相机高度误差 | 线性影响 | 使用激光测距仪校准 |
| 俯角偏差 | 近距离影响大 | 加装水平仪辅助安装 |
| 物体高度假设 | 类别相关 | 建立高度-类别映射表 |
| 镜头畸变 | 边缘区域明显 | 采用更高精度标定 |
建议的验证流程:
- 在5m、10m、15m位置放置标定物
- 采集100帧数据计算平均误差
- 根据误差曲线调整高度参数
对于需要厘米级精度的场景(如自动泊车),可以考虑:
- 增加距离分段补偿系数
- 融合IMU数据补偿车身震动
- 使用车道线等参照物进行动态校准
在工程实践中,单目测距最适合1-20米范围的中距离检测。我曾将这套方案用于智能叉车系统,通过结合二维码定位,最终实现了±5%的测距精度。关键是要根据具体场景做参数调优——就像调整相机焦距一样,需要耐心地反复微调。