YOLOv5实战:三轮车违规停放检测与训练全记录

📅 2026/8/26 10:35:41
YOLOv5实战:三轮车违规停放检测与训练全记录
简介在城市治理中非机动车乱停放是长期难题单纯依靠人工巡查效率低。随着深度学习与目标检测技术的成熟利用视觉算法自动识别违规车辆成为可行方案。YOLOv5作为主流轻量级检测模型凭借推理速度快、部署灵活、社区生态完善等优势在智慧城市场景中广泛落地。其单帧检测耗时仅十余毫秒适合边缘设备实时分析。本文以三轮车为识别对象讲解基于YOLOv5的模型训练、数据处理与违规区域判定方法并分享小样本数据增强、过拟合应对及推理性能优化等实战经验。从目标检测到业务规则联动提供一套可复用的非机动车停放管理技术路径为类似机器视觉项目提供参考。 非机动车乱停放这事只要做过城市治理项目的人都知道单纯靠人工巡查根本盯不过来尤其三轮车这种“大家伙”经常横七竖八堵在路口、盲道上。我今年上半年接了一个相关的小项目核心任务就是识别特定区域内非机动车违规停放重点对象是三轮车。一开始想直接用人脸检测那套思路去套发现根本不现实最后还是老老实实回到yolov5上用一套已经标注好的三轮车数据集tricycle8_images_xmls把模型训了出来。这篇文章就把整个过程中的思路、踩坑、调参细节都写出来给后面做类似机器视觉识别项目的人一个参考。1. 项目整体设计与思路拆解1.1 任务定义与识别难点先明确一下这个项目到底要解决什么问题。单一场景是“非机动车违规停放”落到算法层面其实是两个子任务第一步是检测出画面里的非机动车尤其是三轮车第二步是判断这些车是否停在了违规区域。第二个子任务依赖于第一个子任务的检测精度所以我没走那种“端到端违规行为识别”的复杂路线而是拆成“目标检测 区域判定”两个阶段来做这样每一步都可控出了问题也好排查。三轮车这个目标其实有点特殊它和普通两轮电动车在外形上差异很大但和某些小型三轮快递车又非常像。而且实际监控场景里三轮车经常会被遮挡车斗里装满了货物货箱的轮廓把车头车尾完全盖住这对检测器的特征提取能力是个不小考验。1.2 方案选型为什么是yolov5而不是其他模型当时在选型上其实纠结过一阵子。项目预算有限没有专门的GPU服务器训练和推理都要跑在同一台普通工作站上同时客户要求识别延迟不能太高最好能跑到实时或者准实时的水平。我对比过几套方案Faster R-CNN精度高但是推理速度太慢直接passEfficientDet精度可以但工程化资料相对少社区踩坑案例不好查yolov6、yolov8这些新版本虽然迭代很快但当时我手头的标注工具和已标注数据集格式都是按yolov5的规范来处理的无缝切换成本最低。最终选择yolov5s作为基准模型理由很直接社区生态成熟从数据格式到部署文档都很完善遇到问题基本都能搜到解决方案推理速度快在GTX 1660 Super上跑batch1的推理单帧耗时大约12-15ms完全可以满足准实时需求模型体积适中yolov5s的权重文件大概14MB后续如果要部署到边缘设备比如Jetson Nano或者RK3568这类板子这个体积非常友好1.3 整体实现流程整个项目的技术链路我梳理成了五个环节数据集准备检查并清洗tricycle8_images_xmls标注数据转换成yolov5需要的格式模型配置根据任务类别数调整模型配置文件设置合理的anchor和超参数模型训练在已标注数据集上训练检测模型实时监控loss曲线和mAP指标违规判定设计区域规则结合检测框坐标判断是否违规停放部署验证封装推理接口在真实场景数据上进行测试和调优后面每一环都有不少细节我挨个说。2. 数据集分析与处理tricycle8_images_xmls实战2.1 已标注数据集格式解析tricycle8_images_xmls这个数据集从名字就能看出来对应8张三轮车图片和对应的XML标注文件。这个规模说实话非常小直接拿来训练yolov5s这种参数量百万级的模型几乎肯定会过拟合。先看看这个数据集的具体格式。XML文件是典型的VOC格式核心结构如下annotation foldertricycle8/folder filenameimg_0001.jpg/filename size width1920/width height1080/height depth3/depth /size object nametricycle/name bndbox xmin350/xmin ymin280/ymin xmax780/xmax ymax650/ymax /bndbox /object /annotation这种VOC格式本身没什么问题yolov5官方仓库也提供voc_label.py来做格式转换但实际用的时候比格式转换更麻烦的是数据质量校验。我写了一个小脚本一次性把这几件事都做了检查标注坐标是否有超出图像边界的、检查是否有空的标注框、检查不同XML里是否有重复的图片名。import os import xml.etree.ElementTree as ET from PIL import Image def check_annotations(img_dir, xml_dir): issues [] 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() filename root.find(filename).text img_path os.path.join(img_dir, filename) # 检查图片是否存在 if not os.path.exists(img_path): issues.append(f图片缺失: {filename}) continue # 获取图片实际尺寸 img Image.open(img_path) img_w, img_h img.size for obj in root.iter(object): 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) # 检查坐标范围 if xmin 0 or ymin 0 or xmax img_w or ymax img_h: issues.append(f{filename} 坐标越界: ({xmin}, {ymin}, {xmax}, {ymax})) # 检查框是否太小可能是误标注 if (xmax - xmin) 20 or (ymax - ymin) 20: issues.append(f{filename} 标注框过小: ({xmin}, {ymin}, {xmax}, {ymax})) return issues issues check_annotations(images/, annotations/) for issue in issues: print(issue) # 如果有问题逐个处理8张图的标注量在训练时遇到的第一个问题就是数据增强几乎成了决定模型能不能收敛的关键因素。2.2 小样本数据增强策略如果你也是用小数据集训练yolov5内置的增强参数一定要充分利用起来。在hyp.scratch-low.yaml里这几个参数我调过之后效果比较明显hsv_h、hsv_s、hsv_v颜色通道扰动。三轮车颜色五花八门红蓝绿黄都有适当加大色调扰动hsv_h从0.015调到0.03能增强模型对颜色变化的鲁棒性translate平移增强。调到0.2让目标在画面不同位置出现模拟不同监控视角scale缩放增强。调到0.5让目标有大有小适配不同距离的拍摄场景fliplr左右翻转。设置为0.5这个对三轮车来说很有效因为车头方向左右都有这里有个细节容易被忽略——mosaic增强。yolov5默认在训练前10个epoch开启mosaic它把4张图拼成1张来训练对小目标和小数据集非常友好相当于变相扩大了batch size让模型在更少的迭代里看到更多样的上下文。但是小数据集上mosaic也有副作用因为原始标注框本来就少拼图后有些目标会被截断导致标注框周围出现大量“半截车”的特征。我的做法是close_mosaic10意思是训练最后10个epoch关闭mosaic让模型在纯真实图上做微调避免学习到拼图特有的伪特征。2.3 数据集划分与格式转换8张图的划分方式我用了6:1:1也就是6张训练、1张验证、1张测试。这纯属“锅里有多少米下多少饭”但至少保证了三个集合互斥。格式转换这块yolov5要求的是每个图片对应一个txt文件每行格式是“类别id x_center y_center width height”注意这里的x_center、y_center、width、height都是归一化到0-1之间的值。转换脚本并不复杂import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_file, class_list, output_dir): tree ET.parse(xml_file) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) txt_path os.path.join(output_dir, os.path.basename(xml_file).replace(.xml, .txt)) with open(txt_path, w) as f: for obj in root.iter(object): class_name obj.find(name).text if class_name not in class_list: continue # 跳过不在类别列表中的目标 class_id class_list.index(class_name) 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) x_center (xmin xmax) / 2 / img_w y_center (ymin ymax) / 2 / img_h width (xmax - xmin) / img_w height (ymax - ymin) / img_h f.write(f{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}\n) # 使用 class_list [tricycle] voc_to_yolo(annotations/img_0001.xml, class_list, yolo_labels/)转换完还要在数据集配置文件里指定路径我建了一个data.yamltrain: ./datasets/tricycle/images/train val: ./datasets/tricycle/images/val test: ./datasets/tricycle/images/test nc: 1 names: [tricycle]到这里数据准备工作才算真正结束。3. 模型训练从超参数配置到收敛监控3.1 环境搭建与依赖安装训练环境我建议直接用yolov5官方仓库的requirements.txt避免自己盲装版本踩坑。我当时用的关键版本是这些Python 3.8.10PyTorch 1.12.1torchvision 0.13.1CUDA 11.6有个安装时的坑得提醒一下不要直接pip install torch torchvision这样装的是CPU版本训练速度慢到怀疑人生。正确做法是先去PyTorch官网选好CUDA版本用对应的index-url安装比如pip install torch1.12.1cu116 torchvision0.13.1cu116 --extra-index-url https://download.pytorch.org/whl/cu116装完之后用python -c import torch; print(torch.cuda.is_available())验证一下输出True就说明GPU可用。我当时就是少了这一步跑到一半才发现用的是CPU在跑白白浪费半天时间。3.2 模型配置文件调整yolov5s的模型结构是官方定义好的不需要大改但有几个地方必须根据项目实际情况调整。第一个是nc参数也就是类别数量。这个项目只检测三轮车一个类别所以nc1。在models/yolov5s.yaml里nc: 1 # number of classes depth_multiple: 0.33 # model depth multiple width_multiple: 0.50 # layer channel multiple第二个是anchor设置。yolov5默认的anchor是COCO数据集上聚出来的主要针对80类通用目标。三轮车的宽高比比较特殊车斗部分比较宽整体框形往往比人、车这类目标更扁。虽然yolov5有自动anchor优化机制训练时会重新聚类但在小数据集上自动聚类容易过拟合到几个样本上我建议先手动统计一下标注框的宽高分布再决定要不要改。import os import numpy as np all_wh [] for txt_file in os.listdir(yolo_labels/): with open(os.path.join(yolo_labels/, txt_file), r) as f: for line in f.readlines(): parts line.strip().split() w float(parts[3]) h float(parts[4]) all_wh.append([w, h]) all_wh np.array(all_wh) print(f标注框数量: {len(all_wh)}) print(f平均宽高比: {np.mean(all_wh[:, 0] / all_wh[:, 1]):.2f})统计完发现这批三轮车标注框的宽高比集中在1.2到1.8之间和默认anchor差距不算太大所以anchor这块我没有额外修改直接用了auto anchor。但如果你的数据框形差异明显最好还是手动算一下anchor再填进去。3.3 超参数选择与训练启动yolov5里超参数配置是独立文件我用的是hyp.scratch-low.yaml这是官方推荐的“低资源”配置在小数据集上比较稳。关键超参数含义和我的设置值如下超参数含义我的设置值说明lr0初始学习率0.01小数据集用默认值即可不需要手动降lrf最终学习率因子0.01余弦退火到初始学习率的1%momentumSGD动量0.937默认值加速收敛weight_decay权重衰减0.0005防止过拟合warmup_epochs预热轮数3.0前3个epoch用较低学习率batch_size训练批大小168张图6张训练batch16可以凑够一个batchepochs训练轮数300小数据集需要更多轮次充分学习训练启动命令python train.py --data data.yaml --cfg models/yolov5s.yaml --weights yolov5s.pt --batch-size 16 --epochs 300 --img 640 --device 0 --name tricycle_run --cache这里加--cache参数的意思是把图片提前加载到内存里小数据集完全放得下这样训练过程不会因为频繁读磁盘而变慢。当时8张图加载到内存后每轮epoch的耗时从几十秒降到了几秒效率提升非常明显。--img 640是训练时的输入分辨率。三轮车在原图里通常占比较大不是特别小的目标用640就够了。如果实际场景中目标很小可以调到1280但训练显存消耗和推理延迟都会上升需要自己权衡。3.4 训练过程监控与结果解读训练过程中最直观的监控指标是loss曲线和mAP曲线。我习惯在训练日志里同时盯这几个值train/box_loss边界框回归损失应该持续下降并趋于平稳train/cls_loss分类损失这个项目只有1个类别初期下降很快metrics/mAP_0.5:0.95综合精度指标越接近1越好val/box_loss验证集上的回归损失如果训练集loss持续下降但验证集loss回升说明过拟合了在8张图这个小数据集上我当时训练30个epoch左右就遇到了梯度爆炸的问题loss值直接跳到NaN。排查下来是学习率太大了尤其小数据集上模型“学得太快”一下把参数推到了不稳定区域。解决办法是在hyp文件里手动把lr0从0.01降到0.005同时把warmup_epochs从3改成5让模型用更小的学习率预热更久。改完再跑loss曲线就稳定多了。最终训练完的结果mAP_0.5在验证集上能到0.9以上mAP_0.5:0.95在0.55左右。这个成绩在8张图的训练规模下已经相当不错了毕竟数据量摆在那里再往上提就得靠加数据或者更强的数据增强。3.5 模型导出与推理验证训练完成后best.pt保存在runs/train/tricycle_run/weights/目录下。在部署前建议先转成TorchScript或者ONNX格式方便后续推理和跨平台部署。python export.py --weights runs/train/tricycle_run/weights/best.pt --img 640 --batch 1 --include torchscript onnx转完之后可以用一段简短的脚本验证一下模型效果import torch from PIL import Image # 加载模型 model torch.hub.load(ultralytics/yolov5, custom, pathruns/train/tricycle_run/weights/best.pt, force_reloadTrue) model.conf 0.45 # 置信度阈值 model.iou 0.45 # NMS IoU阈值 # 推理 img Image.open(test_images/img_0008.jpg) results model(img, size640) # 显示结果 results.show() results.print()第一次在真实图片上跑的时候置信度阈值我设的0.5结果漏检了一个远处的小三轮车。后来把阈值降到0.35才稳定出来。这里要提醒一句置信度阈值不是越高越好得根据实际场景里“误检”和“漏检”哪个代价更大来调。城市治理场景里漏检一辆违规三轮车比多报一辆正常停车更严重所以阈值可以适当放低把召回率提上去。4. 违规停放判定逻辑的实现4.1 从检测框到违规判断模型输出的是“哪里有车”但客户要的是“这车停得合不合法”这中间还差一步规则判断。我的做法是引入“违规区域”的概念。具体来说在监控画面中预先用多边形标注出禁停区域比如消防通道、人行横道、盲道等。然后判断检测框是否和禁停区域重叠、重叠面积占检测框面积的比例。超过一定阈值就判定为违规停放。伪代码如下def is_violation(box, forbidden_zone_polygon, overlap_threshold0.3): box: [x1, y1, x2, y2] forbidden_zone_polygon: shapely Polygon 对象 from shapely.geometry import Polygon, box det_box box(x1, y1, x2, y2) forbidden Polygon(forbidden_zone_polygon) # 计算交集面积 inter_area det_box.intersection(forbidden).area det_area det_box.area overlap_ratio inter_area / det_area return overlap_ratio overlap_threshold, overlap_ratio这个方案的好处是规则灵活禁停区域可以随时调整不需要重新训练模型。如果未来客户画了新的禁停区只要改配置文件里的多边形坐标就行模型完全不用动。4.2 实际场景中的误判与优化第一版规则上线后在真实场景测试发现两个比较典型的误判情况第一种是车辆恰好从禁停区边缘驶过车身一小部分进入了禁停区域但整体是移动状态这种会被误判为违规停放。应对办法是引入“时序确认”机制即连续N帧检测结果都是违规状态才触发告警而不是单帧就判定。我当时取N3配合视频流的帧率大约25fps相当于车辆在禁停区域停留超过0.12秒才会触发基本能过滤掉驶过场景。第二种是公交车、外卖车这类车体较大的目标因为检测框比较大即使实际上是正常停在路侧检测框也可能因为包含了一段禁停区域而被误判。这类问题的根源是检测框本身包含了车体周围的环境。优化方式是计算“车辆本体区域”把检测框底部中间40%的区域作为车辆真实着地的区域用这个区域去和禁停区做重叠判断能明显减少外包围环境带来的干扰。5. 常见问题与排查技巧实录5.1 模型训练中的崩溃与恢复用yolov5训练小数据集时最常见的三个问题我列个速查表现象原因解决办法loss变成NaN学习率过高或数据异常降低lr0检查数据是否有空标注框模型只输出背景检不出目标训练不充分或置信度阈值过高调低conf阈值增加epochstrain loss很低但val mAP很低过拟合加强数据增强加dropout减小模型复杂度如果你遇到loss变成NaN的情况不要急着重跑。先看一下是哪个loss先变成NaN的我这次是box_loss先炸的说明问题出在边界框回归上把学习率降一半就能解决。如果cls_loss先炸那大概率是类别标签有问题去检查数据标注。5.2 小数据集过拟合的应对经验8张训练图正常情况下300个epoch之后模型基本能把训练集“背”下来loss降到接近0但在验证集上表现还是不行。这是我第一次跑完后的真实遭遇。后来我做了三件事效果立竿见影第一加大数据增强力度。hsv_h调到0.03translate调到0.3scale调到0.7让模型每轮看到的数据都不一样变相增加了数据多样性。第二把模型从yolov5s降级到yolov5n。yolov5n参数只有yolov5s的大约四分之一对小数据集拟合能力更弱但正因为弱反而不容易过拟合。当时对比下来在8张图的数据规模下yolov5n的验证集mAP反而比yolov5s高将近8个百分点。第三加了早停机制。yolov5里自带EarlyStopping默认patience是100个epoch意思是连续100个epoch验证集mAP没有提升就自动停止。我手动改成50既能省时间又能防止在过拟合阶段浪费太多轮次。5.3 推理落地时容易被忽视的性能瓶颈训练完模型后面真正上线时还能遇到不少“跟训练没关系”的坑。比如我在对接视频流的时候发现单帧推理速度只有15ms但整个处理流程跑一遍却花了200多ms。排查后发现瓶颈在图片解码和结果可视化上而不是模型推理。用OpenCV解码1080p的RTSP流在没有硬解的情况下每帧大约要60-80ms这比模型推理时间还长。后来我用了两种方式缓解一是把视频流分辨率降到960x540再送进模型因为检测精度在目标不算太小的场景下差别不大二是把可视化输出降频比如每5帧才画一次检测框叠加图中间几帧直接跳过画框步骤只保留检测结果。另外一个容易忽略的点是多路视频流并发推理时如果只有一个GPU需要控制并发数否则显存溢出或者GPU计算队列堆积反而增加整体延迟。我当时的方案是做一个简单的“生产者-消费者”队列推理线程固定批量处理每批攒够4帧再一次性推理吞吐量能提升将近3倍。5.4 数据标注质量的重要性最后想单独说说标注质量这个事。tricycle8_images_xmls这套数据集虽然只有8张图但标注质量整体不错框都贴合目标没有太多冗余背景这给训练省了很多麻烦。如果你是自己标注数据我建议一定要统一标注规范比如标注框要贴合车身最外轮廓不要留太多空白边遮挡严重的目标可以不标强行标注反而会引入噪声类别定义要清晰像“三轮车”和“三轮货车”如果归为不同类别需要有明确的区分标准每张图标注完要有第二个人抽检复核避免低级错误标注数据是训练的上限模型再强也补不了标注质量的坑。这个项目能用8张图训出可用的检测器很大程度就是标注框画得准、画得一致。6. 个人实操体会与后续扩展思路整个项目做下来最大的体会是“检测模型只是起点规则和工程化才是落地关键”。模型能在单张图上框出三轮车这当然值得开心但真正让客户觉得“有用”的是后续那套违规判定逻辑、实时告警机制以及应对各种复杂场景的容错处理。数据量方面的教训也比较深刻8张图训练出来的模型泛化能力终究有限换一个光线条件完全不同的摄像头mAP掉20个百分点都有可能。如果后续有条件扩充数据最优先采集的应该是逆光、夜间、雨天这些复杂光照场景以及不同视角下的三轮车样本。哪怕只多二三十张图对模型鲁棒性的提升都会非常明显。再往后扩展的话这个项目的架构完全可以复用。把检测类别从tricycle扩展到bike、ebike、motorcycle数据集格式、训练流程、推理框架都不用改动只需要扩充数据和使用新的类别配置。如果要部署到边缘设备上torchscript格式的模型文件可以直接用RKNN-Toolkit转成RK3568能跑的格式或者用TensorRT在Jetson系列板子上加速。整个过程因为有yolov5这套成熟生态的存在从算法到落地的路其实比想象中顺畅很多。本文还有配套的精品资源点击获取