简介:一份面向目标检测入门实践的人行道(斑马线)检测数据集,采用Pascal VOC与YOLO双格式标注,适合算法学习者、毕业设计及工程验证场景直接使用。资源整体共2000个文件,以795个txt标签、793个xml标注与jpg原始图像三类为主,压缩包39.45MB,体积轻量、目录清晰,无需额外转换即可接入主流检测框架。标注内容仅含cross单一类别,共805个目标框,类别简洁且分布较均匀,便于快速训练、调参和横向对比YOLO、Faster R-CNN等常用模型;同时也可作为VOC与YOLO格式转换练习的素材,帮助理解两种标注体系之间的对应关系。数据集文件名与标注彼此对应,适合在智能交通、行人过街识别、辅助驾驶等场景中做算法验证。截至目前,已有144人学习下载,适合需要轻量级数据快速跑通目标检测全流程的开发者。
1. 拿到人行道检测数据集之后,先别急着开训
“人行道检测数据集700+张VOC+YOLO格式.zip”,这个文件名对做目标检测的人来说很友好:两种标注格式都给了,省掉不少转换功夫。但 700+ 张图属于典型的小规模数据集,直接解压、套用 yolov8 训练自己的数据集,十有八九会遇到 loss 不降、mAP 虚高、类别对不上的问题。这篇文章会按一条能复现的路径走:拆 zip 之后先核对两套格式,再做数据体检和切分,然后讲清训练参数,最后用难例挖掘把数据量小的短板补回来。适合正在为行人、人行道或路口场景做目标检测验证的工程师。
2. 拆开 zip 之后:VOC 与 YOLO 两套格式的真实结构
这类数据集压缩包常见的结构是Annotations、JPEGImages、labels,再加一个classes.txt或class-mapping.txt。很多人习惯只看 YOLO 的 txt,但我建议先从 VOC 的 XML 看起,因为 XML 里保留了原始宽高和绝对坐标,是核对标注质量的“黑匣子”。两套格式在同一个包里的关系可以理解为:VOC 是原始凭证,YOLO 是归一化后的产物,两者一旦不一致,训练时越到后面越难排查。
2.1 VOC 格式:Annotations 里的 XML 到底记了什么
VOC 格式的标注目录通常是Annotations,一张图片对应一个同名.xml文件。一个典型的人行道目标标注长这样:
<annotation> <folder>JPEGImages</folder> <filename>sidewalk_0001.jpg</filename> <size> <width>1280</width> <height>720</height> <depth>3</depth> </size> <object> <name>sidewalk</name> <bndbox> <xmin>320</xmin> <ymin>240</ymin> <xmax>780</xmax> <ymax>520</ymax> </bndbox> </object> </annotation>这份 XML 里有几个关键字段:size里的宽高是后面做归一化必须依赖的原始信息;object下的name是类别名;bndbox里的四个值分别是目标左上角和右下角的绝对像素坐标。注意这里xmin, ymin, xmax, ymax是像素值,不是比例,下一步转 YOLO 格式时要用图片宽高去除。
有些数据包在ImageSets/Main里放了train.txt、val.txt,里面通常是图片文件名或相对路径。如果压缩包没给这部分,那就需要自己做随机切分,后面第 3 章会讲怎么切才不会让同一场景的连续帧同时出现在训练集和验证集里。
2.2 YOLO 格式:每个 txt 对应一张图,类别索引必须从 0 开始
YOLO 格式的标注全部放在labels目录下,每张图片对应一个同名.txt文件。没目标时这个 txt 可能是空文件,也可能是压缩包故意只给有目标的图片。每一行内容如下:
0 0.429688 0.527778 0.359375 0.388889五个值依次是:类别索引、归一化后的目标中心点 x、中心点 y、目标宽度、目标高度。全部是[0,1]范围的比例值,不是像素值。把 VOC 的绝对坐标转成 YOLO 归一化坐标的公式是:
cx = ((xmin + xmax) / 2) / image_width cy = ((ymin + ymax) / 2) / image_height bw = (xmax - xmin) / image_width bh = (ymax - ymin) / image_height这里最容易踩的坑有三处:类别索引是否从 0 开始;VOC 里的name是字符串,而 YOLO 里是整数;归一化时用的宽高必须和图片实际像素一致。部分数据集在压缩前用脚本统一缩放过图片,但 XML 里的size没跟着改,就会导致整份标注整体偏移。所以拿到包后不能只挑几个 txt 看,要全部跑一遍校验脚本。
2.3 用一段脚本核对 VOC 转 YOLO 的结果是否一致
我不太相信手工抽查,遇到这种双格式数据集,会直接写脚本把每个 XML 转成 YOLO 文本,再和原有 txt 逐行对比。这样可以确认两套标注是不是由同一份原始标注生成的,也能提前暴露坐标越界、类别顺序错乱、txt 空行等问题。
import glob import os import xml.etree.ElementTree as ET VOC_DIR = "Annotations" YOLO_DIR = "labels" # 类别顺序必须和压缩包里的 class-mapping 一致 class_map = { "sidewalk": 0, "person": 1, "crosswalk": 2, } def xml_to_yolo_lines(xml_path): tree = ET.parse(xml_path) root = tree.getroot() w = int(root.find("size/width").text) h = int(root.find("size/height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in class_map: print(f"未映射类别: {name}, 文件: {xml_path}") continue class_id = class_map[name] bndbox = obj.find("bndbox") xmin = float(bndbox.find("xmin").text) ymin = float(bndbox.find("ymin").text) xmax = float(bndbox.find("xmax").text) ymax = float(bndbox.find("ymax").text) # 对越界坐标做裁剪,避免 yolo 训练时报错 xmin = max(0, xmin) ymin = max(0, ymin) xmax = min(w, xmax) ymax = min(h, ymax) if xmax <= xmin or ymax <= ymin: continue cx = ((xmin + xmax) / 2) / w cy = ((ymin + ymax) / 2) / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h lines.append(f"{class_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") return lines for xml_path in glob.glob(os.path.join(VOC_DIR, "*.xml")): base = os.path.splitext(os.path.basename(xml_path))[0] yolo_path = os.path.join(YOLO_DIR, base + ".txt") expect = xml_to_yolo_lines(xml_path) if not os.path.exists(yolo_path): print(f"缺少对应 yolo txt: {base}") continue with open(yolo_path, "r") as f: actual = [line.strip() for line in f.readlines() if line.strip()] # 按行比较,允许浮点数末尾轻微误差 if len(expect) != len(actual): print(f"目标数量不一致: {base}, xml={len(expect)}, txt={len(actual)}") else: for a, b in zip(expect, actual): a_parts = a.split() b_parts = b.split() if a_parts[0] != b_parts[0]: print(f"类别索引不一致: {base}, {a} vs {b}") break if any(abs(float(x) - float(y)) > 0.001 for x, y in zip(a_parts[1:], b_parts[1:])): print(f"坐标偏差过大: {base}, {a} vs {b}") break print("校验完成")这段脚本做了四件事:把 XML 里的绝对坐标转成 YOLO 格式;裁剪越界坐标;和原 txt 做一一比对;输出所有不一致文件。跑完后如果输出很少,说明两套格式是同一来源,可以放心使用。如果大量报“坐标偏差过大”,那就要以 VOC 为准重新生成 YOLO 标注,别用原 txt 训练。
3. 训练前处理:对人行道检测数据集做体检,比调参更划算
700+ 张图在目标检测领域不算大,很多真实项目里这类数据是从连续视频里抽帧得到的,存在大量相似背景。如果不去重、不按场景切分,训练出来的模型在测试集上表现好,一到真实路上就露馅。常见的做法是先统计类别分布、图片尺寸、重复样本,再做增强和切分。
3.1 统计类别分布与图片尺寸:700 多张能拆成多少有效样本
先跑一个统计脚本,看每个类别有多少目标、有多少张图是空的、图片分辨率分布如何。这一步能直接决定要不要合并细分类别、要不要使用大分辨率训练。
import glob import xml.etree.ElementTree as ET from PIL import Image import os VOC_DIR = "Annotations" JPEG_DIR = "JPEGImages" from collections import Counter obj_counter = Counter() empty_images = [] size_hist = {} for xml_path in glob.glob(os.path.join(VOC_DIR, "*.xml")): tree = ET.parse(xml_path) root = tree.getroot() objects = root.findall("object") if len(objects) == 0: empty_images.append(os.path.basename(xml_path)) for obj in objects: obj_counter[obj.find("name").text] += 1 img_path = os.path.join(JPEG_DIR, root.find("filename").text) if os.path.exists(img_path): with Image.open(img_path) as img: size_hist[img.size] = size_hist.get(img.size, 0) + 1 print("类别统计:", dict(obj_counter)) print("空标注文件数:", len(empty_images)) print("分辨率分布:", size_hist)跑完之后重点看三点。第一,类别数是否和人行道检测这个标题匹配。如果类别只有person和sidewalk,那模型学的是“人”和“路面”,不是真正语义理解,部署时要先想清楚业务类别。第二,空标注文件是不是很多。一个 700 张的数据集如果有 100 张空图,等于有效标注只有 600 张,训练背景类别时会受影响,最好是保留这些负样本,做二分类/置信度过滤时用。第三,分辨率是否统一。如果既有 640×480 又有 1920×1080,后续训练时imgsz就需要权衡,或者把大图裁剪成 patch。
3.2 用 augmentation 补不足:但别在验证集上做增强
数据增强是缓解 700 张不够用的最直接手段。常见的选择是albumentations,它对检测框的处理比只调 PIL 省心得多。我一般会把增强限制在轻微几何变换和颜色扰动,不做过分的旋转,因为人行道有很强的方向性,把图旋转 90 度后马路牙子变成竖向,语义已经变了。
import albumentations as A from albumentations.pytorch import ToTensorV2 train_transform = A.Compose([ A.HorizontalFlip(p=0.5), A.RandomBrightnessContrast(p=0.3), A.HueSaturationValue(p=0.2), A.RandomGamma(p=0.2), A.Mosaic(p=0.3, width=640, height=640), ], bbox_params=A.BboxParams(format="yolo", min_visibility=0.3, label_fields=["class_labels"])) val_transform = A.Compose([ A.NoOp(), ], bbox_params=A.BboxParams(format="yolo", label_fields=["class_labels"]))注意min_visibility这个参数,它表示增强后目标框最少保留多少面积才算有效。如果目标被人行道边缘切掉太多,再参与训练就会变成误导样本。验证集和测试集一定不要加 Mosaic、翻转之外的强扰动,不然指标会偏乐观,部署时掉的精度会让人一头雾水。
增强不是越多越好,尤其对“人行道”这种大目标,过度旋转和缩放会让模型学会识别增强痕迹而不是路面特征。我建议先不加任何复杂增强训练一版作为 baseline,再逐步加 Mosaic、MixUp,看验证集 mAP 是否真的在涨。这个做法比一次性把所有增强堆上去更容易定位问题。
3.3 划分 train/val/test:按场景而不是按文件随机切
随机切分在数据量小的时候风险很大。如果这个数据集的图片是从几段监控视频里抽帧得到的,同一场景的连续帧都很相似,随机切分会把几乎相同的画面同时放进训练集和验证集,验证 mAP 会虚高。更合理的做法是先把图片名归类,按来源前缀或时间戳分组,再把“组”切分到训练和验证。
import random from collections import defaultdict # 假设文件名形如 streetA_frame_001.jpg, streetB_frame_001.jpg groups = defaultdict(list) for img_name in img_list: group_key = img_name.split("_")[0] # 按场景前缀分组 groups[group_key].append(img_name) group_names = list(groups.keys()) random.shuffle(group_names) train_groups = group_names[:int(len(group_names) * 0.7)] val_groups = group_names[int(len(group_names) * 0.7):] train_files = [] val_files = [] for g in train_groups: train_files.extend(groups[g]) for g in val_groups: val_files.extend(groups[g])这个切分逻辑的关键是分组粒度。如果图片命名看不出来源,那就按文件创建时间排序,连续 20 张作为一个组。没有分组信息时宁可让验证集少一点,也要保证训练集和验证集来自不同时间段或不同路段。标注工具导出的项目里一般都有图片路径,从中提取目录名或摄像机 ID 是常见做法。
4. 把 VOC 标注转成 YOLO 训练配置:YOLOv8 踩通的一次最小流程
数据体检做完,下一步就是用 YOLO 系列训练自己的数据集。我一般优先用 YOLOv8,因为它的命令行接口对新手最友好,指标输出也全。这里不讨论 YOLOv8 和 YOLOv5 谁更强,只看怎么把一个双格式数据集跑通。
4.1 生成 dataset.yaml 的坑:绝对路径还是相对路径
YOLOv8 需要一份dataset.yaml指定训练集、验证集和类别名。很多人直接把别人的 yaml 复制过来改路径,结果训练时提示“image not found”。原因是 yaml 里的路径相对于当前工作目录解析,不是相对于 yaml 文件所在目录。最省事的方式是写绝对路径,但要注意换机器后必须同步改。
# dataset.yaml path: /home/user/sidewalk_dataset # 数据根目录,写绝对路径 train: images/train # 训练图片目录 val: images/val # 验证图片目录 test: images/test # 可选 nc: 3 # 类别数量 names: 0: sidewalk 1: person 2: crosswalk这里的train和val是相对path的目录,它们下面直接放图片文件。如果数据集本身只有JPEGImages一张大目录,没有按 train/val 拆分子目录,那就得先手动建目录并移动图片,或者用一个软链接指向对应切分文件。YOLO 也支持在train里写一个 txt 文件路径,txt 每一行是一张图片的绝对路径,但用目录形式最稳。
还有一个容易忽略的点:nc必须和names列表长度一致,类别索引顺序必须和 labels 里的数字对应。如果压缩包里的class-mapping.txt是乱序的,例如把 person 定义成 0,而 labels 的 txt 里第一行全是 1,那就全错了。先打开一个 txt 手动看一眼,再写 yaml。
4.2 训练命令和参数选择:imgsz、batch、epochs、预训练权重
第一轮训练不建议直接上 YOLOv8x 或超大分辨率,用小模型跑通链路最重要。用yolov8n.pt做预训练权重下载是最快的上手方式,虽然它是在 COCO 上预训练的,人行道类别不在其中,但浅层特征仍然有效。
yolo detect train \ data=dataset.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ device=0 \ project=runs/sidewalk \ name=baseline参数意义和调整方向:
epochs=100:700 张小数据集的基线轮次。如果 40 轮开始验证集 mAP 不涨,可以提前早停,不需要死等 100 轮。imgsz=640:如果原图以 1920×1080 居多,目标又小,可以提到 960 或 1280,但显存占用会明显上升。先 640 跑通,再看小目标漏检情况决定要不要升。batch=16:不是越大越好,batch 会影响 BN 统计。显存不够时调小 batch,同时可以把学习率按比例降低,否则容易引起训练震荡。device=0:指定 GPU。没有 GPU 可以用 CPU 跑,但 700 张也要一两个小时起步,建议至少 4G 显存的显卡。- 预训练权重用
yolov8n.pt,训练完成后会在runs/sidewalk/baseline下生成best.pt和last.pt。后续第二轮难例挖掘就是用这个best.pt去做预测。
训练过程中如果看到 loss 曲线完全不下降,先不要调参,回头看第 5 章的避坑列表。很多问题出在数据标注和路径上,不是模型结构的问题。
4.3 训练完成后看哪些指标:混淆矩阵、PR 曲线、mAP50 和 mAP50-95
训练结束不是看训练集 loss 有多低,而是看验证集指标。YOLOv8 会在输出目录里生成confusion_matrix.png、PR_curve.png、results.csv。我一般按这个顺序看:
confusion_matrix.png:看类别之间是否互相混淆。如果 person 和 sidewalk 大量相互误检,说明类别定义重叠,或标注框区分得不够清楚。PR_curve.png:看每个类的精确率和召回率平衡点。人行道这种大面积背景目标,如果精确率很高但召回率很低,说明模型只测出了最容易的那部分。results.csv里的mAP50和mAP50-95:mAP50 是 IoU 阈值为 0.5 的均值,mAP50-95 把阈值从 0.5 到 0.95 均匀取 10 档。人行道这种没有明显边界的目标,mAP50 高但 mAP50-95 低是正常的,不用过分焦虑。
注意验证集指标和最终部署效果不一定一致。数据集中如果大多数目标框都非常大,mAP 会被这几个大框拉高,小目标漏检在指标里体现不明显。所以跑完还要单独拿一批现场图或视频帧做目视验证。小数据集的另一个特点是随机种子会影响指标,跑完一轮后可以固定种子再跑一轮,确认结果可复现。
5. 人行道检测数据集训练的常见翻车与避坑记录
这个标题写“VOC+YOLO格式”,听起来很稳,但实际训练里最耗时间的往往不是模型,而是数据约束。下面这 5 条是我在手头多个检测项目里都遇到过的坑,按“现象-原因-解决”记录。
5.1 现象:训练正常启动,但 loss 不降,验证集 mAP 一直为 0
训练过程不报错,每个 epoch 都在跑,loss 却在几个值之间横跳,或者干脆不降。打开验证集图片一看,模型把所有目标都预测成背景。
原因:最常见的是 labels 目录下大量 txt 是空文件,或者图片路径指向了错误目录,模型等于在完全没有目标框的数据上训练。还有一种是标注坐标归一化时用错了分母,把图片高度当宽度用,导致所有框都横向压缩。
解决:用第 2 章的校验脚本检查 txt 非空率和坐标范围。确认每个 txt 与图片一一对应,所有坐标值在[0,1]内。如果空文件太多,就把空文件对应的图片从训练集里移出来,保留一部分作为负样本专门用于 confidence 过滤。
5.2 现象:验证集 mAP 很高,但实际视频里大量漏检人行道
mAP 在验证集有 0.85,拿手机随手在街上拍一段人行道视频,检测框却东缺一块西漏一个。
原因:小数据集最常见的坑是数据分布单一。700 张图如果都来自晴朗日间、同一路段,验证集也是同一来源的抽帧,那么模型学到的是“这条路的人行道”,不是通用的人行道特征。随机切分没有切掉这种相关性,指标自然虚高。
解决:按场景分组切分,强制验证集包含模型没见过的路段或时段。如果没有多余数据,至少不要在雨天、夜间、逆光图上做大量剔除,保留难度样本反而能让模型更稳。还可以在增强里加入随机阴影、运动模糊模拟不同天气。
5.3 现象:YOLO 训练时类别名对不上,或者一个类被另一个类完全吞掉
训练完成后发现 class 1 的召回率极低,class 2 的精确率很高,打开混淆矩阵发现两个类别大量互相预测。
原因:VOC 的name是字符串,压缩包里的class-mapping.txt可能没有按字母顺序排列,但有人直接按照字母顺序生成 YOLO txt。这导致 txt 里存的类别索引和 yaml 里的names不一致。比如 XML 里是person,映射文件写0: person, 1: sidewalk,但某批 txt 却按0: sidewalk, 1: person生成,类别就错位了。
解决:不要相信映射文件名,直接打开 txt 和 XML 对比,让脚本以 XML 里的name出现频率排序,再重新生成 class_map。确保 yaml 里的 names 顺序和重新生成的 txt 索引完全一致。类别若有大面积重叠,考虑合并成pedestrian_area一个类,能降低标注不一致的影响。
5.4 现象:训练到中途 loss 突然变成 NaN,或者 BN 层崩溃
训练到几十轮,loss 曲线突然陡增或变 NaN,之后 mAP 归零,重启又能跑一阵但再次崩。
原因:可能是数据里有损坏图片、全黑图片或尺寸异常大的图。也可能是 batch 太小而学习率偏大,梯度更新不稳定。小数据集更容易遇到个别极端样本把 BN 统计带偏。
解决:先跑一个数据完整性检查,用 PIL 打开每一张图并检查通道数、尺寸是否异常,删除无法解码的文件。然后把 batch 调大到至少 8,学习率降到 0.01 或使用 YOLOv8 默认的cos_lr=True。如果继续崩,把workers调低,排除读取线程问题。
5.5 现象:混淆矩阵总和不是 100%,甚至每行每列对不上
很多人在看confusion_matrix.png时发现矩阵里的数字不是整数,或者每行加起来不是该类的实际样本数,怀疑训练算错了。
原因:YOLOv8 输出的混淆矩阵默认是归一化的,横轴代表预测类别,纵轴代表真实类别,每个单元格是比例值。不同类别样本数不一样,所以矩阵总和不是 1,每行也不一定等于 1。这不是 bug,是读法问题。
解决:看混淆矩阵时只看相对比例,不要拿它统计精确数量。若想得到原始计数,可以在yolo val时关闭normalize,或者打印results_dict里的confusion_matrix原始矩阵。另外注意背景列通常被省略,但背景误报会影响整体指标,记得看验证集输出里的Class和Images列。
6. 把 700 张数据集用到极致:难例挖掘与半自动标注
小数据集的优化空间不在网络结构,而在数据循环。我常用的做法是把训练好的best.pt当成“标注预检器”,去预测一批没标注的现场图片,把置信度在 0.3~0.6 之间的检测结果抽出来,这些人眼都觉得难判断的样本,恰恰是补充训练最有价值的部分。
yolo detect predict \ model=runs/sidewalk/baseline/weights/best.pt \ source=raw_unlabeled_videos \ conf=0.3 \ save_conf=True \ project=runs/hard_mining看预测结果时,不要只看保存的图,还要把每张图的conf字段导出来,筛出置信度中等偏低的框。比如person类置信度 0.5,但框得很准,说明模型其实学到了特征,只是样本不够;置信度 0.9 但框错了位置,说明是背景误检,这类样本要作为负样本加入训练。
筛选完难例,用目标检测常用标注工具打开这些图片,加载预测框作为预标注,人工修正后导出成 YOLO 格式,丢回训练集重新训练。这个流程重复两三轮,比直接把随机抓取的 1000 张图全标注有效得多,因为每轮都在补模型最薄弱的地方。
我自己的习惯是给这个流程写一个固定脚本:先把新旧样本合并,重新跑第 2 章的校验脚本,把类别分布、空文件和坐标范围都打印出来,确认没问题后才开始下一轮训练。一次没跑通不丢人,丢人的是连标准都懒得校验。希望这些记录能帮你少走几步弯路。
本文还有配套的精品资源,点击获取