YOLO与VOC双格式行人数据集:从标注解析到目标检测训练实战
简介面向行人检测的YOLO标注数据集基于CUHK Occlusion Dataset构建已完成训练集与验证集划分同时提供YOLO和VOC两种格式标签可直接用于主流目标检测模型的训练与评估。压缩包共计约2000个文件其中2125张jpg原图、2124个txt格式YOLO标签、1062个xml格式VOC标签以及少量缓存文件压缩后大小为315.63MB目录结构清晰可按需选取对应格式。已有593人学习/下载尤其适合计算机、电子信息工程、数学等专业学生用于课程设计、期末大作业或毕业设计也可作为行人检测算法复现的基准数据。整理者为资深算法工程师在计算机视觉领域有多年经验除数据文件外还提供了清晰的标注说明和划分策略能够减少自行处理标注格式的重复劳动帮助读者更快进入模型训练与参数调优阶段。1. 一个能直接喂给 YOLO 的行人数据集意味着什么拿到一个「2125 张图像 YOLO / VOC 双格式标注」的行人数据集压缩包最直接的价值不是省下载时间而是省掉目标检测项目里最枯燥的两周采集、清洗和标注。做行人检测的人都知道标注边际成本很高一个框中一个人还好遇到遮挡、密集人群和小尺寸行人一个下午标不了几百张。这个 zip 把图像和标注文件捆绑好并且同时给出 YOLO 和 VOC 两种格式意味着你可以直接进训练流程也可以反向验证标注一致性。对初学者它是不用碰标注工具就能跑通 yolo 目标检测流程的起点对熟手它是一个可以快速做预训练、迁移学习和格式转换测试的样本集。唯一要做的是把里面的格式和坐标含义先摸清楚再喂给模型。2. 解析 YOLO 与 VOC 标注目录结构、坐标换算与双格式对齐2.1 目录结构两种格式的存放位置常见做法是压缩包解压后包含三个并行的目录images 放原始图像labels 放 YOLO 格式的 txtAnnotations 放 VOC 格式的 xml根目录还有一个 classes.txt 或 names.txt 记录类别名。目录结构类似pedestrian_dataset/ ├── images/ │ ├── person_0001.jpg │ └── person_0002.jpg ├── labels/ │ ├── person_0001.txt │ └── person_0002.txt ├── Annotations/ │ ├── person_0001.xml │ └── person_0002.xml └── classes.txt注意YOLO 的 labels 和 VOC 的 Annotations 可能不在根目录而是在某个子目录下。先看 zip 内顶层目录不要上来就假设路径。图像文件名和标注文件名必须同名前缀否则就是标注错位后面训练必炸。这种并行目录结构是 yolo 数据集最常见的组织方式Ultralytics YOLO、YOLOv5、YOLOv8 都能直接吃这种布局。2.2 YOLO 格式 txt归一化坐标与类别从 0 开始YOLO 的每个 txt 文件对应一张图每行描述一个目标五列class_id x_center y_center width height。class_id 是整数从 0 开始后四列是归一化坐标以图像宽高为分母值域 [0,1]。例如0 0.5125 0.4102 0.2435 0.6803表示这个人框的中心在图像横向 51.25%、纵向 41.02% 处框宽为整图宽的 24.35%高为整图高的 68.03%。很多人第一次拿到标注会以为是像素值直接拿去画框图结果画到角落这就是 yolo 标注训练工具最常见的误用。检查一个数据集是否靠谱先看 txt 里的数值范围如果出现大于 1 的值说明格式不对或没归一化。2.3 VOC XMLbndbox 与 size 缺一不可VOC 格式是 XML每张图一个文件节点层级固定。最关键的是size和bndboxannotation filenameperson_0001.jpg/filename size width1280/width height720/height depth3/depth /size object nameperson/name bndbox xmin349/xmin ymin103/ymin xmax661/xmax ymax593/ymax /bndbox /object /annotationxmin/ymin/xmax/ymax 是像素坐标左上角为原点x 向右y 向下。这个和 YOLO 的中心点表示法差异很大但可以互相转换核心就是 size 里的宽高。很多转换脚本出问题都是漏读 size 或把 width 和 height 搞反导致框被拉伸。2.4 用 Python 交叉校验 YOLO 与 VOC 是否对齐拿到双格式数据集第一步不是训练而是写脚本检查两种标注描述的是不是同一个框。我一般会写一个非常短的解析函数把一个 txt 和一个 xml 分别读出来做映射import xml.etree.ElementTree as ET from pathlib import Path def read_yolo_txt(txt_path): boxes [] with open(txt_path, r) as f: for line in f: line line.strip() if not line or line.startswith(#): continue parts line.split() cls_id int(parts[0]) x_center, y_center, w, h map(float, parts[1:]) boxes.append((cls_id, x_center, y_center, w, h)) return boxes def read_voc_xml(xml_path): root ET.parse(xml_path).getroot() size root.find(size) W int(size.find(width).text) H int(size.find(height).text) objects [] for obj in root.findall(object): name obj.find(name).text bnd obj.find(bndbox) xmin int(bnd.find(xmin).text) ymin int(bnd.find(ymin).text) xmax int(bnd.find(xmax).text) ymax int(bnd.find(ymax).text) # 像素坐标转归一化中心点 x_center ((xmin xmax) / 2) / W y_center ((ymin ymax) / 2) / H w (xmax - xmin) / W h (ymax - ymin) / H objects.append((name, x_center, y_center, w, h)) return W, H, objects逻辑说明read_yolo_txt 按行解析五列数据过滤空行和注释行把宽高保留为归一化值。read_voc_xml 先读 size 得到图像宽高再把 bndbox 的像素坐标换算成中心点格式这样两边的输出就是同一坐标系。注意这里四舍五入导致的微小偏差是正常的但如果同一个框在两种格式里坐标偏差超过 0.01就说明标注文件不同步需要找压缩包的原始版本或重新转换。参数说明width 和 height 来自size如果 XML 里缺少 size这种标注无法可靠转成 YOLO 格式。解析函数的容错点在于过滤空行因为某些标注工具会在 txt 末尾留下换行符或额外空格。这两个函数是后续所有验证的基础。拿到数据后先随机抽 10 对图像和标注用 OpenCV 把 YOLO txt 的框画出来再在同图上画 XML 的框若两张图的重叠率IoU都在 0.95 以上说明双格式是同源的可以放心使用下面表里的对应关系做后续转换。YOLO 与 VOC 两种标注格式对照格式存放文件每目标信息坐标基准类别表示常用解析方式YOLO txtlabels/*.txtclass_id, x_center, y_center, width, height图像宽高归一化中心点整数 class_id按行 split()float() 转换VOC XMLAnnotations/*.xmlname, xmin, ymin, xmax, ymax像素绝对值左上角原点字符串类别名xml.etree.ElementTree 解析这张表也说明VOC 转 YOLO 需要类别名到 id 的映射YOLO 转 VOC 需要 id 到类别名的映射单向转换其实很简单难在两边数据源不一致时无法对齐。3. 用这个数据集走通 yolo 目标检测流程数据划分、data.yaml 与训练参数3.1 按比例划分 train/val/test 并保持文件对同步先别急着训练。yolo 目标检测流程里第一步是数据划分否则验证集失去意义。我一般会把 2125 张图按 8:1:1 划成 train/val/test并且保证 images、labels、Annotations 三个目录同步移动。不要直接手动拖文件写一个脚本用同一个随机种子处理三个目录import random import shutil from pathlib import Path src_images Path(pedestrian_dataset/images) src_labels Path(pedestrian_dataset/labels) src_annots Path(pedestrian_dataset/Annotations) all_images sorted(src_images.glob(*.jpg)) random.seed(42) random.shuffle(all_images) n len(all_images) train_split int(n * 0.8) val_split int(n * 0.9) splits { train: all_images[:train_split], val: all_images[train_split:val_split], test: all_images[val_split:], } for split, images in splits.items(): out_img_dir Path(data) / split / images out_lbl_dir Path(data) / split / labels out_xml_dir Path(data) / split / Annotations for d in (out_img_dir, out_lbl_dir, out_xml_dir): d.mkdir(parentsTrue, exist_okTrue) for img in images: stem img.stem # 三个同名前缀文件一起复制 shutil.copy2(img, out_img_dir / img.name) shutil.copy2(src_labels / f{stem}.txt, out_lbl_dir / f{stem}.txt) shutil.copy2(src_annots / f{stem}.xml, out_xml_dir / f{stem}.xml)逻辑说明先用固定随机种子确保实验可复现再按 8:1:1 切分test 不参与训练也不参与验证只留到最后做最终评估。每个 split 下都建立 images、labels、Annotations 三套目录并保证同名文件同步复制这样后续既可以用 YOLO 格式训练也可以用 VOC 格式做别的实验。参数说明shuffle 的种子固定为 42换数据集时想复现实验就保留这个种子如果对行人类别不平衡敏感可以改成按视频片段分组避免同一个场景的人出现在 train 和 val 里导致指标虚高。3.2 写 data.yaml 时注意绝对路径与类别顺序Ultralytics YOLO 和 YOLOv5 都用 data.yaml 描述数据集。这里最容易踩的坑就是 path 用相对路径换台机器跑直接报错 image not found# data.yaml path: /absolute/path/to/pedestrian_dataset train: images/train val: images/val test: images/test nc: 1 names: 0: person逻辑说明path 是数据集根目录train/val/test 是相对 path 的目录名。nc 是类别数这个行人数据集只有 person 一类因此 nc: 1。names 从 0 开始索引和 YOLO txt 里的 class_id 对应。参数说明如果你的压缩包 classes.txt 里写的不是 person 而是 pedestrian必须改名或映射否则训练能跑但模型输出的类别名是错的。注意数据集中如果夹杂了不是行人的图像比如下载失败的占位图训练前用 python 检查一下所有图片能否被 cv2.imread 正常读取否则会在训练中途报错非常难排查。3.3 YOLOv8/v5 训练命令与必调参数环境配置好后训练命令非常短# YOLOv8 yolo detect train datadata.yaml modelyolov8s.pt epochs100 imgsz640 batch16 lr00.01 # YOLOv5 python train.py --data data.yaml --weights yolov5s.pt --epochs 100 --batch-size 16 --img 640逻辑说明yolo detect train 是 Ultralytics YOLO 的入口命令model 参数可以直接写预训练权重 yolov8s.pt程序会自动下载。epochs100 对 2125 张图的单人数据集略多通常 50 个 epoch 就会过拟合建议配合早停。imgsz640 是训练和推理统一分辨率像素不是 640 的图像会自动缩放加 letterbox。参数说明batch16 取决于显存RTX 3060 12G 可以开 328G 建议 16 或 8YOLOv5 的 --img 同时控制输入尺寸这个参数对行人小目标检测影响很大512 会丢失小行人细节800 以上训练速度明显下降。关键点是yolo 损失函数里对边界框回归和分类有不同的权重训练时如果小目标漏检严重优先调大 imgsz而不是盲目堆 epochs。这个数据集的标注是完整、可直接使用的所以不需要在标注上耗时间把精力放在 imgsz、batch 和 lr0 这三个 yolo 训练参数上就好。3.4 训练过程中的标注自动校验训练时用以下命令看标注真实分布yolo detect train datadata.yaml modelyolov8s.pt epochs1 batch1第一次只跑 1 个 epoch观察输出关注 train/val box loss 和 cls loss 是否正常以及标签是否出现在标注预览里。如果输出中标签数远少于图像数说明许多图片和标注没有形成配对回去检查 3.1 的划分脚本有没有漏文件。4. VOC 转 YOLO 的踩坑清单坐标转换脚本与常见误用边界4.1 为什么还需要 VOC 格式的转换脚本很多标注工具原生导出 VOC XML比如 LabelImg。这个数据集虽然双格式都有但你可能要把它合并进另一个只有 VOC 标注的数据集或者在代码里统一走 VOC 接口。与其依赖图形工具不如自己持有一个可靠的 XML 转 txt 脚本。VOC 转 YOLO 本质做两件事像素坐标变归一化中心坐标类别名映射成数字 id。4.2 一个生产可用的 XML 转 YOLO 脚本下面的脚本能处理 size 缺失、多类别映射和宽高为 0 的情况import xml.etree.ElementTree as ET from pathlib import Path def convert_voc_to_yolo(xml_path, out_txt_path, class_to_id): root ET.parse(xml_path).getroot() size root.find(size) if size is None: raise ValueError(f{xml_path} 缺少 size 节点无法计算归一化坐标) W float(size.find(width).text) H float(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text cls_id class_to_id.get(name) if cls_id is None: print(f跳过未知类别 {name} 在 {xml_path} 中) continue bnd obj.find(bndbox) xmin float(bnd.find(xmin).text) ymin float(bnd.find(ymin).text) xmax float(bnd.find(xmax).text) ymax float(bnd.find(ymax).text) # 检查异常框 if xmax xmin or ymax ymin: print(f忽略无效框 {xmin},{ymin},{xmax},{ymax} 在 {xml_path}) continue x_center ((xmin xmax) / 2) / W y_center ((ymin ymax) / 2) / H w (xmax - xmin) / W h (ymax - ymin) / H lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) out_txt_path.write_text(\n.join(lines) \n, encodingutf-8)逻辑说明先检查 size 是否存在规避最隐蔽的坐标漂移问题。class_to_id 是外部传入的字典例如{person: 0}未知类别直接跳过而不是崩溃这在多数据集合并时很实用。异常框检测放在转换前避免把退化框喂给模型。参数说明输出用 6 位小数对 640 分辨率来说误差在亚像素级够用如果数据集有 4K 大图建议保留 8 位小数。4.3 常见误用边界类别 id、文件配对与坐标方向第一类别 id 从 0 开始。VOC 里如果同时有 person 和 car很多新人习惯让 person1结果训练时模型学到的类别索引和 data.yaml 错位推理时预测的类别全是反的。第二是文件配对转换脚本必须严格用 xml 的 stem 生成 txt 文件名不能复用 images 目录下的文件名否则一旦有漏标注的图txt 和 xml 就错位了。第三是坐标方向VOC 的 y 轴向下和图像坐标系一致但某些标注工具导出时 y 轴向上或原点在左下这种数据流不能直接转需要先用可视化确认。这个数据集本身是标准的 VOC 结构所以不涉及这些脏数据问题但如果你把它和其他来源的数据合并规则就得立住。5. 训练前最后一关5 分钟脚本检查标注质量与异常框5.1 全量扫描 labels 目录的数值合法性最后给一个我自己每次拿到标注都会先跑的质量检查脚本。它扫描所有 txt、检查类别 id 是否在 names 范围内、坐标是否在 [0,1] 区间、框是否退化成线或点python check_labels.py --labels labels --num-classes 1check_labels.py 核心逻辑里只做一个循环和几行判断from pathlib import Path import argparse def check_labels(label_path, num_classes): bad [] for txt_file in Path(label_path).glob(*.txt): for line in txt_file.read_text().splitlines(): if not line.strip(): continue parts line.split() cls_id int(parts[0]) x, y, w, h map(float, parts[1:]) if not (0 cls_id num_classes): bad.append(f{txt_file}: 类别 {cls_id} 超出范围) if not (0 x 1 and 0 y 1): bad.append(f{txt_file}: 中心点越界 {x},{y}) if w 0 or h 0 or not (0 w 1 and 0 h 1): bad.append(f{txt_file}: 宽高异常 {w},{h}) return bad if __name__ __main__: parser argparse.ArgumentParser() parser.add_argument(--labels, requiredTrue) parser.add_argument(--num-classes, typeint, default1) args parser.parse_args() problems check_labels(args.labels, args.num_classes) for p in problems: print(p) print(f共发现 {len(problems)} 个问题)逻辑说明类别越界说明 txt 和 names 定义不同步中心点越界说明 XML 转 YOLO 时 size 用错宽高异常说明 bndbox 坐标写反或漏读。所有异常汇总后统一打印不遇错即停。参数说明--labels指向 labels 目录--num-classes默认 1如果数据集包含多个类别必须改成实际类别数否则误报一大堆。5.2 随机抽 20 张叠加可视化肉眼确认标注语义脚本检查完数值还要画一次图确认语义。很多错误数值检查发现不了比如框中心对但框住了两个人的一半或者类别名是 person 但框里只有自行车。用 OpenCV 画框时注意坐标要乘回原图宽高也就是把归一化中心点和宽高换算成左上右下import cv2 img cv2.imread(str(image_path)) H, W img.shape[:2] x1 int((x_center - w / 2) * W) y1 int((y_center - h / 2) * H) x2 int((x_center w / 2) * W) y2 int((y_center h / 2) * H) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2)提示如果看到大量框整体偏移不是标注坏了而是标注工具导出时使用了不同的坐标原点检查一遍 VOC XML 的 ymin/ymax 是否对称就能确认。5.3 把检查能力固化进训练流程这几个验证步骤熟悉后可以把它们合并成一个 preflight 脚本放在训练命令之前。每次换数据集、合并新标注、或从网上下载 yolo 数据再裁剪子集都先跑一遍检查再启动训练。这样不依赖某个标注工具也能在任何环境里快速判断数据集可用性比等训练完看 NaN loss 再回头排查快得多。脚本本身也不绑定具体模型多模态目标检测、红外小目标检测这些场景里只要标注格式还是 YOLO txt这套检查就一样适用复用价值很高。本文还有配套的精品资源点击获取