ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

车辆检测与计数实战:基于YOLO格式数据集训练到部署全流程

车辆检测与计数实战:基于YOLO格式数据集训练到部署全流程 简介这是一份面向YOLO系列算法的车辆检测与计数数据集围绕1304张车辆图像构建覆盖汽车、摩托车、公共汽车和卡车四类常见车辆目标适用于入门级目标检测项目或算法验证。压缩包内共2000个文件其中包含1089个XML标注、911个TXT标签总大小44.02MBXML对应VOC格式TXT对应YOLO格式两种标注分别存放方便按需选用。数据集已预先划分好训练集、验证集与测试集并附有data.yaml配置文件可直接适配YOLOv5、YOLOv7、YOLOv8、YOLOv9、YOLOv10、YOLO11等主流版本省去自行整理标注与划分数据集的环节。资源中的标签文本格式统一为类别、中心点坐标、宽高坐标均为归一化数值便于快速上手。目前已有88人学习适合需要现成车辆检测数据的开发者直接使用。1. 这个数据集解决什么问题不是“能检测”而是“能数得清”拿到这个 zip 文件第一件事先确认你的目标你大概率不是在找一个随便跑跑演示的 demo 数据集而是想在一批带标签的车辆图像上训练出能区分汽车、摩托车、公共汽车、卡车的检测模型并把它用到一个真实问题上——路口车流量统计、停车场占用分析、或者城市道路拥堵评估。1304 张图像听起来不大但配合四类目标标签和 YOLO 格式的 txt 文件足够让你在一个工作日内把“训练到推理”的完整链路跑通。难点根本不在训练而在之后同一辆车在视频帧里被重复计数、小目标在远处漏检、类别分布不均让公共汽车和卡车的单类精度低到没法用。这篇实战笔记就按这个顺序讲先做标签体检再训练再做计数最后用阈值校准把模型调到可用状态。2. 数据集内部结构1304 张图、四类目标先花半小时做标签体检2.1 目录与文件构成YOLO 格式的常见布局这类数据集下载解压后最常见的是 images 和 labels 平级、一一对应的布局也可能把 train、val 单独拆开。先不急着训练把目录结构打出来看一遍确认图片和标签是不是对得上号这一步非常值得。unzip yolo算法-车辆检测和计数数据集-1304张图像带标签-汽车-摩托车-公共汽车-卡车.zip -d vehicle_dataset cd vehicle_dataset find . -maxdepth 3 -type d | sort ls images | head -5 ls labels | head -5逻辑说明find只列三级目录能快速看出是否有 train/val 子目录ls各看前 5 个文件确认命名前缀是否一致。如果 images 里有IMG_0001.jpg但 labels 里找不到IMG_0001.txt那就是配对缺失后面训练会直接报找不到标签的错。参数说明maxdepth 3是为了不把标注缓存、备份目录也翻出来如果你解压后还有一层嵌套目录可以改成4。这一步的关键输出是“图片数量等于标签数量”这个结论如果不等先别碰训练配置回到数据源头补齐或剔除。2.2 标签格式验证坐标范围与归一化一个脚本查完YOLO 的标签格式严格固定为class_id x_center y_center width height坐标全部归一化到 0~1 之间用空格分隔一行一个目标。很多数据集打包时并没有严格遵守这条约定——有的标签混入了像素坐标有的 width/height 用了百分比但中心点用了小数这类脏数据不会直接报错只会让模型训练时 loss 异常波动。import os from pathlib import Path label_dir Path(labels) issues [] for txt in label_dir.glob(*.txt): with open(txt, r) as f: for line_no, line in enumerate(f, 1): parts line.strip().split() if len(parts) ! 5: issues.append((txt.name, line_no, 列数不为5, line.strip())) continue cls, x, y, w, h parts[0], float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4]) # 坐标必须在0~1区间类别ID必须是0~3的整数 if not 0 x 1 or not 0 y 1 or not 0 w 1 or not 0 h 1: issues.append((txt.name, line_no, 坐标越界, line.strip())) if not cls.isdigit() or int(cls) not in range(4): issues.append((txt.name, line_no, 类别ID异常, line.strip())) print(f共检查 {len(list(label_dir.glob(*.txt)))} 个标签文件) if issues: for item in issues[:30]: print(item) else: print(全部标签格式正常可以进入训练环节)逻辑说明这个脚本逐个 txt 逐行读先检查“一行是否恰好 5 个字段”再检查坐标是否越界、类别 ID 是否落在 0~3 区间内。任何一条不满足都会被记录。坐标越界是训练翻车的第一大元凶——尤其目标贴近图像边缘时标注工具偶尔会输出 1.02 这种超出边界的值YOLO 训练时虽然不崩溃但损失函数会反复震荡。参数说明range(4)对应汽车、摩托车、公共汽车、卡车四个类别如果你的数据集类别顺序不是这个按你自己的类别数改。检查越界用0 x 1已经是宽容标准有些严格团队会要求0 x 1因为等于 0 或 1 的归一化中心点存在的可能性极低一旦出现大概率是转换脚本写错了。2.3 类别分布统计四类目标里谁在拖后腿车辆检测数据集最常见的坑是类别极端不均衡汽车可能是道路上的绝对主力摩托车和公共汽车样本量少得可怜。如果不做统计就开训最后拿到的模型大概率是“汽车检测器附赠其他三类”。from collections import Counter import os counter Counter() class_names [汽车, 摩托车, 公共汽车, 卡车] for txt in os.listdir(labels): with open(os.path.join(labels, txt), r) as f: for line in f: cls int(line.strip().split()[0]) counter[cls] 1 total sum(counter.values()) for idx, name in enumerate(class_names): print(f{name}: {counter[idx]} 个目标, 占比 {counter[idx]/total:.1%})逻辑说明Counter 统计每个类别出现的目标总数。注意这里统计的是“目标数”而不是“图像数”因为一张图里可能有多个同类别目标。这个统计数据直接决定了你的训练策略——如果某一类占比低于 10%就得靠增强、类别加权或者额外收集数据来补。参数说明类别 ID 的映射顺序必须和训练时 data.yaml 里的 names 完全一致这是新手最容易踩的隐性坑。比如你在脚本里认为 0 是汽车但打包数据集的人可能把 0 设为卡车后面训练出来的模型所有预测结果都会错位。所以在跑这个统计之前先找有没有 classes.txt 或 dataset.yaml 之类的说明文件。3. 用 YOLOv5/v8 训练车辆检测模型数据划分、data.yaml 与关键参数3.1 数据划分随机打乱但固定种子保证可复现1304 张图的数据集不算大划分比例我一般用 85% 训练、10% 验证、5% 测试。测试集不是必需品但留着它做最后的精度评估更安心。划分脚本必须固定随机种子否则每次跑出来的训练集都不一样调参时对比不出是参数生效还是数据变了。import random from pathlib import Path import shutil random.seed(42) image_dir Path(images) label_dir Path(labels) train_dir Path(train_images); val_dir Path(val_images); test_dir Path(test_images) train_lbl Path(train_labels); val_lbl Path(val_labels); test_lbl Path(test_labels) for d in [train_dir, val_dir, test_dir, train_lbl, val_lbl, test_lbl]: d.mkdir(exist_okTrue) images sorted(image_dir.glob(*.jpg)) sorted(image_dir.glob(*.png)) random.shuffle(images) n len(images) train_cut int(n * 0.85) val_cut int(n * 0.95) for img in images[:train_cut]: shutil.copy(img, train_dir / img.name) shutil.copy(label_dir / (img.stem .txt), train_lbl / (img.stem .txt)) for img in images[train_cut:val_cut]: shutil.copy(img, val_dir / img.name) shutil.copy(label_dir / (img.stem .txt), val_lbl / (img.stem .txt)) for img in images[val_cut:]: shutil.copy(img, test_dir / img.name) shutil.copy(label_dir / (img.stem .txt), test_lbl / (img.stem .txt)) print(f训练集 {train_cut} 张, 验证集 {val_cut - train_cut} 张, 测试集 {n - val_cut} 张)逻辑说明先收集所有图片随机打乱后按比例切片复制图片和同名标签到对应目录。用copy而不是move是为了保留原始数据万一划分有误可以重新来。train_cut 和 val_cut 用的是索引切片保证三个子集不重叠。参数说明random.seed(42)是个人偏好任何整数都可以关键是固定下来。如果你用的图片既有 jpg 又有 pngsorted保证了跨平台的读取顺序一致避免因为文件系统差异导致划分结果不同。这组脚本跑完训练、验证、测试三个目录就绪接下来只需要写一个 data.yaml 指向它们。3.2 构建 data.yaml类别顺序必须和标签文件严格一致YOLOv8 和 YOLOv5 用同一个 data.yaml 机制内容很简单但类别列表绝不能拍脑袋写。前面标签体检时你可能已经确认了 ID 0~3 的顺序这里直接沿用。如果数据集压缩包里带了说明文件以说明文件为准。# data.yaml path: /absolute/path/to/vehicle_dataset train: train_images val: val_images test: test_images nc: 4 names: 0: car 1: motorcycle 2: bus 3: truck逻辑说明path最好写成绝对路径YOLOv5 在相对路径解析上偶尔会有奇怪的 bug尤其是从别的目录启动训练命令时。nc和names必须与标签文件里的 class_id 一一对应。如果打包数据集的人把 bus 放在 3你的 names 写的是 0:bus模型训练时会把 bus 的标签当 car 学推理结果自然全错。参数说明test字段在 YOLOv5 的严格模式下可省略但 YOLOv8 遇到缺失 test 字段时会自动用 val 替代区别不大。关键是把nc写对——写多了类别训练时 loss 正常但推理输出永远是错位标签写少了训练直接报错。我发现一个非常有效的自检方法用一个真实标签文件的类别 ID 最大值加 1必须恰好等于nc不等就停下来检查。3.3 训练命令与关键参数img、batch、epochs、patience数据集只有 1304 张图从零预训练权重开始会比较吃力我一般直接用 YOLOv8s 或者 YOLOv5s 的 COCO 预训练权重做迁移学习。车辆四类在 COCO 里都有对应类别预训练权重里的特征提取器已经学会了车辆边缘、轮毂、车窗这些视觉模式只需微调最后的分类头。# YOLOv8 训练命令示例 yolo train datadata.yaml modelyolov8s.pt epochs100 batch16 imgsz640 patience15 device0 # 或者 YOLOv5 训练命令示例 python train.py --data data.yaml --weights yolov5s.pt --epochs 100 --batch-size 16 --img 640 --patience 15 --device 0逻辑说明modelyolov8s.pt告诉训练器从预训练权重开始而不是随机初始化epochs100对这个规模的数据集足够配合patience15做早停连续 15 个 epoch 验证集精度不提升就自动结束。batch16是 8GB 显存机器的安全值如果你的显卡显存低于 6GB把 batch 降到 8 或 4同时考虑把 imgsz 从 640 降到 512。参数说明imgsz640是速度和精度的平衡点。如果你发现检测目标里摩托车、行人这类小目标占多数可以提高到960代价是训练时间翻倍、显存占用暴涨。device0指定用第一张 GPU没有 GPU 就删掉这个参数让训练自动落到 CPU但 1304 张图 CPU 训练大概要 2~4 小时起步不推荐。训练完成后weights 目录里会生成best.pt和last.pt永远用best.pt做推理。4. 车辆计数模块从检测框到不重复的数量关键在去重与门限4.1 帧级计数最简单但别指望它直接用于视频检测器输出的是每张图的目标框计数就是在图上数框。这个逻辑可以用在静态图片的车流量统计上但直接用视频流里每一帧的检测框数量作为最终计数结果是灾难性的——同一辆车会被连续几十帧重复计数。帧级计数可以作为你验证模型好坏的起点但做视频级车流量统计必须引入跨帧匹配。from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) results model(street.jpg, conf0.4, iou0.45, verboseFalse) count_by_class {} for box in results[0].boxes: cls int(box.cls.item()) class_name model.names[cls] count_by_class[class_name] count_by_class.get(class_name, 0) 1 print(各类型车辆数量:, count_by_class)逻辑说明results[0].boxes是检测器输出的全部目标框集合遍历每个框通过box.cls取类别 ID再通过model.names映射成可读名称。conf0.4表示置信度低于 0.4 的框直接丢弃这个值不是固定死的而是需要根据场景调整——车流稀疏、目标清晰的场景可以放宽到 0.25目标密集、遮挡严重的场景反而要调高到 0.5 以上减少误检。参数说明iou0.45是 NMS 的 IoU 阈值。值越低重合的框被抑制得越厉害密集车辆场景不容易出现重复框但调太低时并排紧挨的两辆车可能被合并成一个框导致计数偏少。这个参数和conf是计数精度的两个核心旋钮。4.2 跨帧去重用 IoU 匹配实现一个轻量级车辆追踪器真实场景下的车流量统计必须解决重复计数问题。最可靠的做法是引入 DeepSORT 这类追踪器但对 1304 张图训练出来的小模型来说一个基于 IoU 的简单跨帧匹配已经能解决 80% 的重复计数问题而且没有复杂的依赖。import numpy as np from ultralytics import YOLO class VehicleCounter: def __init__(self, iou_thresh0.3, max_lost5): self.iou_thresh iou_thresh self.max_lost max_lost self.tracks {} # id - {bbox, class, lost} self.next_id 0 self.count_by_class {car: 0, motorcycle: 0, bus: 0, truck: 0} def compute_iou(self, a, b): x1, y1, x2, y2 a x3, y3, x4, y4 b inter_w max(0, min(x2, x4) - max(x1, x3)) inter_h max(0, min(y2, y4) - max(y1, y3)) inter_area inter_w * inter_h area_a (x2 - x1) * (y2 - y1) area_b (x4 - x3) * (y4 - y3) return inter_area / (area_a area_b - inter_area 1e-6) def process_frame(self, detections): matched_ids set() for det in detections: x1, y1, x2, y2, conf, cls det best_id, best_score None, 0.0 for tid, track in self.tracks.items(): if track[class] ! cls: continue score self.compute_iou([x1, y1, x2, y2], track[bbox]) if score best_score: best_score score best_id tid if best_id is not None and best_score self.iou_thresh: self.tracks[best_id][bbox] [x1, y1, x2, y2] self.tracks[best_id][lost] 0 matched_ids.add(best_id) else: self.tracks[self.next_id] {bbox: [x1, y1, x2, y2], class: cls, lost: 0} self.count_by_class[model.names[int(cls)]] 1 matched_ids.add(self.next_id) self.next_id 1 for tid in list(self.tracks.keys()): if tid not in matched_ids: self.tracks[tid][lost] 1 if self.tracks[tid][lost] self.max_lost: del self.tracks[tid] counter VehicleCounter(iou_thresh0.3, max_lost5) model YOLO(best.pt) for frame in video_frames: results model(frame, conf0.4, verboseFalse) dets [] for box in results[0].boxes: x1, y1, x2, y2 box.xyxy[0].tolist() dets.append([x1, y1, x2, y2, float(box.conf), int(box.cls)]) counter.process_frame(dets) print(各类型车辆累计数量:, counter.count_by_class)逻辑说明每个追踪对象在tracks字典里存当前最近的检测框、类别和连续丢失帧数。新一帧的目标框进来后先和已有的同类别追踪框算 IoU 得分最高分超过iou_thresh就认为是同一辆车更新它的位置找不到匹配的目标则视为新车辆进入画面计数加一。连续max_lost帧没有匹配到这个追踪对象就认为车辆已经离开画面删除它释放资源。参数说明iou_thresh0.3是跨帧匹配的宽容度——车辆在连续两帧之间的位移通常很小框的重叠面积比例一般高于 0.5取 0.3 是为了容忍低帧率视频里位移较大的情况。如果你处理的视频帧率是 30fps可以放宽到 0.2帧率只有 5fps 的话建议直接用追踪器而不是这个轻量方案因为车辆在帧间可能移动了超过一个车身的位置。4.3 计数评估光看总数对不上要算 ID Switch重复计数问题光靠感觉判断不靠谱。我一般会把视频从头到尾跑三到五遍人工数一遍真正的车辆数量再对计数器的输出和标签对比。评估指标除了准确率还有一个更敏感的数据——ID Switch中文叫“身份切换次数”。它的意思是同一辆车本来应该固定占用一个编号但因为检测框在某一帧丢失、下一帧又出现被重新分配了新编号导致计数虚高。计数虚高的典型症状在这套实现里表现为车辆被计数两次但count_by_class里的数字比实际大。出现这种情况时优先调低iou_thresh从 0.3 降到 0.2 左右同时增大max_lost从 5 调到 10让追踪对象在目标短暂消失后有更高的容错。如果调完还是虚高那就不是计数逻辑的问题而是检测器本身在某一帧漏检了、下一帧又恢复这时该回头调conf和iou两个检测参数。5. 避坑指南5 个让车辆检测与计数翻车的常见问题5.1 标签坐标“看着正常”训练却疯狂震荡现象训练时 loss 曲线锯齿严重验证集 mAP 始终在 0.1 以下徘徊但打开标签文件看坐标、类别都像模像样。 原因部分标注工具尤其是手动改过 YOLO 导出的旧平台会输出像素坐标或者把 w/h 写成真实像素宽高、中心点却写成归一化值两种格式混在一起。脚本只检查范围很难看出问题因为像素坐标也可能小于 1比如目标在左上角、尺寸极小。 解决从 labels 目录随机抽 5 个文件用 Python 把坐标换算回像素值后叠画到图片上人工确认框的位置。这个可视化检查做一次比任何脚本都可靠。5.2 类别 ID 错位汽车被识别成卡车但不报错现象推理时发现汽车框上标着“truck”摩托车标成“bus”但 mAP 数值一切正常。 原因data.yaml 里的 names 顺序与打包者标注时使用的类别顺序不一致。标签体检时如果只看坐标不看类别映射这个问题完全隐身。 解决拿到数据集的第一件事查找是否有classes.txt、dataset.yaml、README之类的说明文件找到后严格按里面的顺序配置 data.yaml。没有说明文件时用 2.3 节的统计脚本打印每类数量再配合可视化抽查确认类别归属。5.3 小目标漏检远处的车辆就是检测不到现象近距离车辆检测正常中远距离车辆忽隐忽现计数结果明显偏低于人工统计。 原因训练时imgsz640会把原始图像等比缩小远处车辆可能在缩放后只剩 20×20 像素超过模型能有效学习的目标尺度下限。 解决先用imgsz960或1280重训一次看看涨幅。如果内存不够不改训练设置改为在推理阶段保持原始分辨率输入检测效果通常能提升不少。5.4 重复计数同一辆车被数了三次现象静态单帧图计数准确视频流里最终总数远超实际通过的车辆数量。 原因单纯逐帧数框没有跨帧匹配逻辑。车辆在画面里停留 30 帧就会被数 30 次。 解决按 4.2 节的轻量匹配方案实现跨帧去重。如果发现用 IoU 匹配在车辆互相遮挡时频繁丢失追踪再引入外观特征匹配升级为 DeepSORT。5.5 类别不平衡汽车 AP 很高公共汽车和卡车几乎不可用现象整体 mAP 勉强看得过去但 per-class AP 差异巨大摩托车或公共汽车的单类 AP 可能低于 0.2。 原因1304 张图里汽车类目标数量可能是其他类的十倍甚至二十倍模型把所有精力都用来拟合汽车了。 解决优先尝试训练参数层面缓解比如在 YOLOv8 里开启mosaic1.0增强、提高hsv_h等数据增强强度或者对少量类别做过采样——把摩托车和公共汽车的样本在训练集里复制 2~3 份。如果还是不行那就只能在数据集层面想办法补充这三类目标的标注图像是唯一根治手段。6. 进阶用置信度门限校准模型把模型调到“能上线”的状态6.1 阈值扫描找到 conf 的最优甜点区检测模型的conf参数不是拍脑袋定的。实际项目里常用的做法是跑一次阈值扫描从 0.1 到 0.7 每隔 0.05 取一个值记录每种阈值下的精确率、召回率和 F1 分数画一条曲线出来找到精确率和召回率交叉点的阈值窗口。import torch from ultralytics import YOLO model YOLO(best.pt) results model(test_images, conf0.01, iou0.45, verboseFalse) # 收集所有检测框的置信度 all_conf [] all_cls [] all_gt [] # 需要从 test_labels 读取 for r in results: for box in r.boxes: all_conf.append(float(box.conf)) all_cls.append(int(box.cls)) conf_t 0.1 while conf_t 0.7: pred_idx [i for i, c in enumerate(all_conf) if c conf_t] precision len(pred_idx) / max(len(pred_idx), 1) print(fconf{conf_t:.2f}, 检出框数{len(pred_idx)}, 精确率估算{precision:.3f}) conf_t 0.05逻辑说明先用一个极低的conf0.01做推理把模型所有带置信度分数的检测框都收集下来收集再在代码里模拟不同阈值下的过滤效果避免每次改阈值都要重新跑一遍推理。对 1304 张图的小数据集来说这种扫描方法又快又直观。注意真实精确率需要和测试集标签比对上面的脚本只是演示了阈值与检出量的关系。参数说明阈值扫描得到的“最优值”只在你测试的这批数据上有效。换一个场景比如从白天换成夜间、从稀疏公路换成拥堵路口最优置信度会明显右移。所以我现在的习惯是每次换场景重新扫一次阈值这个步骤 10 分钟内就能完成比凭经验猜要靠谱得多。6.2 验证方法自己的测试集跑分而不是只看训练日志训练日志上的 mAP 只能说明模型在自己的验证集上表现不错不等于它在新场景里可用。真正的上线前验证我的做法是单独留出一批没参与训练和验证的图像可以是从视频里截取的不同时间段画面跑一次批量推理统计每个类别的精确率和召回率再挑出几个典型的失败样本漏检、误检、重复计数人工分析。这个“失败样本复盘”的习惯帮我解决过不少模型上线后的诡异问题——比如某次车辆计数模型在白天一切正常傍晚突然疯狂漏检。后来复盘发现是下午四点到五点之间太阳角度低车辆在画面里拖出长影子检测器把一部分深色车辆误判为背景。后来我在训练数据里补了一批傍晚时段的图像重新微调问题就消掉了。数据集的标签可直接复用不需要重新打标这就是带标签数据集的额外价值。现在拿到任何数据集我都坚持先做标签体检再训练希望这套流程也能帮到你少走点弯路。本文还有配套的精品资源点击获取
返回列表