ARTICLE DETAIL

资讯详情

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

YOLOv5实战:非机动车违规停放检测的数据集处理与部署

YOLOv5实战:非机动车违规停放检测的数据集处理与部署 简介面向目标检测与智慧城市治理场景此数据集专注于非机动车违规停放识别中的三轮车检测。内容取自完整非机动车数据集中的tricycle1分类共957张真实场景图片与对应XML标注文件标注格式可直接接入YOLOv5等主流检测框架进行模型训练。压缩包共1910个文件以jpg图像与xml标签文件成对组织整体体积82.19MB目录结构清晰便于按分类筛选与扩充训练样本。三轮车数据总计八个类别本包为第一类为后续引入淮海、金彭、宗申等细分车型数据提供了规范化的标注基础。当前已有295人学习使用适合具备一定深度学习基础、希望构建非机动车违停识别系统的开发者作为训练语料。1. 为什么“非机动车违规停放”检测项目最后都落在 YOLOv5 上做城市道路非机动车治理时真正让人头疼的不是骑车人而是那些停在禁停区里的三轮车、电动车和送货平板车。传统视频监控用“区域入侵”判断结果行人、树影、共享单车全被你报警后台一天几百条误报。改用目标检测模型后机器视觉识别的不再是“画面里有没有人动”而是“画面里有没有三轮车”误报率立刻降了一个量级。YOLOv5 正好是这套方案里落地成本最低的一环代码成熟、资料多、训练部署都能控住。这篇就按“吃透数据集 → 转标注格式 → 训练调参 → 边缘部署 → 现场踩坑”这条路线走把三轮车 tricycle1_images_xmls 这类已标注数据集从头用到尾给你一条能在两周内跑通的完整路径。2. 先盘清数据集tricycle1_images_xmls 里的 XML 到底怎么读2.1 先分辨 XML 标注格式VOC 和 LabelMe 的读法不一样拿到一个叫 tricycle1_images_xmls 的数据包常见做法是先看目录结构images 目录放原始图片xmls 目录放标注文件。绝大多数这类包用的是 VOC 格式 XML也就是每个 XML 对应一张图片图片里每个目标用一个 object 节点描述里面带着类别名和 bndbox 坐标。但也要小心有些项目虽然后缀是 xml实际是 LabelMe 导出的多边形坐标object 节点下没有 bndbox而是 polygon 点集。这两者的解析逻辑完全不同所以第一步必须抽查几个 XML 文件不要凭文件名猜。我一般会写十行脚本把全部 XML 里的 object name 去重打印一遍确认这个包里到底只有 tricycle 一类还是混着 bicycle、electric。类别清单决定后面 CLASS_MAP 怎么写这一步漏了后面训练时要么报类别索引越界要么模型把两类东西学成一个类。2.2 转换脚本VOC XML 转 YOLO txt及四个边界坑YOLOv5 训练时不读 XML它需要每个图片对应一个同名 txt 文件每行是“类别ID 归一化中心x 归一化中心y 归一化宽 归一化高”。转换逻辑本身很简单但真实标注数据里各种脏情况很多我一般会在脚本里加保护而不是一把梭全转。import os import xml.etree.ElementTree as ET import cv2 IMG_DIR tricycle1_images_xmls/images XML_DIR tricycle1_images_xmls/xmls OUT_DIR tricycle1_images_xmls/labels CLASS_MAP {tricycle: 0, bicycle: 1, electric: 2} os.makedirs(OUT_DIR, exist_okTrue) for xml_name in os.listdir(XML_DIR): if not xml_name.endswith(.xml): continue xml_path os.path.join(XML_DIR, xml_name) tree ET.parse(xml_path) root tree.getroot() img_name root.find(filename).text img_path os.path.join(IMG_DIR, img_name) img cv2.imread(img_path) if img is None: print(missing image:, img_name) continue h, w img.shape[:2] lines [] for obj in root.findall(object): name obj.find(name).text if name not in CLASS_MAP: print(skip unknown class:, name, img_name) continue box obj.find(bndbox) x1 float(box.find(xmin).text) y1 float(box.find(ymin).text) x2 float(box.find(xmax).text) y2 float(box.find(ymax).text) x1, y1, x2, y2 max(0, x1), max(0, y1), min(w, x2), min(h, y2) if x2 x1 or y2 y1: print(invalid box:, img_name) continue cx (x1 x2) / 2 / w cy (y1 y2) / 2 / h bw (x2 - x1) / w bh (y2 - y1) / h lines.append(f{CLASS_MAP[name]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) out_name os.path.splitext(xml_name)[0] .txt with open(os.path.join(OUT_DIR, out_name), w) as f: f.write(\n.join(lines)) if lines: f.write(\n)这段脚本里有四个高频翻车点。第一XML 里的 filename 和 images 目录里的实际文件名可能大小写不一致或者后缀从 .jpg 变成 .png所以不要用 xml_name 直接拼图片路径必须按 XML 里的 filename 去找图找不到就打印日志。第二归一化计算的宽高应该以真实读取图片的 shape 为准不能完全相信 XML 里 size 节点的值因为有些标注工具在切图缩图后没有刷新标注字段。第三bndbox 坐标可能越界比如 xmin 出现负数或 xmax 大于图像宽度如果不做 clamp转出来的框会是负宽高YOLOv5 训练时会直接报“Boxes are not all positive”。第四某些图片可能没有任何有效目标输出空 txt 即可不要跳过生成因为后面数据划分时需要图像和标签文件一一对应。2.3 可视化抽查用 OpenCV 把标注框画回图片上转换完成后我建议不要急着训练先画一批可视化图出来人眼扫一遍。这一步能发现很多脚本测不出来的问题比如标注框位置整体偏移、有些三轮车只框了半个车身、类别文案对不上。import os import glob import cv2 import xml.etree.ElementTree as ET vis_dir check_vis os.makedirs(vis_dir, exist_okTrue) for xml_path in glob.glob(tricycle1_images_xmls/xmls/*.xml)[:50]: tree ET.parse(xml_path) root tree.getroot() img_path os.path.join(tricycle1_images_xmls/images, root.find(filename).text) img cv2.imread(img_path) for obj in root.findall(object): name obj.find(name).text box obj.find(bndbox) x1 int(float(box.find(xmin).text)) y1 int(float(box.find(ymin).text)) x2 int(float(box.find(xmax).text)) y2 int(float(box.find(ymax).text)) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText(img, name, (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 1) out_name os.path.splitext(os.path.basename(xml_path))[0] _vis.jpg cv2.imwrite(os.path.join(vis_dir, out_name), img)这里抽样 50 张只是为了快速判断数据大概质量。如果框普遍比目标大一圈说明标注比较宽松如果框把三轮车货斗裁掉了说明标注员习惯不一致。这种问题靠 mAP 是反映不出来的但它直接影响模型学到的“三轮车”边界。宁可花半天把明显不合理的标注清掉也不要让模型在脏数据上边训练边挣扎。2.4 划分训练集与验证集用脚本保持比例不要手动挑数据量不大的情况下我按 9:1 划分训练集和验证集偶尔留 10% 专门做测试。麻烦的是图片之间可能存在连续帧如果按文件名排序后直接切一刀某个场景的连续画面会全落到训练集或验证集验证指标虚高。所以要先打乱再划分并且固定随机种子保证每次跑出来的集合一致。import os import random from glob import glob imgs glob(tricycle1_images_xmls/images/*.jpg) random.seed(42) random.shuffle(imgs) n_train int(len(imgs) * 0.9) with open(tricycle1_images_xmls/train.txt, w) as f: for p in imgs[:n_train]: f.write(os.path.abspath(p) \n) with open(tricycle1_images_xmls/val.txt, w) as f: for p in imgs[n_train:]: f.write(os.path.abspath(p) \n)random.seed(42) 看起来像玄学但它保证你每次重新执行脚本得到的是同一份划分。训练超参数实验时只有数据划分固定mAP 变化才能归因到模型和参数上。如果数据集里同一摄像头连续帧太多建议按摄像头编号或场景目录先分组再在组之间打乱避免训练集和验证集出现同一地点的连续画面。3. 用 YOLOv5 训练自己的数据集从 data.yaml 到超参数调整3.1 按 YOLOv5 目录结构整理数据data.yaml 里的三个关键参数YOLOv5 训练自己的数据集第一步不是写代码而是把数据组织成它认可的形态。常见做法是建一个 data 文件夹里面放 images 和 labels 两个目录images 放原始图片labels 放转换出来的同名 txt。之后写一个 YAML 文件描述数据集路径和类别。path: /home/user/tricycle1_images_xmls train: train.txt val: val.txt nc: 1 names: 0: tricycle三个关键点要盯住nc 类别总数必须和标签文件里实际出现的类别 ID 对上标签索引从 0 开始names 的 0 号位对应 tricycle训练时类别输出顺序由它决定path 如果写的是绝对路径train.txt 和 val.txt 里的路径可以只写文件名也可以写完整绝对路径。我习惯在 train.txt 里写绝对路径这样 path 字段即使配错也不会立刻踩雷YOLOv5 会在读取文本时直接按绝对路径找图。3.2 训练命令与模型规模小数据量优先选 YOLOv5s别贪大三轮车、电动车这类目标外形差异很大不需要特别深的模型。YOLOv5s 在 640 分辨率下足够识别YOLOv5m 以上只会拖慢训练和部署速度并不会让 mAP 涨多少。尤其在你只有几百到几千张图的时候大模型非常容易过拟合。python train.py \ --data data/tricycle.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --cache ram \ --device 0batch 16 是多数 16G 显存显卡跑 YOLOv5s 的稳定值如果你的显卡只有 8Gbatch 降到 8不要为了凑 batch 把图像尺寸往下压。图像尺寸用 640 能绝大多数情况够用如果现场摄像头视角比较远三轮车只占画面很小一块把 img 提到 960 往往比换更大的模型更有效。预训练权重选 yolov5s.pt这个文件会从官方地址下载第一次训练时需要联网后续训练不需要。epochs 设 100 轮数据少时一般 60 轮左右 loss 就平了100 轮是为了给学习率下降留足空间。3.3 最值得调整的 YOLOv5 超参数lr、anchor、NMS 的 iou_t网上所谓超参数调优教程很多但真正在违规停车项目里影响结果的主要有三个。第一个是学习率默认 0.01 对大部分数据集可行数据量小的时候我会改成 0.005防止前几轮 loss 跳成 NaN。第二个是锚框YOLOv5 默认会自适应计算但如果你的标注框大多是狭长条或者特别小的目标自适应出来的 anchor 不一定准可以用 --noautoanchor 关掉再手动在模型 yaml 里改 anchor。第三个是后处理里的 iou_t它控制 NMS 时两个框重叠多少算同一个物体默认 0.45 对密集乱停场景不太友好三轮车并排停时经常框到旁边的行人把 iou_t 调到 0.5 或 0.6重复检测会少很多但调太高也可能漏掉真正的多个目标。这些参数不是玄学是每条具体的先验判断。建议每次只动一个参数记录验证集 mAP 和 P/R 曲线而不是一起改三个否则哪个参数起了作用你根本不知道。3.4 从训练日志判断有没有翻车看 P/R/mAP别只盯 loss训练过程中train loss 下降只是模型在训练集上拟合真正要盯的是每个 epoch 结束后的验证集指标。YOLOv5 会打印 Precision、Recall、mAP0.5、mAP0.5:0.95 四项。单类三轮车项目里Precision 代表模型检测出的框里有多少真是三轮车Recall 代表画面里三轮车有多少被找出来。如果 Recall 低说明漏检多优先加大图像尺寸或增加训练数据如果 Precision 低说明误检多优先提高置信度阈值或清理标注噪声。mAP0.5:0.95 通常比 mAP0.5 低很多这个差距正常。真正需要警惕的是训练 loss 还在降验证 mAP 反而开始掉这说明过拟合已经开始可以停掉训练回到前面生成 best.pt 的那个 epoch。训练结束后把 best.pt 保存好它就是后面部署用的模型。4. 从模型到现场机器视觉识别违规停放的最小部署流程4.1 固定摄像头场景用 OpenCV 拉 RTSP 流抽帧推理监控场景里很少对每一帧跑模型尤其是多个摄像头轮流巡检时算力根本不够。常见做法是每个摄像头每秒抓 1 到 2 帧送入模型识别然后叠加区域规则判断是否违停。下面这段代码是在固定机位上做推理的最小流程import cv2 import torch model torch.hub.load(ultralytics/yolov5, custom, pathbest.pt, force_reloadFalse) cap cv2.VideoCapture(rtsp://192.168.1.64:554/stream1) frame_count 0 while True: ret, frame cap.read() if not ret: break if frame_count % 2 0: results model(frame, size640, conf_thres0.35, iou_thres0.45) det results.pandas().xyxy[0] for _, row in det.iterrows(): if row[name] tricycle: print(row[xmin], row[ymin], row[xmax], row[ymax], row[confidence]) frame_count 1conf_thres 设 0.35 是一个折中值太低压不住误检太高会把远距离小目标丢掉。如果你的摄像头安装高度比较高三轮车在画面里偏小我会把 conf_thres 降到 0.25同时加大输入 size 到 960。qiou_thres0.45 控制 NMS现场车多时建议调到 0.5。关键是抽帧逻辑这里按奇偶帧抽实际项目中最好按时间抽每 0.5 秒取一帧避免视频帧率不同造成的节奏问题。4.2 树莓派 4B/5 部署导出 ONNX 再用 onnxruntime 跑推理如果现场只有树莓派 4B 或树莓派 5不能直接拿 PyTorch 模型逐帧推理太慢了。常规做法是把 best.pt 导出成 ONNX再用 onnxruntime 在 CPU 上运行。YOLOv5 官方工程里自带导出脚本python export.py --weights best.pt --img 640 --batch 1 --include onnx --simplify导出时 --simplify 会裁掉一些冗余算子让 onnxruntime 加载更快。导出成功后在同一目录下会生成 best.onnx。import cv2 import onnxruntime as ort sess ort.InferenceSession(best.onnx, providers[CPUExecutionProvider]) input_name sess.get_inputs()[0].name def preprocess(frame): blob cv2.dnn.blobFromImage(frame, 1/255.0, (640, 640), (0, 0, 0), swapRBTrue, cropFalse) return blob blob preprocess(frame) outputs sess.run(None, {input_name: blob})输出 shape 是 [1, 25200, 85]其中 25200 来自 80x80、40x40、20x20 三个特征图在 640 输入下的候选框总数85 是 4 个坐标、1 个目标置信度、80 个类别得分。如果只训练单类三轮车类别维度只有 nc56。后处理需要把坐标从 640 尺度映射回原图再做 NMS和 YOLOv5 推理逻辑一致。树莓派 4B 上跑 YOLOv5s 的 640 输入单帧大概 2 到 4 秒只能满足几秒抽一帧的低频巡检树莓派 5 会快一些但也不建议追求实时。4.3 单帧检测不等于违停告警先压置信度再谈算法优化很多项目死在第一步模型检测出了三轮车立刻报警结果路上行驶的三轮车也报警后台又开始骂街。正确做法是先用区域规则过滤一遍把检测框和禁停区域做遮挡率判断只有目标中心点落在禁停区域内才进入后续判定。比如划定一个多边形 ROI用 cv2.pointPolygonTest 判断框中心点是否在区域内这样可以过滤掉人行道上正常骑行的目标但还无法区分“停着”和“暂时路过”。更稳的时序判定我放在最后面讲这里先强调模型只负责“是什么”区域和时间规则才负责“是不是违规”。5. 常见问题与排查三轮车/非机动车检测项目里的 5 个坑5.1 现象mAP 很高现场视频却频繁漏检模型在验证集上 mAP0.5 到了 0.9一接到现场摄像头就漏检尤其换了一个安装角度后更明显。原因是现场画面和训练集有“领域差异”训练图像大多拍的是车尾或侧面的三轮车现场摄像头却拍车头模型没见过这个视角。解决不要急着调超参数先采集现场摄像头 1 到 2 小时的视频帧选有代表性的几百张补进训练集重新转换标注并做一轮增量训练。如果现场与训练环境差异太大数据扩充比调参有效得多。5.2 现象XML 里的图片尺寸和实际图片对不上转换后部分 txt 落到图像区域之外训练时报“assertion failed: bbox 大于 1”。原因是标注工具导出 XML 时某些文件的 width/height 是缩略图尺寸而 bndbox 坐标是原图坐标两者混在一起。解决不要依赖 XML 的 size 节点统一用 cv2.imread 读取真实图片的 shape 作为归一化基准并在转换脚本里把 bndbox 坐标 clamp 到图像范围内。这类问题光看日志很难发现我的习惯是在转换时统计有多少框被 clamp超过 5% 就要回头重新检查标注。5.3 现象同一辆三轮车被框成两个目标画面里一辆三轮车被树荫或栏杆分成两部分模型输出了两个重叠框。原因是 NMS 的 iou_t 设置太低两个框虽然同属一类但重叠面积不够没有被合并。解决推理时把 iou_thres 提高到 0.5 或 0.6让重叠度稍高一点的框被抑制。或者在后处理中限定每个类别最多保留 20 个目标避免画面密集时出现几十个候选框互相覆盖。注意 iou_t 不是越大越好调太高会漏掉紧挨着的多辆车。5.4 现象夜间/弱光环境下漏检严重晚上路灯昏暗或者摄像头开启了红外夜视模型突然认不出三轮车了。原因是训练集大多来自白天模型学到的纹理特征在低照度下失效。解决最快的方法是采集夜间的视频帧加入训练集配合红外摄像头做训练。如果暂时找不到夜间数据可以先把推理帧做 CLAHE 自适应直方图均衡化增强局部对比度再送入模型。这个方法能缓解一部分漏检但治标不治本。5.5 现象行驶中的三轮车也被判成违规停放单帧检测框落在禁停区域内就告警导致经过禁停区但没停下的车也触发上报。原因是没有任何时间维度模型识别的是“画面中有三轮车”不是“三轮车停在这里”。解决把检测目标加入跟踪列表记录同一个目标在禁停区域内的连续停留时间超过 30 秒或 60 秒才触发告警。用 IoU 做简单的目标匹配即可不必上 DeepSORT 那么复杂关键是给每个框一个“历史年龄”。6. 从检测结果到违停事件用轻量状态机把误报率再降一半先想清楚一个问题违规停放的判定依据不是“画面里有三轮车”而是“同一辆三轮车在禁停区域里待了足够久”。所以我在项目里会用一个轻量状态机来管理目标而不是每次都重新检测。具体做法是模型每 0.5 秒输出一组检测框先把检测框中心点映射到禁停区域 ROI确认目标落在禁停区后再用 IoU 和上一帧的目标做关联。同一个目标连续出现 N 次状态从“候选”转为“违停”此时才抓拍图片、生成告警一旦目标消失在禁停区状态清空。下面是我常用的一个状态记录结构核心是给每个目标保留一个停留计数target_state {} def update_state(bbox, frame_time): x1, y1, x2, y2, score bbox cx (x1 x2) / 2 cy (y1 y2) / 2 key None for k, v in target_state.items(): prev_bbox v[bbox] if iou(prev_bbox, bbox) 0.3: key k break if key is None: key frame_time target_state[key] {bbox: bbox, count: 0} target_state[key][bbox] bbox target_state[key][count] 1 if target_state[key][count] 12: return True # 已停留约6秒触发违停 return False这里的消费阈值看具体项目地铁口我一般设 15 秒小区门口设 30 秒因为违停的严重程度不一样。阈值太低会被临时停靠的车频繁触发阈值太高又漏掉短期卸货。配合前面说的区域规则和置信度阈值这套状态机算法能压掉一半左右的误报。这个方向做到最后真正决定项目好用的不是模型 mAP而是它和业务规则的咬合程度。我踩过几次“模型很准但后台天天被投诉”的坑之后养成的习惯是先压误报率再谈召回率现场规则阈值宁可多测几天也不要拍脑袋。如果你手头的 tricycle1_images_xmls 也打算走这条路建议从数据清理开始一步步来这套流程能帮你少走很多弯路希望帮到你。本文还有配套的精品资源点击获取
返回列表