YOLOv5非机动车违规停放识别:数据集转换、训练调参与后处理规则全解析
发布时间:2026/10/2 20:22:57来源:尧图网络
简介本资源面向从事机器视觉与目标检测的开发者及学生聚焦非机动车违规停放场景下的电动车识别任务提供YOLOv5可直接训练使用的已标注数据集。压缩包内共1722个文件以864张jpg图片和858个xml标注文件为主图片与标注一一对应整体约100MB解压后即可投入模型训练与验证。该批数据为雅迪电动车图像属于非机动车数据集中的第七类样本覆盖多角度、多场景标注格式规范便于直接接入YOLOv5训练流程。目前已有2330人学习下载适合用于课程设计、毕业项目或算法对比实验。读者可借此快速搭建电动车检测基线省去自行采集与标注的时间成本并在此基础上扩展自行车、三轮车等其他类别构建完整的非机动车违规停放识别方案。1. 从一堆电动车乱停的图片说起yolov5非机动车违规停放识别到底能落地成什么样小区门口、地铁站出口、写字楼消防通道电动车横七竖八停一片物业和城管靠人盯盯不过来也拍不全。这个标题讲的就是用 yolov5 做非机动车违规停放的机器视觉识别配套一份已经标注好的 E_bicycle7_images_xmls 数据集直接跳过最耗人的标数据环节把精力放在训练、调参和部署上。适合两类人一类是想拿真实场景练手 yolov5 目标检测的算法同学一类是手里有摄像头、想验证违规停放自动识别到底靠不靠谱的工程同学。它解决的不是能不能检测到电动车而是检测到之后怎么判定它停在了不该停的地方这两件事的难度差着一个量级后面会一层层拆开。2. 数据集先摸透E_bicycle7_images_xmls 的结构、类别与清洗判断拿到一份标注数据集第一件事不是急着写训练脚本而是把它翻个底朝天。E_bicycle7_images_xmls 从命名看是图片配 XML 标注大概率是 Pascal VOC 格式XML 里带 filename、size、object 的 name 和 bndbox。yolov5 训练自己的数据集时最容易被忽略的就是标注质量——框歪了、类别写错了、图片和 XML 对不上号这些在训练日志里不会直接报错只会让 mAP 卡在一个不上不下的数字上属于典型的玄学问题最后还得回头查数据。2.1 先跑一遍数据体检脚本在动手转格式之前我一般会写个小脚本把整个数据集扫一遍统计图片数量、XML 数量、类别分布、框的宽高分布顺便找出没有对应 XML 的孤儿图片和没有对应图片的孤儿 XML。import os import xml.etree.ElementTree as ET from collections import Counter img_dir E_bicycle7_images_xmls/images xml_dir E_bicycle7_images_xmls/annotations img_files {os.path.splitext(f)[0] for f in os.listdir(img_dir) if f.lower().endswith((.jpg, .png, .jpeg))} xml_files {os.path.splitext(f)[0] for f in os.listdir(xml_dir) if f.lower().endswith(.xml)} # 孤儿文件有图没标注或有标注没图 print(有图无标注:, img_files - xml_files) print(有标注无图:, xml_files - img_files) cls_counter Counter() box_sizes [] for name in img_files xml_files: tree ET.parse(os.path.join(xml_dir, name .xml)) root tree.getroot() for obj in root.findall(object): cls_name obj.find(name).text.strip() cls_counter[cls_name] 1 bbox obj.find(bndbox) w float(bbox.find(xmax).text) - float(bbox.find(xmin).text) h float(bbox.find(ymax).text) - float(bbox.find(ymin).text) box_sizes.append((w, h)) print(类别分布:, cls_counter) if box_sizes: ws [b[0] for b in box_sizes] hs [b[1] for b in box_sizes] print(f框宽 min/avg/max: {min(ws):.0f}/{sum(ws)/len(ws):.0f}/{max(ws):.0f}) print(f框高 min/avg/max: {min(hs):.0f}/{sum(hs)/len(hs):.0f}/{max(hs):.0f})这段脚本干三件事集合运算找出图片和标注不匹配的文件这是训练前必须清掉的Counter 统计每个类别出现次数如果某个类别只有个位数样本训练时基本学不出来框的宽高分布能帮你判断 anchor 是否需要重聚类也能发现那种宽高只有几个像素的异常框多半是标注时手抖点错了。参数上没什么可调的路径按你实际解压后的目录改就行。跑完重点看两个输出类别分布是否严重不均衡框尺寸有没有明显离群值。如果发现某个类别占比超过 80%后面训练时要么补样本要么在 loss 里做加权。2.2 类别定义决定了违规能不能被判出来这里有个关键判断E_bicycle7_images_xmls 里的类别名到底是什么。如果只有一类叫 ebike 或者 bicycle那这份数据集只能做检测到电动车违规判定得靠后处理逻辑补。如果类别里已经区分了停车区域、禁停区域、电动车、自行车那恭喜你检测结果可以直接喂给规则引擎。常见做法是数据集负责检测目标违规判定单独写一层。比如检测框中心点落在预先标定的禁停多边形区域内且停留时间超过阈值才判定为违规停放。这个阈值和区域坐标是业务参数不是模型参数别指望模型自己学会。提示如果 XML 里类别名有中文、空格或大小写混用转 YOLO 格式前统一成小写英文否则后面 names 文件和标注对不上训练时 loss 会直接变 NaN。3. 从 XML 到 YOLO txt转换脚本、目录结构和三个必查项yolov5 只认 txt 标注每行格式是类别索引 中心x 中心y 宽 高全部归一化到 0 到 1 之间。VOC 的 XML 给的是左上角和右下角绝对坐标转换时最容易翻车的就是归一化除错了分母——有人用图片实际宽高有人用 XML 里 size 标签的宽高两者不一致时框就整体偏移。3.1 转换脚本与目录组织import os import xml.etree.ElementTree as ET import shutil xml_dir E_bicycle7_images_xmls/annotations img_dir E_bicycle7_images_xmls/images out_img datasets/ebike/images out_lbl datasets/ebike/labels # 类别顺序一旦定下就不能改训练和推理必须一致 classes [ebike, bicycle, no_parking_zone] os.makedirs(out_img, exist_okTrue) os.makedirs(out_lbl, exist_okTrue) for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(xml_dir, xml_file)) root tree.getroot() # 以 XML 里记录的 size 为准和标注时看到的图保持一致 size root.find(size) img_w float(size.find(width).text) img_h float(size.find(height).text) lines [] for obj in root.findall(object): cls_name obj.find(name).text.strip().lower() if cls_name not in classes: continue # 未定义类别直接跳过避免索引越界 cls_id classes.index(cls_name) bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) # 越界裁剪防止标注超出图片范围导致归一化后大于 1 xmin, xmax max(0, xmin), min(img_w, xmax) ymin, ymax max(0, ymin), min(img_h, ymax) cx (xmin xmax) / 2 / img_w cy (ymin ymax) / 2 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) base os.path.splitext(xml_file)[0] with open(os.path.join(out_lbl, base .txt), w) as f: f.write(\n.join(lines)) # 图片按同名拷贝yolov5 靠文件名配对 for ext in (.jpg, .png, .jpeg): src os.path.join(img_dir, base ext) if os.path.exists(src): shutil.copy(src, os.path.join(out_img, base ext)) break逻辑上分四步读 XML 拿图片尺寸遍历 object 转坐标越界裁剪写 txt 并拷图。参数里 classes 列表的顺序就是最终模型输出的类别索引写错顺序不会报错但推理结果会张冠李戴。归一化用 XML 的 size 而不是 PIL 读图是为了和标注工具保持一致如果两者对不上说明标注时图片被缩放过这种图建议直接剔除。3.2 转换后必须抽查的三件事第一随机抽 5 张图用脚本把 txt 里的框画回图片上肉眼看框是否贴合目标。这一步能抓出归一化错误、坐标颠倒这类问题。第二检查有没有空的 txt 文件空标注文件在 yolov5 里会被当成负样本少量可以大量会让模型学不到东西。第三确认图片和 txt 文件名严格一一对应yolov5 是按文件名找标注的差一个字符就配不上。# 统计空标注文件数量 find datasets/ebike/labels -name *.txt -empty | wc -l # 统计图片和标注数量是否一致 ls datasets/ebike/images | wc -l ls datasets/ebike/labels | wc -l两个数字不一致就说明有图片没配上标注或者反过来。常见原因是图片扩展名大小写不统一脚本里只匹配了小写实际文件是 .JPG。3.3 划分训练集和验证集yolov5 官方推荐用脚本自动划分比例一般 8:1:1 或 9:1。数据量不大的话验证集至少留 10%否则评估结果波动大今天 mAP 0.7 明天 0.6你会怀疑人生。import random import os random.seed(42) # 固定种子保证每次划分一致方便复现 img_dir datasets/ebike/images all_imgs [f for f in os.listdir(img_dir) if f.lower().endswith((.jpg, .png))] random.shuffle(all_imgs) n len(all_imgs) train_ratio, val_ratio 0.8, 0.1 train all_imgs[:int(n * train_ratio)] val all_imgs[int(n * train_ratio):int(n * (train_ratio val_ratio))] test all_imgs[int(n * (train_ratio val_ratio)):] for split, files in [(train, train), (val, val), (test, test)]: with open(fdatasets/ebike/{split}.txt, w) as f: for name in files: f.write(os.path.abspath(os.path.join(img_dir, name)) \n)seed 固定是血泪经验不固定的话每次跑出来结果都不一样调参时根本分不清是参数起作用还是数据划分变了。生成的 txt 里写绝对路径避免训练时工作目录不同导致找不到图。4. 训练配置与超参数yolov5 在违规停放场景下的调参取舍环境配置这块conda 建个独立环境最省心python 3.8 到 3.10 都行torch 版本跟显卡驱动匹配。yolov5 源码拉下来后依赖装完先跑一次python detect.py --source data/images确认环境没问题再动训练。很多人一上来就训结果报错分不清是环境问题还是数据问题。4.1 模型选型与输入尺寸yolov5 有 n/s/m/l/x 五个规格参数量和精度递增。非机动车违规停放这个场景目标是大中型物体不是密集小目标n 或 s 通常够用。如果摄像头架得高、电动车在画面里只占几十像素那就上 m。输入尺寸默认 640如果原图分辨率很高且目标小可以提到 960 或 1280代价是显存和训练时间成倍涨。规格参数量级别适用场景显存参考(640)yolov5n最小边缘设备、目标大约 2Gyolov5s小常规服务器、平衡约 4Gyolov5m中目标偏小、精度要求高约 8Gyolov5l/x大数据量大、追求极致12G 以上显存数字是训练时的粗略参考batch size 和输入尺寸都会影响别照搬按自己卡的情况试。4.2 data 配置文件和训练命令# datasets/ebike/ebike.yaml path: /abs/path/datasets/ebike train: train.txt val: val.txt test: test.txt nc: 3 names: [ebike, bicycle, no_parking_zone]nc 必须和 classes 列表长度一致names 顺序也必须一致这是最容易出错又最难排查的地方——顺序错了不报错只是所有预测类别都错位。python train.py \ --data datasets/ebike/ebike.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --hyp data/hyps/hyp.scratch-low.yaml \ --project runs/train \ --name ebike_v1参数逐个说weights 用预训练权重小数据集上从零训基本学不动迁移学习是标配img 是输入尺寸和推理时保持一致batch 按显存调16 跑不动就 8epochs 100 起步看验证集 mAP 什么时候不再涨就早停hyp 选 low 是因为数据量不大增强太猛反而学不到真实分布。4.3 超参数里真正值得动的几个yolov5 超参数文件里几十个参数大部分默认值就够用真正影响这个场景的是这几个学习率 lr0 默认 0.01小数据集可以降到 0.001 避免震荡mosaic 增强默认开启能把四张图拼一张对小数据集提升明显但如果你的违规判定依赖目标在画面中的绝对位置mosaic 会打乱位置分布这时候要关掉anchor 如果框尺寸分布和默认 anchor 差很多用python utils/autoanchor.py重新聚类。注意mosaic 和位置相关的业务逻辑是冲突的。检测电动车在禁停区这种依赖相对位置的场景建议训练后期关掉 mosaic让模型适应真实布局。4.4 训练过程看什么指标训练日志里重点盯三个box_loss 和 cls_loss 是否稳定下降如果震荡剧烈多半是学习率太大验证集 mAP0.5 是否持续上升如果训练集涨验证集不涨就是过拟合加数据或加正则混淆矩阵里类别之间是否互相误判ebike 和 bicycle 长得像误判多的话考虑合并类别或加样本。5. 避坑与排查违规停放识别里最容易翻车的五件事5.1 训练 loss 正常但推理框全错位现象训练指标看着不错推理时框的位置整体偏移或大小不对。原因转换脚本归一化用的图片尺寸和实际推理输入尺寸不一致或者 XML 的 size 标签和真实图片尺寸对不上。解决统一以实际读图的宽高为准重新转换转换后画框抽查别信脚本不验证。5.2 某个类别完全检测不出来现象混淆矩阵里某类别全是漏检。原因该类别样本太少或者类别名在转换时被过滤掉了。解决回到数据体检脚本看类别分布样本少于 50 的类别要么补数据要么在 loss 里加权要么直接合并到相近类别。5.3 验证集 mAP 忽高忽低现象每次训练结果差异很大。原因数据划分没固定种子或者验证集太小。解决划分脚本固定 random.seed验证集占比提到 15% 以上数据量实在小就做交叉验证。5.4 推理速度慢达不到实时现象单帧推理几百毫秒。原因模型规格选大了或者输入尺寸太高或者没开半精度。解决换 yolov5n/s输入降到 640推理时加--half导出 ONNX 或 TensorRT 再提速。树莓派 5 上部署自己训练的 yolov5 模型时基本只能跑 n 规格加量化别指望 s 以上能实时。5.5 检测到了但违规判定不准现象电动车检测没问题但违规判定误报多。原因把检测和判定混在一起指望模型输出违规这个抽象概念。解决检测归检测违规判定单独写规则层用检测框中心点、停留时间、区域多边形做逻辑判断规则可调可解释比让模型硬学靠谱得多。6. 从检测框到违规判定后处理规则层与一个可复现的验证技巧模型输出的是框和类别违规停放是个业务结论中间这层后处理才是这个方案能不能真正落地的关键。我一般会写一个独立的判定模块输入是检测结果加时间戳输出是违规事件。核心逻辑三条目标类别属于非机动车框中心点落在禁停区域内同一目标在区域内连续出现超过设定时长。from shapely.geometry import Point, Polygon # 禁停区域多边形实际项目里从配置文件或标注工具导出 NO_PARK_ZONE Polygon([(100, 200), (500, 200), (500, 600), (100, 600)]) STAY_THRESHOLD 30 # 秒连续停留超过这个值判定违规 # track_history: {track_id: [(timestamp, cx, cy), ...]} def check_violation(track_id, cls_name, cx, cy, timestamp, track_history): if cls_name not in (ebike, bicycle): return None if not NO_PARK_ZONE.contains(Point(cx, cy)): track_history.pop(track_id, None) # 离开区域就清历史 return None history track_history.setdefault(track_id, []) history.append((timestamp, cx, cy)) # 只保留阈值时间窗内的记录避免内存无限增长 history[:] [h for h in history if timestamp - h[0] STAY_THRESHOLD] if history and timestamp - history[0][0] STAY_THRESHOLD: return {track_id: track_id, cls: cls_name, since: history[0][0]} return None这段代码的关键参数是 STAY_THRESHOLD设太小会把路过也算违规设太大响应迟钝实际项目里按业务容忍度调一般 20 到 60 秒。track_id 来自跟踪算法yolov5 本身只做检测不做跟踪需要配 ByteTrack 或 DeepSORT这是另一个模块别指望检测模型自己维持 ID。验证这套逻辑有个技巧拿一段已知有违规事件的视频人工标出违规发生的时间段然后跑完整流程对比系统输出的违规时间和人工标注的重合度。重合度高说明阈值合理误报多就调区域或时长漏报多就检查跟踪是否断 ID。这个验证方法比只看 mAP 更贴近真实效果因为 mAP 高不代表业务判定准。我自己踩过最深的坑是早期直接把检测到电动车在禁停区当成违规结果路过的电动车全被记成违规物业那边投诉不断。后来加了停留时长和跟踪 ID 才稳住。做这类场景检测只是入口业务规则才是决定成败的地方别把两者混为一谈。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网