ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

YOLOv5单目测距实战:无需双目/激光雷达的低成本距离估算

YOLOv5单目测距实战:无需双目/激光雷达的低成本距离估算 简介本资源是一套基于YOLOv5与单目视觉实现目标距离估算的完整Python工程面向计算机视觉初学者及PyTorch实践者解决单摄像头场景下目标检测与物理距离估计的实际问题适用于智能监控、辅助驾驶、机器人避障等轻量级部署场景。压缩包共132个文件含34个核心Python脚本含模型训练、推理、测距计算模块、40个配置类YAML/YML文件定义网络结构、数据路径与相机参数、11个Shell与Docker相关文件支持环境快速部署以及JPG测试图、PT预训练权重、IPython教程笔记等整体大小为13.91MB。已有2287人学习下载。读者可直接复现从YOLOv5目标检测到单目测距的全流程包括特征提取、图像高度映射、三角测距公式实现、相机标定参数集成以及OpenCVNumPyPyTorch协同编码范式代码结构清晰、注释充分配套bus.jpg实测示例与tutorial.ipynb交互式说明显著降低单目测距入门门槛。1. YOLOv5单目实现测距python为什么不用双目、激光雷达也能在普通摄像头下算出“这个箱子离我3.2米”你手头只有一台笔记本、一个USB摄像头、甚至一部手机前置镜头——没有双目基线、没有深度传感器、没接IMU、也没标定过相机内参别急着关页面。YOLOv5单目测距不是玄学它靠的是几何约束先验知识目标尺寸假设三者咬合只要你知道被检测物体在现实世界中的大致物理尺寸比如快递箱宽40cm、人肩宽50cm、汽车轮毂直径65cm再结合YOLOv5输出的像素框高/宽就能反推出物距。这不是毫米级精度的工业方案但对物流分拣、AGV避障、智能货架监控这类“知道大概几米远就足够决策”的场景它成本为零、部署极快、Python脚本5分钟就能跑通。本文不讲论文推导只拆解一线工程师在树莓派、Jetson Nano、Windows笔记本上反复验证过的最小可行路径从YOLOv5检测框怎么取、焦距怎么估、尺寸先验怎么设、误差在哪爆发、以及为什么你调了10次参数还是差2米——全写进代码注释和避坑清单里。2. 搭建YOLOv5检测骨架用官方权重快速获得稳定bbox坐标单目测距的前提是必须拿到目标在图像平面上的精确包围框bbox且该框要尽可能贴合目标真实轮廓。YOLOv5是当前最成熟、开箱即用的2D检测器其v6.1/v6.2版本在COCO上mAP0.5达50.7%对常见物体人、车、包、箱召回率高、误检少特别适合做测距上游。我们不训练、不微调直接用yolov5s.pt作为起点——它体积小14MB、推理快CPU上30ms/frame、泛化强比自己训一个轻量模型更稳。2.1 安装依赖与加载预训练模型提示不要用pip install yolov5——这是第三方封装包版本混乱、API不兼容。官方repo才是唯一可信源。# 创建干净环境推荐conda conda create -n yolo_dist python3.8 conda activate yolo_dist # 克隆官方仓库注意必须是ultralytics官方非fork git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txt # 验证安装会自动下载yolov5s.pt到weights/目录 python detect.py --weights yolov5s.pt --source 0 --img 640 --conf 0.4这段命令启动摄像头实时检测关键在于它默认输出的是xyxy格式bbox左上x, 左上y, 右下x, 右下y而测距需要的是目标在图像中的高度pixel height——因为单目测距公式distance (real_height × focal_length) / pixel_height中高度方向受透视畸变影响最小比宽度更鲁棒。所以后续所有逻辑都基于bbox[3] - bbox[1]即y_max - y_min。2.2 封装成可复用的检测函数剥离GUI专注bbox输出detect.py自带OpenCV显示和保存逻辑但测距模块需要的是纯数据流。我们重写一个轻量函数屏蔽绘图、日志、保存只返回[class_id, confidence, x1, y1, x2, y2]列表# utils/detector.py import torch from models.experimental import attempt_load from utils.general import non_max_suppression, scale_coords from utils.datasets import letterbox import cv2 import numpy as np class YOLOv5Detector: def __init__(self, weightsyolov5s.pt, devicecpu, img_size640, conf_thres0.4, iou_thres0.45): self.device torch.device(device) self.model attempt_load(weights, map_locationself.device) self.img_size img_size self.conf_thres conf_thres self.iou_thres iou_thres self.names self.model.module.names if hasattr(self.model, module) else self.model.names def detect(self, img_bgr): # 1. 图像预处理letterbox 归一化 添加batch维度 img letterbox(img_bgr, new_shapeself.img_size)[0] img img[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB, to 3xHxW img np.ascontiguousarray(img) img torch.from_numpy(img).to(self.device).float() img / 255.0 if img.ndimension() 3: img img.unsqueeze(0) # 2. 推理 pred self.model(img, augmentFalse)[0] # 3. NMS后处理 pred non_max_suppression(pred, self.conf_thres, self.iou_thres) # 4. 坐标还原到原图尺寸 det pred[0] if len(det): det[:, :4] scale_coords(img.shape[2:], det[:, :4], img_bgr.shape).round() # 5. 提取结果[class_id, conf, x1, y1, x2, y2] results [] for *xyxy, conf, cls in reversed(det): x1, y1, x2, y2 map(int, xyxy) results.append([int(cls), float(conf), x1, y1, x2, y2]) return results # 使用示例 if __name__ __main__: detector YOLOv5Detector(weightsweights/yolov5s.pt, devicecpu) cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break bboxes detector.detect(frame) # ← 关键这里拿到原始bbox坐标 for cls_id, conf, x1, y1, x2, y2 in bboxes: if cls_id 0: # 0 is person pixel_height y2 - y1 print(fPerson detected: height{pixel_height}px, conf{conf:.2f}) cv2.imshow(YOLOv5, frame) if cv2.waitKey(1) ord(q): break cap.release()参数说明img_size640YOLOv5输入分辨率越大检测越准但越慢640是速度与精度平衡点conf_thres0.4置信度过滤阈值低于此值的框直接丢弃太低会引入大量误检尤其背景纹理太高会漏检如遮挡目标devicecpu开发阶段用CPU足够部署到Jetson或RTX显卡时换cuda:0速度提升5–10倍scale_coords()必须调用否则bbox坐标是缩放后尺寸直接用于测距会系统性偏大。3. 单目测距核心公式推导与焦距标定不靠张正友用一张A4纸搞定YOLOv5给了你像素高度h_px你要算物理距离D公式长这样$$ D \frac{H_{real} \times f}{h_{px}} $$其中H_real目标真实高度单位米例如成年人肩高≈0.5m标准快递箱高≈0.3mf相机焦距单位像素这是最关键的未知量h_pxYOLOv5输出的bbox像素高度。问题来了f怎么来你当然可以拿棋盘格OpenCV做完整标定cv2.calibrateCamera但那需要10张不同角度照片、手动标注角点、还要处理畸变系数——对快速验证毫无必要。我们用A4纸法利用A4纸已知尺寸210mm×297mm作为标定物仅需1张照片3行代码算出f。3.1 A4纸标定法实测误差3%的焦距估算步骤打印一张A4纸确保打印机没缩放用尺子量实际是210×297mm将A4纸竖直贴在墙上用你的摄像头正对拍摄尽量让纸面平行于像平面避免倾斜记录此时纸下边缘到地面的高度记为H_ground并测量相机镜头中心离地高度H_cam运行YOLOv5检测A4纸类别设为paper或用class_id67——A4纸不在COCO里需临时加类或用通用book替代获取其bbox像素高度h_px真实高度H_real 0.297mA4纸长边物距D |H_cam - H_ground|因纸贴墙近似等于相机到墙面距离代入公式反解f (D × h_px) / H_real# calibrate_focal.py import cv2 from utils.detector import YOLOv5Detector # 假设你已测得相机离地高度1.2mA4纸下边缘离地0.8m → D 1.2 - 0.8 0.4m D 0.4 # meter H_real 0.297 # A4 paper height in meter detector YOLOv5Detector(weightsweights/yolov5s.pt) cap cv2.VideoCapture(0) print(Align A4 paper vertically on wall, press c to capture...) while True: ret, frame cap.read() cv2.imshow(Calibration, frame) if cv2.waitKey(1) 0xFF ord(c): # 检测A4纸此处用book类近似因其长宽比接近 bboxes detector.detect(frame) for cls_id, conf, x1, y1, x2, y2 in bboxes: if cls_id 73: # book in COCO, ID73 h_px y2 - y1 f (D * h_px) / H_real print(f✅ Focal length estimated: {f:.1f} pixels) print(f (D{D}m, H_real{H_real}m, h_px{h_px})) break break cap.release() cv2.destroyAllWindows()为什么A4纸法比张正友标定更适合本场景张正友标定求的是fx, fy, cx, cy四个内参但测距公式只敏感于f且fx≈fy在大多数镜头下成立A4纸法直接在工作距离如0.5–3m下标定避免了广角镜头在远距离标定后近处误差爆炸的问题实测对比同一台罗技C920在1m处用A4纸法得f852.3张正友标定得fx841.7相对误差1.2%但A4纸法耗时1分钟张正友需15分钟10张图。3.2 焦距f的物理意义与典型值参考f不是镜头物理焦距mm而是等效焦距pixel由物理焦距f_mm、传感器尺寸sensor_width_mm、图像宽度img_w_px共同决定$$ f_{pixel} f_{mm} \times \frac{img_w_{px}}{sensor_width_{mm}} $$常见设备f_pixel参考值640×480输出下设备典型f_pixel说明手机前置iPhone 12720–780传感器小等效焦距短罗技C9201080p830–8701/2传感器主流USB摄像头树莓派HQ Camera950–10201/2.3大底f_pixel更高Jetson Nano CSI接口1050–1120配套12MP模组视野窄但f大注意如果你换摄像头、改分辨率如从640→1280f必须重新标定因为f_pixel随img_w_px线性变化。例如C920在1280×720下f≈1650不是640下的两倍因存在镜头畸变补偿实际约1.92倍。4. 测距逻辑封装与多目标支持把公式变成可调、可扩展的Python类有了f和H_real测距就是代入计算。但真实场景中你需要同时测多个目标人、车、箱对不同类别设置不同H_real不能所有人按1.7m算小孩会严重偏高过滤低置信度检测conf0.5的框测距毫无意义加入距离合理性校验如D0.3m或D15m直接丢弃。我们封装一个MonocularDistanceEstimator类支持热插拔H_real映射表并预留depth_refinement钩子后续可加运动模糊补偿、多帧平均等# utils/depth_estimator.py import numpy as np class MonocularDistanceEstimator: def __init__(self, focal_length, height_mapNone): :param focal_length: camera focal length in pixels :param height_map: dict mapping class_id to real_height in meters e.g., {0: 1.7, 2: 1.4, 26: 0.4} for person, car, suitcase self.f focal_length self.height_map height_map or { 0: 1.7, # person: avg height 1: 0.8, # bicycle: wheel diameter 2: 1.4, # car: hood height 26: 0.4, # suitcase: typical height 73: 0.297 # book: A4 paper height } def estimate(self, bboxes, min_conf0.5, min_distance0.3, max_distance15.0): Estimate distance for each bbox :param bboxes: list of [cls_id, conf, x1, y1, x2, y2] :return: list of [cls_id, conf, x1, y1, x2, y2, distance_m] results [] for cls_id, conf, x1, y1, x2, y2 in bboxes: if conf min_conf: continue if cls_id not in self.height_map: continue h_px y2 - y1 if h_px 0: continue H_real self.height_map[cls_id] D (H_real * self.f) / h_px # 距离合理性过滤 if D min_distance or D max_distance: continue results.append([cls_id, conf, x1, y1, x2, y2, round(D, 2)]) return results # 使用示例集成到主循环 if __name__ __main__: detector YOLOv5Detector(weightsweights/yolov5s.pt) # 假设你用A4纸标定得f852.3 estimator MonocularDistanceEstimator(focal_length852.3) cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break bboxes detector.detect(frame) distances estimator.estimate(bboxes) # 可视化在bbox上绘制距离 for cls_id, conf, x1, y1, x2, y2, D in distances: label f{estimator.names[cls_id]} {D}m cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, label, (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow(Monocular Distance, frame) if cv2.waitKey(1) ord(q): break cap.release()关键设计点说明height_map支持动态注入产线测快递箱就设{26: 0.3}工地测安全帽就加{29: 0.25}安全帽高度min_conf0.5是血泪经验YOLOv5在conf0.3时经常把阴影当人测距结果跳变剧烈0.5是精度与召回的临界点max_distance15.0不是拍脑袋当h_px 5像素时D对h_px的微小误差极度敏感导数爆炸此时测距失去意义不如直接判为“超出范围”。5. 避坑指南YOLOv5单目测距的5个真实翻车现场与解法单目测距看似公式简单但落地时90%的问题不出在代码而出在物理假设失效。以下是我在3个不同项目仓储AGV、社区门禁、教育机器人中踩过的坑每条都附现象、根因、解法拒绝空谈。5.1 现象同一个人站在1m和2m处测距结果都是1.8m原因YOLOv5检测框高度h_px未随距离线性变化——因为人不是刚体1m处人全身入镜bbox高度≈身高2m处人只拍到 torsobbox高度≈0.6×身高导致D ∝ 1/h_px计算失真。解法限定检测区域只取bbox中心点y坐标在画面中下1/3区域的目标排除远景头部特写加姿态过滤用轻量姿态估计模型如MoveNet判断是否全身可见visibility_score 0.7才参与测距实践效果某AGV项目中加中下区域过滤后1–3m误差从±0.8m降至±0.25m。5.2 现象白天测距准晚上开灯后所有距离变短20%原因LED灯光造成YOLOv5检测框收缩——亮光下目标边缘锐利bbox更紧致h_px变小 →D变大但实际是h_px被低估公式算出D反而变小因D ∝ 1/h_px。解法统一曝光策略用cv2.VideoCapture.set(cv2.CAP_PROP_AUTOFOCUS, 0)关自动对焦set(cv2.CAP_PROP_EXPOSURE, -6)锁死曝光值亮度自适应阈值统计画面平均亮度mean_bright frame.mean()当mean_bright 60暗时conf_thres下调至0.35避免漏检导致h_px虚高验证方法在暗光下拍一张A4纸重新标定f你会发现f_dark ≈ 0.92 × f_day证明光照改变等效焦距。5.3 现象侧身站立的人测距比正面远30%原因YOLOv5 bbox高度h_px反映的是图像投影高度侧身时人体在像平面投影高度减小透视压缩但H_real仍用1.7m导致D被高估。解法引入宽高比修正计算aspect_ratio (x2-x1)/(y2-y1)当aspect_ratio 0.6偏胖/侧身时用H_real_adj H_real × (1 - 0.4×(aspect_ratio - 0.5))动态缩放更优方案用ReID特征向量聚类同一ID连续帧中取h_px最大值对应最正面姿态参与测距数据佐证在门禁项目中加宽高比修正后侧身误差从0.5m降至0.08m。5.4 现象树莓派上跑着跑着距离突然跳到100m原因内存不足触发Linux OOM Killer杀掉Python进程后重启但detector对象未重置内部CUDA缓存错乱pred返回全零tensorscale_coords()输出异常坐标h_px为负或极大。解法强制重载模型每1000帧后del detector.model; torch.cuda.empty_cache(); detector YOLOv5Detector(...)加兜底校验if h_px 5 or h_px frame.shape[0]*0.8: continue硬件级规避树莓派4B加sudo nano /boot/config.txt添加gpu_mem256避免GPU内存被系统抢占。5.5 现象同一摄像头换不同YOLOv5权重s/m/l/x测距结果差2倍原因不同模型输入尺寸不同yolov5s默认640yolov5x默认1280但你没重标定ff必须与img_size匹配。解法标定与模型强绑定f_640 852, 则f_1280 ≈ 852 × (1280/640) × 0.97 1655×0.97是畸变补偿系数实测经验值自动化适配在YOLOv5Detector.__init__()中根据img_size自动查表或插值f教训某客户用yolov5l替换s后未重标定导致叉车防撞误触发停机2小时——从此我的交付包里必带calibrate_focal.py和f_table.csv。6. 进阶技巧用运动一致性提升测距稳定性及如何验证你的系统是否靠谱单帧测距注定抖动真实系统必须跨帧优化。这里不讲卡尔曼滤波太重分享一个3行代码解决80%抖动的轻量方案滑动窗口中位数滤波 速度约束。同时教你怎么用一把卷尺、一部手机10分钟完成端到端精度验证。6.1 三步抗抖中位数滤波 速度门限 ID关联# utils/tracker.py from collections import defaultdict, deque import numpy as np class DistanceTracker: def __init__(self, window_size5, max_speed2.0): # m/s self.window defaultdict(lambda: deque(maxlenwindow_size)) self.last_pos {} # {track_id: (x_center, y_center, D)} self.max_speed max_speed def update(self, detections): :param detections: list of [cls_id, conf, x1, y1, x2, y2, D] :return: list of [cls_id, conf, x1, y1, x2, y2, D_smooth] results [] for det in detections: cls_id, conf, x1, y1, x2, y2, D det x_c, y_c (x1x2)//2, (y1y2)//2 # 1. 简单IOU关联可替换为ByteTrack等 track_id self._match_target(x_c, y_c, cls_id) # 2. 存入滑动窗口 self.window[track_id].append(D) # 3. 中位数滤波 速度校验 D_med np.median(self.window[track_id]) if track_id in self.last_pos: last_D self.last_pos[track_id][2] if abs(D_med - last_D) self.max_speed * 0.1: # 100ms内最大位移 D_med last_D # 丢弃突变值 self.last_pos[track_id] (x_c, y_c, D_med) results.append([cls_id, conf, x1, y1, x2, y2, round(D_med, 2)]) return results def _match_target(self, x_c, y_c, cls_id, iou_thresh0.3): # 简化版找最近的已有track欧氏距离 min_dist 9999 best_id None for tid, (lx, ly, _) in self.last_pos.items(): dist np.sqrt((x_c-lx)**2 (y_c-ly)**2) if dist min_dist and dist 50: # 50px内认为同一目标 min_dist dist best_id tid if best_id is None: best_id len(self.last_pos) # 新ID return best_id为什么中位数比均值好均值会被单帧误检如h_px2导致D100m拖垮中位数天然鲁棒5帧窗口下容忍2帧异常加速度约束max_speed进一步过滤突变实测将标准差降低65%。6.2 端到端精度验证卷尺手机录像10分钟出报告别信理论误差用物理世界验证。方法如下步骤操作目标1在地面贴一条胶带标出0m、1m、1.5m、2m、3m位置提供真值距离2固定摄像头正对胶带高度1.2m消除俯仰角影响3请一人站到1m标记处手机录30秒视频含走动获取动态数据4用上述代码处理视频导出每帧D_estimated得到算法输出5用OpenCV逐帧读取用cv2.getTextSize()测胶带在画面中的像素位置反算D_ground_truth (H_real × f) / h_px_gt得到真值比卷尺更准消除视角误差关键指标表格某次实测距离区间帧数平均误差最大误差标准差是否达标0.8–1.2m12400.03m±0.12m0.04m✅0.15m1.5–2.0m980-0.05m±0.18m0.06m✅2.5–3.0m4200.11m±0.25m0.09m⚠️超0.2m需调H_real我的习惯每次换摄像头、换环境、换目标类型必做此验证。有一次发现3m外误差超标排查发现是H_real设了1.7m成人但测试用的是12岁小孩1.45m调后误差直降50%。测距不是调参游戏是不断逼近物理真实的校准过程。希望帮到你。本文还有配套的精品资源点击获取
返回列表