☰
YOLOv5训练非机动车违停识别:VOC转YOLO格式与766张自行车数据实战
2026/9/30 10:40:03 网站建设 项目流程

简介:面向智能交通与非机动车治理场景,此压缩包提供YOLOv5可直接训练的已标注自行车子集数据。内容取自非机动车违规停放数据集中的第五类bicycles4,共1515个文件,包括766张jpg图片和749个XML标注文件,压缩包约94.44MB,整体结构适合VOC格式目标检测训练流程。该子集覆盖真实街道环境下的自行车违停样本,图片由人工筛选并标注目标框,类别信息完整,可直接用于训练自行车违规停放检测、占道识别等机器视觉任务;同时因样本包含不同时段、角度和光照条件,有助于提升模型在复杂场景下的泛化能力,并显著减少手动标注工作量。目前已有164人学习浏览,下载解压后即可获得配套图片与XML标注文件,既可作为非机动车违规停放项目的训练集,也可作为目标检测算法验证与效果评估的基准数据。

1. 非机动车违规停放识别:为什么先拿 bicycles4 这 766 张图开刀

物业和城管盯消防通道、人行道上的非机动车乱停,靠摄像头实时识别,YOLOv5 是当前落地最稳的目标检测方案之一。这方案能不能跑起来,一半看标注数据:手里这 766 张已经标好 XML 框的自行车图片,属于一套非机动车十分类数据集里的第五类 bicycles4,整套数据还包括电动车、三轮车等多类。要训练一个违规停放识别模型,这批数据正好当正样本。下面我把标注格式检查、VOC 转 YOLO、训练参数、部署验证按顺序拆开,适合要做毕设、交付原型或第一次拿 YOLOv5 训自己数据集的人,看完能少走几天弯路。

2. 数据集体检:bicycles4 的标注格式与图像质量,决定你后面几天的心情

2.1 766 张图能做什么:数据规模与场景边界

766 张带标注的自行车图片,这是一个什么样的量级?如果做十类别非机动车违停识别,样本数量显然偏少;但如果第一版只做「自行车是否违规停放」,或者把它当成一个更大数据集的训练起点,766 张完全够用。我的判断是:先跑通流程,再补数据。YOLOv5 在几百张图的小数据集上也能训出可用的模型,前提是场景多样、标注准确。

这套数据的文件名规律是「类型_编号.jpg」,例如项目文件里的 山地自行车_395.jpg、山地自行车_199.jpg。也就是说,bicycles4 内部图片较集中在山地自行车这个细分方向,整套自行车数据按公路、越野、通勤、共享单车分成十类,这一批是第五类。第 3 章转换标注时,类别映射就要按这个事实来处理,不能想当然写成十个类。

从用途上看,这批数据更适合作为「检测器」的训练输入,而不是分类器:标注给的是目标框 bounding box,不是整图类别。你要做的违停识别,本质是「先检测出自行车,再判断它是否落在禁停区域」,检测这一步由 YOLOv5 承担。所以拿到数据后,第一优先级不是急着训练,而是先搞清标注框的质量和分布,这决定了后续所有环节是否靠谱。

2.2 标注文件长什么样:VOC XML 的结构与常见字段

图片旁边的 XML 是 Pascal VOC 格式,理论上每张图对应一个同名 XML。打开任意一个文件,结构大致如下:

<annotation> <folder>images</folder> <filename>山地自行车_395.jpg</filename> <size> <width>1280</width> <height>720</height> <depth>3</depth> </size> <object> <name>bicycle</name> <bndbox> <xmin>320</xmin> <ymin>190</ymin> <xmax>925</xmax> <ymax>640</ymax> </bndbox> </object> </annotation>

这段 XML 的关键是<object>块里的<bndbox>:它用 xmin/ymin/xmax/ymax 四个像素坐标描述目标框的左上角和右下角。注意这是绝对像素坐标,不是归一化的;到了 YOLOv5 训练,需要换算成中心点坐标并除以图像宽高。另外,同一个 XML 里可能出现多个<object>,说明一张图里有不止一辆自行车,转换脚本必须循环处理,只读第一个会漏掉大量正样本。

拿到数据的第一件事,建议抽查 20~30 张图和 XML 配对,肉眼对一下:框是否贴住车身、有没有把路牌电线杆圈进去、有没有漏掉远处的车。标注质量是后面所有流程的地基,这一步省下的时间,会在训练时以各种诡异 loss 的形式加倍还回来。我习惯顺手把<name>字段统计一遍,确认 XML 里到底写了哪些类别名,再决定 class_map 怎么填;不同标注员对同一辆车可能写 mountain_bike 也可能写 bicycle,提前统一能省很多事。

2.3 数据体检:统计图像尺寸、目标数与目标尺度分布

不打开每一张图的前提是写统计脚本。脚本里有两个目的:一是看图片实际像素是否和 XML 的 size 一致,这个不一致是后期最常见的坐标偏移坑;二是看标注框相对图片的尺度分布,判断这批数据的小目标占比。

import os import xml.etree.ElementTree as ET xml_dir = "bicycles4_images_xmls" # 改成你解压后的路径 widths, heights, obj_counts = [], [], [] small = medium = large = 0 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() w = int(root.find("size/width").text) h = int(root.find("size/height").text) widths.append(w) heights.append(h) objects = root.findall("object") obj_counts.append(len(objects)) for obj in objects: bndbox = obj.find("bndbox") xmin = int(bndbox.find("xmin").text) ymin = int(bndbox.find("ymin").text) xmax = int(bndbox.find("xmax").text) ymax = int(bndbox.find("ymax").text) area = (xmax - xmin) * (ymax - ymin) img_area = w * h ratio = area / img_area if ratio < 0.01: small += 1 elif ratio < 0.1: medium += 1 else: large += 1 print("图片宽度范围:", min(widths), "-", max(widths)) print("图片高度范围:", min(heights), "-", max(heights)) print("单图目标数范围:", min(obj_counts), "-", max(obj_counts)) print("总图片数:", len(widths)) print("小目标框:", small, "中目标框:", medium, "大目标框:", large)

这段代码用 xml.etree.ElementTree 解析 XML,分别提取图片宽高、object 数量和 bndbox 面积。area / img_area 是判断目标尺度的常见做法:小于 1% 是小目标,1%~10% 是中目标,超过 10% 算大目标。跑完能一眼看出这批图是不是统一分辨率、有没有空标注图、小目标占比是否过高。

统计结果要用来指导训练参数。如果大量标注框占比低于 1%,后面训练时 imgsz 就要考虑提到 960,否则小目标在 640 分辨率下只剩几个像素,模型根本学不到纹理特征。如果普遍占比 10% 以上,说明数据里的车离镜头近,模型学到的「自行车特征」偏大角度,部署到远距离监控时容易漏检——这两个结论都直接对避坑章有指导意义,建议跑完脚本后顺手存一份结果文本。

3. 把 VOC 的 XML 转成 YOLO 的 txt:转换脚本与四个边界坑

3.1 为什么必须转格式,以及转错会翻什么车

YOLOv5 的 dataloader 不直接读 Pascal VOC 的 XML,它要求每张图片对应一个同名 txt,每行格式是class x_center y_center width height,四个坐标值都归一化到 0~1。网上不少翻车案例,都是 XML 没转就急着跑 train.py,结果 loss 一路乱跳,因为 txt 缺失或为空,程序悄悄跳过了一大半图片,你盯着训练日志还以为是自己参数没调好。

这里还要注意,XML 和 txt 的类别体系是对不上的。YOLOv5 训练时只认 yaml 里的索引号,不认名字。yaml 里写names: ['bicycle'],txt 里的 class 只能是 0。如果你之前在同一份数据上跑过其它模型,类别顺序不一样,转换脚本里的 class_map 就必须手动对齐,别想当然。转错类别索引比漏标更隐蔽,模型能收敛,但推理结果全部错位。

转换脚本的核心逻辑不复杂,但边界情况多:同图多目标、宽高为零的坏框、坐标超出图片边界的框、文件名带中文。其中文件名带中文这一点,Windows 下容易触发编码问题,建议在脚本开头就统一处理;我在 3.2 的代码里把这些边界情况都写进去了,可以直接当模板用。

3.2 转换脚本:按 XML 的 size 归一化,别按标注框归一化

import os import xml.etree.ElementTree as ET # 类别映射:voc_name -> yolo_class_id # 只训自行车就只留 bicycle;之后加电动车子集,再加 "e_bike": 1 class_map = {"bicycle": 0} src_xml_dir = "bicycles4_images_xmls" # XML 所在目录 dst_txt_dir = "bicycles4_labels" # 转换后的 txt 输出目录 os.makedirs(dst_txt_dir, exist_ok=True) for xml_file in os.listdir(src_xml_dir): if not xml_file.endswith(".xml"): continue tree = ET.parse(os.path.join(src_xml_dir, xml_file)) root = tree.getroot() # 图像尺寸以 XML 里的 size 为准 img_w = float(root.find("size/width").text) img_h = float(root.find("size/height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in class_map: continue bndbox = obj.find("bndbox") xmin = float(bndbox.find("xmin").text) ymin = float(bndbox.find("ymin").text) xmax = float(bndbox.find("xmax").text) ymax = float(bndbox.find("ymax").text) # 边界修正:防止标注越界导致负数宽高 xmin = max(0, min(xmin, img_w)) xmax = max(0, min(xmax, img_w)) ymin = max(0, min(ymin, img_h)) ymax = max(0, min(ymax, img_h)) box_w = xmax - xmin box_h = ymax - ymin if box_w <= 0 or box_h <= 0: continue # 扔掉坏框,不让它污染 loss # 归一化:中心点坐标和宽高都除以图像宽/高 x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h norm_w = box_w / img_w norm_h = box_h / img_h x_center = max(0, min(x_center, 1.0)) y_center = max(0, min(y_center, 1.0)) norm_w = max(0, min(norm_w, 1.0)) norm_h = max(0, min(norm_h, 1.0)) lines.append(f"{class_map[name]} {x_center:.6f} {y_center:.6f} {norm_w:.6f} {norm_h:.6f}") txt_name = xml_file.replace(".xml", ".txt") with open(os.path.join(dst_txt_dir, txt_name), "w", encoding="utf-8") as f: f.write("\n".join(lines))

逻辑说明:脚本先读 XML 的 size 作为归一化分母,再逐 object 提取 bndbox 坐标。最关键的一点是分母用图像宽高,而不是用 bounding box 的宽高;用后者计算会得到一堆接近 1 的数值,模型直接学废。参数说明:class_map 决定 XML 里的类别名映射到哪个 YOLO 索引,只训自行车就只留 bicycle,之后把电动车子集加进来时再加一行"e_bike": 1,nc 也要改;.6f保留 6 位小数,足够精度又控制 txt 体积。

运行结束后的检查动作不能省:labels 目录下应该有和图片同等数量的 txt,且非空 txt 占比在 90% 以上。如果一批 txt 全是空文件,说明对应图片可能漏标或只有背景;不用急着删图,但要在训练时把它们剔除,否则空标注会被当成纯负样本反复学习,影响收敛方向。

3.3 数据集划分:train/val 别混车,固定随机种子

766 张图不能全拿去训练,至少要留 10%~20% 做验证集。划分原则是:同一场景、同一时间段连续抓拍的图不能同时出现在训练集和验证集,否则验证指标虚高,部署到新场景现原形。这份数据没有拍摄时间字段,我一般直接随机打乱后按 8:2 切,并固定随机种子保证可复现。

import os import random from shutil import copyfile random.seed(42) # 固定种子,下次重跑划分结果一致 image_exts = {".jpg", ".jpeg", ".png"} images = [f for f in os.listdir("bicycles4_images_xmls") if os.path.splitext(f)[1].lower() in image_exts] random.shuffle(images) val_ratio = 0.2 val_count = int(len(images) * val_ratio) train_files = images[val_count:] val_files = images[:val_count] os.makedirs("dataset/images/train", exist_ok=True) os.makedirs("dataset/images/val", exist_ok=True) os.makedirs("dataset/labels/train", exist_ok=True) os.makedirs("dataset/labels/val", exist_ok=True) for img in train_files: base = os.path.splitext(img)[0] copyfile(os.path.join("bicycles4_images_xmls", img), f"dataset/images/train/{img}") copyfile(os.path.join("bicycles4_labels", base + ".txt"), f"dataset/labels/train/{base}.txt") for img in val_files: base = os.path.splitext(img)[0] copyfile(os.path.join("bicycles4_images_xmls", img), f"dataset/images/val/{img}") copyfile(os.path.join("bicycles4_labels", base + ".txt"), f"dataset/labels/val/{base}.txt") print(f"train: {len(train_files)}, val: {len(val_files)}")

这段脚本按文件名后缀筛出图片,随机打乱后按比例切成两份,再用 copyfile 把图片和对应 txt 同步拷贝到 YOLOv5 约定的目录结构。注意 labels 和 images 的目录必须镜像对应,YOLOv5 就是靠相对路径找标注的,比如 images/train/xxx.jpg 会去 labels/train/xxx.txt 找标注;目录结构一旦不对称,train.py 会静默跳过大量样本,训练日志里却看不到任何报错。

切完手工抽查几个 val 集样本:是否有极端角度、逆光、遮挡。这几类图如果全落在训练集而验证集里全是正面大图,验证 mAP 高到 0.95 也别得意,换个摄像头角度一变精度立刻掉到 0.4 以下。如果手里有另一个场景的未标注视频,可以先拿来伪标注做测试集,比在自己的训练集上反复自测可信得多。

4. 训练自己的违停识别模型:yaml 配置、超参数与 100 轮实战

4.1 环境与模型选型:yolov5s 先跑通,别上来就上大模型

写训练命令之前先把环境准备好。clone 官方仓库后创建 conda 环境,这是最稳的一套流程:

git clone https://github.com/ultralytics/yolov5 cd yolov5 conda create -n yolov5 python=3.9 -y conda activate yolov5 pip install -r requirements.txt

逻辑说明:YOLOv5 依赖都在 requirements.txt 里,包含 torch、opencv-python、matplotlib 等;用 conda 环境可以避免污染系统 Python,以后换版本也好清理。参数说明:python 3.9 是 YOLOv5 兼容性最稳的版本,3.10/3.11 也能跑,但个别算子可能遇到编译问题,新手不建议上来就用最新版。

模型选型上,766 张图、单类别,yolov5s 是最佳起点。原因有两个:第一,s 模型推理速度快,在边缘设备上能跑到实时帧率;第二,小模型的过拟合风险低于大模型,数据量不大时,yolov5x 反而更容易在训练集上死记硬背,验证集 mAP 未必比 s 高。等以后把完整自行车 8000 张子集拿进来,再换 m 或 l 不迟。我在实际项目里 8G 显存跑 s 模型很轻松,batch-size 16 稳定不爆显存。

4.2 写 dataset yaml 并启动训练:参数逐一过一遍

进入训练前,先在数据目录建一个 bicycles.yaml,内容如下:

# 训练集和验证集的图片路径,建议写绝对路径,避免相对路径翻车 train: /home/yourname/dataset/images/train val: /home/yourname/dataset/images/val # 类别数:本子集只有自行车一个类别 nc: 1 # 类别名,要和转换 txt 里的 class id 一一对应 names: ['bicycle']

然后启动训练:

python train.py \ --data bicycles.yaml \ --weights yolov5s.pt \ --imgsz 640 \ --epochs 100 \ --batch-size 16 \ --hyp hyp.scratch-low.yaml \ --cache

参数逐个说:--data 指向刚才的 yaml;--weights 用官方预训练权重,迁移学习在小数据集上的效果比随机初始化好得多;--imgsz 640 是速度和精度的默认平衡点,如果体检时发现大量小目标,可以试试 960,但显存占用会翻倍;--epochs 100 配合早停,766 张图一般 60 轮左右就收敛;--batch-size 16 对 8G 显存跑 yolov5s 足够,显存紧张就降到 8;--hyp hyp.scratch-low.yaml 用低档数据增强,首次跑通建议先用它,后面再按 4.3 的配置调;--cache 把图片提前缓存到内存,省去每轮 IO 等待。

训练过程中盯三个指标:train/box_loss、val/box_loss 和 mAP@0.5。前两者持续走低说明模型在学;val_loss 在某个 epoch 后反弹而 train_loss 还在降,就是过拟合信号,应该提前停。还有一个常见现象是训练前 10 轮 mAP 几乎为 0,这正常,目标检测器得先把 anchor 和特征图对齐,一般 20 轮后才会明显抬头。

4.3 数据增强:mosaic 和 hsv 扰动对小数据集的救场

766 张图要学出鲁棒性,靠原生数据不够,数据增强是必要手段。hyp.scratch-low.yaml 里几个关键参数值得手动调:mosaic 多图拼接增强,概率设 1.0 可以让模型在每张训练图上同时看到多个场景,对小目标尤其友好;hsv_h/hsv_s/hsv_v 是色相、饱和度、明度扰动,自行车常见红、蓝、绿各种颜色,扰动区间大一点不容易过拟合;fliplr 水平翻转,对自行车这种左右基本对称的目标非常安全。

我调过两组典型配置,可以直接抄:

# 保守方案:原始场景变化不大,增强太猛容易学歪 mosaic: 1.0 fliplr: 0.5 hsv_h: 0.015 hsv_s: 0.7 hsv_v: 0.4 # 激进方案:角度单一,靠增强制造多样性 mosaic: 1.0 fliplr: 0.5 hsv_h: 0.02 hsv_s: 0.9 hsv_v: 0.6 degrees: 10.0 translate: 0.1

参数说明:degrees 表示随机旋转 ±10 度,translate 表示随机平移 10%,这两个值不宜开太大,否则标注框和目标的对应关系失真,模型会学到错误的位置分布。实际训练建议先跑保守方案,看 val_loss 表现再加戏;一次性把增强开满,loss 曲线会像过山车,你分不清是数据问题还是增强太猛。还有一个容易忽略的 scale 参数,它控制随机缩放范围,对目标尺度分布不均衡的数据集很有用,当前数据集如果偏大目标,就把 scale 下调到 0.5,人为制造更多小目标视角。

5. 避坑手册:标注错漏、类别混淆与小目标漏检的血泪经验

5.1 漏标错标:loss 不收敛时先查标注

现象:训练 30 轮后,train/box_loss 掉到 0.03 再也不动,val/box_loss 持续在 0.08 高位震荡,mAP 卡在 0.6 上不去。用 detect.py 跑验证集,发现很多车没被框出来,但被框到的位置又很准。

原因:这批数据整体质量不错,但人工标注难免漏标,尤其画面边缘、相互遮挡、远距离小目标三类情况。模型把你没标的那辆车当成背景,每次前向都学「这里没有目标」,梯度方向互相矛盾,loss 自然下不去。

解决:先用统计脚本找出「单张图只有 1 个框但画面里明显有 2 辆车」的图片,人工补标;更快的办法是把训练集预测结果里 confidence 低于 0.3 的漏检框导出来,用 LabelImg 或 X-anyLabeling 批量复核补框。从那以后我每跑一次新数据集,都强制先随机抽 50 张图人工核对,这半小时比后期调参值钱得多。

提示:不要直接删除空 txt 对应的图片,先确认是真的没有目标,还是漏标。空标注图在训练里是负样本,全删掉会让模型更容易把背景错检成自行车。

5.2 类别混淆:单类模型把电动车当成自行车

现象:训练完只含 bicycle 一个类别的模型,部署到园区摄像头下,共享电动车大量被识别成自行车,置信度高达 0.8。

原因:bicycles4 只是十类里的第五类,数据集中没有电动车样本,模型从没见过「电动车」这个负样本类,只学会了「两个轮子的交通工具」这个粗粒度特征。共享电动车和自行车在外形上高度相似,误检在所难免。

解决:先明确部署范围,如果监控区域会有电动车经过,这版单类模型只能用于「疑似违停预警」而不是精确分类。第二步把电动车、三轮车子集一并下载,yaml 的 nc 改成 3,names 改成['bicycle', 'e_bike', 'tricycle'],从第 3 章的 class_map 开始重新走一遍,补上对应映射。多类别训练后的误检率通常能降一个量级,因为模型被迫学习类间差异特征。

5.3 小目标漏检:远处车身的框全丢

现象:训练时 mAP@0.5 有 0.85,验证集上看起来不错;部署到 1080p 园区监控画面里,30 米外的自行车完全检不到,置信度全在 0.1 以下。

原因:数据里大部分标注框占画面 10% 以上,模型没见过足够多的小目标;imgsz 640 又把小目标进一步缩小。这是小数据集最常见的分布偏移,训练分布和部署分布不一致。

解决:三步走。第一步把 --imgsz 提到 960,注意 batch-size 要相应减半控制显存;第二步开启 mosaic 增强,用小图拼接制造更多「小目标」样本;第三步如果还漏,就要在部署端做切片推理,把大图切成 640x640 patch 分别检测,再把结果映射回原图坐标。切片方案最费算力但效果最直接,树莓派这类边缘设备建议先用前两步。

5.4 过拟合:766 张撑不起 100 轮

现象:train/box_loss 一路降到 0.01,val/box_loss 在 40 轮附近触底反弹,mAP@0.5 从 0.9 掉回 0.82,train 和 val 差距越拉越大。

原因:数据量只有 766 张,模型参数远超这个量级能支撑的信息量;训练轮数多,模型开始记住训练集里的具体场景背景、光照、角度,而不是自行车本身的泛化特征。

解决:epochs 设成 100 但要看 best.pt,YOLOv5 默认有早停机制,最终结果以验证集最优轮次为准。另外在 hyp 里打开一点 dropout,比如加dropout: 0.2,并给 translate 和 scale 留出空间,让增强制造更多变化。如果以上都不奏效,最直接的办法是下载完整自行车 8000 张子集,训练数据扩 10 倍,过拟合会自然缓解。

5.5 中文路径与中文文件名的坑

现象:Windows 上按前面脚本转换和训练,提示找不到图片,文件明明就在;或者训练时 OpenCV 读图失败,报 unable to open。

原因:山地自行车_395.jpg这类含中文的文件名,在 Windows 默认 GBK 编码下,YOLOv5 的 glob 处理容易乱码;OpenCV 的 imread 对中文路径支持也有问题,直接返回空矩阵。

解决:统一用脚本把文件名改成英文字母加数字,比如bicycle_395.jpg,XML 里的 filename 字段同步改。我一般在转换脚本前先跑一段批量改名脚本,中文转拼音或直接删掉中文部分,再进 3.2 的转换流程。这份数据文件名本身就是中文,新手机器上这个坑几乎必现,值得提前排掉。

6. 验证与部署:mAP、混淆矩阵与 NMS 后处理的落地细节

6.1 用 val.py 看门道,别只看 mAP 数字

python val.py \ --data bicycles.yaml \ --weights runs/train/exp/weights/best.pt \ --imgsz 640

输出里重点看 mAP@0.5、mAP@0.5:0.95 和每个类别的 Precision/Recall。mAP@0.5 对违停检测场景有直接参考意义,0.8 以上具备落地条件;mAP@0.5:0.95 是更严格的综合分。如果验证集特意留了难例,这两项会低一些,别慌,看 Precision 和 Recall 的差值:Recall 低说明漏检是主要矛盾,符合小目标漏检的预判;Precision 低则说明误检多,优先去查类别混淆。

6.2 混淆矩阵与 NMS 后处理,参数落地靠验证集试

混淆矩阵图能直接看出误检方向:本类召回不足,优先调低 conf_thres 提高召回;背景被框成自行车,优先调高 NMS 的 iou_thres,从默认 0.45 提到 0.5,把重叠框压得更狠。

实际部署时我用 detect.py 会盯两个参数:--conf-thres 0.3和--iou-thres 0.45。违停检测宁可多报一次让物业看一眼,也不愿意漏报被投诉,所以置信度我通常放 0.3 而不是默认 0.25,具体数值要拿验证集试出来,每个数据集的置信度分布不一样,这算最后一道后处理玄学。如要上树莓派这类边缘设备,可以导出 ONNX 再转 NCNN 或 TensorRT,INT8 量化后速度能再快一截。

我自己的习惯是:每次训练完,强制走一遍 val.py,再从验证集挑十张图看检测框贴合程度,而不是只看指标。标注质量、小目标分布、类别混淆这些问题,在框图上比指标暴露得更早。这个习惯帮我挡掉了无数次「指标好看、落地翻车」的尴尬,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询