YOLOv5非机动车违停识别全流程:数据转换、训练与规则判定
简介面向基于YOLOv5的机器视觉开发者这份已标注的非机动车数据子集用于训练自行车目标检测模型可服务于非机动车违规停放场景下的智能监控与城市巡检。压缩包共1515个文件以766张jpg图像和749个xml标注文件为主整体大小约94.44MB属于自行车十类数据中的第5类bicycles4。图像样本包含不同角度和背景的自行车XML标注中带有目标类别与边界框位置可直接用于YOLOv5训练也便于开展迁移学习与精度调优。该完整数据集包含自行车8000张、电动车8000张、三轮车6000张等分类当前子集仅为其中一部分方便开发者按需获取特定类别样本。目前已有164人学习下载适合用于算法验证、课程设计或违规停放检测项目能大幅减少数据采集和手工标注时间提高模型迭代效率。1. 非机动车违停识别不是“检测一个自行车框”那么简单把“yolov5非机动车违规停放已标注数据集自行车bicycles4_images_xmls”这串关键词拆开看其实是一个完整的落地链条用YOLOv5对监控画面里的自行车做目标检测再叠加“停在哪、停了多久、周围是什么”的规则判断最后输出违停告警。很多团队拿到这个标题就以为只是“训练一个自行车检测模型”结果模型精度做到0.95业务上一测还是没法用——因为停在划线停车位里的自行车也被框出来报违停了。真正的难点在检测框之后如何判断“违规停放”而不是“识别出自行车”。这个标题里的bicycles4_images_xmls已经暗示了数据形态——Pascal VOC格式的图片和XML标注文件训练部分反而是整个链路里最成熟、最不容易翻车的一段。适合谁做手里有监控视频或园区巡检需求、想用机器视觉替代人工巡查的团队以及刚入门YOLOv5、想用一个完整数据集跑通“标注→训练→推理→规则判定”全流程的开发者。这篇就按这个顺序把每段的做法、参数和坑讲清楚。2. 把VOC格式的bicycles4_images_xmls转成YOLOv5能吃的格式目录组织与转换脚本2.1 先认清VOC和YOLO格式的差异别拿到XML就直接开训bicycles4_images_xmls这个数据集名称里的“images_xmls”已经说明它的原始形态一张JPEG图片对应一个同名的XML标注文件。XML里记录的是object节点每个节点包含name类别名和bndboxxmin、ymin、xmax、ymax四个坐标。这是典型的Pascal VOC布局但YOLOv5训练时读的不是XML而是每个图片对应一个同名的TXT文件每行格式是“类别id 中心点x 中心点y 宽度 高度”坐标全部归一化到0到1之间。不少新手直接把XML丢给YOLOv5报错找不到标签文件后才发现格式不对。转换的核心就两件事一是把bndbox的左上角和右下角坐标换算成中心点加宽高二是把类别字符串映射成从0开始的整数id。这个数据集里如果只有“bicycle”一类那类别id恒为0看起来简单但后面加类别时容易埋坑。2.2 目录结构照YOLOv5的约定来dataset.yaml里要写绝对路径还是相对路径YOLOv5对数据集目录只有两条硬性要求images和labels两个目录名必须固定且图片和TXT文件的主文件名必须完全一致。常见做法是在项目根目录下建datasets/bicycles4/里面再分images/train、images/val、labels/train、labels/val。如果原始数据集已经把图片和XML混在一个文件夹里需要先按比例拆分再分别放好。cd datasets/bicycles4 # 假设原始数据在 raw/ 下图片和同名xml混在一起 mkdir -p images/train images/val labels/train labels/val # 用脚本按9:1拆分先列出所有jpg再随机分配 python - EOF import os, random, shutil raw raw imgs [f for f in os.listdir(raw) if f.endswith(.jpg)] random.seed(42) random.shuffle(imgs) split int(len(imgs) * 0.9) for i, name in enumerate(imgs): sub train if i split else val shutil.copy(os.path.join(raw, name), fimages/{sub}/{name}) xml_name name.replace(.jpg, .xml) if os.path.exists(os.path.join(raw, xml_name)): shutil.copy(os.path.join(raw, xml_name), ftemp_{sub}/{xml_name}) EOF这段先把图片按文件名随机拆到train和valXML先复制到一个临时目录因为后面还要做格式转换。random.seed(42)保证每次拆分结果一致别小看这个固定种子——换一次随机序列训练集和验证集的内容就变了后面复现实验结果会非常痛苦。2.3 XML转TXT的转换脚本顺带清洗无效标注真正的转换脚本要处理的坑比想象中多有的bndbox坐标超出图片边界有的name写的是“Bicycle”大写开头有的XML里嵌套了segmented节点干扰解析。稳妥的做法是解析完直接做一轮数据清洗坐标越界的就裁剪类别名统一转小写解析失败的单独记日志而不是直接崩掉。import xml.etree.ElementTree as ET import os from PIL import Image def convert_xml_to_yolo(xml_path, txt_path, class_names): tree ET.parse(xml_path) root tree.getroot() img_name root.find(filename).text img_path os.path.join(raw, img_name) try: w int(root.find(size/width).text) h int(root.find(size/height).text) except AttributeError: img Image.open(img_path) w, h img.size img.close() lines [] for obj in root.iter(object): name obj.find(name).text.strip().lower() if name not in class_names: continue cls_id class_names.index(name) 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) # 坐标裁剪防止归一化后出现大于1的值 xmin max(0, min(xmin, w)) xmax max(0, min(xmax, w)) ymin max(0, min(ymin, h)) ymax max(0, min(ymax, h)) x_center (xmin xmax) / 2.0 / w y_center (ymin ymax) / 2.0 / h bw (xmax - xmin) / w bh (ymax - ymin) / h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {bw:.6f} {bh:.6f}) with open(txt_path, w) as f: f.write(\n.join(lines)) class_names [bicycle] for split_name in [train, val]: xml_dir ftemp_{split_name} out_dir flabels/{split_name} os.makedirs(out_dir, exist_okTrue) for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue txt_file xml_file.replace(.xml, .txt) convert_xml_to_yolo( os.path.join(xml_dir, xml_file), os.path.join(out_dir, txt_file), class_names )代码里的坐标裁剪是个关键细节。原始标注里偶尔会出现xmax比图片宽度还大的脏数据不裁剪的话归一化后坐标值大于1YOLOv5训练时OpenCV读图后标签和图像尺寸对不上表现为loss不降或训练中期mAP突然跳变。另外class_names这里只写了bicycle如果数据集里混着motorcycle或person的标注想好是清掉还是保留——清掉就跳过保留就得把类别id顺延这一步决定了后面的标签映射表回头再改代价很大。2.4 写dataset.yaml训练前先跑一次验证脚本# datasets/bicycles4.yaml path: ../datasets/bicycles4 train: images/train val: images/val nc: 1 names: 0: bicycle# 在yolov5根目录下执行验证标签和图片能否正确配对 python train.py --data datasets/bicycles4.yaml --epochs 1 --img 640 --batch-size 8 --weights yolov5s.pt跑1个epoch的意义不是训练而是确认数据链路通不通。如果标签路径写错、图片和TXT文件名对不上程序会在第一次迭代时报AssertionError: Label class 0 exceeds nc1或者一直卡在Scanning labels阶段。这一步过了再动正式训练的超参数能省下大量排错时间。3. 训练YOLOv5模型环境配置、超参数选型与收敛判断3.1 conda环境配置的版本对应关系torch 1.8.1是分水岭YOLOv5对环境的要求说高不高但版本错位是新手最常见的翻车现场。这个数据集作者在标注和验证时大概率用的是torch 1.8.1那一代组合因为YOLOv5 v5.0/v6.0的requirements.txt锁定的就是这组版本。现在装环境我一般建议Python 3.83.9也能跑但不稳、torch 1.8.1或1.10.0、torchvision版本跟着torch走CUDA 10.2或11.1。conda create -n yolov5 python3.8 conda activate yolov5 conda install pytorch1.8.1 torchvision0.9.1 cudatoolkit10.2 -c pytorch pip install -r requirements.txtrequirements.txt里有个特别容易忽略的包叫scipyYOLOv5在计算mAP时依赖它做聚类缺了会在训练结束后epoch 0验证阶段报错。还有一个是tqdm进度条库少装的话终端会直接空白卡住看着像死锁其实是依赖没装全。装完环境先跑python detect.py --source data/images/bus.jpg跑一次官方自带样例能出框就说明torch和CUDA层面的依赖没炸再回来训练自己的数据集就少一层黑匣子。3.2 预训练权重怎么选yolov5s还是yolov5m看你的目标是精度还是速度bicycles4这个数据集如果是单类别、几千张的量级yolov5s是最合适的起点。s模型参数量7.3M左右一张1080Ti上训练几百个epoch也就两三个小时m模型精度提升有限但训练和推理时间都上涨明显。看监控视频做实时检测的场景s模型在GPU上的推理速度能到3ms一帧CPU上也有40ms左右余量足够再跑违停判定逻辑。python train.py \ --data datasets/bicycles4.yaml \ --epochs 200 \ --img 640 \ --batch-size 16 \ --weights yolov5s.pt \ --cache \ --name bicycles4_v1--cache参数值得多说一句它把训练图片一次性加载进内存第一次会占用RAM但能省掉每个epoch反复读磁盘的时间。数据集有几万张图片时机器内存不够就别加几千张的话加上能让训练提速30%以上。--name是给每次训练一个独立目录名YOLOv5默认在runs/train/下递增编号显式命名方便后面对比不同实验的结果。3.3 超参数里的三个关键旋钮学习率、batch size和mosaic数据增强YOLOv5的默认超参在data/hyps/hyp.scratch-low.yaml里训练自己的数据集时不要上来就动lr先把batch size跑得动再说。batch size设为16、初始学习率0.01在单卡12GB显存条件下是最稳的组合。显存不够就降到8但学习率最好也按比例下调到0.005左右——stochastic gradient descent的更新幅度是lr除以batch sizebatch size减半学习率不变的话模型震荡会明显加剧。mosaic增强是YOLOv5在v5.0之后默认开启的它把四张图拼成一张训练对小目标检测提升显著但如果你发现训练集里自行车普遍较大、标注框都在画面中心区域mosaic反而会让目标被裁剪得支离破碎。遇到loss正常下降但mAP在0.85附近上不去的情况试试把--mosaic 0关掉再训一轮两种模式在这个数据集上可能差出5个百分点的mAP。注意mosaic关闭后训练epoch次数建议同步增加20%因为数据多样性下降了模型需要更多轮次才能看到足够丰富的样本。3.4 看训练曲线判断是否欠拟合bbox_loss和mAP_0.5的关系训练过程中runs/train/bicycles4_v1/results.csv记录了每个epoch的loss和mAP。判断模型有没有训起来先看train/box_loss——它从0.08级别开始下降属于正常如果200个epoch后还在0.06以上很大概率是标注质量有问题而不是模型结构问题。再看metrics/mAP_0.5单类别场景下这个值低于0.9就要回头检查数据集里有没有漏标、错标别盲目加深模型。# 训练结束后用tensorboard看曲线 tensorboard --logdir runs/train/bicycles4_v1如果box_loss和cls_loss都降得不错、mAP_0.5也过了0.95但val阶段的mAP_0.5:0.95明显偏低比如只有0.5这说明模型对目标框的定位精确度不够。原因多半是标注框本身画得不严实XML里bndbox贴合度差。这个数据集的XML如果来自半自动标注工具框边缘普遍比手标的宽三五个像素直接影响IoU的严格指标。解决办法是拿精度要求换时间——只盯着mAP_0.5评判或者用标注工具手动精修一批困难样本。4. 推理与违停判定从检测框到“违规停放”的业务规则引擎4.1 用detect.py跑一批测试图先把模型输出和可视化调对python detect.py \ --weights runs/train/bicycles4_v1/weights/best.pt \ --source test_images/ \ --conf-thres 0.5 \ --iou-thres 0.5 \ --save-txt \ --save-conf--save-txt会把检测结果存成TXT每行是“类别id 置信度 x1 y1 x2 y2”后续做规则判定时直接读这个文件就行不需要重新起Python进程去解析detect.py的输出。--save-conf在TXT里追加置信度数值初学者容易漏了这个参数结果拿着只有坐标的TXT去算业务逻辑置信度阈值根本无从下手。--conf-thres 0.5是检测置信度门槛业务上如果误报比漏报更可接受就调到0.4反之调到0.6以上。这个参数的背后逻辑是违停识别的代价结构决定了阈值取向——误报顶多多一条待复核工单漏报意味着违停车辆没人管所以宁可调低置信度让模型“多检出、少漏网”再用后续规则把误报过滤掉。4.2 违停判定的两条硬规则区域重叠度与停留时长检测到自行车不等于违停。一套可落地的判定规则至少包含两条第一空间规则。在监控画面里预先划定禁停区域比如消防通道、盲道、写字楼出入口用检测框与禁停区域的IoU或中心点落入判断。常见做法是先在图像上标一个多边形禁停区用cv2.pointPolygonTest判断检测框中心点是否在区域内面积重叠超过30%或中心点落入就算违停候选。第二时间规则。单帧检测到违停不告警同一位置连续出现N帧才确认。因为行人推着自行车短暂经过、停下来看手机这类场景太常见一帧的瞬时检测结果会批量制造噪音。import cv2 import json # 预置禁停区域多边形顶点坐标从标定工具导出 forbidden_zone [(500, 200), (800, 200), (800, 500), (500, 500)] def is_in_forbidden_zone(cx, cy): return cv2.pointPolygonTest( np.array([forbidden_zone], dtypenp.int32), (int(cx), int(cy)), False ) 0 def judge_violation(detections, frame_id, zone_ruleTrue, duration_frames15): candidates {} for det in detections: cls_id, conf, x1, y1, x2, y2 det if cls_id ! 0 or conf 0.5: continue cx, cy (x1 x2) / 2, (y1 y2) / 2 if zone_rule and not is_in_forbidden_zone(cx, cy): continue key (int(cx // 20), int(cy // 20)) # 粗略网格位置容忍小幅移动 if key not in candidates: candidates[key] {count: 0, last_det: det} candidates[key][count] 1 candidates[key][last_det] det alerts [] for key, info in candidates.items(): if info[count] duration_frames: alerts.append(info[last_det]) return alerts这段代码把坐标量化到20像素的网格里容忍自行车在禁停区内小幅挪动实现了一个最简版本的“轨迹停留检测”。实际项目中可以用跟踪算法DeepSORT或ByteTrack拿到稳定的ID后再计时效果更准。但注意如果监控帧率是25fps15帧只代表0.6秒这个阈值至少要拉到75帧3秒以上才能过滤掉行人推车经过的场景具体数值需要拿现场视频实测调整。提示中心点判定的缺陷是检测框略微偏移就会误判区域归属工程上建议用“检测框与禁停多边形的IoU 0.3”或“框的底部中心点落入区域”作为判定条件后者更符合“车停在地面”的物理直觉。4.3 树莓派5上部署的取舍NCNN还是ONNX Runtime帧率与精度的平衡监控场景不是非得用GPU服务器树莓派5这类ARM设备上跑推理是完全可行的。把训练好的PyTorch权重导出为ONNX再用NCNN框架做ARM平台的推理加速。这个方向的部署要注意YOLOv5后处理层——detect.py里的NMS非极大值抑制在导出ONNX时需要显式带上或丢掉NCNN不支持动态shape所以导出时输入尺寸要固定。python export.py \ --weights runs/train/bicycles4_v1/weights/best.pt \ --img 640 \ --include onnx \ --dynamic False # 在树莓派5上安装NCNN的Python绑定 pip install ncnn树莓派5上跑yolov5s的640输入NCNN优化后单帧推理大概在300到500ms之间相当于2到3帧的实时性。但这个速度做违停判定恰好够用——因为时间规则本身就是多帧累计不需要每帧都全速处理。反过来如果连300ms都达不到优先把输入尺寸降到416精度会损失3到5个点但推理时间能压到200ms以内。这个取舍在监控视角下通常是可以接受的因为自行车的目标尺寸在1080P画面里相对较大。5. 避坑手册数据集和训练过程中最常见的5个翻车现场5.1 XML和图片文件名对不上训练时莫名报“No labels found”现象train.py执行后在Scanning labels阶段打印一堆警告或者直接报No labels found in datasets/bicycles4/labels/train。原因这个数据集的约束是“同名XML对应同名JPG”但下载解压后可能出现Windows系统截断长文件名比如bicycle_123456.jpg被截成bicycle_1234~1.jpg或者XML是_xml后缀变了命名规则。肉眼看不出来程序扫描时一个都匹配不上。解决转换脚本里先做一次严格的配对校验打印出图片目录里有但XML目录里缺失的文件清单。反方向也要查——XML存在但图片缺失这也会让训练时读不到数据。import os img_files set(os.listdir(raw)) xml_files set(os.listdir(raw)) imgs_no_xml [f for f in img_files if f.endswith(.jpg) and f.replace(.jpg, .xml) not in xml_files] print(fimages without xml: {len(imgs_no_xml)}) for f in imgs_no_xml[:10]: print(f)5.2 数据集里混入误标注motorcycle被标成bicycle模型怎么调都“学不干净”现象训练时cls_loss降到0.01以下但验证集里摩托车频繁被识别成自行车mAP_0.5:0.95上不去。原因这个数据集标题里的“bicycles”未必保证样本纯净。共享单车和电动自行车在外形上的重叠度极高标注员如果只按“两个轮子”判断极可能把电动车标成自行车。机器视觉模型学到的不是“自行车”的概念而是“两轮车”的视觉特征所以推理时摩托车自然也会出框。解决把训练集里所有标注框的置信度分布导出来用聚类方法先筛一遍。每个标注框对应的图片区域裁剪出来人工快速过一遍把明显是摩托车的样本从训练集里剔除或者单独建一个motorcycle类别。单类变两类后违停判定逻辑也要同步调整——如果监控区域里摩托车同样禁停那两条规则都要跑。5.3 训练时CUDA显存溢出调小batch size后模型又不收敛了现象RuntimeError: CUDA out of memory把--batch-size从16降到4后能跑起来但训练200轮mAP只有0.7欠拟合明显。原因YOLOv5的--img 640意味着输入图resize到640x640每张图在12GB显存上约占1.5到2GB显存含梯度16的batch确实满负荷。降到4后batch太小BN统计量不稳定且默认学习率0.01没有同步缩放SGD更新幅度相对变大训练震荡。解决用梯度累积等效大batch--batch-size 8 --accumulate 4让YOLOv5自己累积4步梯度等效于32的batch同时学习率降到0.005。这一步改完训练曲线会平稳很多mAP也能回到正常水平。python train.py \ --data datasets/bicycles4.yaml \ --epochs 200 \ --img 640 \ --batch-size 8 \ --accumulate 4 \ --weights yolov5s.pt \ --name bicycles4_v25.4 置信度阈值调低了检测框把行人和自行车一起框出来现象--conf-thres 0.3时共享单车旁站着的行人也被输出为一个bicycle框且置信度有0.4左右。原因训练数据里大量图片是“人推着车”或“人坐在车上”的状态标注框把人和自行车的整体包围盒一起标进去了。模型学到的特征是“人车组合体”而不是单独的“自行车”。解决从训练集里把人和车重叠的标注框裁剪出来看看如果这类样本占比超过10%建议直接把目标定位改为只检测“无人的自行车”——标注时只标车身不标人体。这需要重新过一批数据但违停判定本来就是“车在无人的状态下停放”才有意义数据层面的修正比后置规则过滤要省心得多因为后置过滤根本不知道框里是不是坐了个人。5.5 导出的ONNX在NCNN上推理结果全是0或乱框不报错现象ncnn加载ONNX模型后推理输出为空或者一堆坐标超出图像范围的框程序不抛任何异常。原因YOLOv5导出ONNX后包含大量Split、Sigmoid、Transpose节点NCNN的转换工具对这些算子的支持比ONNX Runtime差。最常见的是模型输出层的reshape逻辑变了NCNN读到的输出张量shape和解析代码预期不一致后处理阶段把坐标算飞了。解决用onnx2ncnn转换后先跑NCNN自带的yolov5示例验证推理结果再对后处理代码做针对性修改。别指望直接拿detect.py的后处理逻辑套到NCNN输出上——YOLOv5的后处理在PyTorch和NCNN之间不是完全透明的需要重写decode部分的坐标计算。没有把握就用ONNX Runtime代替NCNN牺牲一点推理速度换开发周期的确定性。import onnxruntime as ort import numpy as np sess ort.InferenceSession(best.onnx) input_name sess.get_inputs()[0].name # 输入shape: [1,3,640,640]图像需做letterbox预处理 outputs sess.run(None, {input_name: img_blob}) # 输出shape通常是[1,25200,6]解析时需要按YOLOv5的anchor网格逻辑处理6. 用热力图验证模型到底“看”到了什么一个比mAP更直观的模型诊断技巧训练完模型、mAP也达标了但上线前一定要做一次可解释性验证。mAP告诉你“检出率不错”但不告诉你模型有没有学偏——比如是不是只识别了特定颜色的自行车、是不是把背景里的车轮图案当成了目标。用Grad-CAM类方法给模型的最后一层卷积输出做热力图是快速发现这类隐性偏差的可靠手段。YOLOv5的检测头结构是三个尺度P3、P4、P5的C3模块加Detect层热力图分析一般选P3尺度最后一层卷积的输出。图里如果热力区域除了自行车轮廓还覆盖了旁边的行道树纹理或者路面阴影就要警惕模型把背景特征也学进去了典型的表现是背景越复杂的画面误检率越高。import torch from models.experimental import attempt_load model attempt_load(runs/train/bicycles4_v1/weights/best.pt, map_locationcpu) model.eval() # 注册P3层输出的hook拿最后一层卷积特征图 activation {} def hook_fn(module, input, output): activation[p3] output.detach() # 模型结构里的model.model[12]是Detect层往前找P3对应的C3模块 model.model[8].register_forward_hook(hook_fn)拿到特征图后求均值得到和输入同分辨率的响应图叠加到原图上就能直观看到模型注意力分布。这类验证在违停场景里尤其有用——如果监控画面里恰好有共享单车的品牌标志或特定颜色的车棚模型可能学到这些“捷径特征”换个园区摄像头就失效。做一次热力图检查相当于给模型做了体检提前发现这种隐性过拟合远好过线上跑两周后才发现误报率异常。我自己在跑类似数据集时的习惯是每训完一版模型先抽十张画面复杂度不同的测试图看热力图再跑一次完整测试集统计mAP。两张图对不上就先信热力图——mAP是统计指标会被大量简单样本拉高热力图能暴露模型在困难样本上的真实行为。这个习惯帮我避开过好几次“训练的模型换个场景就翻车”的事故希望帮到你。本文还有配套的精品资源点击获取