简介:YOLOv5+单目测距(python)是一套结合目标检测与单目深度估计的可运行工程,面向计算机视觉开发者及自动驾驶、机器人导航、智能监控等场景,使开发者无需深度传感器即可从单目图像中估算目标物体距离。压缩包共包含132个文件,大小约13.87MB,涵盖yaml/yml模型配置、Python训练与推理脚本、pyc编译文件、sh部署脚本、Dockerfile环境配置、Markdown说明文档、预训练pt权重和jpg示例图,覆盖模型定义、训练测试、环境搭建与快速上手。目前已有3856人学习下载,具有较高实用参考价值。工程基于YOLOv5-6.1目录组织,附带tutorial.ipynb入门教程、bus.jpg测试图及Docker环境配置,用户可获得从数据准备、模型训练到距离输出的完整源码和操作指引,适合有一定深度学习基础、希望将测距功能集成到视觉项目中的开发者。 做了几年视觉项目,YOLOv5+单目测距这套组合,我前后接过很多次类似的咨询:毕设要做车辆测距、车间想做安全区域预警、实验室想给小车加个避障功能。大家的诉求其实很统一,就是用一个普通USB摄像头,把画面里目标的位置距离算出来。相比激光雷达动辄几千的成本,单目方案只需要一个摄像头加一台能跑Python的设备,综合成本和落地门槛都低很多。
这篇文章把整套方案从原理、环境搭建、相机标定到最终的Python代码实现完整过一遍。我会重点讲清楚每个步骤背后的计算逻辑,而不是只丢一段代码让你复制跑通。适合的目标人群是:对YOLOv5目标检测有基础认识、想在此基础上做距离估计的同学,或者正准备做相关毕设、工程原型的开发者。读完你不仅能复现,还能根据实际场景调参。
1. 项目拆解:YOLOv5和单目测距各自的角色
1.1 检测和测距是怎么耦合的
先理清楚整个系统的数据流。YOLOv5负责从图像中找出目标,输出每个目标的检测框坐标(x1, y1, x2, y2)、置信度和类别。但这只是告诉你“画面里哪里有目标”,并没有回答“目标离你多远”。
单目测距要做的事情,就是基于这个检测框,结合摄像头本身的参数,推算目标在真实世界中的距离。检测框在这里面承担一个关键角色:它给出了目标在图像中的位置和尺寸,而这些像素信息正是单目测距公式的输入。
比较常用的做法是取检测框的底边中点作为测距参照点。为什么要用底边而不是中心点?因为检测框底边通常对应目标与地面的接触位置。对于行人、车辆这类直立物体,底边中点可以近似认为是目标“脚底”的位置,而这个位置刚好在地平面上。有了这个前提,才能用几何关系把图像坐标换算成真实距离。
如果取的是检测框中心点,中心点的高度会随目标的高度和远近不断变化,距离计算就会出现明显偏差。所以工程上建议统一使用 (x1+x2)/2, y2 即底边中点,作为测距的目标点。
1.2 单目测距的三种主流实现思路
单目测距没有深度相机那种像素级深度信息,只能靠几何假设和先验知识反推。目前主流的做法有三种:
第一种是相似三角形测距法。已知目标的真实宽度或高度(比如车辆宽度约1.8米),再结合焦距,通过比例关系计算距离。公式很简单:距离 = (真实宽度 × 焦距) / 像素宽度。这种方式实现最方便,但缺点也很明显,目标姿态变化、检测框不够精确时误差会放大。
第二种是地面平面假设法(本文推荐)。假设目标底边接触地面,已知相机安装高度和俯仰角,通过针孔相机模型反推目标到底边的水平地面距离。这种方式不需要知道目标的长宽,对车辆、行人等目标都适用,配合YOLOv5的检测框底边点非常自然,也是目前开源社区项目里最常采用的方式。
第三种是基于PNP的位姿估计。需要已知目标的三维模型或关键点的对应关系,通过求解相机外参获得目标的位置。精度最高,但对模型要求也高,通用性差,跑毕设或者快速原型一般用不到。
2. 环境搭建与依赖取舍
2.1 Python环境和YOLOv5依赖安装要点
不同教程推荐的Python版本差异很大,我实测下来建议用Python 3.8到3.10之间的版本,太高或太低都容易和PyTorch出现兼容性问题。PyTorch建议根据显卡CUDA版本选择1.10到2.x的稳定版本。如果你用的是纯CPU环境,也能跑,只是推理速度会慢不少,后面我会讲CPU环境下的优化方案。
YOLOv5的依赖安装,最简单的做法是直接拉官方仓库:
git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txt这里有个新手特别容易踩的坑:requirements.txt会同时装opencv-python,这个库的安装包比较大,网络不好时容易超时。建议先单独装:
pip install opencv-python -i https://pypi.tuna.tsinghua.edu.cn/simple另外,import cv2装完之后记得验证一下,命令行输入python -c "import cv2; print(cv2.__version__)",能正常输出版本号再继续下一步。很多人卡在环境问题上一两个小时,90%都是opencv或torch版本不匹配导致的。
2.2 选择哪种YOLOv5模型和运行方式
YOLOv5官方提供了n/s/m/l/x五个尺寸的模型,从n到x参数量逐级增加。单目测距项目对检测框的准确性要求较高,太小或太大的框都会直接影响测距结果。如果是笔记本或台式机跑实时视频,推荐yolov5s或yolov5m,平衡速度和框精度。如果是树莓派这类边缘设备,推荐yolov5n或yolov5s导出的ONNX模型。
运行方式也有两类选择:一是直接用官方detect.py脚本,二是通过torch.hub在自定义Python脚本中加载模型。detect.py适合快速验证效果,但如果要和测距逻辑集成,必须走第二种方式,把YOLOv5当成一个库调用。
import torch model = torch.hub.load('ultralytics/yolov5', 'yolov5s', force_reload=False)这里额外提醒一下,force_reload=False首次运行会从网上下载权重,之后会走本地缓存。如果你有自己训练好的权重,用torch.hub.load('ultralytics/yolov5', 'custom', path='best.pt')加载。自己训练的模型在特定场景下的检测框通常比官方预训练模型更准,测距精度也会随之提升。
3. 相机标定与测距公式推导
3.1 用OpenCV做棋盘格标定获取相机内参
单目测距的核心依赖之一是相机内参,尤其是焦距fx、fy和主点坐标cx、cy。这些参数可以从相机出厂参数中得到,但实际使用中摄像头可能存在畸变,所以强烈建议自己做一次标定。
标定方法用OpenCV的棋盘格方案。打印一张棋盘格(比如8x6内角点),用摄像头从不同角度拍15到20张照片,然后用以下代码提取角点并计算内参:
import cv2 import glob import numpy as np # 棋盘格尺寸:内角点数量 (横, 竖) pattern_size = (8, 6) 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) objpoints = [] imgpoints = [] images = glob.glob('calib_images/*.jpg') for fname in images: img = cv2.imread(fname) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners = cv2.findChessboardCorners(gray, pattern_size, None) if ret: objpoints.append(objp) corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)) imgpoints.append(corners2) ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None) print("内参矩阵:\n", mtx)标定要诀是让棋盘格尽量覆盖画面各个区域,尤其是边缘位置,因为畸变在边缘最明显。计算完内参矩阵后顺便看一下重投影误差,通常控制在0.1到0.3像素之间就算合格。内参矩阵中,mtx[0][0]就是fx,mtx[1][1]是fy,mtx[0][2]是cx,mtx[1][2]是cy。
3.2 测距公式:从像素到底边地面距离
有了相机内参,还需要两个额外的安装参数:相机光心离地面的高度 h_cam,以及相机光轴与水平面的俯仰角 pitch。这两个参数可以用尺子量,也可以用已知距离反推,二选一。
测距公式基于针孔相机模型。目标底边在图像中的行坐标 v_bottom,对应像素在相机坐标系下的俯角可以表示为:
angle = pitch + atan((v_bottom - cy) / fy)
其中 pitch 是相机俯仰角(光轴相对水平面向下的角度),(v_bottom - cy)/fy 是目标底边像素相对光轴的角度的正切值。两者相加,就是目标底边相对相机水平视线的实际俯角。由于目标底边在地平面上,结合相机高度,用三角函数可得水平距离:
distance = h_cam / tan(angle)
这个公式的推导逻辑可以用一个生活场景来理解:你站在高处,向下看到地面上的一个点,这个点的距离等于你的双眼高度除以你视线与水平面夹角的tan值。相机高度固定之后,图像上目标的v坐标变化,直接反映视线夹角的变化,距离也就跟着出来了。
代码实现如下:
import math def compute_distance(v_bottom, cam_h, pitch_deg, fy, cy): pitch = math.radians(pitch_deg) r = (v_bottom - cy) / fy angle = pitch + math.atan(r) if angle <= 0: return None return cam_h / math.tan(angle)如果你的相机安装基本水平(pitch接近0),还可以用简化公式 distance = cam_h * fy / (v_bottom - cy)。实测下来,当目标在20米以内、pitch小于10度时,简化公式和完整公式的误差在工程可接受范围内。但摄像机如果有明显俯视角度,必须用完整公式,否则距离会整体偏大。
4. YOLOv5检测与测距集成实战
4.1 从检测结果提取测距关键点
这步是重点。模型推理后拿到的结果是检测框坐标,需要先解析出底边中点的像素坐标:
results = model(frame) boxes = results.xyxy[0].cpu().numpy() # x1, y1, x2, y2, conf, cls for box in boxes: x1, y1, x2, y2, conf, cls = box if conf < 0.5: continue bottom_x = int((x1 + x2) / 2) bottom_y = int(y2) cls_name = model.names[int(cls)] # 后续将 bottom_y 带入测距公式为什么用y2而不是y1或中心点的y?因为YOLO的检测框是矩形包围盒,目标站立于地面时,包围盒的底边基本贴合目标与地面的接触位置。使用底边中点,意味着我们认为该像素在世界坐标系中的高度近似为0,这正好满足地面平面假设。
这里有个容易忽视的细节:检测框受目标姿态、遮挡等影响,底边不一定完全贴地。比如行人弯腰、车辆被障碍物挡住底部,底边像素会偏上或偏下,导致测距误差。常见处理方法是:只对特定类别(person、car、truck等)启用测距逻辑,并配合多帧平滑来抑制跳变。
4.2 完整集成代码:视频流实时测距
下面给出一套可直接运行的完整流程。代码按“读取视频帧 → YOLOv5推理 → 解析检测框 → 计算距离 → 绘制结果”的顺序组织:
import math import cv2 import torch def compute_distance(v_bottom, cam_h, pitch_deg, fy, cy): pitch = math.radians(pitch_deg) r = (v_bottom - cy) / fy angle = pitch + math.atan(r) if angle <= 0.02: return None return cam_h / math.tan(angle) # ---------- 参数配置 ---------- CAM_H = 1.5 # 相机安装高度,单位:米 PITCH_DEG = 5.0 # 相机俯仰角,单位:度 FY = 700.0 # 相机内参fy,来自标定 CY = 360.0 # 相机主点cy,来自标定 CONF_THRESH = 0.5 # 检测置信度阈值 # ---------- 加载模型 ---------- device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = torch.hub.load('ultralytics/yolov5', 'yolov5s', force_reload=False) model.to(device).eval() # ---------- 视频流处理 ---------- cap = cv2.VideoCapture('test_video.mp4') if not cap.isOpened(): print("无法打开视频源") exit() while cap.isOpened(): ret, frame = cap.read() if not ret: break results = model(frame) dets = results.xyxy[0].cpu().numpy() for det in dets: x1, y1, x2, y2, conf, cls_id = det if conf < CONF_THRESH: continue bottom_x = int((x1 + x2) / 2) bottom_y = int(y2) dist = compute_distance(bottom_y, CAM_H, PITCH_DEG, FY, CY) label = f"{model.names[int(cls_id)]} {dist:.2f}m" if dist else \ f"{model.names[int(cls_id)]} ???" cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) cv2.circle(frame, (bottom_x, bottom_y), 4, (0, 0, 255), -1) cv2.putText(frame, label, (int(x1), int(y1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) cv2.imshow("YOLOv5 + Mono Distance", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()如果跑的是本地图片序列或在线摄像头,把cap = cv2.VideoCapture('test_video.mp4')换成cap = cv2.VideoCapture(0)即可,0表示第一个USB摄像头。
实际运行过程中,距离数值会存在帧间抖动,尤其是目标处在10米外的时候,几个像素的框波动可能带来半米以上的距离跳变。建议对同一个目标的距离值做滑动平均或卡尔曼滤波。简单做法是维护一个距离列表,每次取最近5帧的中位数作为最终输出,能明显稳定画面显示和后续逻辑判断。
5. 精度优化与常见问题排查
5.1 误差来源与对应补偿策略
用这套方案做测距,误差来源主要集中在四个方面。
第一是相机标定误差。fx和cy如果标定不准,所有距离都会系统性偏移。解决方法是标定时多拍照片,保证棋盘格覆盖画面边缘,并检查重投影误差在0.3像素以内。如果条件允许,把相机装到固定位置后再标定一次,因为安装后的镜头角度和标定时可能存在微小差别。
第二是安装参数不准。相机高度h_cam可以用尺子量,但pitch角很难直接测准。一个非常实用的标定方法是反推法:在相机正前方放置一个已知距离的目标或标记物,量出它到相机光心的水平距离,然后利用测距公式反解pitch。我在工程中经常用这个方法,比用角度尺估得准得多。
第三是检测框的底边不贴合真实接触点。比如车辆尾部被遮挡、行人抬脚等,都会造成底边像素偏移。处理手段是:只对置信度高的目标测距,对不同类别设置不同置信度阈值;目标截断在画面边缘时直接跳过,因为边缘区域畸变和出框都会极大影响精度。
第四是地面非绝对水平。摄像头安装的位置地面如果有坡度,测距公式默认目标在地面的假设就会失效。这个在户外场景比较常见,建议尽量选择平整地面布置相机,否则就要在标定时额外引入地面倾角参数。
5.2 实测精度与距离范围参考
拿我个人做的测试来看,这套方案在室内走廊场景、3到15米的范围内,平均误差基本能控制在10%以内。5米内的误差可以做到5%左右,15米外误差会逐步扩大到15%到20%。误差随距离增大的原因很好理解:同等像素波动在远处对应更大的真实距离变化。
这里建议做距离可视化输出时,在画面中标记出置信度较低的可疑结果,而不是直接把偏差很大的数字打上去。另外,对超远距离(大于30米)的目标,单目测距的参考意义已经不大,不建议再参与业务逻辑判断。
5.3 新手踩坑速查表
| 现象 | 常见原因 | 解决办法 |
|---|---|---|
| 测距数值整体偏大或偏小 | 相机俯仰角pitch没有标定 | 用已知距离反推pitch,别靠目测 |
| 同一个目标距离值剧烈跳动 | 检测框底边不稳定 | 加入滑动平均或卡尔曼滤波 |
| 摄像头画面存在明显畸变 | 没有做畸变校正 | 标定后用cv2.undistort校正 |
| 目标在近处距离准、远处不准 | 单目测距固有误差 | 限定测距范围在15米内使用 |
| 测距返回None | bottom_y位置高于图像中心太多或angle小于0 | 只对画面下半区域目标测距 |
| CPU上跑实时视频特别卡 | 模型过大或没调推理参数 | 换yolov5n、缩小输入尺寸(320)、关闭验证集增强 |
很多人在模型部署阶段发现自己的电脑带不动大模型,这里多说一句:纯CPU环境下可以把图像输入尺寸从640降到416甚至320,推理速度会快不少,代价是检测小目标的精度下降。若是树莓派这类设备,导出ONNX再用OpenCV DNN模块或TensorRT/NCNN部署,是更稳妥的方向。YOLOv5训练出的检测模型本身和测距公式是完全解耦的,换推理后端不影响后续距离计算逻辑。
结合这些经验,我个人最推荐的做法是:项目启动初期先用手头现有的手机或USB摄像头把整套链路跑通,确认检测和测距结果符合预期后,再针对实际业务场景标定相机、优化模型和滤波参数,最后去考虑边缘设备的推理加速。把第一步跑通永远是性价比最高的事情。
本文还有配套的精品资源,点击获取