简介:这是一份面向计算机视觉目标检测任务、适合训练盲道识别模型的盲道检测数据集。数据共包含2173张现场场景图片,采用Pascal VOC与YOLO两种主流标注格式,类别为“mangdao”,全部由labelImg工具以矩形框完成精确标注,共2371个标注框,可直接用于YOLO、Faster R-CNN等常见检测网络的训练与评估,省去格式转换环节。压缩包共2000个文件,以xml标注文件为主(1999个),另含1个txt说明文件,整体大小约632.96MB。已有1387人学习下载,适合需要真实盲道样本进行算法验证、课题研究或无障碍应用开发的初中级开发者。需要说明的是,资源仅保证标注准确合理,不对训练所得模型精度作任何承诺,使用时应结合实际场景评估。
1. 盲道检测数据集:先别急着训练,先看这 2173 张图怎么用
拿到一个写着“盲道检测数据集VOC+YOLO格式2173张1类别.7z”的压缩包,先别急着解压训练。做助盲避障项目最难的往往不是模型选型,而是带干净标注的盲道图像从哪来;这个数据集把图片资源、VOC 的 XML 标注和 YOLO 的 txt 标注一起给到,才是它能省时间的地方。它解决的是“从零采图、标框、转格式”这段最枯燥的前置工作,适合做视障辅助导航、城市无障碍设施巡检、盲道占用检测的工程师,也适合刚入门 YOLO 的人拿一份真实单类数据跑通完整流程。正因为类别只有 1 个,你不需要处理类别不平衡,但要把注意力放在标注坐标、数据划分和部署性能这三件更容易翻车的事上。
2. 拆开 VOC+YOLO 双重格式:目录结构、7z 解压与完整性校验
压缩包后缀 .7z 决定了你第一件事不是训练,而是用什么工具解开。我习惯在服务器上用 p7zip,在 Windows 上用 7-Zip;两边的核心动作一样:先测试完整性,再列出文件树,最后解压到指定目录。把这一步做好,后面训练时少掉一半的“我明明解压了为什么找不到文件”问题。
2.1 VOC 与 YOLO 两份标注,为什么一个数据集要塞两种格式
VOC(Pascal VOC)的标注是 XML,每张图对应一个同名 .xml。XML 里有<size>保存图像宽高,<object>保存目标类别名和<bndbox>里的 xmin、ymin、xmax、ymax 四个像素坐标。YOLO 格式的 txt 每行五列:类别 id、归一化中心 x、归一化中心 y、归一化宽、归一化高。两份标注描述的是同一个几何信息,但读法完全不同。
为什么一个数据集要塞两种格式?常见做法是原始标注保留 VOC,因为 XML 可读、可审核,之后还能转成 COCO、旋转框数据集或其它框架的结构;YOLO txt 是为了训练时不用写转换器,YOLO 系列脚本读起来最顺。保留 VOC 相当于留了一份后悔药,所以我平时不会直接把 Annotations 目录删掉,哪怕训练只用 labels。
2173 张单类目标属于中间体量。从零随机初始化训练不太够,但从 YOLOv8n 或 YOLOv8s 的预训练权重往下微调,已经足够。目标如果是 T4 这类显卡上做实时部署,更不适合一上来选 x 级模型,数据量撑不起那么大的参数量。
2.2 Linux 解压 7z 文件:先测压再解压,目录一次规划好
服务器上常规做法是先用 p7zip 做一次完整性测试。7z 压缩率虽然高,但下载过程一旦中断,解压到一半才报 CRC 错误,浪费的时间比重新下载更难受。
# 先测压,确认压缩包没坏再解压 7z t blind_tactile.7z # 看压缩包内目录结构,不必全部刷出来 7z l blind_tactile.7z | head -50 # 解压到指定目录,注意 -o 和路径之间不能有空格 mkdir -p /data/blindside 7z x blind_tactile.7z -o/data/blindside第一行7z t做的是 CRC 完整性校验,如果输出里有Open ERROR或者CRC Failed,直接重新下载,不要硬解。第二行7z l只看文件树,能在解压前确认有没有 Annotations、labels、ImageSets 这些关键目录。最后一行才真正解压;-o/data/blindside这个写法很容易写错,-o后必须紧跟路径,中间有空格会被当成两个参数。如果发行版没装 7z,先执行sudo apt install p7zip-full。
Windows 上我一般打开 7-Zip 后开启侧边栏压缩文件树,把压缩包当文件夹浏览,快速确认里面目录是不是符合预期。但正式使用不要直接在压缩树里拖小文件,右键选“提取到当前文件夹”更稳妥。千万别用 Windows 自带压缩文件夹处理 .7z,兼容性极差,很容易解出一半文件。
解压后建议按下面这个结构整理,避免后续训练时路径混乱:
| 数据角色 | 常见目录 | 内容 |
|---|---|---|
| 原始图片 | JPEGImages/ | 2173 张 jpg 或 png |
| VOC 标注 | Annotations/ | 每张图对应的同名 xml |
| 数据集划分 | ImageSets/Main/ | train.txt、val.txt 等 |
| YOLO 标注 | labels/ | 每张图对应的同名 txt |
严格说,压缩包内部不一定就叫这些名字,但多数数据集遵循 VOC 遗产目录。如果你看到 ImageSets/Main 里的 txt 文件,打开后通常是图片文件名去掉后缀的一行一个 ID,不是完整路径。
2.3 解压后核对三件事:图片数、标签数、空标签
解压完成后不要急着写训练脚本。先跑三行命令,把数量对齐,这是最容易暴露压缩包损坏、标注缺失的一步。
cd /data/blindside echo "images: $(find JPEGImages -type f | wc -l)" echo "labels: $(find labels -type f -name '*.txt' | wc -l)" echo "empty labels: $(find labels -type f -name '*.txt' -size 0 | wc -l)"第一行统计图片数量,标题写 2173 张,核对时如果差得不多,可能是解压中断或者压缩包里本来就带缩略图;差得多就要回到7z t重新验包。第二行统计 label 数,第三行专门找空 txt。空标签在 YOLO 训练里会被跳过,但不会直接报错,于是有效训练量比 2173 少很多时,你完全没有感知。
如果 labels 下面只有几百个 txt,而 Annotations 里有 2173 个 xml,说明 VOC 标注是全的,YOLO 标签可能漏生成。这种情况不需要反编译压缩包,用第三章的转换脚本重新生成就行。
提示:在服务器上解压时,目录不要放在带中文和空格的路径下。Ultralytics YOLO 依赖相对路径和 glob 匹配,路径一乱,训练时大概率报图片文件找不到。
3. 把 VOC 转成 YOLO 格式:转换脚本与四个边界坑
如果压缩包里只有 VOC XML,或者你想重建一份干净的 YOLO 标签,建议先用脚本转换,而不是手工改。核心逻辑是把 bndbox 四个像素坐标变成相对图像宽高的中心点与宽高,再写成每行五列的 txt。这一步听着简单,实际容易在坐标越界、类别编号、图像尺寸不一致三个地方翻车。
3.1 最小可用转换脚本:把 VOC XML 变成 YOLO txt
我一般用一个不到 60 行的 Python 脚本完成转换。脚本不做花哨处理,只解决“像素框转归一化框”这一件事,但把越界和退化框一起处理掉。
import glob import xml.etree.ElementTree as ET from pathlib import Path voc_dir = Path("Annotations") img_dir = Path("JPEGImages") label_dir = Path("labels") label_dir.mkdir(exist_ok=True) # 单类别时直接写死;多类别时从 classes.txt 读取 id 映射 class_name = "blind_road" def convert_label(xml_file): tree = ET.parse(xml_file) root = tree.getroot() size = root.find("size") w_img = int(size.find("width").text) h_img = int(size.find("height").text) if w_img <= 0 or h_img <= 0: print(f"[skip] bad image size: {xml_file}") return False txt_name = xml_file.stem + ".txt" txt_path = label_dir / txt_name lines = [] for obj in root.findall("object"): name = obj.find("name").text if name != class_name: print(f"[warn] {xml_file}: {name} != {class_name}, skip") continue 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) # 边界裁剪:防止标注框超出图像尺寸 xmin = max(0, min(xmin, w_img)) xmax = max(0, min(xmax, w_img)) ymin = max(0, min(ymin, h_img)) ymax = max(0, min(ymax, h_img)) w_box = xmax - xmin h_box = ymax - ymin if w_box <= 0 or h_box <= 0: print(f"[skip] zero-size box in {xml_file}") continue x_center = (xmin + xmax) / 2.0 / w_img y_center = (ymin + ymax) / 2.0 / h_img box_w = w_box / w_img box_h = h_box / h_img lines.append(f"0 {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") if lines: txt_path.write_text("\n".join(lines) + "\n", encoding="utf-8") return True else: print(f"[skip] no valid target: {xml_file}") return False xml_files = sorted(glob.glob(str(voc_dir / "*.xml"))) converted = [convert_label(Path(p)) for p in xml_files] print(f"converted {sum(converted)} / {len(xml_files)}")这段脚本的逻辑分四步:先从 XML 里读出图像宽高,再遍历所有 object 节点,然后对每个 bndbox 做越界裁剪和退化框过滤,最后把像素坐标转成归一化坐标写入 txt。其中xml_file.stem用来拿不含后缀的文件名,保证 txt 和图片同名,这是 YOLO 找标签的唯一依据。
参数上要留意class_name = "blind_road"。盲道检测这类单类别数据里类名通常叫 blind_road 或 tactile_paving,但标注者不一定用这个名字,必须先从 Annotations 里抽一个 xml 看一眼<name>的实际内容。如果压缩包里已经有 labels 目录,不要直接覆盖,先抽几行 xml 和 txt 对比一下数值,确认原本的 YOLO 标签转换没问题再决定保留谁的。
3.2 类别编号与 difficult 框:YOLO 标签行第一列不是 1
YOLO 的类别编号从 0 开始,COCO80 怎么读在 YOLO 里也是一样的:names 文件第一行对应 id 0,第二行对应 id 1,没有“从 1 开始数”的说法。单类数据里 blind_road 映射成 0 没有争议;但以后要是追加普通路面、斑马线、公交站台这些类,必须固定 names 文件顺序,不然旧标签和新标签的 id 错位,模型学到的是错配语义,mAP 再高也没用。
VOC 标注里偶尔会有<difficult>1</difficult>,表示这个框内容确实存在但很难辨识。检测训练要不要过滤 difficult 框是个取舍。助盲场景里盲道被树叶、杂物遮挡本来就是常态,直接过滤会让模型对遮挡鲁棒性更差;但如果这批数据里 difficult 框错标太多,就加一行判断把它跳掉。我一般倾向保留,先训练再根据 badcase 决定清理策略。
除了转换,还要把数据按 train 和 val 分开。ImageSets/Main/train.txt 里存的是图片 ID,不是完整路径,用 shell 循环就能复制:
mkdir -p images/train labels/train while read sid; do [ -f "JPEGImages/${sid}.jpg" ] && cp "JPEGImages/${sid}.jpg" images/train/ [ -f "labels/${sid}.txt" ] && cp "labels/${sid}.txt" labels/train/ done < ImageSets/Main/train.txt这段命令把图片和标签按同一份 ID 列表复制到 train 目录,val 用同样写法跑一遍。注意labels/里的 txt 要和images/train下的 jpg 一一对应;YOLO 不会帮你自动配目录,它只看见images/train的兄弟目录labels/train。如果复制时漏了某个 txt,那一张图等于没有标签,训练时会被静默跳过。
3.3 转换后的可视化抽查:一张 check 图比十个指标都值
转换完必须画框看一眼。坐标偏移这种事,看 loss 曲线看不出来,但画出来一眼就懂。
import cv2 img_path = "images/train/0001.jpg" label_path = "labels/train/0001.txt" img = cv2.imread(img_path) h, w = img.shape[:2] for line in open(label_path, encoding="utf-8"): cls, 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) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.imwrite("check.jpg", img)这里用img.shape[:2]拿的是实际解码出来的图像尺寸,不是 XML 里的 size。如果 XML 的 size 和真实 jpg 长宽不一致,画出来的框就会整体偏移或拉伸。转换脚本里用的是 XML size,所以抽查时要特别关注意见“框对不上盲道”的情况。训练前把这类可视化跑一遍,比训练完再查 mAP 省事得多。
提示:图像如果带 EXIF 旋转信息,
cv2.imread默认不解旋,可能和标注者看到的方向不一致。抽查发现所有框都旋转了 90 度,先用 PIL 按 EXIF 把方向归一再查。
4. 用 YOLOv8 训练自己的数据集:最小命令与参数该调什么
标注和目录确认后,进入训练环节。我一般用 Ultralytics YOLOv8,因为它把数据加载、训练、验证、导出绑在一条命令里,对 2173 张这种规模的数据集,跑通很快。模型先选 n 或 s,不要选 x;单类目标用大模型只会增加部署成本,边际收益非常有限。
4.1 组织 data.yaml 并跑通第一条训练命令
data.yaml 不要沿用压缩包里可能存在的旧配置,最好自己写一份路径干净的。YOLO 对path、train、val的解析很直接,你把数据根目录写绝对路径,能少踩很多坑。
# blindside.yaml path: /data/blindside train: images/train val: images/val names: 0: blind_road这里train和val是相对path的目录。图片放到images/train,标签就默认去同级的labels/train找。如果标签目录不叫 labels,训练会提示找不到 label。
训练命令最小化:
cd /data/blindside yolo detect train data=blindside.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ device=0这段命令用预训练的 YOLOv8n 权重在 2173 张图上微调 100 个 epoch。imgsz=640是兼顾检测精度和后续 TensorRT 部署的常规值;如果你在 T4 上做实时检测,不要一上来就 1280,边界框精度不一定赚回来,帧率一定亏。batch=16在 16G 显存的 T4 上跑 n 模型没压力,8G 卡就把 batch 降到 4 或 8。
训练时不要盯着终端输出一分钟刷新一次,去看runs/detect/train/results.csv,里面有 box_loss、cls_loss、dfl_loss、mAP50、mAP50-95 每一轮的完整记录。训练突然中断后想续跑,直接加resume=True,不用重新起一轮。
4.2 进入调参前,先看损失函数三个组成
YOLOv8 的损失函数不是单一 loss,而是由 bbox 回归损失、分类损失和 DFL 损失加权相加。在官方超参里对应的字段是 box、cls、dfl,默认大概在 box=7.5、cls=0.5、dfl=1.5 这个量级。DFL 让回归输出集中在边界框边缘附近,对盲道砖块这种细长连续目标比较有用。
单类检测任务里,分类不再是瓶颈,所以cls权重可以适当降低,把更多梯度留给box和dfl。我一般先把超参写成一个小文件,再喂给训练命令:
# hyp_blindside.yaml box: 8.0 cls: 0.3 dfl: 1.5训练时加上hyp=hyp_blindside.yaml即可。不要一次把 box 提到 15,边界框回归会因为梯度过大在训练前期震荡,loss 反而更难看。正确做法是先小步改,看 three loss 的相对变化:box_loss 降得慢,增加 box;cls_loss 降得过快,降 cls。如果出现 NaN loss,最先怀疑的是 AMP 和 batch size,不是数据。
4.3 单类盲道任务,最常改的其实不是超参
比损失函数权重更值得调的是数据增强。YOLO 默认打开 mosaic、scale、fliplr。mosaic 对 2173 张小数据集的帮助很大,相当于每张图都在跟另外三张图拼样本,能缓解数据量不足的问题。但盲道有方向性纹理,如果你的项目最终要输出“左侧盲道还是右侧盲道”的方向语义,fliplr=0.5会让方向信息直接翻转;只有检测框的话一般无所谓。
命令行可以直接覆盖:
yolo detect train data=blindside.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ device=0 \ fliplr=0.5 \ mosaic=1.0 \ scale=0.5scale=0.5控制随机缩放幅度,不需要动;fliplr按你的业务决定,做助盲提醒时我倾向于关掉,否则模型学到的“左/右”特征会被翻转增强弄混。真正需要反复试的是degrees和translate,盲道不会正着出现在画面里,视角歪一点很常见,但degrees超过 30 会把砖块纹路拼坏,先设在 10 左右试。
5. 盲道检测数据集常见问题:5 个训练前必须排查的坑
下面的问题都是训练盲道检测数据时容易被忽略、但一旦出现就非常浪费时间的坑。每条按现象、原因、解决的顺序写,是我翻车换来的血泪经验。
5.1 训练日志一直显示 no labels found,mAP 一直是 0
现象:YOLO 已经开始跑 epoch,但每轮都提示找不到标签,precision 和 recall 一直是 0。
原因:data.yaml 的 train 路径写成了images/train,但 labels 目录不在labels/train下;或者path写的是相对路径,而当前工作目录并不在数据根目录。
解决:先确认目录命名,再确认path写绝对路径。用下面三条命令自查:
find /data/blindside/labels -type f -name '*.txt' | wc -l find /data/blindside -maxdepth 2 -type d head -3 /data/blindside/images/train/0001.txt第一行看标签数量,第二行看目录结构,第三行实际读一条标签内容。如果 txt 内容是空的,说明转换阶段就没有写入有效框,回到第三章脚本看输出的 skip 日志。
5.2 loss 降了,验证集框整体往左上偏移
现象:训练 loss 正常下降,但可视化预测时,盲道检测框整体偏左上或偏右下,和真实盲道对不齐。
原因:VOC XML 里的<size>和真实 jpg 长宽不一致。常见情况是标注者先在缩略图上标注,后来图片被重新保存或裁剪,XML 的 size 没跟着更新。
解决:转换时用实际图片尺寸代替 XML size。
import cv2 h_img, w_img = cv2.imread(str(img_dir / (xml_file.stem + ".jpg"))).shape[:2]把脚本里读<size>的逻辑换成这一行。如果原始图像带 EXIF 旋转,先统一转正再取宽高。标注偏移这种问题,训练前画 check 图能拦下,训练后才发现的成本高很多。
5.3 标签里出现 x_center=1.0 或 width=1.2
现象:训练时某个 step 的 loss 突然飙到十几,或者模型输出一堆贴边的框。
原因:VOC 标注框有一部分跑到了图像外,xmax 或 ymax 大于 w_img、h_img,归一化后出现了大于 1 的坐标。
解决:转换脚本里先做 clip 再归一化,已经写了;但如果你用的是压缩包自带的 labels,需要自己扫一遍。
awk '{if($3<=0 || $4<=0 || $3>=1 || $5>=1) print FILENAME, $0}' labels/train/*.txt | head$3是 x_center,$4是 y_center,$5是宽。这个命令会把越界行打出来。越界框直接删掉或裁掉,不要硬留着训练;模型会把“贴边目标”学成常态,部署后误检率会高很多。
5.4 7z 解压到一半 CRC Error,重试还是一样
现象:7z x解压到一半报 CRC Failed,图片数量始终对不上 2173。
原因:压缩包本身下载不完整,或者解压目标在机械硬盘/网络存储上,坏扇区和同步延迟会让 7z 无法完成校验。
解决:先7z t 文件名.7z完整测一遍,报 CRC 就重新下载。解压时不要直接拖到 Windows 桌面,也不要解到网络共享盘;在本地 SSD 上建一个纯英文目录再解。7-Zip 的侧边栏压缩文件树适合快速看结构,不适合当最终工作目录直接从压缩包里读文件。
5.5 GPU 利用率只有 30%,训练一个 epoch 要半天
现象:nvidia-smi看到显存占着,GPU-Util 只有 20% 到 40%,训练非常慢。
原因:图片是分散的小文件,CPU 读取速度跟不上 GPU 消费速度。尤其数据放在机械硬盘或网络盘时,DataLoader 成了瓶颈。
解决:调大 workers,并且把数据缓存到内存或本地磁盘。
yolo detect train data=blindside.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ device=0 \ workers=8 \ cache=ramworkers=8让数据加载多进程并行;cache=ram把图片一次性加载进内存。2173 张图如果单张几百 KB,内存总量是够的;内存紧张就把cache=ram换成cache=disk。跑完一个 epoch 再调超参,别在数据瓶颈下白等。
提示:这类“模型该不该换”的问题先别多想。单类盲道检测,YOLOv8n 或 s 已经够用;先修数据加载,再修 badcase,最后才轮到网络结构。
6. 验证与部署边界:先修 badcase,再用 TensorRT 估路数
训练结束先做正式验证,再看部署。很多工程师习惯只看 mAP,但盲道检测这种场景,漏检比误检危险得多。先看验证集里的漏检帧,再去谈 T4 上能跑多少路。
6.1 用 val 和真实片段看漏检,而不是只看 mAP
yolo detect val model=runs/detect/train/weights/best.pt \ data=blindside.yaml \ imgsz=640 \ batch=32 \ split=val这条命令输出 precision、recall、mAP50、mAP50-95。对助盲场景,recall 优先于 precision;错过一块盲道砖,可能直接导致导航判断失误。mAP50-95 在单类目标上不用太纠结,它的区间拉长后对小目标不友好,盲道砖块在画面里通常不是小目标。
验证完还要做一次真实片段回放。把测试视频丢给模型:
yolo detect predict model=runs/detect/train/weights/best.pt \ source=test_sidewalk.mp4 \ save_txt=True \ save_conf=Truesave_txt会输出每帧检测框的归一化坐标,save_conf会带上置信度。跑完后按时间轴翻一翻,重点看盲道被树叶遮挡、盲道断头、人行道砖块和盲道砖块混在一起这三类帧。如果这类帧漏检严重,说明训练数据里缺遮挡样本,只调超参救不回来。
6.2 TensorRT 导出与 T4 单卡并发估算
模型要上实时检测,我通常导出成 TensorRT engine。训练好的 best.pt 在 T4 上转 engine:
yolo export model=runs/detect/train/weights/best.pt \ format=engine \ device=0 \ half=True \ workspace=4half=True用 FP16 半精度,T4 的 FP16 支持很好;workspace=4限制 TensorRT 最大工作空间。注意导出的 engine 绑定 GPU 架构和 TensorRT 版本,同一台 T4 上生成的文件,复制到另一台 GPU 上通常不能直接用,需要重新导一次。
很多人拿到这种数据集训练完会问:T4 1080p25帧每秒用TensorRT YOLO 640分辨率检测可以支持多少路。这个问题不能只看模型前向推理毫秒数。YOLOv8n 在 T4 上 FP16 engine 单帧大约 2 到 4 毫秒,YOLOv8s 大约 4 到 8 毫秒;按 n 模型算,单路 25fps 的帧间隔是 40 毫秒,理想并发量接近 10 路。但 RTSP 拉流、图像缩放、NMS 后处理都会抢资源,实际压测能稳定跑到六到八路就很好了,还要给主控留 30% 左右的余量,别把 GPU 推到 95% 利用率。
如果助盲终端用的是 D435i 这类深度相机,测距不用另外训网络。检测框拿到后,取框中心附近深度值的中位数,再按距离阈值决定要不要提醒,比硬套一个端到端深度估计网络简单得多,也更稳。
这个数据集后续如果要扩展,可以往鸟瞰视角方向想。无人机看盲道时,水平框表达不了连续方向变化,那就要参考 mmrotate 训练 DOTA、HRSC2016 那类旋转框数据的流程,设计旋转框的标注和评估方式;但那是另一套工程,不适合在 2173 张的单类水平框数据上做过路桥。
最后说一个我自己的教训:第一次用这种 VOC+YOLO 数据集时,我把 train 和 val 分得太随意,造成同一条街的照片同时出现在两个集合里,结果 mAP 很好看,助盲实测一跑就翻车。现在不管数据多干净,我都先按场景分组,再抽 val,最后再做一轮 badcase 回放。这个习惯帮你省下的时间,比多训三组超参还值。希望帮到你。
本文还有配套的精品资源,点击获取