ARTICLE DETAIL

资讯详情

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

基于YOLOv5的非机动车违规停放检测全流程

基于YOLOv5的非机动车违规停放检测全流程 简介这份资源是面向机器视觉与智慧城管场景的已标注自行车数据集适用于基于YOLOv5的非机动车违规停放检测模型训练。包内为bicycles4分类子集包含766张自行车图片及对应749个XML标注文件共1515个文件压缩包大小约94.44MB图片与标注一一对应可直接转换为YOLO格式使用。数据集已按车型分类整理该子集属于十个自行车类别中的第五类完整数据集还涵盖山地车、公路车、共享单车等可用于扩充训练样本。目前已有163人学习下载适合具备一定目标检测基础、需要真实标注数据完成违规停放识别项目的开发者。通过该数据集可快速开展模型训练、验证与调优节省自行采集和标注的时间成本提升非机动车违停识别系统的开发效率。1. 从一辆乱停的自行车说起为什么是 YOLOv5 而不是更“新”的模型城市管理者每天面对的海量监控画面里非机动车违规停放是比机动车违停更难处理的问题——目标小、密度高、角度多变而且自行车和电动车在视觉上高度相似。单纯用背景差分或传统图像处理做“区域入侵检测”晴天还行一到树影摇晃、夜间补光或者下雨天误报率直接拉满。这正是 YOLOv5 这类基于深度学习的机器视觉识别方案能站稳脚跟的原因它不是去“理解”规则而是学习“自行车长什么样”再从检测框的空间关系推出“是否违规”。选择 YOLOv5 而不是 YOLOv8 或 RT-DETR不是因为它最强而是因为落地最稳。YOLOv5 的训练生态最成熟GitHub 上的 issue 讨论几乎覆盖了所有能踩的坑部署端从 Jetson Nano 到 x86 服务器都有现成方案而且它对“已标注数据集”的格式兼容性极好——无论是 LabelImg 导出的 Pascal VOC XML还是 Roboflow 导出的 YOLO txt都能直接进训练管线。对于非机动车违停这类“类别少、场景固定、实时性要求高”的任务YOLOv5 的性价比明显高于需要更多算力和调参成本的 Transformer 系检测器。本文会沿着一条完整落地的路径展开先把 bicycles4_images_xmls 这套数据集从 XML 标注转成 YOLO 训练格式再讲如何配置 YOLOv5 环境并训练出可用的权重然后重点拆解“怎么从检测框判定违规停放”——这一步才是项目真正区别于普通目标检测的地方。最后给出精度调优和部署验证的实操技巧。全程基于命令行和 Python 脚本可以直接在本地复现。2. 把 bicycles4_images_xmls 变成 YOLOv5 能吃的格式2.1 hearts 先看数据集里有什么VOC XML 的结构和坑bicycles4_images_xmls 这个名字已经把要素写清楚了Images 是图片文件夹XMLs 是对应的标注文件夹4 可能代表 4 个场景或 4 类目标但最常见的组织方式就是每张图一个同名 XML 文件。用任意文本编辑器打开一个 XML你会看到典型的 Pascal VOC 结构filename指向图片名size给出宽高object块里是name类别名和bndbox边界框的 xmin、ymin、xmax、ymax。这套格式是 LabelImg 的默认导出也是机器视觉识别任务里最通用的标注格式之一。但直接拿 XML 训练 YOLOv5 是行不通的。YOLOv5 要求每张图对应一个同名.txt每行格式为class_id x_center y_center width height其中坐标全部归一化到 0~1。这里有两个容易踩的坑第一VOC 的坐标是整数像素值转换时必须除以图片宽高不能除以 416 或 640 之类的训练尺寸否则边界框全偏第二XML 里的name是字符串需要先映射成从 0 开始的整数 id而且映射顺序一旦在训练和推理时不统一类别就全乱了。这步转换建议汇总到训练前检查清单里因为即使后面模型结构换成 YOLOv8 或其他框架这份映射表也得保持一致。voc_xmls/ bicycles_001.xml bicycles_002.xml ... images/ bicycles_001.jpg bicycles_002.jpg ... labels/ # 转换后生成 bicycles_001.txt bicycles_002.txt classes.txt # 类别清单每行一个类名提示先抽查 3~5 个 XML确认width和height是否和实际图片尺寸一致。有的标注工具会把尺寸写错导致归一化坐标异常这问题光看数字很难发现。2.2 写一个 Python 脚本批量转成 YOLO txt下面这个脚本是标准做法可以直接放进项目根目录运行。它遍历 XMLs 文件夹解析每个标注忽略difficult为 1 的难例如果你不想把它们纳入训练然后写出 YOLO 格式的 txt。import xml.etree.ElementTree as ET import os # 类别映射表名称必须和 XML 里的 name 完全一致 CLASS_MAP {bicycle: 0, ebike: 1} # 按实际数据调整 def voc_to_yolo(xml_path, out_path, img_w, img_h): tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.findall(object): name obj.find(name).text if name not in CLASS_MAP: continue # 跳过未映射类别 difficult obj.find(difficult) if difficult is not None and int(difficult.text) 1: continue box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) # 归一化到 0~1注意是除以图片实际宽高 x_center ((xmin xmax) / 2) / img_w y_center ((ymin ymax) / 2) / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{CLASS_MAP[name]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(out_path, w) as f: f.write(\n.join(lines)) # 批量处理 xml_dir voc_xmls img_dir images out_dir labels os.makedirs(out_dir, exist_okTrue) for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue xml_path os.path.join(xml_dir, xml_file) # 读取对应图片尺寸 img_name os.path.splitext(xml_file)[0] .jpg img_path os.path.join(img_dir, img_name) from PIL import Image with Image.open(img_path) as im: w, h im.size out_txt os.path.join(out_dir, os.path.splitext(xml_file)[0] .txt) voc_to_yolo(xml_path, out_txt, w, h)逻辑说明脚本先把 XML 里的像素坐标转成中心点加宽高的形式再分别除以图片的实际宽度和高度完成归一化。最后一行f{CLASS_MAP[name]} ...里的 6 位小数精度足够训练用无需刻意保留更多位。注意CLASS_MAP里的顺序它决定了类别 id之后的训练配置和推理代码都得沿用同一份映射。参数调优提醒如果数据集里自行车和电动车都有建议把两个类分开标注而不是合并成一个bicycle类因为非机动车违规停放场景中两类车辆的管理规则不同。类别 id 从 0 开始连续编号不要跳号否则 YOLOv5 在读标签时会报错。如果 XML 里出现了你没见过的类别名脚本会静默跳过——这会直接减少有效样本数所以转换后要统计一下每类的实例数。2.3 划分训练集、验证集和测试集别忘了检查标注质量数据集准备好之后下一步是按比例划分。常见做法是 8:1:1 或 9:0.5:0.5分别对应 train/val/test。YOLOv5 的dataset.yaml文件只要求提供 train 和 val 的图片路径test 是可选的但训练完做最终评估时有个独立测试集会更有说服力。import os import random import shutil random.seed(42) # 固定随机种子保证可复现 img_dir images label_dir labels train_ratio, val_ratio 0.8, 0.1 imgs [f for f in os.listdir(img_dir) if f.endswith(.jpg)] random.shuffle(imgs) n len(imgs) train_imgs imgs[:int(n * train_ratio)] val_imgs imgs[int(n * train_ratio):int(n * (train_ratio val_ratio))] test_imgs imgs[int(n * (train_ratio val_ratio)):] def move(imgs, dest_img, dest_label): os.makedirs(dest_img, exist_okTrue) os.makedirs(dest_label, exist_okTrue) for img in imgs: shutil.copy(os.path.join(img_dir, img), dest_img) label os.path.splitext(img)[0] .txt if os.path.exists(os.path.join(label_dir, label)): shutil.copy(os.path.join(label_dir, label), dest_label) else: print(f警告: {img} 缺少标注文件 {label}) move(train_imgs, dataset/train/images, dataset/train/labels) move(val_imgs, dataset/val/images, dataset/val/labels) move(test_imgs, dataset/test/images, dataset/test/labels)划分完成后强烈建议做一次可视化检查随机挑几张训练图用 OpenCV 把 txt 里的框画回原图确认坐标没有偏移、类别没有串位。这一步的价值在于如果原标注本身的框就不准比如把车筐也算进去了模型学到的边界会带着噪音之后做违停判定时 IoU 阈值怎么调都别扭。3. YOLOv5 环境准备与训练配置聚焦超参数3.1 环境配置从裸机到能跑起train.py的最小步骤YOLOv5 环境配置是热搜里的高频词很多人卡在依赖版本上。这里给一套经过验证的 Python 3.8 PyTorch 组合CUDA 版本按你的显卡驱动来定但训练非机动车检测这类小模型 6GB 显存就够用。# 创建虚拟环境避免污染系统 Python conda create -n yolov5 python3.8 -y conda activate yolov5 # 安装 PyTorch注意 cu118 对应 CUDA 11.8按实际驱动调整 pip install torch1.13.1cu117 torchvision0.14.1cu117 -f https://download.pytorch.org/whl/torch_stable.html # 拉取 YOLOv5 仓库并安装依赖 git clone https://github.com/ultralytics/yolov5.git cd yolov5 pip install -r requirements.txt依赖装完后先跑一个最小的推理命令验证环境python detect.py --weights yolov5s.pt --source data/images/bus.jpg --device 0如果能在runs/detect/exp下看到带框的输出图说明环境配置完成可以进入训练环节。这里有个经常遇到的坑torchvision版本如果和torch不匹配会在导入yolov5的 utils 模块时报错解决方法是严格按官方 requirements 里的版本号来装不要用pip install torch torchvision这种不带版本号的方式图省事。注意如果你的机器没有 NVIDIA GPUCPU 训练也不是不行但 bicycles4_images_xmls 这类数据如果只有几百张图CPU 训练一轮要几十分钟调参效率太低。建议至少用 Google Colab 的免费 GPU 资源顶一下或者租云 GPU。3.2 写对dataset.yaml和yolov5s.yaml这是训练地基YOLOv5 训练自己数据集的关键在于两个文件数据配置和模型配置。数据配置写在dataset.yaml里告诉训练脚本图片和标签在哪、有几个类别、类别名是什么。# dataset.yaml path: /absolute/path/to/dataset # 数据集根目录建议写绝对路径 train: train/images # 相对于 path 的训练图片路径 val: val/images # 验证图片路径 test: test/images # 测试图片路径可选 nc: 2 # 类别数量和 CLASS_MAP 长度一致 names: [bicycle, ebike] # 类别名顺序和 id 对应模型配置则选择models/yolov5s.yaml作为起点。如果你的数据集只有 500 张图yolov5s是平衡速度和精度的选择如果图特别多超过 2000 张或者检测目标很小比如远处的自行车可以考虑yolov5m但训练时间会多出近一倍。改模型配置时只需修改nc值网络结构里的深度和宽度倍数默认不动。# models/yolov5s.yaml 的关键部分 nc: 2 # 把默认的 80 改成你的类别数 depth_multiple: 0.33 # 控制网络深度一般不改 width_multiple: 0.50 # 控制网络宽度一般不改3.3 训练命令与 6 个必调超参数训练命令可以直接抄下面这行重点看参数怎么设python train.py \ --data dataset.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 300 \ --workers 4 \ --device 0 \ --project runs/train_vehicle \ --name exp_bicycles参数说明--img 640是训练时缩放到的输入尺寸非机动车检测用 640 是性价比最高的往上提到 960 对小目标有改善但显存占用指数上升--batch 16配合 6GB 显存刚好如果你有 12GB 以上显存可以调到 32--epochs 300对于几百张的小数据集是必要的YOLOv5 的 early stopping 默认开启如果连续 100 轮没提升会自动停所以设大一点没坏处--weights yolov5s.pt是从 COCO 预训练权重开始迁移学习这是小数据集能训练出可用模型的关键。训练过程中重点盯box_loss、obj_loss和mAP0.5三个指标。如果obj_loss一直在降但mAP涨不动大概率是正负样本不均衡问题如果box_loss降得很慢检查标注框是不是有大量错位。YOLOv5 的超参数搜索是热搜词它的核心思想不是随机乱试而是通过遗传算法在训练中动态调整。最简单的用法是用默认的hyp.scratch-low.yaml它已经过大量验证。想手动调的话三个最值得动的参数是lr0初始学习率默认 0.01小数据集可以降到 0.005 防止震荡、mosaic数据增强概率默认 1.0如果目标经常被截断可以降到 0.8、fliplr水平翻转概率默认 0.5对自行车这类左右对称的目标可以不改。4. 从检测框到“违规停放”判定机器视觉识别里的空间逻辑4.1 检测框只是“在哪里”违规判定需要区域 IoU 和停留逻辑YOLOv5 输出的是一组置信度和边界框坐标但“违规停放”是一个空间规则自行车出现在不该停的地方。这个判定逻辑一般分成两层。第一层是静态判定预先在画面里画出一个或多个禁停区域ROI只要检测框的中心点落在 ROI 内就触发嫌疑。第二层是动态判定单帧出现可能是路过或短暂停留所以需要连续多帧都在同一位置才确认是停放。禁停区域的表示方式最常见的是多边形坐标而不是简单的矩形因为监控画面里自行车禁停区往往不是标准的水平矩形——比如地铁口出口的弧形区域、商场门口的斜向通道。OpenCV 的pointPolygonTest可以直接判断一个点是否在多边形内计算开销很小适合在检测后处理里实时跑。4.2 实现一个violation_check函数直接集成到检测输出里下面是完整的判定函数输入是 YOLOv5 检测结果中每个框的坐标和置信度输出是标记为违规的框列表。import cv2 import numpy as np # 禁停区域多边形顶点坐标按画面尺寸归一化到 0~1 # 示例是一个五边形实际使用中用 labelme 或任何标注工具画出来 NO_PARKING_ZONES [ np.array([[0.1, 0.5], [0.3, 0.4], [0.4, 0.6], [0.2, 0.8], [0.1, 0.6]], dtypenp.float32) ] def point_in_zones(x, y, img_w, img_h, zones): 检查归一化坐标点是否落在任意禁停区内 px, py x * img_w, y * img_h # 还原成像素坐标 for zone in zones: # 用像素坐标做多边形判定OpenCV 要求 float32 类型 zone_px zone * np.array([img_w, img_h], dtypenp.float32) dist cv2.pointPolygonTest(zone_px, (float(px), float(py)), False) if dist 0: return True return False def violation_check(detections, img_w, img_h, track_dict, frame_id, conf_thresh0.5): detections: list of [x1, y1, x2, y2, conf, class_id] track_dict: 用于跨帧跟踪每个目标的停留帧数格式 {track_id: frame_count} 返回违规框列表 violations [] for det in detections: x1, y1, x2, y2, conf, cls_id det if conf conf_thresh: continue cx (x1 x2) / 2 / img_w # 中心点归一化 x cy (y1 y2) / 2 / img_h # 中心点归一化 y if point_in_zones(cx, cy, img_w, img_h, NO_PARKING_ZONES): # 这里省略了目标跟踪关联逻辑实际需要给每个框分配 track_id # 假定 track_id cls_id * 1000 int(cx * 100) track_id int(cx * 1000) int(cy * 1000) track_dict[track_id] track_dict.get(track_id, 0) 1 if track_dict[track_id] 10: # 连续 10 帧以上在禁停区 violations.append((x1, y1, x2, y2, conf, track_dict[track_id])) return violations逻辑说明point_in_zones先把检测框中心点转换回像素坐标再用cv2.pointPolygonTest判断是否落在任意多边形内。这个函数返回负值表示点在外部大于等于 0 表示在内部或在边上。violation_check里用一个简化版的 track_id 来做跨帧计数——真实项目中这一步会替换成 DeepSORT 或 ByteTrack但核心逻辑不变单帧命中不代表违停连续 N 帧命中才触发告警。参数调优提醒conf_thresh在检测阶段用 0.25 保证召回率但在违停判定阶段用 0.5 过滤低质量框能显著减少误报。停留帧数 10这个值需要按视频帧率调整25fps 下 10 帧等于 0.4 秒行人推车短暂逗留也会被误判。建议用 25fps 视频的话设 20~30 帧也就是 1 秒左右的容忍窗口。4.3 关联 YOLOv5 的detect.py输出做一次实时推理YOLOv5 的detect.py本身只输出画好框的图片和坐标 txt要接入上面的违停判定推荐的做法是写一个独立脚本调用torch.hub加载训练好的权重然后在每次推理后跑violation_check。import torch import cv2 # 加载训练好的自定义模型 model torch.hub.load(yolov5, custom, pathruns/train_vehicle/exp_bicycles/weights/best.pt, sourcelocal) model.conf 0.25 # 检测阶段用较低阈值保证召回 model.iou 0.45 # NMS 的 IoU 阈值默认 0.45 cap cv2.VideoCapture(test_video.mp4) track_dict {} frame_id 0 while cap.isOpened(): ret, frame cap.read() if not ret: break h, w frame.shape[:2] results model(frame) # 前向推理 dets results.xyxy[0].cpu().numpy() # [x1, y1, x2, y2, conf, cls] violations violation_check(dets, w, h, track_dict, frame_id) # 在画面上叠加违规框和告警文字,注意中文需要用 PIL 绘制 for v in violations: x1, y1, x2, y2, conf, cnt v cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 0, 255), 2) cv2.putText(frame, fPARKING: {cnt}, (int(x1), int(y1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) frame_id 1 if frame_id % 30 0: print(fFrame {frame_id}, violations: {len(violations)})这里有个部署细节值得展开YOLOv5 推理出的xyxy坐标是像素值而violation_check的 ROI 是归一化多边形坐标两者必须在同一个坐标系下比较。代码里point_in_zones内部已经把 ROI 乘以了img_w和img_h所以在传入dets时不再做缩放直接传原始像素框即可。如果你在别的项目里复用了这段代码这步最容易漏。5. 训练后的精度瓶颈与验证技巧叠加 IoU 融合模型训练到mAP0.5超过 0.9 并不代表项目交付了因为非机动车违规停放场景里的“目标”和“环境”往往比数据集里的分布更复杂。这里给两个实际调优方向和一套验证流程。第一个方向是解决“小目标漏检”如果你的禁停区离摄像头远自行车在画面里可能只有 30×40 像素。YOLOv5 的原始输入是 640小目标信息经过多次下采样后极易丢失。低成本的改进是把训练和推理的--img提高到 1280代价是推理速度从 10ms 涨到 30ms 左右但对于固定机位的监控场景来说完全值得。如果提速后还是漏下一个方案是切分检测把原图按 2×2 分块每块放大后分别检测再合并结果这一步能立竿见影。第二个方向是减少误报常见误报源是“共享单车和行人重叠”或“车辆阴影”。如果你用同一个模型检测自行车和电动车conf_thresh低于 0.45 时阴影框的置信度也常能到 0.3 以上。一个过滤技巧是在violation_check前加按检测框宽高比的过滤条件自行车的宽高比通常在 1.2~2.5 之间侧面视角超过这个范围的高置信度框大概率是误检可以直接丢弃。但要小心俯视视角下自行车宽高比接近 1这个过滤必须按摄像头角度调参不能直接照搬。验证时不要只看整体 mAP要单独计算“禁停区域内的召回率”。做法是导出测试集里目标落在 ROI 内的样本单独跑一遍推理统计检出率。如果你有 100 个违规样本但只检出了 85 个剩下的 15 个要看具体原因——是框太乱、目标太小还是两个车叠在一起被 NMS 合并了。一个实用技巧把 NMS 的--iou从 0.45 降到 0.3能在“密集停放的自行车”场景下保住更多互相遮挡的框误检率上升有限但召回率往往能提升 2~3 个点。最后一个值得落地的小技巧是“Post-NMS 框融合”。YOLOv5 的 NMS 对同一目标可能输出多个置信度接近的框你可以在检测后增加一个轻量级权重融合def weighted_box_fusion(boxes, scores, iou_thresh0.8): 对高 IoU 的框按置信度加权平均得到更稳定的坐标 from scipy.optimize import linear_sum_assignment # 简化实现先按 score 排序然后逐对融合 idx np.argsort(scores)[::-1] fused [] used set() for i in idx: if i in used: continue group [i] for j in idx: if j in used or j i: continue if box_iou(boxes[i], boxes[j]) iou_thresh: group.append(j) used.add(j) # 加权平均坐标 w scores[group] / scores[group].sum() fused_box np.average(boxes[group], axis0, weightsw) fused.append((fused_box, scores[group].max())) used.add(i) return fused这个函数的实际收益在监控视频里非常明显因为自行车目标边缘复杂单帧检测框通常有 2~3 个像素的抖动直接拿原始框去判断“中心点是否在禁停区”会出现目标骑在边界上来回横跳的抖动。融合后的框更稳定track_dict里的连续计数也不会因为框的小幅抖动而中断违停判断的时序逻辑自然更可靠。本文还有配套的精品资源点击获取
返回列表