简介:这份斑马线人行横道检测数据集面向计算机视觉目标检测入门与进阶学习者,可用于解决模型训练中标注数据难获取、格式不统一的问题。资源主体为一百九十二张jpg原图,并配套对应的VOC格式xml与YOLO格式txt标注文件;图片统一采用矩形框标注zebra_crossing类别,共五百四十二个目标框,覆盖典型的人行横道视角。资料包共五百七十八个文件,压缩后大小约三十四兆字节,主要文件类型为jpg图片、xml标注和txt标签,xml与txt可分别对接VOC系和YOLO系训练框架,省去手动格式转换步骤。需特别说明,图片为模拟拍摄而非真实街道场景,适合算法验证与流程练习,不适合追求真实场景泛化效果的项目。目前已有178人浏览学习。所有标注均使用labelImg完成,类别和边界框信息规范完整,且压缩包目录结构简洁,便于按批次划分训练集与验证集,下载后即可开展目标检测训练与评估。
1. 斑马线人行横道检测数据集VOC+YOLO格式:192张图、542个框,能不能拿来训模型
“斑马线人行横道检测数据集VOC+YOLO格式192张1类别”这几个关键字,是我在给辅助驾驶项目找数据时搜到的。下载解压之后看了一圈:192张jpg、192个xml、192个txt,1个类别zebra_crossing,labelImg用矩形框标了542个目标。数据量不大,而且是模拟拍摄图,不是真实街景。直说结论:它不适合直接当量产模型的训练集,但特别适合三类人——刚入门目标检测想搞懂VOC和YOLO标注差异的、要拿小数据集验证训练pipeline能不能跑通的、准备自建数据集但还没摸清格式转换的。这篇文章就顺着“怎么看清格式、怎么验证质量、怎么绕开坑”往下走。
2. 数据集结构先看清:VOC与YOLO两套标注的对应关系
解压拿到手,最忌讳的是直接开训。先摸清包里的三类文件是怎么对应的,再决定下一步。这个数据集同时给了VOC和YOLO两份标注,意味着你既可以用labelImg生态里的脚本做可视化,也可以直接丢给yolov8训练,两条路都通,但要先弄清它们之间的关系。
2.1 VOC侧:jpg、xml与labelImg的矩形框约定
labelImg是VOC时代最常用的标注工具,它保存出来的xml走的是Pascal VOC结构。先贴一个真实xml的骨架:
<annotation> <filename>firc_rxd_47.jpg</filename> <size> <width>640</width> <height>640</height> <depth>3</depth> </size> <object> <name>zebra_crossing</name> <bndbox> <xmin>120</xmin> <ymin>210</ymin> <xmax>460</xmax> <ymax>380</ymax> </bndbox> </object> </annotation>这段xml的关键字段:filename是图片名,size里的width和height是原图尺寸,bndbox里xmin/ymin/xmax/ymax是标注框左上角和右下角的像素坐标。注意这里只列了一个object节点,实际一个xml对应一张图,一张图里可能有多个斑马线区域,object节点会相应地重复出现。训练yolov8自己的数据集时,data.yaml里的names写的就是zebra_crossing,xml里的name要和它严格一致,大小写、下划线都不能改。
| 文件类型 | 数量 | 内容 | 在训练中的角色 |
|---|---|---|---|
| jpg | 192 | 原始图片 | 模型输入 |
| xml | 192 | VOC像素坐标与目标类别 | 可视化、格式转换的数据源 |
| txt | 192 | YOLO归一化坐标 | yolov8/5直接读取的训练标签 |
再强调两个容易看漏的点:txt文件的内容里不含图片路径,它只存坐标和类别,文件名才是和jpg、xml关联的主键;这份数据集也没有任何分割掩码路径,去找segmentation相关节点属于白费功夫。
2.2 YOLO侧:txt一行一个目标,坐标是怎么从xml换算的
YOLO格式txt的每一行是class x_center y_center width height。class在yolov8里从0开始,所以只有一个类别时,zebra_crossing对应的class id就是0,而不是字符串。换算公式如下:
x_center = (xmin + xmax) / (2 * W) y_center = (ymin + ymax) / (2 * H) width = (xmax - xmin) / W height = (ymax - ymin) / HW和H来自xml的size节点。坐标必须归一化的原因在于:yolo损失函数在特征图的相对位置上回归边界框,同一张图缩放到640×640还是1280×1280,归一化坐标都不会变,模型才能吃任意输入尺寸。如果给了像素坐标,模型会把几百像素的偏移当成特征图上的偏移去回归,梯度算出来通常是nan。
下面贴一段解析xml并按上述公式输出YOLO格式文本的python脚本,我每换一个数据集都会先跑一遍:
import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, out_txt_path): root = ET.parse(xml_path).getroot() W = int(root.find("size/width").text) H = int(root.find("size/height").text) lines = [] for obj in root.iter("object"): name = obj.find("name").text if name not in class_id_map: continue cls_id = class_id_map[name] b = obj.find("bndbox") xmin = float(b.find("xmin").text) ymin = float(b.find("ymin").text) xmax = float(b.find("xmax").text) ymax = float(b.find("ymax").text) cx = (xmin + xmax) / 2.0 / W cy = (ymin + ymax) / 2.0 / H w = (xmax - xmin) / W h = (ymax - ymin) / H lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") with open(out_txt_path, "w") as f: f.write("\n".join(lines)) return lines if __name__ == "__main__": class_id_map = {"zebra_crossing": 0} print(voc_to_yolo("Annotations/firc_rxd_47.xml", "labels/firc_rxd_47.txt"))逻辑说明:class_id_map是类别到数字id的映射,只有一个类别时写死成0即可。cx/cy/w/h保留6位小数,yolov8训练时读float不会因为精度翻车,而0.000001的误差换算到640px也就是0.4px左右,不影响框的贴合度。xml里的xmin这些值有的是整数有的是字符串,统一转float再参与除法,可以避免整除带来的坐标偏移。这个脚本跑完还不算完,我在第3章还会用另一个脚本把txt反算回像素坐标,和xml做交叉验证,这一步别跳过。
常见做法是直接把整个Annotations目录循环跑一遍,生成对应的labels目录。实际操作时注意xml目录里可能存在备份文件,最好先过滤掉,只处理严格以.xml结尾的文件。生成的txt命名要和jpg基名完全一致,firc_rxd_47生成firc_rxd_47.txt,这决定了后面训练集加载时能不能对得上。
3. 用Python做三遍自检:标注质量与格式一致性验证
拿到标注数据就开训,是新手最常犯的错。我一般会把下面三个脚本存成check_dataset.py,每次换新数据先跑一遍,全程依赖标准库,不需要在PyCharm里额外pip install任何东西。三条检查分别解决“文件有没有缺”“坐标有没有越界”“两套格式是不是真的对应”这三个问题。
提示:三个脚本都基于文件名的基名做关联,解压路径里不要有中文和空格,Windows下尤其注意。
3.1 数量一致性检查
这个脚本把jpg、xml、txt三个目录下的文件名基名提取出来,用set做差集。set比list查找快得多,192张图差距不明显,但自建数据集到几千张时这个习惯能节省不少时间:
import os base = "zebra_crossing" # 解压后根目录,按实际路径改 folders = { "jpg": os.path.join(base, "JPEGImages"), "xml": os.path.join(base, "Annotations"), "txt": os.path.join(base, "labels"), } def basename_set(folder, suffix): if not os.path.isdir(folder): return set() return {f[: -len(suffix)] for f in os.listdir(folder) if f.endswith(suffix)} jpgs = basename_set(folders["jpg"], ".jpg") xmls = basename_set(folders["xml"], ".xml") txts = basename_set(folders["txt"], ".txt") print(f"jpg={len(jpgs)} xml={len(xmls)} txt={len(txts)}") print("缺xml:", sorted(jpgs - xmls)[:5] or "无") print("缺txt:", sorted(jpgs - txts)[:5] or "无") print("多余xml:", sorted(xmls - jpgs)[:5] or "无") print("多余txt:", sorted(txts - jpgs)[:5] or "无")逻辑说明:jpg是主键,xml和txt的基名必须与jpg完全一致,firc_rxd_47不行、firc_rxd_47_副本也不行。多余标注是好兆头的反面,说明有张图被重复标过,训练时数据会被重复采样,验证集上出现同样内容就是数据泄漏,mAP虚高。参数说明:basename_set里的suffix只处理了.jpg,有些数据集混着.png或.jpeg,遇到那种情况把函数改造成接收一个后缀列表即可。
3.2 VOC边界框越界与空框检查
xml是labelImg直接生成的,一般不会有大问题,但笨办法最可靠。脚本遍历所有xml,检查size节点是否存在、边界框是否越界、是否有空框:
import os import xml.etree.ElementTree as ET xml_dir = "Annotations" problems = [] for fname in os.listdir(xml_dir): if not fname.endswith(".xml"): continue root = ET.parse(os.path.join(xml_dir, fname)).getroot() size = root.find("size") if size is None: problems.append((fname, "缺size节点")) continue W = int(size.find("width").text) H = int(size.find("height").text) for obj in root.findall("object"): b = obj.find("bndbox") if b is None: problems.append((fname, "缺bndbox")) continue xmin = float(b.find("xmin").text) ymin = float(b.find("ymin").text) xmax = float(b.find("xmax").text) ymax = float(b.find("ymax").text) if xmin >= xmax or ymin >= ymax: problems.append((fname, "空框面积<=0")) if xmin < 0 or ymin < 0 or xmax > W or ymax > H: problems.append((fname, "越界")) if problems: for fname, err in problems[:10]: print(fname, err) else: print("所有xml坐标边界通过")逻辑说明:labelImg画框时手抖一下,可能存出xmin等于xmax这种退化框,yolo系训练时w或h为0,loss直接变nan。越界框在letterbox缩放后会被截掉一部分,导致标注框和实际目标位置对不上,可视化阶段迟早暴露。参数说明:这里把“越界”判成xmax大于图片宽度或ymax大于高度,严格模式。如果你的训练代码对越界框做了裁剪容错,可以适当放宽,但我建议还是先严格查一遍,把有问题的图单独挑出来,而不是让训练脚本悄悄吞掉。
3.3 YOLO txt与VOC xml交叉验证
前面只检查了VOC侧,还不能保证txt和xml是一致的。我见过不少数据集,xml是准的,txt是别人用脚本批量转的,转完对不上。这段脚本把txt的归一化坐标反算回像素,再和xml逐字段比较:
import os import xml.etree.ElementTree as ET EPS = 1.5 # 像素容差,单位px def read_yolo(txt_path): with open(txt_path) as f: return [line.split() for line in f if line.strip()] for fname in os.listdir("labels"): if not fname.endswith(".txt"): continue base = fname[:-4] xml_path = os.path.join("Annotations", base + ".xml") if not os.path.exists(xml_path): continue xml_root = ET.parse(xml_path).getroot() W = int(xml_root.find("size/width").text) H = int(xml_root.find("size/height").text) xml_boxes = [] for obj in xml_root.findall("object"): b = obj.find("bndbox") xml_boxes.append((obj.find("name").text, float(b.find("xmin").text), float(b.find("ymin").text), float(b.find("xmax").text), float(b.find("ymax").text))) yolo_data = read_yolo(os.path.join("labels", fname)) if len(xml_boxes) != len(yolo_data): print(f"{base}: 框数不一致, xml={len(xml_boxes)}, txt={len(yolo_data)}") continue for xml_box, yolo_line in zip(xml_boxes, yolo_data): cx, cy, w, h = [float(v) for v in yolo_line[1:]] xmin = cx - w / 2 ymin = cy - h / 2 xmax = cx + w / 2 ymax = cy + h / 2 px_bbox = [xmin * W, ymin * H, xmax * W, ymax * H] diffs = [abs(xml_box[i + 1] - px_bbox[i]) for i in range(4)] if max(diffs) > EPS: print(f"{base}: 坐标偏差 {max(diffs):.2f}px")逻辑说明:反算时w/2受txt小数位影响,6位小数换算到640px大约是0.4px的误差,EPS给到1.5px比较合理,不会误报。类别名也可以顺便比对,yolo_line[0]在yolov8里通常是数字id,xml_box[0]是字符串,这一步要配合第2章里的class_id_map一起看,这里就不重复展开了。框数不一致基本说明这个txt被增删过目标,坐标偏差大说明转换脚本有bug,必须回到第2章的换算公式逐项排查。
三遍检查全部通过之后,再谈训练的事。
4. 避坑指南:这五处翻车点我全替你踩过
以下这些坑,有些是这个数据集特有的,有些是所有VOC转YOLO数据集的通病。按现象、原因、解决的顺序写,你自己遇到时直接对照排查。
4.1 坑一:类别名大小写不一致,loss迟迟不降
现象:头十几个epoch的loss下降极慢,loss数值看起来也在掉,但验证集mAP一直是0。
原因:xml里标注的是zebra_crossing,yaml里names写成了Zebra_Crossing或zebraCrossing,类别id错位,模型把正样本当背景学。这种问题不会报错,只会安静地让训练白跑几十个epoch。
解决:先跑第3章的3.3脚本,把txt第一个字段和xml的name统一打印成一行,肉眼扫一遍是否完全一致。我在这个数据集上就遇到过xml里某个文件把zebra_crossing标成了zebra_crossin,差一个字符就是两种类别。yolov8的data.yaml写法是names: [zebra_crossing],只有一个类别时索引0映射到唯一元素,写错就白训。每次训练前先打印出类别字典,确认class id唯一再开始。
4.2 坑二:自己转的txt没归一化,yolo损失函数直接nan
现象:某些epoch的loss打印成nan,或者训练没崩但预测框全部挤在图像左上角。
原因:转换脚本没有除以W和H,把像素坐标当归一化坐标喂给了yolo。yolo损失函数在特征图每个grid cell上做相对偏移回归,输入中心点cx=300这种值,cell索引直接越界,梯度算出nan或inf。
解决:严格按第2章公式,cx/cy/w/h四项全部除以W或H。转完后随机抽三张图,打开txt看一眼cx是否在0到1之间,有一项超出就是没归一化。这是VOC转YOLO最容易出问题的一步,没有之一。
4.3 坑三:xml的filename字段和实际文件名对不上
现象:自己写的可视化脚本画框,框和斑马线错位;某些图显示不出框,程序直接报错。
原因:样本被重命名或resize过,xml里filename字段还留着旧值;labelImg另存的操作也会产生类似现象。
解决:别信任xml内部filename,做可视化或转格式时,直接用外部遍历到的jpg文件名作为主键。批量修正的思路是先读取xml里的size节点,再强制把filename覆盖成jpg实际名称。这条是通用经验,不适限于这个数据集。
4.4 坑四:192张图随机划分,验证集可能一张正样本都没有
现象:yolov8训练正常,loss正常下降,但val/box_loss从一开始就很高,val mAP恒为0。
原因:数据集里有一部分图是背景,不含斑马线。192张按8:1:1随机划分,样本量太小,正样本没分进val的概率不低。
解决:先按txt是否为空把图片分成含目标和不含目标两组,再从两组内分别抽验证集,保证val里有正样本。固定seed之后每次跑同样的划分,不要用随机seed,不然踩了坑连复现都做不到:
import os import random random.seed(42) labels_dir = "labels" pos = [] neg = [] for f in os.listdir(labels_dir): if f.endswith(".txt"): path = os.path.join(labels_dir, f) if os.path.getsize(path) > 0: pos.append(f[:-4]) else: neg.append(f[:-4]) random.shuffle(pos) random.shuffle(neg) val_size = max(1, len(pos) // 10) val = pos[:val_size] train = pos[val_size:] + neg print(f"train={len(train)} val={len(val)} 正样本val={len(val)}")逻辑说明:val_size按正样本数量的10%取整并保底1张,确保val里至少有一张含斑马线的图。背景图全放进训练集是模拟场景的惯用策略,给模型补充负样本比例。参数说明:seed固定42,换seed会改变划分结果,正式实验前先记录下这个值。这份数据集的txt按摘要说明有192个,但某些可能有内容为空的情况,用getsize判断是否为空文件是最稳妥的。
4.5 坑五:7z解压后路径异常,Windows与Linux表现不一致
现象:Windows下解压后文件名乱码,yolov8读图直接路径报错;Linux下输7z x报command not found。
原因:7z压缩包内的文件名编码在不同系统解释不同,Windows默认解压工具按当前区域设置解码。Linux下没装p7zip,解压命令当然不存在。
解决:Windows下建议用官方7-Zip软件右键解压到英文路径,别用系统自带“全部提取”。Linux先装解压工具再执行:
apt install -y p7zip-full 7z x zebra_crossing.7z -ozebra_crossing参数说明:-o后面紧跟目标目录,注意-o和目录名之间不能有空格,这是7z命令的固定写法。解压完成后检查一遍路径,确保没有中文、空格和括号。yolov8在部分编译环境下用opencv读中文路径会失败,表现就是train跑到一半报找不到图,前功尽弃。
5. 把这份数据用出价值:先跑通流程,再谈迁移
模拟数据有个特点:干净、光照单一、目标角度固定。用它训练出来的模型在模拟域内能收敛,但到了真实街景,mAP大概率会掉,作者在说明里也明确不对精度作保证。所以我的用法是三层递进。
第一步,用yolov8n这类小模型跑通全流程。imgsz取640,batch取8,epoch先给30。重点不是刷mAP,是确认数据加载、验证划分、评估指标这些环节一个不落地跑通。小模型在192张图上过拟合很快,loss曲线会难看,但框大致能贴合斑马线的走向,就说明pipeline通了。
第二步,针对模拟场景做数据增强。mosaic开关建议保留,它把四张图拼在一起,等于变相扩大样本空间;hsv的saturation增强幅度缓一点,模拟图颜色本来就寡淡,拉太大会让斑马线变成奇怪的颜色域。yolov8的超参数里hsv_s给0.3左右就够,别学真实街景数据那套激进参数。
第三步,把这份数据和真实数据混训。模拟数据权重别给太高,真实街景图加入后,模拟数据的作用就变成了防过拟合的正则项。如果你想量化它有没有用,修一个只用真实数据的baseline,再来一个真实数据加本数据集的对照组,各自训到同样epoch数,比较val mAP和PR曲线。这个对比会直观告诉你模拟数据的贡献有多少,也算给这个“不对精度作保证”的声明一个交代。
最后说个习惯:从那以后我拿到任何带“模拟拍摄”字样的数据集,都会先跑第3章那三个自检脚本,把格式验证通过当作开训的前提,而不是训练翻车之后才回头查数据。希望帮到你。
本文还有配套的精品资源,点击获取