简介:本资源是一套基于YOLOv5与单目视觉实现目标测距的完整Python工程,面向计算机视觉初学者及PyTorch实践者,解决在无深度传感器条件下利用单摄像头估算物体距离的实际问题,适用于智能监控、辅助驾驶、机器人避障等轻量化部署场景。压缩包共132个文件,含40个配置类YAML/YML文件(定义模型结构与训练参数)、34个核心Python脚本(涵盖数据预处理、YOLOv5推理、单目测距计算、相机标定与三角测量实现)、5个Shell部署脚本及Jupyter Notebook教程,辅以Dockerfile、XML标注示例与测试图像,整体大小为13.91MB,结构清晰、模块解耦度高。已有2293人学习下载,提供从YOLOv5目标检测到单目距离回归的端到端代码链路,包含bus.jpg实测图、tutorial.ipynb交互式教学、pt模型权重及完整环境配置说明,助读者快速复现原理并理解高度-距离映射、焦距标定、像素坐标转换等关键环节。
1. 单目测距不是玄学:YOLOv5 输出的 bbox 高度,凭什么能换算成真实米数?
你有没有试过——把 YOLOv5 检测出的汽车 bounding box 高度(比如h_px = 127像素)直接代入一个公式,就得出“距离摄像头 8.3 米”?结果一实测,偏差 ±2.5 米。更诡异的是:同一辆车停在 5 米和 10 米处,YOLOv5 给出的 bbox 高度比值 ≈ 2.1,但理论几何比值应是 exactly 2.0;而换一辆 SUV,这个比值又变成 1.85。这不是模型不准,而是你漏掉了单目测距最隐蔽的底层约束:它根本不是纯视觉问题,而是一个被相机内参、物体物理尺寸、成像平面畸变三重锚定的刚体几何反演问题。这份 YOLOv5+单目测距的 Python 实现之所以“原理比较简单”,恰恰因为它绕开了端到端深度学习的黑匣子,用可解释、可调试、可溯源的几何链路,把检测框像素坐标 → 物理高度 → 实际距离的每一步都暴露在阳光下。它不追求 SOTA 精度,但每一步误差都能定位到具体参数、具体图像区域、具体物理假设——这才是工业级部署敢用的底气。你不需要重新训练 YOLOv5,也不需要标注几千张带距离标签的图;你只需要理解:为什么h_px和distance_m之间必须经过real_height_m这个中间变量,以及为什么这个变量不能靠“估”,而必须靠“标定”。
2. 几何根基:单目测距不是拟合曲线,而是解三角形
单目测距的本质,是利用小孔成像模型,将图像平面上的像素坐标反推回三维空间中的物点位置。YOLOv5 提供的 bbox(尤其是 yolo 格式:[x_center, y_center, width, height])本质上是图像坐标系下的二维投影结果。要从中恢复深度(Z 轴),必须补全缺失的第三维信息——而这第三维,无法从单张图像中凭空获得,必须引入外部先验。这个先验,就是物体在真实世界中的已知物理尺寸(通常是高度,因车辆/行人高度相对稳定)和相机的内参矩阵(focal length, principal point)。整个过程不是“用神经网络拟合距离”,而是用几何约束求解一个确定性方程。
2.1 小孔成像模型:从像素坐标到归一化相机坐标
我们从最基础的针孔相机模型出发。设相机坐标系原点为光心 O,Z 轴指向场景(即光轴),X、Y 轴与图像平面平行。物点 P 在相机坐标系下的坐标为(X, Y, Z),其在理想无畸变图像平面上的投影点 p 的归一化坐标(即除以 Z 后的坐标)为:
x_norm = X / Z y_norm = Y / Z而实际图像传感器上记录的是像素坐标(u, v),它与归一化坐标的关系由相机内参矩阵K定义:
K = [[fx, 0, cx], [ 0, fy, cy], [ 0, 0, 1]]其中fx,fy是焦距(单位:像素),cx,cy是主点(图像中心,单位:像素)。于是有:
[u] [fx 0 cx] [x_norm] [v] = [ 0 fy cy] [y_norm] [1] [ 0 0 1] [ 1 ]即:
u = fx * (X / Z) + cx v = fy * (Y / Z) + cy提示:这份资源里的
tutorial.ipynb第 3 节就直接调用了cv2.calibrateCamera标定得到K,而不是用默认的fx=fy=1000, cx=640, cy=480。这就是精度分水岭——没标定过的 K,所有后续距离都是空中楼阁。
2.2 从 bbox 高度到物体底部-顶部的 Z 差值
YOLOv5 输出的 bbox 高度h_px,对应图像中物体底部(如车轮接触地面处)到顶部(如车顶)的像素距离。设物体底部在图像中的纵坐标为v_bottom,顶部为v_top,则h_px = v_bottom - v_top(注意 y 轴向下为正)。
根据成像公式,这两个点对应的相机坐标系 Z 坐标不同(因为它们在真实世界中 Z 不同),但如果我们做关键简化:假设物体是竖直的,且底部位于地面上(Z = Z_ground),顶部高度为 H_real(已知),那么顶部点的 Z 坐标近似为Z_top ≈ Z_ground - H_real * cos(θ),其中 θ 是物体与光轴夹角。在大多数前视/俯视场景中,若相机安装高度合理(如 1.2m),且目标为车辆/行人,cos(θ) ≈ 1,因此Z_top ≈ Z_ground - H_real。
此时,v_bottom和v_top可分别表示为:
v_bottom = fy * (Y_bottom / Z_ground) + cy v_top = fy * (Y_top / (Z_ground - H_real)) + cy但Y_bottom和Y_top并非独立变量——对于竖直物体,Y_bottom ≈ Y_top(即横向偏移极小),记为Y_obj。于是:
v_bottom - v_top = fy * Y_obj * [1/Z_ground - 1/(Z_ground - H_real)] = fy * Y_obj * H_real / [Z_ground * (Z_ground - H_real)]这个式子依然复杂。工程上采用更鲁棒的近似:当Z_ground >> H_real(即距离远大于物体高度)时,Z_ground - H_real ≈ Z_ground,分母近似为Z_ground²。此时:
h_px ≈ fy * Y_obj * H_real / Z_ground²但Y_obj仍未知。真正的突破口在于:我们不关心Y_obj,只关心Z_ground。而Y_obj与v_center(bbox 中心纵坐标)强相关。v_center = (v_bottom + v_top)/2 ≈ fy * Y_obj / Z_ground + cy,所以Y_obj ≈ (v_center - cy) * Z_ground / fy。
代入上式并整理,最终得到经典单目测距公式(忽略高阶项):
Z_ground ≈ (fy * H_real) / h_px * (1 + (v_center - cy)² / fy²)^(1/2)但这份资源采用了更简洁、更易标定的线性近似形式(见tutorial.ipynb中estimate_distance()函数):
def estimate_distance(h_px, v_center, fx, fy, cy, H_real, alpha=1.0): """ 简化单目测距:基于相似三角形 + 主点偏移校正 h_px: bbox 像素高度 v_center: bbox 中心纵坐标(像素) fx, fy: 焦距(像素) cy: 主点纵坐标(像素) H_real: 物体真实高度(米) alpha: 经验缩放因子,用于补偿模型简化误差 """ # 步骤1:将像素高度转换为归一化平面上的角度高度(弧度) # tan(theta_h) ≈ h_px / fy (小角度近似) theta_h = h_px / fy # 步骤2:计算物体底部到光轴的垂直角(考虑主点偏移) # 光轴对应 v = cy,物体中心在 v_center,故偏角 theta_v = (v_center - cy) / fy theta_v = (v_center - cy) / fy # 步骤3:利用三角关系:H_real / Z = tan(theta_h) / (1 + tan(theta_v)^2)^(1/2) # 实际代码中采用更稳定的线性组合:Z = (fy * H_real) / (h_px * (1 + k * theta_v^2)) # 其中 k 是经验系数,在 bus.jpg 标定时确定 k = 0.15 # 该值来自 bus.jpg 的实测标定,非理论推导 correction = 1 + k * (theta_v ** 2) distance = (fy * H_real * alpha) / (h_px * correction) return distance这段代码背后,是把复杂的射影几何压缩成一个可解释、可调参的物理模型。alpha和k不是神经网络权重,而是标定过程中对模型简化的补偿系数。它们的存在,恰恰说明:单目测距不是“一键 magic”,而是“先建模、再标定、后验证”的闭环。
2.3 为什么必须用v_center而不是v_top或v_bottom?
很多初学者会误以为:“我只要知道车顶高度和车轮高度,就能算”。但v_top和v_bottom在图像中极易受检测框抖动、边缘模糊、遮挡影响。而v_center是 bbox 四个顶点坐标的均值,抗噪性更强。更重要的是,v_center直接关联到物体在相机坐标系中的Y_obj坐标,而Y_obj决定了物体相对于光轴的俯仰角——这个角度直接影响Z的计算精度。tutorial.ipynb中对比实验显示:用v_center计算的 10 米处车辆距离标准差为 ±0.18m,而用v_bottom则飙升至 ±0.43m。这不是玄学,是统计学必然。
3. YOLOv5 如何成为可靠的距离“传感器”:检测输出的稳定性分析
YOLOv5 本身不是为测距设计的,但它输出的 bbox 具有极高的结构一致性——这正是单目测距赖以生存的土壤。我们不依赖它的分类置信度,而依赖其回归分支(regression head)对x, y, w, h的稳定预测。这份资源的价值,正在于它没有魔改 YOLOv5,而是榨干了原始模型输出的几何信息潜力。
3.1 YOLOv5 的 bbox 输出格式与坐标系对齐
YOLOv5 默认输出的是x_center, y_center, width, height,单位为归一化图像坐标(0~1)。这意味着:
x_center = u / img_widthy_center = v / img_heightwidth = w_px / img_widthheight = h_px / img_height
tutorial.ipynb的第 2 节明确做了这一步转换:
# 加载 YOLOv5 模型并推理 model = torch.hub.load('ultralytics/yolov5', 'yolov5s', pretrained=True) results = model('bus.jpg') df = results.pandas().xyxy[0] # 获取 pandas DataFrame # 提取第一个检测框(假设是 bus) if not df.empty: x1, y1, x2, y2 = df.iloc[0][['xmin', 'ymin', 'xmax', 'ymax']] # 转换为 yolo 格式:x_center, y_center, width, height(归一化) img_w, img_h = 1280, 720 # bus.jpg 分辨率 x_center = (x1 + x2) / 2 / img_w y_center = (y1 + y2) / 2 / img_h width = (x2 - x1) / img_w height = (y2 - y1) / img_h # 关键!转回像素单位,用于测距计算 h_px = height * img_h # ← 这一步绝不能省! v_center = y_center * img_h这里h_px和v_center必须是像素单位,因为焦距fy的单位是“像素”,只有单位一致,h_px / fy才有物理意义(无量纲的 tan(theta))。如果直接用归一化height去除fy,结果会错 1000 倍——这是新手踩坑第一高频点。
3.2 检测框抖动对距离计算的放大效应
距离Z与h_px成反比(Z ∝ 1/h_px)。这意味着h_px的微小误差会被剧烈放大。例如,真实h_px = 100,若检测给出h_px = 105(+5%),则计算距离变为Z' = Z * 100/105 ≈ 0.952Z,即 -4.8% 误差。但如果真实h_px = 50(远距离小目标),同样 +5% 误差(h_px=52.5),Z' = Z * 50/52.5 ≈ 0.952Z,误差仍是 -4.8%,但绝对误差翻倍(从 0.48m 变成 0.96m)。
tutorial.ipynb中通过滑动窗口平均法抑制抖动:
# 对连续 N 帧的 h_px 取中位数,而非单帧 h_px_history.append(h_px) if len(h_px_history) > 5: h_px_history.pop(0) h_px_smooth = np.median(h_px_history) # 中位数比均值更抗异常值为什么是中位数?因为 YOLOv5 在小目标或低对比度下,偶尔会输出h_px=0或h_px=200(误检为大目标)的离群值。均值会被拉偏,中位数则岿然不动。这是血泪经验:某开发者曾用均值,导致高速公路上的测距结果在 50m 和 10m 间疯狂跳变。
3.3 类别置信度与测距可靠性的解耦
YOLOv5 的confidence分数(如0.92)反映的是“这是一个 bus 的概率”,但它完全不反映“这个 bbox 高度有多准”。一个confidence=0.99的 bus 检测框,其h_px可能因车顶反光而严重低估;而一个confidence=0.55的检测,若边缘清晰,h_px反而更准。
tutorial.ipynb的设计哲学是:置信度过滤只用于“是否启用测距”,而非“如何加权测距结果”。代码中:
# 只有 confidence > 0.6 才参与测距,但一旦参与,就用原始 h_px 计算 if conf > 0.6: dist = estimate_distance(h_px, v_center, fx, fy, cy, H_bus) distances.append(dist)这种解耦,让系统逻辑清晰:检测负责“找目标”,几何模型负责“算距离”,二者错误模式正交,不会相互污染。
4. 避坑指南:单目测距的 5 个致命误区与现场排查法
单目测距项目失败,90% 不是因为算法,而是因为这五个看似 trivial 的细节。以下全是tutorial.ipynb实测和bus.jpg标定过程中踩出的深坑,按“现象→原因→解决”结构列出,每一条都附带可立即验证的检查命令。
4.1 现象:同一辆车,白天测距准,傍晚测距偏大 15%
原因:bus.jpg是白天拍摄,其标定参数(fx, fy, cy)在傍晚因镜头热胀冷缩发生漂移;更关键的是,YOLOv5 的检测框在低光照下ymin/ymax边界模糊,导致h_px系统性偏小(h_px ↓ → Z ↑)。
解决:
- 用
cv2.undistort对输入图像做实时畸变校正(tutorial.ipynb第 4 节已集成); - 在
estimate_distance()中加入光照自适应缩放:h_px *= (1 + 0.2 * (1 - avg_brightness/255)),其中avg_brightness = np.mean(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)); - 验证命令:
python -c "import cv2; import numpy as np; img=cv2.imread('bus.jpg'); print(np.mean(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)))",确保白天基准值在 120~180。
4.2 现象:车辆侧向行驶时,测距结果随角度增大而持续偏小
原因:公式Z ≈ (fy * H_real) / h_px假设物体竖直。当车辆与光轴夹角 θ > 15°,其图像高度h_px不再正比于H_real,而是正比于H_real * cos(θ),导致Z被低估。
解决:
- 引入
v_center与cy的差值作为角度代理:theta_est = abs(v_center - cy) / fy(弧度); - 在
estimate_distance()中增加修正项:h_px_corrected = h_px / max(0.9, np.cos(theta_est)); - 验证命令:
python -c "import numpy as np; print(np.degrees(np.arctan(100/800)))"(假设v_center-cy=100, fy=800→ θ≈7°,cosθ≈0.99,无需修正;若差值 300 → θ≈21°,cosθ≈0.93,必须修正)。
4.3 现象:模型加载后首次推理距离为inf或nan
原因:tutorial.ipynb使用torch.hub.load加载的 yolov5s 模型,其预处理默认将图像 resize 到 640x640,但bus.jpg是 1280x720。若未在estimate_distance()中同步更新img_w, img_h为 resize 后的尺寸(640x640),则h_px = height * img_h会用错分母,导致h_px错 2 倍,进而h_px=0触发除零。
解决:
- 在推理前显式指定
model.conf = 0.25并model.iou = 0.45,然后强制model = model.autoshape(); - 关键修复:在获取
df后,立刻用model.img_size覆盖img_w, img_h:img_w, img_h = model.img_size, model.img_size # yolov5s 默认 640 h_px = height * img_h - 验证命令:
python -c "from ultralytics.yolov5 import models; m=models.yolov5s(); print(m.img_size)"。
4.4 现象:Dockerfile构建镜像后,cv2.calibrateCamera报错CV_32F
原因:Docker 镜像中opencv-python-headless版本过低(<4.5.0),不支持新版calibrateCamera的flags参数;同时setup.cfg中numpy版本锁定为1.19.5,与cv2冲突。
解决:
- 修改
Dockerfile,升级 opencv:RUN pip install --upgrade opencv-python-headless==4.8.1.78; - 修改
setup.cfg,放开 numpy 限制:numpy>=1.21.0,<1.25.0; - 验证命令:进入容器执行
python -c "import cv2; print(cv2.__version__); import numpy as np; print(np.__version__)",确认cv2≥4.8.1且np≥1.21。
4.5 现象:tutorial.ipynb运行到estimate_distance()时,distance输出为负数
原因:v_center < cy(物体在光轴上方),导致theta_v为负,correction = 1 + k * theta_v^2仍为正,但fy * H_real若为负(因fy被误设为负值),则结果必负。tutorial.ipynb中fx, fy来自标定,但若标定板放置不水平,cv2.calibrateCamera可能返回负fy。
解决:
- 强制
fy = abs(fy),并在日志中警告:if fy < 0: print("WARNING: fy negative, using abs"); fy = abs(fy); - 验证命令:
python -c "import cv2; import numpy as np; objp=np.zeros((6*9,3), np.float32); objp[:,:2] = np.mgrid[0:9,0:6].T.reshape(-1,2); print(cv2.calibrateCamera([objp], [np.array([[0,0],[1,0],[1,1],[0,1]],np.float32)], (640,480), None, None)[1][0][1])"(模拟标定,检查fy符号)。
5. 标定:不是调参,而是重建你的“视觉尺子”
单目测距的精度上限,不由 YOLOv5 决定,而由标定质量决定。tutorial.ipynb里那张bus.jpg,不是随便拍的测试图,而是一次微型标定实验的产物。它教会我的第一课是:标定不是为了得到一组数字,而是为了建立“像素 ↔ 米”的可信映射关系。这个关系必须经得起三个拷问:可复现、可分解、可证伪。
5.1 标定的最小可行单元:一张图,两个已知量
bus.jpg的标定逻辑极其精悍:
- 已知量 1:公交车真实高度
H_real = 3.2m(行业标准值,非估算); - 已知量 2:公交车在图像中的精确
h_px(手动用cv2.selectROI框选,避开透视变形区域,取中门高度); - 待求量:等效焦距
f_eff = fy(因v_center ≈ cy,theta_v ≈ 0,公式退化为Z = (f_eff * H_real) / h_px)。
tutorial.ipynb第 5 节的标定代码:
# 手动标定:在 bus.jpg 上框选公交车中门区域 img = cv2.imread('bus.jpg') roi = cv2.selectROI("Select Bus Door Height", img) # 返回 (x,y,w,h) h_px_manual = roi[3] # ROI 的高度(像素) # 已知:bus 高度 3.2m,实测距离 12.5m(用激光测距仪打点) Z_known = 12.5 H_real = 3.2 # 反解 f_eff f_eff = (Z_known * h_px_manual) / H_real print(f"Calibrated f_eff = {f_eff:.2f} pixels") # 输出 823.67这个f_eff不是cv2.calibrateCamera得到的fy,而是针对该特定场景(bus + 12.5m)的等效焦距。它自动吸收了镜头畸变、安装俯仰角、甚至 YOLOv5 检测框的系统性偏差。这才是工程思维:不追求理论完美,而追求场景精准。
5.2 为什么不用棋盘格标定?——场景特异性优先
cv2.calibrateCamera需要多张不同角度的棋盘格图像,它标定的是镜头的固有属性(fx, fy, cx, cy, k1, k2...)。但单目测距的误差,70% 来自场景建模误差:车辆不是点,而是有体积的刚体;地面不是理想平面;YOLOv5 的 bbox 不是亚像素精度的轮廓。bus.jpg标定法,用一张图、一个已知物理量、一个实测距离,直接构建h_px ↔ Z的端到端映射,绕过了所有中间环节的误差累积。tutorial.ipynb的 Table 1 对比了两种标定法在 5~20m 区间的平均绝对误差(MAE):
| 标定方法 | 5-10m MAE | 10-15m MAE | 15-20m MAE | 总体 MAE |
|---|---|---|---|---|
| 棋盘格标定 + 理论公式 | 0.38m | 0.52m | 0.71m | 0.54m |
bus.jpg单图标定 | 0.21m | 0.29m | 0.37m | 0.29m |
差距不是参数精度,而是问题抽象层级。棋盘格标定在“镜头层面”求解,bus.jpg标定在“任务层面”求解。
5.3 标定的可证伪性:三组数据交叉验证
任何标定都必须能被证伪,否则就是玄学。tutorial.ipynb设计了三组验证实验:
- 组1:固定 Z,换物体—— 用同一张
bus.jpg,标定H_real=3.2m,再用此参数测行人(H_real=1.7m),看误差是否在预期内(±0.3m); - 组2:固定物体,变 Z—— 对同一辆公交,在 5m、10m、15m 处各拍一张图,用同一套
f_eff计算,看Z_calc是否线性跟随Z_true; - 组3:固定物体 & Z,换模型—— 用 yolov5s、yolov5m、yolov5l 分别检测
bus.jpg,看h_px方差是否 < 3px(即距离误差 < 0.1m)。
注意:
tutorial.ipynb的 “Verification” 小节里,group2_validation()函数会画出Z_truevsZ_calc的散点图,并计算 R²。R² < 0.98 就意味着标定失败,必须重来——这是硬性红线。
从那以后我每次做单目测距,都强制走一遍这三组验证。哪怕客户只要一个 demo,我也坚持标定 + 验证闭环。因为我知道,bus.jpg里那个f_eff=823.67不是魔法数字,而是我亲手校准的视觉尺子。它可能不适用于越野车,但对城市公交,它就是可靠的。希望帮到你。
本文还有配套的精品资源,点击获取