ARTICLE DETAIL

资讯详情

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

基于YOLOv5的非机动车违规停放识别:数据集转换、训练调参与部署避坑

基于YOLOv5的非机动车违规停放识别:数据集转换、训练调参与部署避坑 简介本资源面向从事机器视觉与目标检测的开发者、学生及竞赛选手聚焦非机动车违规停放场景下的电动车识别任务提供可直接用于YOLOv5训练与验证的已标注数据集。压缩包内共1976个文件以994张jpg图像和982个xml标注文件为主图像用于模型输入xml记录对应目标框与类别信息整体约160.58MBrar格式便于一次性获取。该批数据属于电动车E_bicycle2分类共994张是完整非机动车数据体系中十个分类的第二类同系列还涵盖自行车、电动车、三轮车等共两万余张已标注样本类别覆盖共享单车、山地自行车、绿源与雅迪电动车、淮海与金彭三轮车等常见车型。已有212人学习下载适合用于违规停放检测、车辆细粒度分类等课题的模型训练、消融实验与数据增强验证也可作为毕业设计或算法竞赛的现成数据基础。1. 从一堆电动车乱停的图片说起yolov5非机动车违规停放识别到底能落地成什么样小区门口、地铁站出口、写字楼楼下电动车横七竖八停一片物业和城管靠人盯盯不过来也说不清楚。这个标题讲的就是用 yolov5 做机器视觉识别把「非机动车违规停放」这件事从人工巡查变成摄像头自动判断配套一份已经标注好的数据集 E_bicycle2_images_xmls省掉最耗时的标框环节。它解决的不是「识别电动车」这么简单而是识别出车之后判断它停的位置是否越界、是否压线、是否堵住通道。适合谁适合手上有摄像头、有少量算力设备、想快速验证一套违规停放检测方案的工程师也适合拿它当 yolov5 训练自己数据集的第一块试验田。数据集是 xml 格式意味着走的是 VOC 标注路线后面要转 YOLO 格式这一步的坑我在第 3 章会细说。2. 数据集先摸清楚E_bicycle2_images_xmls 的结构与转 YOLO 格式的完整脚本拿到一份标注数据集第一件事不是急着训练而是先搞清楚它长什么样。E_bicycle2_images_xmls 从命名看是图片加 xml 标注的组合xml 大概率是 PASCAL VOC 格式每张图对应一个同名 xml里面记录了图片尺寸、目标类别、边界框坐标。这个格式 yolov5 不直接吃yolov5 要的是每张图一个 txt每行类别索引 中心x 中心y 宽 高且坐标要归一化到 0 到 1 之间。所以第一步是转换第二步是划分训练验证集第三步是写 data.yaml。2.1 先看清 VOC xml 里到底存了什么一个典型的 VOC xml 结构是这样的size里有width、height、depthobject里有name和bndbox的xmin、ymin、xmax、ymax。转换时最容易翻车的地方是坐标越界——标注时框拉到了图片外面xmax 大于 width或者 xmin 小于 0。这种框如果不处理转出来的归一化坐标会大于 1 或小于 0训练时 loss 直接炸。我一般会在转换脚本里加一层裁剪把坐标卡在[0, width]和[0, height]范围内。另一个坑是类别名不统一。同一批数据里可能有的写ebike有的写electric_bicycle有的写E-bike。yolov5 是按类别名映射索引的名字不一致会导致同一个类别被拆成多个类。转换前先统计一遍所有 xml 里的name取值统一成一套命名。2.2 转换脚本从 xml 到 yolov5 可读的 txt下面这个脚本我用了很多次逻辑是遍历 xml 目录读图片尺寸解析每个 object归一化后写入对应的 txt。类别映射用一个列表控制顺序就是 yolov5 训练时的类别索引。import os import xml.etree.ElementTree as ET from PIL import Image # 类别列表顺序即索引必须和 data.yaml 里的 names 一致 CLASSES [ebike, bicycle, tricycle] def convert_bbox(size, box): 把 VOC 的 xmin,ymin,xmax,ymax 转成 yolov5 的 cx,cy,w,h 归一化值 dw 1.0 / size[0] dh 1.0 / size[1] xmin, ymin, xmax, ymax box # 裁剪越界坐标防止归一化后超出 [0,1] xmin max(0, min(xmin, size[0])) xmax max(0, min(xmax, size[0])) ymin max(0, min(ymin, size[1])) ymax max(0, min(ymax, size[1])) cx (xmin xmax) / 2.0 * dw cy (ymin ymax) / 2.0 * dh w (xmax - xmin) * dw h (ymax - ymin) * dh return cx, cy, w, h def convert_xml(xml_path, img_dir, out_dir): tree ET.parse(xml_path) root tree.getroot() # 优先从 xml 读尺寸读不到再用 PIL 打开图片读 size_node root.find(size) if size_node is not None: w int(size_node.find(width).text) h int(size_node.find(height).text) else: img_name root.find(filename).text img Image.open(os.path.join(img_dir, img_name)) w, h img.size lines [] for obj in root.iter(object): name obj.find(name).text.strip() if name not in CLASSES: continue # 跳过不在类别表里的目标 cls_id CLASSES.index(name) bnd obj.find(bndbox) box (float(bnd.find(xmin).text), float(bnd.find(ymin).text), float(bnd.find(xmax).text), float(bnd.find(ymax).text)) cx, cy, bw, bh convert_bbox((w, h), box) lines.append(f{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) # 没有有效目标的图不生成 txt避免空标签干扰训练 if lines: base os.path.splitext(os.path.basename(xml_path))[0] with open(os.path.join(out_dir, base .txt), w) as f: f.write(\n.join(lines)) if __name__ __main__: xml_dir E_bicycle2_images_xmls/annotations img_dir E_bicycle2_images_xmls/images out_dir E_bicycle2_images_xmls/labels os.makedirs(out_dir, exist_okTrue) for f in os.listdir(xml_dir): if f.endswith(.xml): convert_xml(os.path.join(xml_dir, f), img_dir, out_dir)逻辑说明convert_bbox里先做坐标裁剪再归一化这是防止越界框污染训练的关键一步。CLASSES列表的顺序决定了 txt 里第一个数字的含义改类别顺序必须同步改 data.yaml。跳过不在类别表里的目标是为了避免数据集里混入无关标注导致类别索引错位。空标签图不生成 txt因为 yolov5 默认会把没有 txt 的图当负样本但如果你确实需要负样本可以保留空文件这个后面避坑章会讲。参数说明xml_dir和img_dir按实际目录改out_dir是输出标签目录。cx:.6f保留六位小数足够yolov5 内部会再处理。如果你的数据集里图片和 xml 不在同一级目录注意filename字段可能带路径用os.path.basename取文件名更稳。2.3 划分数据集与 data.yaml 的写法转换完标签下一步是把图片和标签按比例分成 train 和 val。我一般用 8:2数据量少于 500 张时用 7:3保证验证集有足够样本。划分脚本很简单但要注意图片和同名 txt 必须一起移动漏一个就会在训练时报「找不到标签」。# 假设图片在 images/标签在 labels/按 8:2 划分 mkdir -p datasets/images/train datasets/images/val mkdir -p datasets/labels/train datasets/labels/val # 用 shuf 随机打乱后按行数切分这里示意前 80% 进 train ls images/*.jpg | shuf all.txt total$(wc -l all.txt) train_n$(( total * 8 / 10 )) head -n $train_n all.txt | while read f; do base$(basename $f .jpg) cp $f datasets/images/train/ cp labels/$base.txt datasets/labels/train/ done tail -n $(( train_n 1 )) all.txt | while read f; do base$(basename $f .jpg) cp $f datasets/images/val/ cp labels/$base.txt datasets/labels/val/ donedata.yaml 是 yolov5 训练时的数据入口路径写对、类别数写对、names 顺序和转换脚本一致这三点错了训练直接跑不起来。path: ./datasets train: images/train val: images/val nc: 3 names: [ebike, bicycle, tricycle]nc是类别数必须等于 names 的长度。names 的顺序就是 txt 里类别索引的顺序改一个就要全改。path 可以用相对路径但建议用绝对路径避免从不同目录启动训练时找不到数据。3. 训练自己的违规停放模型yolov5 超参数怎么调、显存怎么省、指标怎么看数据集准备好接下来是训练。yolov5 的训练入口是train.py但直接跑默认参数往往效果一般尤其是违规停放这种目标尺度差异大、遮挡多的场景。这一章讲清楚模型选型、关键超参数、显存不够时的处理以及训练过程中该盯哪些指标。3.1 模型选型n/s/m/l/x 到底选哪个yolov5 提供五个规模的模型从轻到重是 n、s、m、l、x。选型不是越大越好要看你的部署端。如果最终要上树莓派 4b 或 5或者要量化到 RK3568 这类边缘芯片直接选 yolov5n 或 yolov5s大模型量化后精度掉得厉害推理速度也撑不住。如果只在服务器 GPU 上跑数据量又大可以上 m 或 l。我一般先用 yolov5s 跑一版基线看 mAP 和召回够不够。违规停放检测里电动车被遮挡、只露出一部分的情况很多召回比精度更重要——漏检一辆乱停的车比误检一辆正常停的车代价大。如果基线召回低于 0.8再考虑换 m 或加数据增强而不是盲目上大模型。3.2 关键超参数从 epochs 到 anchor 的实操设置yolov5 的超参数分两类一类在命令行传一类在hyp.scratch-low.yaml这类超参文件里。下面这张表是我在违规停放场景下常用的起点值不是万能但能让你少走几轮弯路。参数常用值作用与调整方向img-size640输入分辨率小目标多可上 1280显存翻倍batch-size16显存不够就降到 8 或 4配合 accumulateepochs100~300数据少时 300数据多 100 够收敛lr00.01初始学习率batch 变小要相应调小lrf0.01最终学习率系数控制退火终点mosaic1.0马赛克增强小目标场景建议开mixup0.1混合增强数据少时可开太多会糊anchor自动训练前用 kmeans 重算 anchor 更贴合anchor 这块值得单独说。yolov5 默认 anchor 是基于 COCO 的你的数据集里电动车框的宽高比和 COCO 目标差别大直接用默认 anchor 会让回归起点偏离。训练前跑一次python utils/autoanchor.py --data data.yaml重算或者直接在 train.py 里加--noautoanchor关掉自动锚框手动填一组用 kmeans 算出来的值。我遇到过不重算 anchor 导致前期 loss 震荡、收敛慢半拍的情况重算后前 10 个 epoch 就稳了。3.3 显存不够时的三个降级手段训练时最常见的报错就是 CUDA out of memory。除了降 batch-size还有几个手段可以组合用。第一是开梯度累积--accumulate 2表示每两个 batch 更新一次权重等效 batch 翻倍但显存不涨。第二是开混合精度--amp在支持的卡上能省不少显存速度也快。第三是降 img-size从 640 降到 416 或 320显存占用按平方降但小目标召回会掉要权衡。# 显存紧张时的训练命令示例 python train.py \ --data data.yaml \ --weights yolov5s.pt \ --img 640 \ --batch-size 8 \ --accumulate 2 \ --epochs 200 \ --amp \ --project runs/train \ --name ebike_parking--weights yolov5s.pt是从预训练权重起步比从头训收敛快很多数据量少于 1000 张时几乎必须用。--project和--name控制输出目录方便多次实验对比。--amp在部分老卡上可能不稳定如果出现 loss 变 nan去掉这个参数再试。3.4 训练过程中该盯的指标与早停判断训练日志里重点看几个box_loss、obj_loss、cls_loss三条 loss 是否稳定下降mAP0.5和mAP0.5:0.95是否在涨precision和recall的平衡。违规停放场景里如果 recall 一直上不去先查标注有没有漏标再查 anchor 和 img-size。如果 precision 低但 recall 高说明误检多可能是背景类样本太少或者类别定义太宽泛。早停不是看 epoch 数而是看验证集 mAP 连续 20 到 30 个 epoch 不涨就可以停。yolov5 自带--patience参数设成 30 表示 30 轮不涨就停。但要注意mAP 在训练后期可能小幅波动别因为一两轮下降就手动停容易停在局部低点。4. 避坑与排查xml 转 yolo、训练不收敛、部署掉点这些坑我替你踩过了这一章集中讲我在这个方案里踩过的具体坑每条按现象、原因、解决写。有些坑不踩一次真的想不到但提前知道能省好几天。4.1 转换后训练报「No labels found」现象train.py 启动后提示找不到标签或者训练时所有标签数为 0。原因通常是三个txt 目录结构和 data.yaml 里的路径对不上图片和标签文件名不一致比如图片是001.jpg标签是001.xml.txt或者转换脚本输出的 txt 里类别索引超出了 nc 范围。解决先手动打开一个 txt 看内容格式再检查 data.yaml 的 train 路径是相对谁最后确认图片和标签同名。我习惯在转换后跑一句ls labels/train | wc -l和ls images/train | wc -l对比数量数量对不上就是命名或漏转。4.2 训练 loss 变 nan 或震荡不收敛现象前几个 epoch loss 正常突然变 nan或者一直在高位震荡。原因可能是学习率太大、标注框越界、图片损坏、或者混合精度在特定卡上不稳定。解决先把 lr0 降到 0.001 试再去掉--amp然后抽查一批 txt 看有没有坐标大于 1 或小于 0 的。坐标越界在转换脚本里裁剪过一般不会出现但如果 xml 里 width 和 height 读错了归一化就会错。用 PIL 重新读图片尺寸对比 xml 里的 size能查出这类问题。4.3 验证集 mAP 高但实际部署漏检严重现象训练日志里 mAP0.5 有 0.9部署到摄像头画面里却频繁漏检。原因通常是训练数据和实际场景分布不一致——训练集里电动车都是完整、清晰、白天拍的实际场景有逆光、遮挡、夜间。解决从实际摄像头抽一批图人工标一小部分加进训练集做一轮微调。另外检查推理时的 img-size 和训练时是否一致预处理方式是否一致yolov5 的 letterbox 填充如果推理时没做框会偏。4.4 类别不平衡导致小类几乎检不出现象数据集里 ebike 样本多tricycle 样本少训练后 tricycle 的召回极低。原因就是类别不平衡loss 被多数类主导。解决可以在 data.yaml 里给少数类加权或者用--balance类的采样策略更直接的办法是复制少数类样本做过采样。我一般先统计每个类的框数差距超过 10 倍就要处理否则小类基本学不到。4.5 转 RK3568 或树莓派后精度掉一大截现象PC 上 mAP 0.9量化部署到 RK3568 或树莓派后掉到 0.6。原因是量化对权重和激活的精度损失尤其是 yolov5 的 focus 层和 siou 类损失对量化敏感。解决优先用 yolov5n 或 yolov5s量化时用带代表性的校准集校准集要从实际场景抽不能只用训练集。树莓派上如果跑 ONNX Runtime注意算子支持情况部分算子会回退到 CPU 导致速度骤降。5. 从识别到判断违规后处理逻辑与一个可复用的验证技巧模型能检出电动车只是第一步真正要输出「违规停放」这个结论还得加一层后处理。常见做法是先在画面里画一条或多条虚拟线代表禁停区域边界然后判断检测框的中心点或底边是否落在禁停区内。这个逻辑不复杂但有几个细节决定成败。5.1 用多边形区域做违规判断的代码骨架下面这段代码演示如何用 shapely 判断检测框中心点是否落在禁停多边形内。实际部署时禁停区坐标可以从配置文件读方便不同摄像头复用。from shapely.geometry import Point, Polygon # 禁停区域多边形按实际画面坐标填顺序为顺时针或逆时针 NO_PARK_ZONE Polygon([(100, 200), (500, 200), (500, 600), (100, 600)]) def is_violation(bbox): bbox 为 xyxy 格式判断中心点是否在禁停区内 x1, y1, x2, y2 bbox cx (x1 x2) / 2 cy (y1 y2) / 2 return NO_PARK_ZONE.contains(Point(cx, cy))逻辑说明用中心点判断是最简单的方式但电动车停靠时车头可能在线外、车身在线内中心点法会漏判。更稳的做法是判断检测框底边中点因为底边对应车轮着地点更接近实际停放位置。参数说明NO_PARK_ZONE的坐标要和摄像头分辨率匹配换摄像头就要重标。如果画面有透视禁停区应该用多边形而不是矩形贴合地面实际边界。5.2 一个验证后处理逻辑是否靠谱的小技巧后处理逻辑写完别急着上生产。我习惯用一段历史视频或一批截图把检测结果和违规判断结果叠加画出来人工看一遍。重点看三类正常停在区外的车有没有被误判成违规压在边界线上的车判断是否稳定禁停区边缘附近的车框中心点抖动时会不会反复横跳。如果抖动导致误报可以加一个连续 N 帧命中才报警的计数器牺牲一点实时性换稳定性。5.3 这套方案值不值得投入我的判断如果你的场景是固定摄像头、禁停区域明确、电动车是主要目标这套 yolov5 加后处理的方案投入产出比很高。数据集已经标注好省掉最贵的人工标框成本训练和部署的链路也成熟。但如果场景里电动车和自行车、摩托车混在一起类别定义要先想清楚否则模型学到的边界很模糊。另外违规停放判断最终是要对接业务系统的报警频率、误报容忍度、取证图片存储这些工程问题比模型本身更影响落地效果。我自己的习惯是先用最小闭环跑通——一个摄像头、一个禁停区、一周数据——再决定要不要扩。希望帮到你。本文还有配套的精品资源点击获取
返回列表