ARTICLE DETAIL

资讯详情

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

单目测距系统原理与YOLOv5工程实现:从标定到误差校准

单目测距系统原理与YOLOv5工程实现:从标定到误差校准 简介面向毕业设计场景的YOLOv5单目测距系统源码包适合计算机视觉、目标检测与距离估计方向的本科生或研究生尤其适合需要快速搭建可演示系统的开发者。资源整合了模型训练、推理检测、距离解算与部署环境配置除了目标框识别还加入基于几何关系的单目测距模块解决从数据配置、YOLOv5训练到实际距离输出的完整链路问题。压缩包共78个文件包含28个Python脚本、26个YAML配置、6个Shell脚本、2个PyTorch权重文件及2个演示视频等总大小215.3MBPython脚本覆盖训练、检测和测距流程YAML配置对应模型结构、训练超参与数据路径权重文件和演示视频可用于快速验证效果。配套Docker环境配置与依赖声明降低复现门槛模型、数据、工具、权重等目录划分清晰便于按模块修改和二次开发。已有3146人学习下载适合需要系统完成毕业设计、优化单目测距算法或对照完整项目源码查漏补缺的开发者。1. 单目测距的真相一个相机能把距离估算到什么程度拿到一份基于YOLOv5的单目测距系统源码毕业设计.zip你是不是先解压、配环境、跑 demo我建议你先找到算距离的那一行。严格说单目相机测不出距离——一个 50 米外的大货车和 3 米外的电动车在图像上投影出来的轮廓大小几乎一样透视投影天然丢失了深度。所以单目测距的本质不是“测”而是“估”假设目标物理尺寸已知再拿相机模型反推深度。这套系统成不成取决于三个参数是否可靠焦距、相机安装高度、地平线对应的像素行。做毕设、做辅助驾驶预警、做安防周界和巡检机器人都需要弄懂这条链路。本文按工程落地顺序把这套源码拆开讲先立数学模型再做内参和外参标定然后说 YOLOv5 训练阶段哪些细节影响远距离测距最后给出推理集成和误差校准技巧。2. 测距数学与相机内参YOLOv5测距系统先迈过的两道坎2.1 相似三角形测距公式的边界条件最常见的单目测距代码只有一行D f_y * h / h_pixel其中f_y是相机内参里的纵向焦距像素单位h是目标的真实物理高度h_pixel是检测框的像素高度。这个公式来源于针孔相机模型在光轴与地面近似平行时的退化形式物体在图像中的高度与距离成反比比例系数就是焦距。把它写成更完整的形式D f_y * h * cos(θ) / h_pixelθ 是相机光轴相对水平面的俯仰角。θ 小于 5° 时cos(θ) 在 0.996 以上省略它问题不大一旦相机向下压了 10° 朝路面看这一项会让距离整体偏大约 1.5%随着目标变远还会继续放大。这个公式成立还有三个前提一是目标底部能被检测框准确抓住人的话必须贴脚底二是目标的真实高度在场景里基本恒定行人站立的高度可以假设为 1.7 米但蹲下、骑车的目标不能套用三是不能让检测框包含阴影或反光多一个像素远处目标的距离误差就会跳好几个百分点。很多毕设源码把检测框的ymin/ymax直接丢进公式测出来的曲线在 20 米内还能看再远就向上翘不是模型坏了是这三个前提先被破坏了一个。2.2 棋盘格标定求 fy、cx、cy 的完整流程要让f_y可信必须做相机内参标定。常见做法是用 OpenCV 的棋盘格流程整体步骤如下打印一张 9×6 内角点的棋盘格贴在硬板上固定好相机光圈和分辨率在画面各个区域拍 20 到 30 张不同角度和距离的照片然后跑下面的脚本import cv2 as cv import numpy as np import glob criteria (cv.TERM_CRITERIA_EPS cv.TERM_CRITERIA_MAX_ITER, 30, 0.001) # 9x6 内角点棋盘格 objp np.zeros((6*9, 3), np.float32) objp[:, :2] np.mgrid[0:9, 0:6].T.reshape(-1, 2) obj_points [] # 世界坐标系中的棋盘角点 img_points [] # 图像坐标系中的角点 for fname in sorted(glob.glob(calib/*.jpg)): img cv.imread(fname) gray cv.cvtColor(img, cv.COLOR_BGR2GRAY) ret, corners cv.findChessboardCorners(gray, (9, 6), None) if ret: obj_points.append(objp) # 亚像素角点精化能明显提升标定稳定性 corners2 cv.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) img_points.append(corners2) cv.drawChessboardCorners(img, (9, 6), corners2, ret) ret, mtx, dist, rvecs, tvecs cv.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None) print(fx , mtx[0, 0]) print(fy , mtx[1, 1]) print(cx , mtx[0, 2]) print(cy , mtx[1, 2])逻辑说明objp以棋盘格左上角为原点建立世界坐标每个内角点的z为 0单位是格子的物理边长这里简化为一个格子一个单位。cornerSubPix在初始角点附近做亚像素迭代把角点定位精度从整数像素提升到亚像素级直接决定标定结果。calibrateCamera同时解出内参mtx、畸变系数dist和每张图的外参输出里的fx、fy是像素单位焦距cx、cy是主点。实际标定时dist的五维畸变系数要在后续推理阶段用cv.undistort或cv.undistortPoints去掉否则画面边缘的目标框位置会偏。2.3 标定结果的三个质量检查点标定完不要直接抄数字就关终端。第一看ret值即重投影均方根误差工业相机一般小于 0.3手机镜头在 0.5 以内都能接受超过 1.0 说明有模糊照片或角点误检。第二看fx和fy的比值常见镜头两者相差不超过千分之几如果差距超过 2%要么是标定照片太少要么是图像被缩放变形过。第三看主点cx、cy是否接近图像中心附近普通的 1920×1080 图像主点一般在 (960±30, 540±30) 区间偏差过大往往是只拍了棋盘在角落的照片或者镜头畸变模型选错。提示做毕设和短期项目时内参标定可以跳过。直接用 3.2 节的现场拟合方法把f_y * H合并成一个系数k同样能算出距离。内参标定的价值在于相机高度改变时按比例缩放k就能复用不必重新拟合。3. 外参标定不拆相机也能拟合相机高度与地平线像素行3.1 用线性回归代替深度测量v_bottom 对 1/D 的关系内参标定之后还需要知道相机安装高度H和地平线对应的像素行v_h。直接量相机距地面高度并不难难的是地平线在图像里的位置它不是一个能被肉眼精确识别的图像特征。工程上有个更聪明的做法把外参当成黑盒去拟合。物理模型是这样推导的当相机光轴与地面近似平行时地面上一个目标接地点在图像中的行号v_bottom与目标距离D满足v_bottom - v_h f_y * H / D变形之后得到v_bottom v_h (f_y * H) * (1 / D)也就是说v_bottom关于1/D是一条直线直线的斜率是f_y * H截距是地平线像素行v_h。因此只要在已知距离处放置标记物记录它底边对应的像素行就可以用最小二乘把v_h和k f_y * H都解出来。全程不需要拆相机、不需要量安装高度、不需要知道俯仰角这是这套方法最省事的地方。如果相机带明显俯仰角v_bottom与1/D的关系会偏离直线此时完整反解公式是D k / (Δv * cos²θ) - H * tanθ其中Δv v_bottom - v_h。但工程现场更常见的做法不是解这个三角公式而是分近距、中距、远距三段做分段线性拟合把残余偏差在部署前压掉。3.2 最小二乘标定脚本10 分钟完成现场标定外参拟合的现场操作步骤找一段平直路面在距离相机 5、8、12、18、25、35、50 米处放置锥桶或让一个人站在标记点每处拍一张照片记录目标脚底或底边对应的像素行v。数据量不需要多7 组点足够。脚本如下import numpy as np # 记录距离米与对应接地点像素行 D np.array([5, 8, 12, 18, 25, 35, 50], dtypefloat) v np.array([155, 210, 260, 305, 340, 378, 400], dtypefloat) # 拟合: v v_h k * (1/D) A np.vstack([np.ones_like(v), 1 / D]).T coef, *_ np.linalg.lstsq(A, v, rcondNone) v_h coef[0] # 地平线像素行 k coef[1] # k f_y * H单位: 像素行*米 # 验证每个距离的重构误差 D_est k / (v - v_h) err (D_est - D) / D * 100 for d, d_est, e in zip(D, D_est, err): print(f实测 {d:5.1f}m 估算 {d_est:5.2f}m 误差 {e:.1f}%)逻辑说明lstsq求解A * [v_h, k] v最小二乘让所有标定点的重构误差平均最小。代码里的验证循环把拟合系数代回原式重建每个距离的估计值这一步很重要——如果某个点误差超过 15%大概率是记录行号时看错了目标位置直接把该点删掉再拟合。这里v的取值要从图像顶部向下数单位为像素行YOLOv5 的ymax输出与之一致可以直接对接。得到v_h和k后部署阶段的测距公式就是一个除法D_est k / (v_bottom - v_h)这个形式已经不含内参也不含相机高度非常适合嵌入到源码里当作标定参数。3.3 目标不是接地点怎么办按类别分配等效高度外参拟合适合目标底部接地的场景行人和车辆都满足。但 YOLOv5 检测出的类别还有自行车、摩托车、狗它们底边并不总是清晰着地或者目标底部被遮挡。这种情况下强行用v_bottom会导致距离偏高处理方法是给每个类别一个“等效高度”参数用检测框像素高度除以类别高度去换算。在源码配置里常见做法是维护一个字典REAL_HEIGHTS { person: 1.70, # 行人按站立高度 car: 1.45, # 轿车按车顶到地面高度 bus: 2.60, truck: 3.00, bicycle: 1.25, }然后计算每个目标的距离D f_y * REAL_HEIGHTS[cls] / (ymax - ymin)这个方法的误差来源很直接真实高度越不稳定的类别测距越不可靠。行人蹲下时按 1.7 米估算距离会被夸大 30%。所以工程上建议把目标分成刚体和非刚体车、集装箱、路牌属于刚体可以放心按高度算人、动物属于非刚体能接地点就用底角法不能接地就不要输出测距结果。毕设里通常只需要对人和车两类做测距把这两类的高度参数标定准确展示效果就已经足够。4. 用YOLOv5训练测距专用模型数据、标注与超参数的四个细节4.1 采集数据时锁死相机参数别让训练分布漂移很多团队直接把公开数据集拿来训练 YOLOv5检测精度挺好但一旦把模型部署到自己相机的画面上测距数值就开始飘。原因在于“检测框高度”这个量对相机分辨率和视野极其敏感。公开数据集图像尺寸、拍摄视场角和部署相机不一致模型输出的ymax - ymin与真实目标的成像高度存在系统性偏差而这个偏差直接进测距公式。采集训练数据时应该用与部署环境相同的相机参数拍摄。需要锁死的是四项分辨率比如固定 1920×1080、镜头焦距、安装高度、俯仰角。采集场景要覆盖 10 到 50 米的不同距离阴天、顺光、逆光都要有样本。连续视频抽样容易产生大量相似帧这里有个技巧隔 5 帧采一帧并且丢弃检测框高度变化小于 3 像素的样本这样能在有限的图片张数里覆盖更多距离梯度。4.2 标注框贴着脚底/轮胎下缘远距离测距误差小一半YOLOv5 标注时最容易犯的错误是“包住整个目标边缘”或者“给行人头顶多留一圈空白”。如果检测框比真实目标大 4 个像素在 30 米处行人框高可能只有 30 像素4 个像素的虚高会让测距缩短约 13%。标注规范应该是人的 top 贴头顶最亮像素bottom 贴脚底与地面交界的下缘不包阴影不包背后反光车的 top 贴车顶最高点bottom 贴轮胎与地面接触线不要包住保险杠下沿的黑色阴影。实现标注时用常见标注工具导出 YOLO 格式的class x_center y_center width height宽高均为除以原图宽高后的归一化数值YOLOv5 训练直接读取。4.3 data.yaml 与训练命令以 960 分辨率替代默认 640数据准备好之后建立一份针对测距任务的 YAML 配置# datasets/distance.yaml path: ./datasets/road train: images/train val: images/val nc: 3 names: [person, bicycle, car]训练命令建议把默认的 640 分辨率提高到 960python train.py \ --data datasets/distance.yaml \ --weights yolov5s.pt \ --img 960 \ --epochs 120 \ --batch-size 16 \ --hyp data/hyps/hyp.scratch-med.yaml \ --device 0分辨率的选取逻辑是测距精度不取决于分类置信度而取决于检测框底边的像素定位精度。相同目标在 640 分辨率下高 30 像素提升到 960 就有 45 像素同一个 1 像素定位误差对距离的影响从 3.3% 降到 2.2%。代价是显存占用变高12GB 显存跑yolov5s的 960 输入、batch size 16 基本是上限显存不够就降到 768 或 batch size 8。hyp.scratch-med.yaml是中等增强方案相比scratch-low增加了 mosaic 和 mixup 的概率对路面遮挡场景更稳。4.4 关闭多尺度、放大推理尺寸稳定远距离框高YOLOv5 训练默认开启多尺度训练随机把输入尺寸缩放到 0.5 到 1.5 倍之间。这个策略能提升检测鲁棒性但对测距是双刃剑模型在训练时见过不同尺度的目标输出框的回归会更偏向“归一化尺寸”导致某个固定物理高度目标的像素高度条曲线不够稳定。常见做法是训练时保留多尺度让模型见多识广推理时关闭它直接用固定尺寸--img 960或 1280 输入保证同一目标在不同帧里的框高波动尽量小。推理尺寸建议按部署场景选近距离 2 到 30 米用 640 就够检测速度快要做 50 米以上的远距离测距推理尺寸提高到 1280远距离小目标底边能多出几个像素的定位余量。与此同时YOLOv5 的 NMS 参数conf_thres和iou_thres也要按场景调整。测距场景宁可漏检也不要误检建议把conf_thres从默认 0.25 提高到了 0.35iou_thres保持 0.45减少乱七八糟的误检框进入测距模块。5. 推理集成把距离结果画进YOLOv5的输出框5.1 完整推理脚本检测、算距、过滤、绘制一条龙把前面所有参数串起来一个完整的推理脚本长这样import math import cv2 as cv import torch import numpy as np # 模型与标定参数 model torch.hub.load(ultralytics/yolov5, custom, pathruns/train/exp/weights/best.pt, force_reloadTrue) model.conf 0.35 model.iou 0.45 model.multi_label False K_DIST 4760.0 # 现场标定的 k f_y * H V_HORIZON 235.0 # 标定得到的地平线像素行 img cv.imread(test.jpg) results model(img, size960) df results.pandas().xyxy[0] for _, row in df.iterrows(): cls_name row[name] if cls_name not in (person, car): continue v_bottom row[ymax] delta v_bottom - V_HORIZON if delta 5: # 底边太接近地平线误差爆炸直接丢弃 continue dist K_DIST / delta cv.rectangle(img, (int(row[xmin]), int(row[ymin])), (int(row[xmax]), int(row[ymax])), (0, 255, 0), 2) cv.putText(img, f{cls_name} {dist:.1f}m, (int(row[xmin]), int(row[ymin]) - 10), cv.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) cv.imwrite(output.jpg, img)逻辑说明torch.hub.load直接加载训练好的best.ptpandas().xyxy[0]返回每个框的坐标、类别名和置信度。delta是底边像素行相对地平线的差值差值越小代表目标越远数值小于 5 个像素时距离的分母趋近于零一个像素的波动会造成几十米的跳变直接过滤是工程惯例。K_DIST和V_HORIZON来自第 3 章的拟合结果不同摄像头的值不一样不要直接抄别人的参数。5.2 高度法与底角法的选型对照表测距公式选错了后边的校准都白搭。两种方法的适用边界差异很大方法依赖参数适用目标主要误差源实现成本高度法目标真实高度、焦距车、固定高度的刚体目标高度不恒定、框高受遮挡影响低一行公式底角法相机高度或拟合系数行人、接地点清晰的目标地平线位置标定误差、俯仰角漂移中需要外参标定高度法适合车辆检测因为各种车型的高度相对稳定底角法适合行人检测因为行人脚底总在地面上且不受行人蹲起影响。一个实用的源码设计是行人用底角法车用高度法两类分开算而不是所有目标共用一个公式。5.3 分段线性校准消除系统偏差的最后一公里即使标定完成实测曲线依然会有系统性偏差。常见现象是近处5 到 10 米偏低远处30 米以上偏高。这个偏差主要来自检测框底边定位误差和镜头畸变残留标定过程很难完全消除。工程做法是在源码里加一张分段校准表用实测数据修正输出未校准测量值米实测真值米校准系数真值/测量值4.85.01.04212.313.01.05727.830.01.07946.550.01.075校准逻辑是先测量一组已知距离算出每个区间的比例系数部署时按当前测量值所属区间查表插值。比全局乘同一个系数的效果好得多因为 “框高像素定位误差” 在远处放大的比例不是线性关系。6. 测距误差验证与三个进阶技巧RMSE压进5%以内6.1 距离-误差验证表怎么写验证测距系统时不要只在 10 米处拍三张照片就完事。标准做法是选 5、10、15、20、30、40、50 米这 7 个点每个点连续测 10 次记录平均估计距离、最大、最小值和均方根误差。一个合格的验证表应该长这样真值米平均估计值米最大误差米相对误差5.05.120.356.9%10.010.260.606.0%20.020.841.457.2%50.052.104.308.6%多组数据的平均值接近真值但单次误差很大说明随机波动来自检测框抖动平均误差也整体偏移说明标定系数或校准表需要重做。6.2 亚像素底边提取让ymax不再是一格整数像素框底边ymax是整数像素一个小目标在 30 米外可能只有 25 像素高1 像素的定位误差就带来 4% 距离误差。进阶做法是在检测框底边附近做亚像素边缘提取把ymax上下各取 10 行灰度逐列求梯度峰值再取中位数作为底边位置。roi gray[int(ymax) - 10:int(ymax) 10, int(xmin):int(xmax)] col_edges [] for c in range(roi.shape[1]): grad np.gradient(roi[:, c].astype(float)) col_edges.append(np.argmax(np.abs(grad))) sub_row int(ymax) - 10 np.median(col_edges)逻辑说明底边与地面交界处在图像中通常是灰度突变点梯度最大值所在位置就是边缘。按列分别求梯度再取中位数能避免单列噪点干扰得到亚像素量级的底边估计。这个技巧能把远距离测距的相对误差压缩 2 到 3 个百分点是源码包里最值得替换的一处优化。6.3 突发滤波与姿态漂移后的快速重标定最后一公里的稳定性靠滤波。对距离序列做低通滤波时要注意防止目标本身快速变化被平滑掉。一个简单的跳变剔除滤波实现是如果当前帧距离与上一帧之差超过 1.5 米则认为检测框定位异常保留上一帧结果否则用D 0.7 * D_prev 0.3 * D_obs更新。系数0.7/0.3在响应速度和稳定性之间比较平衡部署时按帧率调整帧率越高D_prev的权重可以越大。相机安装姿态漂移比如固定支架被碰歪会让V_HORIZON失效。现场快速重标定不需要重新走完整流程只要找 20 米处放一个锥桶拍一帧反推新的V_HORIZON v_bottom - K_DIST / 20更新配置里的地平线参数即可。K_DIST保持不变两台相机之间微小的外参差异用这个单点重标定法可以在 30 秒内完成是毕设答辩现场演示前最实用的救场操作。本文还有配套的精品资源点击获取
返回列表