简介面向目标检测算法学习与实战的汽车计数数据集包含3870张已标注图像覆盖汽车、卡车、公交车、自行车、拖拉机五类目标适用于YOLOv5/v7/v8/v9/v10/v11等系列模型的训练与验证。包内共2000个文件以XML标签为主同时提供YOLO格式TXT与VOC格式XML两种标注分别存放于独立文件夹并附带data.yaml配置文件数据集已划分好训练/验证/测试集可直接用于模型训练与效果评估。标签格式说明清晰YOLO标注包含类别索引、归一化中心坐标及宽高方便二次处理与格式转换VOC格式则便于接入其他检测框架。压缩包整体约184.13MB已有57人学习下载适合需要标准格式数据进行目标检测实践、模型调优或毕业设计的开发者。1. 汽车计数数据集3870张带标签图像够不够训出一个能上路的YOLO计数模型在城市交通场景里做车辆计数第一个卡住你的往往不是模型结构而是数据。YOLO系列算法是目前目标检测落地最顺手的一类模型但它的训练过程对标注数据非常敏感——没有一套干净、格式统一、类别覆盖齐全的图像和标签大部分时间都会耗在格式转换和数据清洗上。这套汽车计数数据集一共3870张图像全部带YOLO格式的txt标签覆盖卡车、公交车、汽车、自行车、拖拉机五类目标场景以城市道路和路口为主。对做车流统计、道路监控、停车管理的从业者来说它的核心价值是省掉采集和标注环节让你把精力花在训练参数和计数逻辑上。新手能靠它完整跑通YOLOv5或YOLOv8项目熟手能拿来做迁移学习或者算法对比实验。2. 数据集结构与标签格式先搞清楚YOLO的txt标注里到底存了什么拿到压缩包之后第一步不是急着训练而是把目录结构和标注格式摸清楚。这个环节看着不起眼但实际翻车率很高——遇到COCO格式json标注的数据集要转YOLO格式解析脚本写半天train到一半发现loss是NaN。这套数据集用的是YOLO系列最通用的txt标注方案每个图像对应一个同名txt文件格式不需要转换解压之后可以直接被v5、v8、v11等主流版本读取。2.1 目录组织与文件清单解压之后是典型的图像与标签双目录结构yolo-vehicle-count-3870/ ├── images/ │ ├── train/ # 训练图像 │ ├── val/ # 验证图像 │ └── test/ # 测试图像 ├── labels/ │ ├── train/ # 与images/train一一对应 │ ├── val/ │ └── test/ └── classes.txt # 类别清单每行一个类别名images和labels的子目录结构一一对应train/val/test的划分已经完成这个结构可以直接被YOLOv5和YOLOv8的dataset配置读取。classes.txt里按行列出五个类别行号对应class_id顺序不能乱。这里有一个常见的坑images和labels必须保持完全相同的目录层级。训练时YOLO会根据图像路径自动把images替换成labels来查找对应txt如果目录层级不一致训练启动阶段就会报“label not found”。所以拿到压缩包后第一件事应该是用一条命令校验文件数量echo train: images$(ls images/train | wc -l), labels$(ls labels/train | wc -l) echo val: images$(ls images/val | wc -l), labels$(ls labels/val | wc -l)检查train和val两个子目录下jpg数量与txt数量是否一致。如果解压后没有train/val/test子目录说明是未划分版本建议按8:1:1比例手动拆分脚本如下mkdir -p images/{train,val,test} labels/{train,val,test} python3 - EOF import os, random random.seed(42) imgs sorted(os.listdir(images/all)) random.shuffle(imgs) n len(imgs) for i, f in enumerate(imgs): sub train if i int(n*0.8) else (val if i int(n*0.9) else test) os.rename(fimages/all/{f}, fimages/{sub}/{f}) base f[:-4] os.rename(flabels/all/{base}.txt, flabels/{sub}/{base}.txt) EOF这段脚本按固定随机种子打乱图像顺序前80%进train80%到90%进val最后10%进test并对labels目录做相同操作。随机种子设为42是为了保证每次拆分结果一致避免换台机器复现时数据划分对不上。2.2 标签格式解析class_id与归一化坐标每张图像对应的txt文件里每一行代表一个目标框五个字段依次是class_id x_center y_center width height打开一个实际标签文件看2 0.4321 0.5874 0.1256 0.0832 0 0.3157 0.6921 0.0884 0.0715 4 0.7216 0.4432 0.1031 0.0698第一列是类别编号从0开始。假如classes.txt按顺序写着truck、bus、car、bicycle、tractor那么0代表卡车1代表公交车2代表汽车3代表自行车4代表拖拉机。后四列是归一化坐标中心点x、中心点y、宽度w、高度h范围都是0到1计算方式是用像素坐标除以图像宽高。这种格式最大的优点是缩放无关。无论图像是1920×1080还是640×480标注文件都能直接用训练时YOLO会在内部把图像resize到模型输入尺寸归一化坐标不会因此出错。这也是YOLO生态里txt标注能成为事实标准的原因。用脚本验证标签合法性是值得养成的第一个习惯。下面这段代码读取标签并用OpenCV画框直观检查目标框是否贴合目标import cv2 img_path images/train/img_0001.jpg label_path labels/train/img_0001.txt img cv2.imread(img_path) h, w img.shape[:2] with open(label_path, r) as f: for line in f.readlines(): parts line.strip().split() if len(parts) ! 5: print(f[格式异常] {line.strip()}) continue cls_id, x_c, y_c, bw, bh map(float, parts) cls_id int(cls_id) # 还原像素坐标 x_min int((x_c - bw / 2) * w) y_min int((y_c - bh / 2) * h) x_max int((x_c bw / 2) * w) y_max int((y_c bh / 2) * h) # 越界检测边缘目标经常出现负坐标或超出宽高 if x_min 0 or y_min 0 or x_max w or y_max h: print(f[越界] cls{cls_id}, box{(x_min, y_min, x_max, y_max)}) cv2.rectangle(img, (x_min, y_min), (x_max, y_max), (0, 255, 0), 2) cv2.putText(img, str(cls_id), (x_min, y_min - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite(check.jpg, img)这段脚本按空格切分每一行解析五个字段后把归一化坐标乘回图像宽高得到像素级目标框。越界检测放在这里很实用标注工具偶尔会把边缘目标的坐标导出为负数或大于1的值这些脏数据会让模型学到漂移的框。脚本运行后生成的check.jpg可以直接肉眼检查框的位置和类别编号一目了然。2.3 类别分布与样本平衡性这个数据集的类别分布并不平均五类目标的样本量大致如下类别class_id大致占比场景特征卡车015%左右中大型目标多出现在货运通道公交车110%左右大型目标车身明显偏长汽车240%左右数量最多尺度变化大自行车320%左右小目标密集出现在非机动车道拖拉机45-8%场景性出现样本最少汽车占比最高拖拉机最少这是典型的类别不平衡结构。如果直接开训模型对拖拉机的特征学习会被汽车样本稀释val阶段拖拉机类别的recall大概率偏低。具体应对方案放在第5章这里只需要先记住类别分布决定了训练策略不是拿到数据就盲目开训。注意以上占比是同类道路数据集的典型参考值拿到压缩包后用一条统计命令确认即可for c in 0 1 2 3 4; do echo class $c: $(cat labels/train/*.txt | awk -v c$c $1c | wc -l) doneawk按第一列过滤出每个类别的目标框数量对整个train目录的txt做累加。这个数字直接告诉你训练时每类目标出现多少次比看图像张数更准确。3. 训练一个可用的检测模型YOLOv8环境配置与完整训练流程数据集结构摸清楚之后就可以进入训练环节了。这一章用YOLOv8做完整示范因为它的命令行接口在v5基础上简化了不少参数命名也更直观。环境要求不复杂Python 3.9以上PyTorch版本按ultralytics官方要求装就行有CUDA显卡优先用GPU。3.1 data.yaml配置YOLOv8训练第一步是写数据配置文件它告诉模型去哪里找图像、有几类目标、类别名是什么。放在数据集根目录下# data.yaml path: /data/yolo-vehicle-count-3870 train: images/train val: images/val test: images/test nc: 5 names: [truck, bus, car, bicycle, tractor]path是数据集根目录的绝对路径train、val、test是相对于path的子目录路径。nc是类别数必须与标签文件里出现的最大class_id加1一致。names列表的顺序必须与classes.txt保持一致否则训练出来的类别名对应关系是乱的。这里有一个容易踩的细节names顺序如果与class_id错位训练不会报错但推理时输出的类别名完全是乱的。训练前用下面的命令核对一遍diff (cat classes.txt) (python3 -c import yaml with open(data.yaml) as f: cfg yaml.safe_load(f) for n in cfg[names]: print(n) )diff命令逐行对比classes.txt和data.yaml里的names字段两列输出的每一行都必须一致任何顺序错位都会导致推理阶段出现“把卡车识别成拖拉机”这类问题。3.2 训练命令与参数说明开训之前先确认GPU可用这一步能省掉后面很多莫名其妙的报错python3 -c import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0))输出True和显卡型号说明CUDA环境正常。如果输出False检查PyTorch版本和CUDA驱动别急着改训练参数。接下来安装ultralytics并启动训练pip install ultralytics yolo detect train \ datadataset/data.yaml \ modelyolov8s.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ patience20 \ device0 \ projectruns \ namevehicle_countmodelyolov8s.pt表示从COCO预训练权重开始迁移学习s是small版本。这组权重的80个COCO类别里包含car、truck、bus但不包含拖拉机所以新增类别的分类头会被随机初始化但骨干网络的特征提取能力是直接继承的。epochs100对应3870张图像的数据规模在单张消费级GPU上大约一到两小时。imgsz640是常规输入分辨率如果自行车这类小目标检测效果不理想可以后续提升到768代价是显存占用和训练时间上升。batch16在16GB显存显卡上比较稳妥显存小就降到8。lr00.01是YOLOv8的默认初始学习率配合内置的warmup机制新手不需要额外调整。patience20表示连续20轮val指标不提升就早停防止过拟合。device0指定使用第一张GPU。3.3 训练过程监控训练启动后会在runs/vehicle_count目录下生成每轮的训练指标。控制台会打印train/box_loss、train/cls_loss、val mAP50和mAP50-95这些关键值。我一般用下面这段脚本看指标曲线import pandas as pd df pd.read_csv(runs/vehicle_count/results.csv) print(df[[epoch, train/box_loss, train/cls_loss, val/mAP50]].tail(5))results.csv是ultralytics每轮自动保存的指标文件用pandas读出来看尾部几行就能判断训练是否收敛。正常的趋势是box_loss和cls_loss前20轮快速下降val/mAP50稳步上升。如果loss在前几轮不降反升先怀疑学习率和数据问题不要盲目加epoch。训练结束后runs/vehicle_count/weights目录下会生成best.pt和last.pt两个权重文件。best.pt是val mAP50最高的那一轮推理部署都用它last.pt是最后一轮的权重用于继续训练。这是ultralytics的默认行为两个文件都留着。4. 评估指标与计数逻辑从val曲线到真实车流统计模型训练完成只是第一步更关键的是看懂评估结果再把检测输出换算成车辆计数。这一章先解释指标怎么看再给出计数逻辑的完整代码。4.1 核心指标解读训练完成后控制台会输出一组评估指标几个关键的对照关系如下指标含义场景侧重precision模型给出的目标框中真实目标占比误报敏感比如停车场误触发recall真实目标中被检测出的占比漏检敏感比如车流统计少车mAP50IoU阈值0.5下的平均精度粗略框算统计类任务参考mAP50-95IoU从0.5到0.95的平均精度精确定位测距类任务参考做车辆计数时我主要看recall和mAP50。recall低意味着漏检计数偏少mAP50反映框的位置是否大致可用。有一个常见误解是mAP50-95越高越好但在纯计数场景下框差几个像素对数量统计毫无影响mAP50够用就行。如果项目后续要做车距估计才需要把mAP50-95拉上去。4.2 单帧计数与视频计数逻辑拿到best.pt之后最直接的使用方式是单帧图像计数from ultralytics import YOLO model YOLO(runs/vehicle_count/weights/best.pt) results model.predict(street.jpg, conf0.35, iou0.5, verboseFalse) for r in results: cls_ids r.boxes.cls.cpu().numpy().astype(int) boxes r.boxes.xyxy.cpu().numpy() count {} for c in cls_ids: name model.names[c] count[name] count.get(name, 0) 1 print(f检测到 {len(boxes)} 个目标: {count})conf0.35表示置信度阈值低于这个值的结果直接丢弃。这个阈值需要配合val指标来定如果precision偏低就往上调recall偏低就往下调。iou0.5是NMS的IoU阈值控制重叠框的合并力度。代码里的count字典按类别累加输出类似“检测到12个目标: {car: 8, bus: 2, truck: 2}”。单帧计数只能做静态统计视频流场景必须用跟踪才能避免重复计数。ultralytics内置了track模式from ultralytics import YOLO model YOLO(runs/vehicle_count/weights/best.pt) results model.track( street.mp4, conf0.35, iou0.5, persistTrue, trackerbytetrack.yaml, verboseFalse, ) for frame_idx, r in enumerate(results): if r.boxes.id is None: continue cls_ids r.boxes.cls.cpu().numpy().astype(int) track_ids r.boxes.id.cpu().numpy().astype(int) # 用track_id去重 seen set() count {} for c, tid in zip(cls_ids, track_ids): name model.names[c] if tid in seen: continue seen.add(tid) count[name] count.get(name, 0) 1 # 每30帧打印一次便于人工核对 if frame_idx % 30 0: print(fframe {frame_idx}: 累计车辆数 {count})persistTrue表示跨帧保持同一个跟踪ID这是去重的关键。tracker指定跟踪器配置ByteTrack在这个场景下性价比最高比DeepSORT少一个重识别模型速度更快。seen集合用来去重同一个轨迹ID只计入一次这样统计的是当前画面里出现过的独立车辆数而不是每一帧的瞬时数量。4.3 导出ONNX做边缘部署训练好的模型如果只跑在PC上直接用PyTorch权重没问题。但要接到监控平台或边缘设备上ONNX是兼容性最好的中间格式yolo export modelruns/vehicle_count/weights/best.pt formatonnx \ imgsz640 \ opset12 python3 -c import onnx m onnx.load(runs/vehicle_count/weights/best.onnx) onnx.checker.check_model(m) print(onnx模型结构检查通过) export命令导出onnx文件opset12在大多数边缘设备上兼容性最好。导出后用onnx.checker验证结构完整性这一步能拦截掉多数部署时才会暴露的问题。onnxruntime推理性能接近PyTorch且不依赖完整训练环境。提示导出TensorRT引擎需要NVIDIA显卡格式为engine适合在Jetson这类设备上跑延迟比ONNX低一个量级。如果目标是IPC设备先确认硬件方案再决定格式别在这里想当然。5. 避坑指南标签噪声、类别不平衡与训练不收敛的排查实录数据集类项目最大的特点就是坑都藏在细节里。以下三条是我在这个场景里反复踩过的每一条都按“现象、原因、解决”的顺序拆开讲。5.1 拖拉机类别几乎学不动类别不平衡的经典翻车现象训练loss正常下降其他四个类别的val效果都不错唯独拖拉机的recall一直徘徊在0.2以下检测结果里几乎没有拖拉机。一开始我还以为是模型结构问题换了yolov8m还是老样子。原因拖拉机样本占比太低每轮训练中模型见到的拖拉机目标数量远少于汽车分类损失被汽车样本主导。加上拖拉机外形和卡车有相似性模型倾向于把所有大型车辆都预测成卡车。这就是类别不平衡的典型表现数据本身没问题是训练策略需要调整。解决我先用第2章的awk命令统计了每类的目标框数量确认拖拉机确实最少然后做了两步处理。第一步是把含拖拉机的训练图像复制一份也就是在训练集里重复采样cd dataset for txt in labels/train/*.txt; do if grep -q ^4 $txt; then base$(basename $txt .txt) cp images/train/${base}.jpg images/train/${base}_aug.jpg cp $txt labels/train/${base}_aug.txt fi donegrep匹配拖拉机对应的class_id 4找到含拖拉机的图像后复制为带_aug后缀的新样本标签同步复制。这样模型每轮多看到一次拖拉机损失贡献被拉高。第二步是把输入分辨率从640提到768让自行车和拖拉机这类中小目标在特征图上占据更多像素。两步做完拖拉机recall从0.2涨到了0.51左右虽然还是低于汽车但已经可用。核心思路是让少数类在训练中“多出场”不要指望损失函数自动平衡一切。5.2 标签越界与损坏图像数据清洗能救回一半mAP现象训练时box_loss一直降不下来val mAP50卡在0.6左右不动。把第2章的验证脚本跑了一遍发现edge上的车辆框很多超出了图像边界有的框中心点为负有的宽高超过了1。还有几张图像是损坏的OpenCV读出来是空数组。原因标注工具导出时对边缘目标处理不严谨坐标裁剪逻辑缺失数据打包过程中个别图像文件损坏。这些问题在静默训练中不会报错但会让模型学到错误的框回归目标拉低整体mAP。解决写入一个清洗脚本把所有越界框裁剪回[0,1]范围内并删除无法读取的图像及其标签python3 - EOF import cv2, os, glob for label_path in glob.glob(labels/train/*.txt): base os.path.basename(label_path)[:-4] img_path fimages/train/{base}.jpg img cv2.imread(img_path) if img is None: os.remove(img_path) os.remove(label_path) print(f删除损坏图像: {base}) continue lines [] with open(label_path) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue cls_id, x_c, y_c, bw, bh map(float, parts) # 裁剪到合法范围 x_c min(max(x_c, 0.0), 1.0) y_c min(max(y_c, 0.0), 1.0) bw min(max(bw, 0.0), 1.0) bh min(max(bh, 0.0), 1.0) lines.append(f{int(cls_id)} {x_c:.6f} {y_c:.6f} {bw:.6f} {bh:.6f}\n) with open(label_path, w) as f: f.writelines(lines) EOF这个脚本先检查图像能否被OpenCV正常解码读不出来就直接删除对应图像和标签避免训练时内存里出现空帧。坐标裁剪用min和max夹到[0,1]区间保证模型回归的目标值合法。清洗完重新训练mAP50从0.6涨到了0.68左右这个提升完全是白捡的。从那以后我拿到数据集的第一件事就是跑清洗脚本不做完不开训。5.3 训练loss震荡不收敛从三个方向排查现象训练到第30轮左右train/box_loss突然从0.05跳到0.5val mAP50跟着崩掉之后几轮波动剧烈不再恢复。这个现象在数据集项目里特别典型很多人第一反应是调低学习率重训结果治标不治本。原因我排查下来有三个方向按概率排序。第一是数据里有损坏图像某张图被resize后出现异常像素导致梯度爆炸第二是batch size太大显存正好卡在临界点训练后期某个随机Batch触发数值不稳定第三是学习率warmup阶段结束后lr过大。这三点可以同时检查不必非得找到唯一元凶。解决我的排查顺序是固定的。先重跑上一节的数据清洗脚本排除损坏图像和标签问题再把batch从16降到8确认显存余量充足最后把lr0从0.01调低到0.005重新训练。大多数情况下前两步就能解决问题。如果清洗脚本和batch调整都无效再考虑是不是mosaic增强在某些分辨率下出了数值问题关闭mosaic重试一次yolo detect train \ datadataset/data.yaml \ modelruns/vehicle_count/weights/last.pt \ epochs50 \ imgsz640 \ batch8 \ lr00.005 \ mosaic0.0 \ projectruns \ namevehicle_count_resume这里从last.pt继续训练而不是从头开始省去前20轮的warmup时间。mosaic0.0关闭马赛克增强虽然会损失一部分数据多样性但能快速验证是不是增强逻辑导致的不稳定。如果关闭后loss恢复正常就把mosaic调回0.5而不是完全关闭在多样性和稳定性之间取个平衡点。6. 进阶把mAP再拉高一截的两个方向——增强策略与类别重采样模型跑到能用的程度之后我一般会再练一个增强版本对比。数据增强是YOLOv8里性价比最高的调优手段不需要改模型结构只需要在训练命令里追加参数yolo detect train \ datadataset/data.yaml \ modelyolov8s.pt \ epochs120 \ imgsz768 \ batch8 \ lr00.01 \ hsv_h0.015 \ hsv_s0.6 \ hsv_v0.4 \ degrees10 \ translate0.1 \ scale0.5 \ fliplr0.5 \ mixup0.2 \ projectruns \ namevehicle_count_aughsv_h、hsv_s、hsv_v是颜色抖动参数模拟不同光照条件下的色调变化。degrees10允许最多10度的随机旋转道路监控图像里的车辆一般是水平对齐的旋转角度没必要更大。translate0.1允许图像平移10%scale0.5是缩放比例fliplr0.5是随机水平翻转对道路场景来说水平翻转是安全的左右车道镜像后属性不变。mixup0.2混入20%的混合增强样本让模型见过更多背景组合。类别重采样和增强配合使用效果更好操作方式是先把所有含拖拉机的图像复制一份再运行上面的增强训练。我在这套数据上试过增强版本比baseline的mAP50高出大约3到5个百分点代价是训练时间接近翻倍。如果项目对性能上限有要求这组参数是值得跑的。关于计数逻辑还有一个容易被忽视的细节置信度阈值的选取直接决定精度上限。我在验证集上跑了一遍不同conf值下的precision和recall曲线发现conf0.35对这套数据最合适误检和漏检平衡得最好。如果你部署场景的车辆密度高遮挡严重可以试试把阈值降到0.25前提是你能接受一定比例的误报。从那以后我每次拿到新的检测数据集都会强制走一遍流程先diff类别顺序再跑标签合法性检查和清洗脚本接着统计类别分布最后根据不平衡程度决定增强策略和重采样方案然后才开训。这套流程救过我很多次尤其是涉及不平衡数据集的训练希望帮到你。本文还有配套的精品资源点击获取