摩托车与行人目标检测数据集:从zip到YOLOv8训练全流程
发布时间:2026/10/1 17:34:35来源:尧图网络
简介这份摩托车与行人目标检测数据集面向交通监控、自动驾驶感知与智慧城市方向的算法开发者及研究人员聚焦道路场景中摩托车与行人两类关键目标的识别需求。数据来源于实际道路监控画面覆盖多种光照与视角条件共含1095张JPEG图片其中训练集937张、验证集158张均采用YOLO格式的归一化边界框与类别标签标注可直接对接YOLOv5/v7/v8等主流检测框架。压缩包共2000个文件以1095个txt标注文件、903个jpg图像为主另附1个yaml配置文件与1份docx说明文档整体约62.91MB目录结构清晰便于快速投入训练。目前已有124人学习下载。借助该数据集读者可完成交通流量统计、危险行为预警、行人聚集识别等任务的模型训练与验证也可用于摩托车与行人互动行为的学术研究为道路安全策略提供数据支撑。1. 摩托车与行人目标检测数据集从一份 zip 到能跑通训练的全过程城市路口监控里最容易被漏检的两类目标一个是贴着车道线窜行的摩托车一个是突然从车流缝隙里走出来的行人。这两类目标尺度变化大、遮挡严重、姿态随意用通用 COCO 预训练权重直接推理摩托车经常被识别成自行车行人被识别成柱子。一份专门针对「摩托车 行人」标注的目标检测数据集价值就在这里它把标注预算集中砸在这两个类上让模型在细分场景里真正收敛。这份「摩托车与行人目标检测数据集.zip」解压后通常是一批图像加对应标注文件标注格式可能是 VOC XML、YOLO txt 或 COCO JSON 中的一种。拿到它之后真正要解决的问题不是「有没有数据」而是「怎么把它变成 YOLOv8 能直接吃的格式、类别映射怎么定、划分比例怎么切、训练时哪些参数必须改」。这篇笔记按我实际处理这类数据集的顺序写先看清结构再转格式再切分再训练最后把踩过的坑摊开讲。适合已经跑过 YOLO 基础训练、手里正拿着一份 zip 不知道从哪下手的同学。2. 先摸清 zip 里的结构标注格式判定与类别映射2.1 三种常见标注格式的快速判定方法解压之后别急着写训练脚本先花五分钟确认标注格式。摩托车与行人这类数据集来源不同格式差别很大从 LabelImg 标出来的多半是 VOC XML从 Labelme 转出来的可能是 COCO JSON从 Roboflow 导出的通常是 YOLO txt。判定方法很直接看标注文件的后缀和内容# 看目录结构判断标注和图像是否分离 find ./dataset -maxdepth 2 -type d # 统计标注文件后缀分布 find ./dataset -type f \( -name *.xml -o -name *.json -o -name *.txt \) | \ sed s/.*\.// | sort | uniq -c如果输出里xml占绝大多数基本就是 VOC如果只有一个大的annotations.json那是 COCO如果每张图旁边都有一个同名txt且内容形如0 0.512 0.634 0.081 0.142那就是 YOLO 格式五个字段分别是class_id x_center y_center width height且都是归一化到 0~1 的值。判定清楚之后打开任意一个标注文件看类别名。VOC 里是namemotorbike/name这种COCO 里在categories数组YOLO 里只有数字 id需要找classes.txt或data.yaml对照。这一步的关键是确认类别名到底叫motorbike还是motorcycle、叫person还是pedestrian后面写类别映射表全靠它。2.2 类别映射表怎么定两类还是多类很多摩托车行人数据集实际标注的不止两类可能还混了car、bus、bicycle、rider。这里有个决策点是保留全部类别训练还是只留摩托车和行人两类。我的建议是先看每类的实例数量。用下面这段脚本统计import os, glob from collections import Counter # 统计 YOLO 格式下每个类别的实例数 counter Counter() for txt in glob.glob(./dataset/labels/*.txt): with open(txt) as f: for line in f: cls_id line.strip().split()[0] counter[cls_id] 1 for cls_id, cnt in sorted(counter.items(), keylambda x: -x[1]): print(fclass {cls_id}: {cnt} instances)如果某个类别实例数不到总数的 5%训练时它基本学不动还会拉低整体 mAP直接合并或丢弃更划算。摩托车和行人这两类如果实例数都在几千以上就保留两类如果行人只有几百个考虑把rider骑手合并进person因为骑手本质也是人只是姿态特殊。类别映射一旦定下来就写进data.yaml顺序不能乱因为 YOLO 的class_id是从 0 开始按这个顺序索引的# data.yaml path: ./dataset train: images/train val: images/val nc: 2 names: 0: motorcycle 1: person提示names的顺序必须和转换脚本里写的映射字典完全一致否则训练出来的模型会把摩托车框成行人这种错误在验证阶段才暴露白跑几小时。2.3 图像与标注的一一对应检查转换之前必须做一次配对检查因为 zip 里经常出现有图无标注、有标注无图的情况尤其是从多个来源拼起来的数据集。跑一段脚本把不配对的挑出来import os img_dir ./dataset/images lbl_dir ./dataset/labels imgs {os.path.splitext(f)[0] for f in os.listdir(img_dir)} lbls {os.path.splitext(f)[0] for f in os.listdir(lbl_dir)} only_img imgs - lbls only_lbl lbls - imgs print(f有图无标注: {len(only_img)}) print(f有标注无图: {len(only_lbl)}) # 有图无标注的图如果确实没有目标可以保留作为背景负样本 # 有标注无图的直接删掉标注有图无标注的图不一定要删如果它确实是一张没有摩托车和行人的纯背景图保留下来当负样本反而能降低误检率。但有标注无图的必须清理否则训练时 dataloader 找不到图会直接报错中断。3. 把 VOC 或 COCO 转成 YOLO 格式转换脚本与四个边界坑3.1 VOC XML 转 YOLO txt 的完整脚本VOC 格式的坐标是左上角xmin, ymin和右下角xmax, ymaxYOLO 要的是归一化的中心点和宽高转换公式是x_center (xmin xmax) / 2 / img_width y_center (ymin ymax) / 2 / img_height width (xmax - xmin) / img_width height (ymax - ymin) / img_height完整脚本如下注意里面处理了图像尺寸读取和类别映射import os import xml.etree.ElementTree as ET from PIL import Image # 类别映射VOC 里的名字 - YOLO 的 id CLASS_MAP { motorbike: 0, motorcycle: 0, person: 1, pedestrian: 1, } def convert_voc_to_yolo(xml_path, img_path, out_txt_path): tree ET.parse(xml_path) root tree.getroot() # 用 PIL 读真实尺寸不要信 XML 里写的 size with Image.open(img_path) as im: w, h im.size lines [] for obj in root.findall(object): name obj.find(name).text.strip().lower() if name not in CLASS_MAP: continue # 不在映射表里的类别直接跳过 cls_id CLASS_MAP[name] bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) # 边界裁剪防止标注越界 xmin max(0, min(xmin, w)) xmax max(0, min(xmax, w)) ymin max(0, min(ymin, h)) ymax max(0, min(ymax, h)) if xmax xmin or ymax ymin: continue # 宽高为 0 的脏标注丢弃 xc (xmin xmax) / 2 / w yc (ymin ymax) / 2 / h bw (xmax - xmin) / w bh (ymax - ymin) / h lines.append(f{cls_id} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}) with open(out_txt_path, w) as f: f.write(\n.join(lines)) # 批量执行 img_dir ./dataset/images xml_dir ./dataset/annotations out_dir ./dataset/labels os.makedirs(out_dir, exist_okTrue) for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue stem os.path.splitext(xml_file)[0] img_path os.path.join(img_dir, stem .jpg) if not os.path.exists(img_path): continue convert_voc_to_yolo( os.path.join(xml_dir, xml_file), img_path, os.path.join(out_dir, stem .txt) )这段脚本里几个参数值得说清楚。CLASS_MAP用.lower()做了大小写归一因为不同标注工具导出的大小写不统一。坐标裁剪那两行是必须的我见过不少数据集标注框超出图像边界不裁剪的话归一化后会出现大于 1 的值YOLO 训练时虽然不报错但那个框的梯度是错的。宽高为 0 的判断也不能省有些标注员手抖点出两个相同坐标转出来就是一条无效行。3.2 COCO JSON 转 YOLO 的差异点COCO 格式的坐标是[x, y, width, height]注意这里的width, height是绝对像素值不是右下角坐标转换时不用再做减法import json from PIL import Image with open(./dataset/annotations.json) as f: coco json.load(f) # 建立 image_id - 文件信息 的索引 img_info {img[id]: img for img in coco[images]} # 建立 category_id - 类别名 的索引 cat_info {cat[id]: cat[name] for cat in coco[categories]} # COCO 的 category_id 往往不是从 0 连续需要重映射 CAT_MAP {motorcycle: 0, person: 1} # 按 image_id 聚合标注 from collections import defaultdict anns defaultdict(list) for ann in coco[annotations]: anns[ann[image_id]].append(ann) for img_id, ann_list in anns.items(): info img_info[img_id] w, h info[width], info[height] lines [] for ann in ann_list: name cat_info[ann[category_id]] if name not in CAT_MAP: continue x, y, bw, bh ann[bbox] xc (x bw / 2) / w yc (y bh / 2) / h nw bw / w nh bh / h lines.append(f{CAT_MAP[name]} {xc:.6f} {yc:.6f} {nw:.6f} {nh:.6f}) with open(f./dataset/labels/{info[file_name].rsplit(.,1)[0]}.txt, w) as f: f.write(\n.join(lines))COCO 转换最容易翻车的地方是category_id不连续。COCO 里category_id可能是 1、3、7 这种跳号直接拿来当 YOLO 的class_id会导致类别数对不上。必须像上面那样建一个显式的CAT_MAP重映射别偷懒。3.3 转换后必须做的三项校验转换完不要直接开训先跑三项校验。第一项随机抽 20 张图把 YOLO 框画回去肉眼看框是否贴合import cv2 import random def draw_yolo_box(img_path, txt_path, save_path): img cv2.imread(img_path) h, w img.shape[:2] with open(txt_path) as f: for line in f: cls_id, xc, yc, bw, bh map(float, line.split()) x1 int((xc - bw / 2) * w) y1 int((yc - bh / 2) * h) x2 int((xc bw / 2) * w) y2 int((yc bh / 2) * h) color (0, 255, 0) if int(cls_id) 0 else (255, 0, 0) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.imwrite(save_path, img) # 随机抽 20 张 import glob samples random.sample(glob.glob(./dataset/images/*.jpg), 20) for i, p in enumerate(samples): stem os.path.splitext(os.path.basename(p))[0] draw_yolo_box(p, f./dataset/labels/{stem}.txt, f./check_{i}.jpg)第二项统计所有归一化坐标是否都在 0~1 之间超出范围的说明转换有 bug。第三项统计每类实例数确认没有某类为 0。这三项跑完数据才算干净。4. 数据集划分与 YOLOv8 训练参数从 8:1:1 到能收敛的配置4.1 训练验证测试集怎么切才不泄漏划分比例常见 8:1:1 或 7:2:1但比比例更重要的是不能泄漏。如果数据集是从视频抽帧来的相邻帧几乎一样随机划分会让验证集里出现和训练集高度相似的图mAP 虚高。判断方法看文件名如果形如frame_0001.jpg、frame_0002.jpg那就是连续帧必须按时间段切不能随机切。import os, random, shutil random.seed(42) # 固定种子保证可复现 img_dir ./dataset/images lbl_dir ./dataset/labels stems [os.path.splitext(f)[0] for f in os.listdir(img_dir)] random.shuffle(stems) n len(stems) n_train int(n * 0.8) n_val int(n * 0.1) splits { train: stems[:n_train], val: stems[n_train:n_train n_val], test: stems[n_train n_val:], } for split, names in splits.items(): os.makedirs(f./dataset/images/{split}, exist_okTrue) os.makedirs(f./dataset/labels/{split}, exist_okTrue) for stem in names: shutil.copy(f{img_dir}/{stem}.jpg, f./dataset/images/{split}/{stem}.jpg) shutil.copy(f{lbl_dir}/{stem}.txt, f./dataset/labels/{split}/{stem}.txt)如果是视频抽帧数据把random.shuffle换成按文件名排序后切片保证同一段视频的帧落在同一个 split 里。4.2 YOLOv8 训练命令与关键参数数据准备好之后训练本身一条命令就能起yolo detect train \ data./data.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ patience20 \ project./runs \ namemoto_person参数逐个说。model选yolov8n.pt还是yolov8s.pt取决于数据量实例数低于 1 万先用 n高于 3 万可以上 s。imgsz640是默认值但如果你的数据集里摩托车和行人都很小比如高空俯拍要提到 960 或 1280否则小目标在下采样时信息就丢了。batch受显存限制8G 显存跑 640 大概能到 16跑 1280 只能到 4。lr00.01是 SGD 的初始学习率如果换成optimizerAdamW要降到 0.001。patience20表示 20 轮 mAP 不涨就早停避免过拟合。训练过程中重点看两个指标metrics/mAP50-95和每一类的metrics/mAP50。如果整体 mAP 在涨但行人那一类一直很低说明行人的样本太少或者太小需要针对性补充数据或调imgsz。4.3 训练完怎么验证模型真的能用训练结束别只看最后的 mAP 数字拿测试集跑一次推理把预测框画出来看yolo detect predict \ model./runs/moto_person/weights/best.pt \ source./dataset/images/test \ conf0.25 \ saveTrueconf0.25是置信度阈值摩托车和行人这种容易误检的场景可以试 0.3 到 0.4宁可漏检一点也别把背景框成目标。看预测结果时重点关注三类错误摩托车被框成行人类别混淆、远处小目标漏检、密集人群里框粘连。这三类错误对应的调参方向不同混淆要查标注质量漏检要提imgsz粘连要调 NMS 的iou阈值。5. 避坑与排查处理这类数据集时最常翻车的五件事5.1 现象训练 loss 正常下降但 mAP 一直是 0原因通常是类别映射错位。data.yaml里names的顺序和标注文件里class_id的含义对不上模型学到的「0 号类是行人」但验证时按「0 号类是摩托车」去算自然全错。解决方法是抽一张训练图用yolo detect predict跑一遍看输出的类别名和框是否匹配不匹配就回去核对CLASS_MAP和data.yaml。5.2 现象验证集 mAP 很高实际部署一塌糊涂原因是数据泄漏。视频抽帧的数据集随机划分后验证集里全是训练集的近邻帧模型等于在背答案。解决办法是按视频源或时间段划分确保同一段视频的帧只出现在一个 split 里。判断有没有泄漏看验证集图片和训练集图片的文件名是否连续连续就大概率泄漏了。5.3 现象小目标摩托车几乎全漏检原因是imgsz太小。640 输入下一个在原图里占 30 像素的摩托车经过 backbone 下采样 32 倍后只剩不到 1 个像素的特征检测头根本看不到。解决办法是把imgsz提到 960 或 1280或者用带 P2 检测层的模型变体。提imgsz会显著增加显存和训练时间权衡后再定。5.4 现象同一辆车被框出好几个重叠框原因是 NMS 的iou阈值设太高。默认 0.7 对密集场景偏松摩托车并排行驶时容易互相干扰。解决办法是在推理时把iou降到 0.5 左右yolo detect predict modelbest.pt sourcetest imgsz960 conf0.3 iou0.5但iou也不能太低低于 0.4 会把正常相邻的两个行人框合并成一个需要根据实际场景试。5.5 现象转换脚本跑完发现标注文件是空的原因是 XML 里的类别名不在CLASS_MAP里全被continue跳过了。常见于数据集用的是motorbike而映射表里只写了motorcycle。解决办法是先把 XML 里所有出现过的name值统计出来再补全映射表grep -rh name ./annotations | sort | uniq -c这条命令会列出所有类别名及出现次数照着补CLASS_MAP就不会漏。6. 让这份数据集真正发挥价值的两个进阶技巧第一个技巧是难例挖掘。第一轮训练完之后用best.pt在训练集上跑推理把那些「有标注但模型没检出」或「检出但 IoU 低于 0.3」的图挑出来这些就是难例。把难例复制一份加权采样或者单独微调几轮mAP 通常能再涨 2 到 5 个点。挑难例的脚本核心逻辑是拿预测结果和 GT 做匹配# 伪代码思路对每张图计算预测框与 GT 框的最大 IoU # 若某 GT 框找不到 IoU 0.3 的预测框标记为漏检难例 # 若某预测框与所有 GT 的 IoU 都 0.1标记为误检难例 # 两类难例的文件名分别存成两个 txt供后续加权采样第二个技巧是类别平衡。摩托车和行人的实例数往往差好几倍直接训练会让多的那类主导 loss。可以在data.yaml同级写一个带权重的采样配置或者更简单粗暴地把少的那类图复制多份。我一般先看两类实例数比值超过 3:1 就做重采样低于 3:1 就不折腾因为 YOLOv8 本身的损失函数对轻度不平衡已经有一定鲁棒性。最后说个我自己的习惯每次处理完一份新数据集我都会把转换脚本、data.yaml、划分脚本和训练命令存进一个dataset_pipeline文件夹下次拿到类似的数据集直接改CLASS_MAP就能复用。摩托车行人这类数据集我前后处理过七八份真正花时间的从来不是训练而是格式转换和标注清洗这两步。把这两步脚本化、可复现比调参重要得多。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网