尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

YOLO车辆计数数据集实战:3870张图像从标注到训练调参全指南

YOLO车辆计数数据集实战:3870张图像从标注到训练调参全指南 简介面向yolo系列算法实战的目标检测数据集覆盖卡车、公交车、汽车、自行车与拖拉机五类车辆适合车辆计数、交通流监控等视觉任务。数据集已划分训练集、验证集与测试集并附带data.yaml配置文件可直接接入yolov5、yolov7、yolov8、yolov9、yolov10、yolo11等主流框架进行模型训练与验证。包内同时提供yolo格式txt标签和voc格式xml标注分别存放于独立文件夹便于在不同检测管线间切换使用。资源包共2000个文件主要文件类型为xml标注文件整体大小约184.13MB目录结构按标注格式分类检索方便。目前已有57人学习下载适合有一定深度学习基础、希望快速获得规范车辆检测数据集的开发者与研究者。1. YOLO汽车计数数据集的真实用法3870张图能解决什么问题拿到一份「yolo算法-汽车计数数据集-3870张图像带标签-卡车-公交车-汽车-自行车-拖拉机.zip」很多人第一反应是赶紧解压、把图片丢进训练脚本跑一轮。实际上这份数据真正值钱的不是那 3870 张图像本身而是每张 jpg 后面跟着的标签文件——它把 car、truck、bus、bicycle、tractor 五类车辆按城市交通场景常见的分布做好了标注。对正在做停车场统计、高速断面流量监测、工地车辆识别的从业者来说它能省下一周左右的标注时间。适合看这篇笔记的人你已经能跑通 YOLOv5 或 YOLOv8 的官方 demo想用带标签的交通数据集做迁移学习或者你刚接手一个车辆计数项目急需一份能直接喂进训练的干净数据源。2. 拆开zip先摸清标注底细目录结构、标签格式和类别均衡2.1 解压后的目录结构与图像标签数量核对第一步不是训练而是把压缩包的目录结构摸清楚。这类数据集的常见组织方式有几种一种是images/与labels/平级另一种是train/、valid/、test/三个目录下分别挂images/和labels/。用下面这组命令解压并检查目录层级unzip yolo算法-汽车计数数据集-3870张图像带标签-卡车-公交车-汽车-自行车-拖拉机.zip -d car_count cd car_count find . -maxdepth 2 -type d | sort-d参数指定解压目标目录避免文件直接散落一地。跑完find之后再看一眼 train/valid/test 划分是否存在然后核对图像和标签的数量关系for d in train valid test; do if [ -d $d/images ] [ -d $d/labels ]; then echo $d: images$(ls $d/images | wc -l) labels$(ls $d/labels | wc -l) fi done如果 images 和 labels 的文件数量完全相等说明基本没有漏标数量不相等就要立即排查一定是某些图漏了 txt 或者多了孤立标签。再补一条空文件检查find . -name *.txt -size 0 | wc -l空 txt 文件在某些老旧训练脚本里会被忽略在 Ultralytics YOLO 的 Dataset 类里则会被当作负样本正常处理这里只统计数量。注意空标签本身不一定是错误路面上没有车的图像在车辆计数任务里同样有价值关键是你心里有数。2.2 用嗅探命令识别标签是 YOLO txt 还是 VOC xml压缩包里的标签到底长什么样不能靠猜。先抽样看一个标注文件的内容ls labels/train | head -5 head -3 labels/train/00001.txt如果每一行都是五个字段形如0 0.4867 0.3547 0.1324 0.1872第一列是类别 ID后面四列是归一化中心坐标和宽高那这就是 YOLO 训练脚本可以直接读取的 txt 格式。如果目录里全是.xml文件说明标签是 VOC 格式需要走坐标转换。还有少数数据集会以 COCO 的annotations.json形式组织这种要先判断结构我在下一章统一给转换方案。嗅探时眼睛要仔细重点看几件事一五个字段是否都以空格分隔而不是逗号二坐标值是否都在 0 到 1 之间如果出现大于 1 的数值说明这个标签没有做归一化训练前必须先处理否则 YOLO 的框会飞出图像边界三同一个小目标文件里的多个标注是否都是同一类别如果第一列全是 0基本可以确认这份标注把汽车单独排在了第一位后续映射时别配错。2.3 按类别统计样本tractor类是最大隐患车辆计数数据集的核心价值在于类别分布但分布不均也是最大的坑。写一个十几行的小脚本统计每个类别在标签里出现的总次数import glob from collections import Counter cnt Counter() for f in glob.glob(labels/**/*.txt, recursiveTrue): for line in open(f, encodingutf-8): parts line.strip().split() if len(parts) 1: cnt[int(parts[0])] 1 print(class_id count) for c, n in sorted(cnt.items()): print(c, n)如果你解压后看到的是 YOLO txt第一列就是类别 ID把 ID 对应到classes.txt或其他类别文件里去看名字。城市道路场景下大概率出现的情况是car 数量最多truck 和 bus 中等bicycle 和 tractor 偏少。尤其拖拉机在市区采集的素材里可能只有几十个实例这种数量级在训练时会被模型当作罕见类最终表现在混淆矩阵里就是 tractor 那一行几乎全是漏检。类别统计结论直接影响后续操作如果某个类少于 50 个实例要么给它做过采样要么把这类合并到相近类比如 tractor 并入 truck然后重新定义类别映射。反过来如果你的业务只关心机动车数量bicycle 可以直接作为背景类忽略在转换标签时过滤掉就行不需要硬训练一个自己用不上的类。3. 把标注统一成 YOLO 坐标格式转换脚本和类别映射套路3.1 类别映射表怎么定五类车辆如何重排 ID无论压缩包自带的是 VOC xml 还是已成型的 YOLO txt都建议先明确你自己的业务类别映射。一个常见做法是把五类车辆按表重排让 ID 更贴合模型输出的使用习惯原标签名目标 YOLO 类别 ID说明car0最常出现放前面有利于模型先拟合主要类别truck1卡车注意与bus的口径统一bus2公交车bicycle3自行车样本少时考虑过滤tractor4拖拉机样本极少时并入truck这张表的价值在于当模型推理结果直接在业务系统里使用时0往往被默认当作第一个类别把 car 放在 0 位能减少很多后续硬编码写错索引的尴尬。如果你的部署环境和这份映射不一致改表比改代码更安全后面所有脚本都以这张表为唯一真源。3.2 从 VOC xml 转到 YOLO txt 的转换脚本如果嗅探时发现标注是 XML需要写一个转换脚本核心工作是解析object节点里的bndbox再把绝对像素坐标归一化到 0 到 1 区间import os import xml.etree.ElementTree as ET # 源XML中的类别名 - 目标YOLO类别ID CLASS_MAP { car: 0, truck: 1, bus: 2, bicycle: 3, tractor: 4, } def convert_xml_to_yolo(xml_path, out_dir): tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in CLASS_MAP: continue cls_id CLASS_MAP[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) # YOLO需要的其实是中心点和宽高不是角点 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{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) out_name os.path.splitext(os.path.basename(xml_path))[0] .txt with open(os.path.join(out_dir, out_name), w, encodingutf-8) as f: f.write(\n.join(lines)) # 示例调用 convert_xml_to_yolo(annotations/00001.xml, labels/train)这段脚本最关键的是归一化用的分母必须来自对应图像的宽高不能从别的图像复制。如果 XML 里的size节点和真实图像尺寸不一致转换出的坐标就会错位。另一个细节是x_center、y_center保留 6 位小数就足够太多小数只会让 txt 文件变大对精度提升没有实际意义。3.3 已经是 YOLO txt 时只需要重映射类别 ID很多车辆计数数据包在发布时已经转成了 YOLO txt这时候不要重新走坐标转换你只需要做类别重映射。举个常见例子压缩包里自带的classes.txt可能是truck、car、bus、bicycle、tractor而你的模型要求car排在 0这时写一个小脚本把第一列替换掉import os # 旧类别ID - 新类别ID例如旧的0号truck映射到新类别1 OLD_TO_NEW {0: 1, 1: 0, 2: 2, 3: 3, 4: 4} def remap_label_file(src_dir, dst_dir): os.makedirs(dst_dir, exist_okTrue) for fname in os.listdir(src_dir): if not fname.endswith(.txt): continue with open(os.path.join(src_dir, fname), encodingutf-8) as f: lines f.readlines() new_lines [] for line in lines: parts line.strip().split() if len(parts) ! 5: continue # 过滤非法行尽量避免脏数据污染训练集 cls, cx, cy, bw, bh parts old_cls int(cls) if old_cls not in OLD_TO_NEW: continue # 不在映射表里的类别直接丢弃 new_lines.append(f{OLD_TO_NEW[old_cls]} {cx} {cy} {bw} {bh}) with open(os.path.join(dst_dir, fname), w, encodingutf-8) as f: f.write(\n.join(new_lines)) remap_label_file(origin_labels/train, labels/train)Category remap只动第一个字段后面四个坐标值原样保留。因为YOLO txt里坐标已经归一化重映射不会改变框的位置风险比坐标转换低得多。容易踩坑的是映射表写反举个例子如果你把0-1和1-0两个规则写在一个 dictionary 里Python 的 dict 不会按顺序覆盖目标值但最终结果等价于交换这一点只要在输出后抽检一个文件即可。3.4 按 85:15 划分训练与验证集如果压缩包没有自带划分或者自带划分里验证集和训练集场景重叠严重建议自己重新划分。这里我用固定随机种子确保后续多次实验可以复现import os import random import shutil IMAGES_DIR images/all LABELS_DIR labels/all TRAIN_RATIO 0.85 imgs [f for f in os.listdir(IMAGES_DIR) if f.lower().endswith((.jpg, .jpeg, .png))] random.Random(42).shuffle(imgs) split int(len(imgs) * TRAIN_RATIO) train_imgs imgs[:split] val_imgs imgs[split:] for phase, img_list in [(train, train_imgs), (valid, val_imgs)]: os.makedirs(fimages/{phase}, exist_okTrue) os.makedirs(flabels/{phase}, exist_okTrue) for name in img_list: src_img os.path.join(IMAGES_DIR, name) src_lab os.path.join(LABELS_DIR, os.path.splitext(name)[0] .txt) shutil.copy(src_img, fimages/{phase}/{name}) if os.path.exists(src_lab): shutil.copy(src_lab, flabels/{phase}/{os.path.basename(src_lab)}) # 没有标签的图在目标是纯检测时建议丢弃否则会作为负样本random.Random(42)里的42是固定种子换一台机器跑结果也是同一份划分。85:15 是车辆检测里的常见比值3870 张图按这个比例训练集约 3290 张、验证集约 580 张够用。如果后续发现验证集的 mAP 波动大可以把 ratio 改成 90:10 重跑一次。4. 用这份数据训练 YOLO 做车辆计数检测命令和调参4.1 YOLOv5 还是 YOLOv8显存和数据量决定选择车辆计数数据集规模不算大3870 张图属于中小型数据集这个量级不需要上大模型。YOLOv8 是当前更省心的选择因为它的 CLI 封装了训练、验证、导出还内置了跟踪功能正好和车辆计数的目标贴合。YOLOv5 的价值在于生态成熟、资料多、对旧显卡支持更好但它的 API 设计和后续版本相比稍显繁琐。这里给一个简单的选型表对比项YOLOv5YOLOv8最小可用显存4GB 可跑 v5s4GB 可跑 v8n跟踪支持需要单独接 ByteTrack内置 track 模式训练命令复杂度较高较低自定义数据格式同样吃 YOLO txt同样吃 YOLO txt我的建议是如果你的部署环境是 Jetson 之类的小设备YOLOv8n 或 YOLOv5s 都行如果要做视频车辆计数直接选 YOLOv8后面省的代码量不是一个量级。预训练模型权重从这里的主要考虑点出发去处理选择yolov8s.pt作为起点它比yolov8n.pt精度高但显存占用依旧在 6GB 范围内。4.2 写 data.yaml 并指定预训练模型权重YOLO 训练前要准备一个数据配置文件路径可以用相对路径也可以写绝对路径我建议在项目根目录建car_count.yaml# 训练集和验证集的图片目录路径 train: ./data/car_count/images/train val: ./data/car_count/images/valid # 类别数量与类别名顺序必须和标签txt里的ID一致 nc: 5 names: 0: car 1: truck 2: bus 3: bicycle 4: tractor注意train和val指向的是 images 目录而不是 labels 目录YOLO 会自动在相同路径下找同名 txt 标签文件。如果标签还存放在labels/train不要写在 yaml 里程序会按图片同级目录的约定查找这是新手最容易配错的地方。nc必须和 labels 里的类别 ID 最大值加一相等否则训练脚本会在数据加载阶段直接报错。指定预训练模型有两条路用modelyolov8s.pt表示加载 COCO 预训练权重后迁移学习适合数据量不大、和 COCO 类别有重叠的场景用modelyolov8s.yaml表示完全从头训练没有任何先验知识。3870 张图和 COCO 里的车辆类别有一定重合选yolov8s.pt可以让收敛速度明显加快我一般不会碰从头训练的选项除非你确认迁移带来的类别偏好影响很严重。4.3 训练命令里必调的三个参数imgsz、batch 和 patience核心训练命令其实很短yolo taskdetect modetrain \ modelyolov8s.pt \ datacar_count.yaml \ epochs50 \ imgsz640 \ batch8 \ device0 \ patience15 \ plotsTrue逐项说明关键点。imgsz640是训练时缩放的图像尺寸车辆计数数据集的原始图像往往在 1080p 以上但训练时没有必要保留原分辨率640 是精度和显存的平衡点如果你的显卡是 8GB 以下降到 512 也能跑代价是远处小目标的检出率下降。batch8是最保守的起步值8GB 显存通常能跑到 16先把 8 跑通再往上调。patience15表示验证集 mAP 连续 15 个 epoch 不提升就提前停止防止过拟合。还有两个容易忽略的点。plotsTrue会输出混淆矩阵和训练曲线这些图在跑完后直接决定你是否要调整数据后面排查时会反复用。Ultralytics 默认开启了 mosaic 数据增强它在车辆计数场景下对小目标是有利的但如果发现训练后期 loss 震荡可以加mosaic0.5降低增强概率实验。关于epochs50 轮对这个数据量足够了再多容易让模型记住验证集的背景而不是真正学会检测车辆。4.4 用验证集看 confusion matrix 再决定是否回炉训练结束后先别急着接视频做计数跑一遍验证和推理确认模型的检测能力符合预期yolo taskdetect modeval modelruns/detect/train/weights/best.pt datacar_count.yamlyolo taskdetect modepredict modelruns/detect/train/weights/best.pt \ source./test_imgs \ conf0.3 \ save_txtTrue \ save_confTrue验证命令会输出每个类别的 precision、recall 和 mAP50。这里的 mAP50 对车辆计数任务来说比 mAP50-95 更有参考意义因为计数场景不要求框和真实框完全重合只需要中心位置稳定。推理命令里的conf0.3是置信度阈值计数场景建议别低于 0.25否则会把路牌、树影都算成车。打开runs/detect/train/confusion_matrix.png重点看两件事一是 tractor 这一行的召回率是否出现断层二是 car 和 truck 之间是否大量互串。出现问题时不要急着调超参数大概率是数据层面的问题回到第 5 章的排查清单去核对。5. 3870 张车辆数据集避坑五条翻车记录和排查思路5.1 归一化坐标越界训练 loss 下不去现象训练前几个 epoch 的 box_loss 下降正常到了第十几个 epoch 开始剧烈振荡loss 曲线像锯齿。打开验证集的预测结果看框的位置明显不对而不是差一点。原因标签里的归一化坐标出现了小于 0 或大于 1 的数值。这种现象通常在 VOC 转 YOLO 时发生XML 里某个xmax大于了图像宽度转换脚本又没有做截断于是中心点坐标跑出了图像。解决写一个扫描脚本检查所有标签的坐标是否在合法区间内。最省事的做法是在转换函数里对最终值做一次 clamp但如果是已经生成的 txt就直接扫描并修正import glob for f in glob.glob(labels/**/*.txt, recursiveTrue): lines open(f, encodingutf-8).readlines() new_lines [] for line in lines: parts line.strip().split() if len(parts) 5: _, cx, cy, bw, bh parts cx max(0.0, min(1.0, float(cx))) cy max(0.0, min(1.0, float(cy))) new_lines.append(f{parts[0]} {cx:.6f} {cy:.6f} {bw} {bh}) if new_lines ! lines: open(f, w, encodingutf-8).writelines(new_lines)5.2 tractor 类过少召回率几乎为零现象训练完成后precision 整体还行但混淆矩阵里 tractor 那行的召回率接近 0也就是说模型几乎不把任何框分到拖拉机这个类。原因类别不平衡。城市道路数据里拖拉机出现的场景太少可能整份数据集只有几十个实例模型学到的主要是汽车和卡车特征。解决先回到 2.3 的统计结果确认数量。如果确实低于 50有两个方案可选。方案一是过采样把包含拖拉机目标的标签文件复制几份混进训练集让模型在每个 epoch 多看到几次方案二更实际把 tractor 并入 truck因为工程上拖车的形状和卡车高度接近合并后等于增加了一个更难分但样本更多的类。5.3 连续帧近似重复验证集 mAP 虚高现象训练集 mAP50 和验证集 mAP50 都很高比如 0.96 对 0.94但把模型接到真实监控视频里检测结果却抖得厉害同一辆车一会儿识别成 car、一会儿识别成 truck。原因数据包里很可能包含从同一段连续视频抽取出来的帧相邻帧画面高度相似。如果这些连续帧同时出现在训练集和验证集验证集就失去了独立性mAP 高是假的。解决对图片做 perceptual hash 去重按相似度把重复帧归到同一个集合再按集合划分训练验证集。我这里用的实用做法是先跑一个轻量脚本查相似度再用 3.4 的划分脚本按视频片段维度拆分而不是按单张图随机拆。影响最大的其实是车辆计数的最终效果如果检测器只记住了某个路口的固定背景换一个路口就会原形毕露。5.4 卡车和公交车互相误判标注口径不一致现象混淆矩阵里 car 和 truck 边界尚可但 truck 和 bus 互相串得很厉害误判的方向大多是 truck 被算成 bus。原因标注人员对“带箱货车”和“公交车”的边界理解不一致。一部分标注把车身长、侧面带窗的车标成 bus另一部分把相同外观的标成 truck。这类数据问题靠调模型解决不了只能靠统一数据口径。解决查看数据里的图片样本挑出被标注为 bus 但实际是封闭厢式卡车的样本统一规则以乘坐用途为 bus以货运用途为 truck车身造型次要。如果压缩包里的标签无法轻易追溯修改就放弃精确区分在业务计数里把这两类合并成“大型机动车”来输出规避标注噪声。5.5 YOLO 版本问题AttributeError 或 OSError 的排查路径现象运行yolo train直接报AttributeError: module ultralytics has no attribute detect或者在加载数据时报OSError: [Errno 22] Invalid argument训练中断。原因大概率是 Python 环境里的 ultralytics 版本和缓存不匹配或者项目路径里有中文和空格。压缩包的目录名本身就有多个中文字符如果直接解压到路径含中文的项目根目录Windows 环境下经常触发文件路径解析问题。解决先锁版本再跑训练不要用最新的 dev 版python -m venv yolo_env source yolo_env/bin/activate pip install ultralytics8.2.0务必把整个项目复制到纯英文路径下例如/home/yourname/car_count_project/。这个坑足够隐蔽因为训练脚本本身不涉及中文但标签读取时 PIL 和 numpy 在 Windows 下对路径编码的处理会让某些图像无法加载。这里没有捷径可走路径规范和版本锁定是唯一靠谱的后悔药。6. 从框检测到路口计数接跟踪器加虚拟计数线6.1 用 YOLO 的 track 模式对视频持续计数搞定了检测器车辆计数才算真正开始。单帧检测直接数框数会出现严重的重复计数同一辆车在 30 帧视频里被数 30 次。常见做法是接一个跟踪器给每个目标分配唯一 track_id再按 track_id 去重。YOLOv8 的 CLI 已经内置了跟踪能力起一条命令就能看到持续稳定的轨迹yolo track modelruns/detect/train/weights/best.pt sourcetraffic.mp4 \ imgsz640 conf0.3 persistTrue \ trackerbytetrack.yaml \ line_width2这段命令的关键在persistTrue它告诉模型在相邻帧之间保留跟踪状态否则每帧都是全新的检测结果和没接跟踪器没有区别。trackerbytetrack.yaml指定跟踪算法ByteTrack 对低置信度框的处理更好在车辆遮挡场景下比单纯用 IoU 的 SORT 稳。6.2 用一条水平线统计双向车道车流接着把 track_id 和车辆中心点的位置变化组合起来就能实现真正的断面计数。假设我要统计视频画面中一条水平线上下穿过的车辆数逻辑非常直接def update_count(track_id, prev_pt, cur_pt, line_y, counter): # prev_pt 和 cur_pt 分别是上一帧和当前帧的车辆中心点坐标 if prev_pt[1] line_y cur_pt[1]: counter[track_id] (down, True) # 从上往下穿过 elif prev_pt[1] line_y cur_pt[1]: counter[track_id] (up, True) # 从下往上穿过 # 遍历每一帧跟踪结果时调用 update_count判断穿过线的核心是“上一帧在线一侧、当前帧在线另一侧”单纯看当前帧的位置无法区分方向。counter 里保存每个 track_id 的穿过状态就避免了同一辆车在一条线上来回抖动时被重复计数。这里的line_y要选在画面中车辆轮廓清晰、遮挡较少的位置不要贴着画面边缘放。我现在的习惯是拿到任何新的车辆检测数据先不看论文不看榜单直接训练一个 10 轮左右的影子模型跑混淆矩阵把所有类别串扰和数据异常都暴露出来再去处理。第 5 章那些坑基本都是在影子模型阶段发现的省下的时间远比训练本身多。希望帮到你。本文还有配套的精品资源点击获取
返回列表