yolov5训练与部署:从VOC XML数据集到非机动车违规停放识别
简介面向YOLOv5目标检测与机器视觉识别场景的已标注非机动车数据集内容聚焦自行车类别精选其中某一细分类型的图像与XML标注文件可用于违规停放检测、车辆识别等模型的训练、验证与算法实验。资源压缩包共包含1896个文件以951张JPG图片和945个XML标签文件为主体图片与标注基本一一对应节省了自行采集和标注的时间整体大小约138.26MB便于下载与快速部署。数据规模适中适合作为小样本训练集、模型效果预验证或与其他自行车子类合并扩充训练数据。该资源目前已有275人学习使用适合具备一定深度学习基础、正在开展目标检测或智慧城市相关项目的开发者参考。1. yolov5非机动车违规停放识别先搞清想识别的是车还是违规行为小区门口、园区通道、地铁站周边自行车和电动车横七竖八停一地的画面相信你不陌生。物业或城管想靠摄像头自动发现违规停放最直接的想法就是用yolov5做机器视觉识别。但这里有个关键分水岭yolov5是一个目标检测模型它只能告诉你画面里哪个位置有一辆自行车而违规停放是一个需要结合禁停区域、停放方向、停留时长才能判断的业务规则。bicycles2_images_xmls这套已标注数据集正好是用来训练看见自行车这一步的。本文会把这条链路完整拆开从VOC XML标注转成yolov5需要的格式到训练出能检测自行车的模型再在检测框之上加一层区域判定逻辑最后落到边缘部署和参数调优。适合正在做智慧社区、停车场管理或城市精细化管理手头有相似数据集但还没跑通全流程的从业者。2. 从bicycles2_images_xmls到yolov5能吃的格式数据集转换与目录结构2.1 VOC XML标注里到底存了什么bicycles2_images_xmls 这类命名通常意味着图像集是 JPEG/PNG标注文件是 PASCAL VOC 风格的 XML。打开一个 XML 你会看到 object 节点里的 name、bndboxname 是类别名bndbox 里是 xmin、ymin、xmax、ymax 四个整数表示目标左上角和右下角在图像坐标系下的像素坐标。有些 XML 还带 truncated、occluded、difficult 标记转换时一般不直接透传。yolov5 原生训练用的是 YOLO 格式每张图对应一个同名 txt 文件每行是class_id cx cy w h前四个数都是相对图像的归一化值除以图像宽高不是像素绝对值。所以第一步是把 XML 里的绝对坐标做归一化并换成 YOLO 的中心点加宽高表示。如果数据集里只有自行车一类class_id 就是 0如果还有电动车、三轮车需要先确认类别清单避免 ID 错位。2.2 XML转TXT的Python脚本一个文件跑通下面这段脚本能处理整个 images_xmls 文件夹把每张图的 XML 转成对应的 txt并按类别写入 classes.txt。我用的是纯标准库加 xml.etree不依赖第三方在任何有 Python 3 的环境都能跑。import os import xml.etree.ElementTree as ET from glob import glob # 配置你的路径 XML_DIR bicycles2_images_xmls/xmls # XML所在文件夹 IMG_DIR bicycles2_images_xmls/images # 图像所在文件夹 OUT_DIR yololabels # 输出txt的文件夹 os.makedirs(OUT_DIR, exist_okTrue) # 如果有多个类别按你想要的顺序写 CLASSES [bicycle] def convert(xml_path): tree ET.parse(xml_path) root tree.getroot() filename root.find(filename).text # 原图文件名 img_path os.path.join(IMG_DIR, filename) if not os.path.exists(img_path): img_path img_path[:-4] .jpg # 有些标注的记录名和实际扩展名不一致 # 用PIL读宽高避免依赖xml里的size可能不准 from PIL import Image w, h Image.open(img_path).size out_lines [] for obj in root.iter(object): name obj.find(name).text if name not in CLASSES: continue bnd obj.find(bndbox) xmin float(bnd.find(xmin).text) ymin float(bnd.find(ymin).text) xmax float(bnd.find(xmax).text) ymax float(bnd.find(ymax).text) # 防止坐标出界导致负数 xmin max(0, min(xmin, w - 1)) ymin max(0, min(ymin, h - 1)) xmax max(0, min(xmax, w - 1)) ymax max(0, min(ymax, h - 1)) # 超过边界的标注直接剔除 if xmax xmin or ymax ymin: continue # 转YOLO格式中心点 宽高除以宽高归一化 cx ((xmin xmax) / 2.0) / w cy ((ymin ymax) / 2.0) / h bw (xmax - xmin) / w bh (ymax - ymin) / h cls_id CLASSES.index(name) out_lines.append(f{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) # 输出txt与图像同名扩展名改为.txt out_txt os.path.join(OUT_DIR, os.path.splitext(filename)[0] .txt) with open(out_txt, w) as f: f.write(\n.join(out_lines)) for xml_path in glob(os.path.join(XML_DIR, *.xml)): convert(xml_path) print(转换完成txt文件已生成, len(glob(os.path.join(OUT_DIR, *.txt))))这段脚本的逻辑不复杂先解析 XML 拿文件名和标注再用 PIL 读真实图像宽高最后按 YOLO 要求做归一化。注意我做了两层防御一是坐标绝对值超出图像尺寸时截断二是截断后如果宽高为负直接丢弃该目标。实际数据里经常有标注框画到图外的情况不加这两层训练时会报 negative bbox 之类的错误。2.3 目录布局与YAML配置训练前必须对齐的路径yolov5 训练不是简单把 txt 放一起就行它要求 images 和 labels 分开放且同一张图的 txt 与图像同名。目录结构一般是dataset/ images/ train/ # 训练集图片 val/ # 验证集图片 labels/ train/ # 训练集txt val/ # 验证集txt你需要把转换好的 txt 按图像划分拷贝到对应的 labels/train、labels/val 里同时把原图拷贝到 images/train、images/val。有些标注集没有现成的 train/val 划分需要自己按比例切。我常用的做法是把所有图片按 8:2 随机分到 train 和 val关键点是同一张图的图像和标签必须落到同一个子目录下否则训练时找不到对应的 txt。然后在 yolov5 项目根目录的 data 文件夹下新建一个 yaml内容如下# dataset.yaml train: dataset/images/train val: dataset/images/val nc: 1 names: [bicycle]train 和 val 写的是图像目录yolov5 会自己根据图像路径推导 labels 路径把 images 换成 labels。nc 是类别数names 是类别名列表顺序必须和转换脚本里的 CLASSES 一致。如果数据集里还有电动车nc 就改成 2names 写成 [bicycle, ebike]转换脚本里 CLASSES 也要同步。2.4 数据集划分与类别参数别让验证集和训练集重叠划分数据时最容易犯的错是按文件顺序前 80% 当训练、后 20% 当验证。这类违规停放数据往往是同一个场景连着拍的连续帧高度相似顺序划分会导致验证集和训练集长得太像mAP 虚高一换场景立刻掉点。正确做法是先把所有图像文件名打乱比如用 Python 的 random.shuffle再按比例切。更稳妥的做法是按拍摄时间或地点分组切保证同一场景的视频帧只出现在一个集合里。另一个容易忽略的是类别不平衡。如果目标是自行车但数据集中绝大部分图片里只有一辆车少数图有五六辆那训练时 yolov5 默认按 batch 内正负样本比例处理一般问题不大。但如果标注漏得厉害比如图片里七八辆车只标了三辆模型会把没标注的车当背景学推理时就容易漏检。这个数据集的 XML 质量如何建议先随机抽 20 个 XML 和对应图像叠一下看看。3. 用yolov5训练自己的非机动车检测模型环境、命令与超参数3.1 conda环境配置与依赖安装yolov5 源码跑起来不算费劲但环境踩过的坑能排一长串。我习惯用 conda 建独立环境Python 版本固定在 3.8 或 3.9避开 3.10 以上某些依赖编译报错。下面是一套能稳定跑通的安装命令conda create -n yolov5 python3.9 -y conda activate yolov5 cd yolov5 # 进入你 clone 的 yolov5 源码目录 pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple pip install torch1.12.0 torchvision0.13.0 --extra-index-url https://download.pytorch.org/whl/torch_cu113requirements.txt 里包含 opencv、matplotlib、numpy、pyyaml 等。torch 版本不用追新1.12 在 CUDA 11.3 下对训练自行车这种小目标足够用。如果你机器没有 NVIDIA 显卡可以装 CPU 版 torch但训练速度会慢几十倍建议至少搞一张 8G 显存的卡再动手。安装完先跑个最简单的自检确认能加载模型python -c import torch; print(torch.__version__, torch.cuda.is_available())如果输出 True说明 GPU 可用。如果 False 别急着继续先解决 CUDA 和 torch 的匹配问题否则后面训练时会卡在显卡奔溃重启之类的坑。3.2 训练命令与超参数速查表训练入口是 train.py最常见的命令如下python train.py --data dataset.yaml --weights yolov5s.pt --img 640 --batch 16 --epochs 100 --cache --name bicycle_run--weights 可以填预训练权重yolov5s.pt / yolov5m.pt也可以填 yolov5s.yaml 从零开始。对自行车这类目标不算太小、但容易被遮挡的物体我建议用 yolov5s 预训练权重做迁移学习收敛快mAP 也比从零训高几个点。--cache 会把图像一次性加载进内存训练提速明显前提是你的内存有 16G 以上。下面这张表是必须关心的超参数我给出了推荐值和调参方向参数命令行推荐值调参逻辑输入尺寸--img640目标太小就提到 960但显存占用翻倍批大小--batch16显存不够就减半保证不 OOM学习率--lr00.01不收敛就降一半收敛慢就升一点训练轮数--epochs100看验证集 mAP 不再上升就停数据缓存--cache默认关闭内存够就开省 30% 训练时间权重--weightsyolov5s.pt数据量少用预训练数据多万茬再用 yaml多尺度--multi-scale关闭目标大小变化大就开但训练变慢另外几个不常改但会影响结果的后处理参数在 detect.py 里用 --conf-thres 和 --iou-thres 设置。训练阶段不需要动推理阶段再调。3.3 训练日志怎么看loss、P、R、mAP曲线训练时终端会实时打日志重点看这几个值box_loss、obj_loss、cls_loss 和最后的 P精确率、R召回率、mAP50、mAP50-95。开头几轮 loss 下降快是正常的如果 train 的 loss 一路降但 val 的 mAP 不动说明过拟合了可以早停或加数据增强。如果 P 高 R 低说明模型保守很多车没框出来适合调低置信度阈值如果 R 高 P 低说明框了一堆误检适合调高置信度阈值。训练结束后runs/train/bicycle_run 目录下会有 weights/best.pt 和 last.pt还有 results.png 和混淆矩阵。best.pt 是验证集上 mAP 最高的权重后面部署用它。如果偶尔发现 best.pt 的 mAP 只比 last.pt 高一点点但 best 对应的轮次很靠前可能是验证集波动建议继续训完再看。3.4 超参数调优这几个最影响小目标的参数自行车在画面里的尺寸差异极大近处一辆占半个画面远处可能只有 30 像素高。yolov5 默认的 anchor 是按 COCO 目标定的对远距离小自行车不太友好。可以在训练前用下面命令分析你数据集中目标的尺寸分布python utils/autoanchor.py --cfg yolov5s.yaml --data dataset.yaml这个脚本会计算数据集中目标的平均宽高比如果算出来的建议 anchor 和默认差异超过 30%它会提示你更新。更新方法是在 yaml 里加一行 anchors再配合 --noautoanchor 参数训练。按我的经验自行车数据集常见的 anchor 调整是增加一组 5x7、7x10、10x14 这种小尺寸 anchor效果立竿见影。另一个影响小目标的关键是 --img 尺寸。640 输入下一个 30 像素的车在 feature map 上就 1 个点几乎无法区分。如果路边停的车离摄像头远建议训练直接用 960 甚至 1280 输入。代价是显存占用和推理速度但对违规停放这种静态抓拍场景慢一点完全值得。4. 从检测框到违规停放区域判定与业务逻辑4.1 为什么纯目标检测不够需要划定禁停区训练好的 yolov5 模型输出是N 个目标每个目标有类别、置信度、边界框。但违规停放不是一个模型能独立输出的概念。举个例子一辆自行车停在规范的停车线内模型照样检测出它业务上却不算违规另一辆停在消防通道上模型也检测出它这才是违规。所以检测模型解决有没有车规则引擎解决停得对不对。常见做法是在摄像机画面里人工划定禁停多边形区域然后用检测框的中心点或底部中心点是否落在多边形内来判定违规。自行车检测框容易抖动用底部中心点x_center, ymax比用框中心更稳因为底部中心点对应车胎与地面的接触点不太受车把、反光镜等高处结构影响。4.2 用坐标判定自行车是否在禁停区内的思路写一个点是否在多边形内部的函数不需要引入大库纯数学就能做。我常用射线法从点出发向右作水平射线计算与多边形边的交点个数奇数则在内。# point_in_polygon.py def point_in_polygon(pt, poly): 射线法判断点是否在多边形内 pt: (x, y) poly: [(x1, y1), (x2, y2), ...] 顶点按顺序排列 返回 True 表示在内部 x, y pt n len(poly) inside False x1, y1 poly[0] for i in range(1, n 1): x2, y2 poly[i % n] if (y1 y) ! (y2 y) and x (x2 - x1) * (y - y1) / ((y2 - y1) or 1e-9) x1: inside not inside x1, y1 x2, y2 return inside # 示例把检测结果送进来做判定 def is_parking_violation(box, forbidden_zone): box: [x1, y1, x2, y2] 像素坐标 forbidden_zone: 多边形顶点列表像素坐标 x_center (box[0] box[2]) / 2.0 bottom_y box[3] # ymax return point_in_polygon((x_center, bottom_y), forbidden_zone)注意坐标系要统一。yolov5 后处理输出的是图像像素坐标直接拿来和手工标注的禁停区比较即可。如果禁停区是在原始高清图上画的而推理时把图像缩放到 640 再输入那必须把检测框坐标按缩放比例映射回原图不然区域判定全是错的。4.3 集成单应性变换用投影坐标代替像素坐标可选如果摄像头有俯仰角地面上的禁停区在画面里是梯形。用像素多边形也能人工画但当日光变化导致摄像头角度轻微偏移时像素多边形会失真。更规范的做法是先标定一个地面坐标系用单应性矩阵把图像坐标投影到俯视平面再在俯视图上画禁停区。这样区域判定对角度变化更健壮也方便和 GIS 或地图数据对齐。OpenCV 里用 findHomography 求矩阵需要地面四个参考点的像素坐标和对应的真实世界坐标通常在地面贴四个标记物。变换代码如下import cv2 import numpy as np # 四个点在图像中的像素坐标 pixels np.array([[100, 200], [400, 200], [100, 500], [400, 500]], dtypenp.float32) # 四个点在真实地面坐标系中的坐标单位米 world np.array([[0, 0], [5, 0], [0, 3], [5, 3]], dtypenp.float32) H, _ cv2.findHomography(pixels, world) # 检测框底部中心点投影到地面坐标 def to_world_point(img_pt): pt np.array([[img_pt]], dtypenp.float32) # shape (1,1,2) world_pt cv2.perspectiveTransform(pt, H) return (float(world_pt[0][0][0]), float(world_pt[0][0][1]))加了这一步后禁停区就不再是画在图像上的多边形而是地面的矩形或任意封闭区间。对多摄像头覆盖同一片区域的情况统一到地面坐标还能避免各视角各自的像素规则冲突。不过单应性标定需要现场操作如果只做一个测试摄像头用像素多边形更快等验证有效再升级。4.4 违规事件触发与去重同一辆车别重复报警业务上最尴尬的事是同一辆自行车每隔几秒触发一次违规告警把平台通知淹没。去重逻辑一般在后处理层做给每个检测目标分配一个短时 ID用 IoU 跟踪它在连续帧中的位置当目标在禁停区内的连续时间超过阈值比如 30 秒才生成一条事件。之后只要目标没有移出禁停区就不重复上报。# 简单去重伪代码 track_dict {} # target_id - {iou, last_time, reported_flag} MOTION_THRESH 0.1 # 两帧之间的IoU低于这个值视为新目标 for box in detected_boxes_in_forbidden_zone: matched_id None for tid, tinfo in track_dict.items(): if compute_iou(box, tinfo[box]) MOTION_THRESH: matched_id tid break if matched_id is None: new_id len(track_dict) 1 track_dict[new_id] {box: box, last_time: time.time(), reported_flag: False} else: track_dict[matched_id][box] box # 已存在超过30秒且没报过警触发 if time.time() - track_dict[matched_id][last_time] 30 and not track_dict[matched_id][reported_flag]: report_event(matched_id, box) track_dict[matched_id][reported_flag] True这个去重方案很糙临时用足够。生产环境我会加卡尔曼滤波跟踪或直接用现成的 ByteTrack 和 yolov5 的检测框做关联这里不展开但方向是一致的模型只做感知业务逻辑管时间维度和去重。5. 常见问题与避坑训练翻车和部署采坑记录5.1 现象loss降不下去或变成NaN训练到十几轮loss 突然跳出 nan或从 0.5 附近完全不再下降。原因最常见是两个学习率太大导致梯度爆炸或数据集里存在损坏的图片/标注。解决先把 --lr0 从 0.01 降到 0.001 试一轮如果还 nan就写脚本检查 images 里有没有损坏文件用 PIL 打开失败的直接剔除同时检查 txt 里有没有宽度或高度为 0 的行。很多标注集的 XML 里坐标写错了转换时没做好过滤导致归一化后出现负宽度。5.2 现象mAP一直是0或特别低训练正常跑完mAP50 却是 0或者只有个位数。先看 classes.txt 里类别 ID 和数据集是不是对应。我踩过一次XML 里类别名是 Bicycle大写 B而转换脚本里写的是 bicycle导致所有标注都被跳过模型什么都没学到。另外检查验证集标注是否被正确加载可以在训练时打开 --verbose 看每个 batch 里目标数量的日志。还有个隐蔽原因是数据集划分出了问题验证集里的图片在训练集里不存在但标注文件放错了位置导致验证时标签全部为0。5.3 现象检测框框住车轮但漏掉车身自行车侧面照还好但很多摄像头是从斜上方俯拍正对车身的角度下轮廓是一堆交叉辐条模型容易把车座到前轮这一截识别成两个碎片目标。我处理这个问题的思路是把训练图里这类目标尽量单独筛出来手动修正标注让框把整辆车完整包住。不要觉得麻烦这类半遮挡样本对模型提升最大。另外可以把 --img 提到 1280让小特征更清晰mAP50 通常能涨 3-5 个点。5.4 现象树莓派5上部署yolov5模型推理太慢如果在树莓派5上用 CPU 直接跑 yolov5s 的 .pt 权重单帧推理可能要 1-2 秒做实时监控不现实。解决思路是先用 TorchScript 导出再把模型做一个半精度或量化转换。树莓派5上可以用 NCNN 或 ONNX Runtime我在树莓派5上跑过 yolov5s 量化后的 ONNX640 输入能到 4-6 fps对付非机动车停放这种慢变化场景已经够用。如果还嫌慢就把输入降到 416 或者只在禁停区附近的裁剪区域做检测。5.5 现象白天正常晚上漏检夜景是机器视觉识别最常见翻车点。自行车反光条在夜间很亮但车身其他部位漆黑模型会把暗的车当成背景。如果项目要覆盖夜间最好准备一批傍晚和夜间的图像人工标注后加入训练集。另一个成本低的技巧是训练阶段开启 --hyp 里的 HSV 增强把 saturation 和 value 的变化范围调大让模型对亮度变化更鲁棒。部署时如果相机支持夜间红外补光检测效果会比纯可见光好很多。6. 部署到实际场景用TorchScript导出模型与后处理优化训练完拿到 best.pt 只是第一步实际投入运行时还要做模型导出和后处理参数微调。yolov5 官方提供 export.py可以把权重导出为 TorchScript、ONNX、TensorRT Engine 等格式。在边缘设备上我首推 TorchScript因为它不需要额外框架Python 或 C 都能加载树莓派5上也能直接跑。python export.py --weights best.pt --img 640 --include torchscript onnx --simplify导出后会有 best.torchscript 和 best.onnx 文件。TorchScript 在 Python 端加载import torch model torch.jit.load(best.torchscript) model.eval() results model(img_tensor) # img_tensor 是 [1,3,640,640] 的 Tensor # results 是 [1, num_preds, 6] 的 Tensor每行是 cx, cy, w, h, conf, cls # 注意这里坐标是归一化的需要乘回 640 才是像素坐标导出后有一个重要事情模型后处理NMS是包含在 TorchScript 里的输出已经滤掉重复框。但它的默认 NMS IoU 阈值是 0.45置信度阈值是 0.25。在密集停放场景里自行车一辆挨一辆IoU 阈值过大可能导致相邻车辆被合并成一个大框过小又会让同一辆车出现多个碎片框。我一般把 conf-thres 调到 0.3-0.4iou-thres 调到 0.5具体在导出时通过参数传python export.py --weights best.pt --img 640 --include torchscript --conf-thres 0.3 --iou-thres 0.5如果条件允许用 TensorRT 导出的 Engine 模型在 NVIDIA 边缘卡如 Jetson Orin上吞吐提升明显但导出过程需要和显卡算力匹配部署环境不能轻易换机器。我对小项目的第一版建议永远是 TorchScript省心稳定后面确有人力再折腾 TensorRT。最后一个我反复强调的落地习惯不要一上来就追求极致精度先把整个链路跑通用真实视频流让模型跑 48 小时再把误报多的场景截图找规律。违规停放检测的召回率固然重要但误报一封接一封客户会对系统失去信心。我自己的做法是先冷启动用阈值保守一点conf 调高到 0.5靠区域规则和停留时长去减少干扰等收集到更多夜间和遮挡样本再迭代模型。这个思路让我少走了很多弯路也希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →