简介:导盲犬拐杖检测数据集配套标注资源,面向目标检测学习者和开发者,聚焦“导盲犬”与“白色拐杖”两类目标。数据基于LabelImg绘制矩形框,VOC与YOLO格式均可用于常见检测框架,适合进行模型训练、验证或算法对比。包内文件总计2000个,其中包含1999个XML标注文件与1个使用说明TXT,压缩包大小约286.53MB。标注覆盖两类物体,导盲犬框数1620,白色拐杖框数4430,共计6050个标注框,类别信息明确。需要留意,数据集中部分图片为增强图片,也有来自视频连续截取场景的样本,使用前建议逐类核查。目前已有89人学习下载,可用作迁移学习、数据增强实验或检测效果评估的参考数据集。包内仅提供准确合理的标注文件,不承诺训练精度,也不包含模型权重。
1. 导盲犬拐杖检测数据集:两个类别撑起的专用目标检测任务
导盲犬拐杖检测数据集把目标锁定在视障人士出行最常见的两个参与方:导盲犬和拐杖(白杖)。在通用行人检测里这两个目标通常被归入行人或直接当背景,导致辅助出行设备在路口、地铁站这类场景下经常漏检。这份VOC+YOLO格式的双标注数据集提供4635张图片、2个类别,把标注格式的兼容问题一次解决:VOC用于兼容旧标注流程和后续语义分割再加工,YOLO格式开箱就能喂给YOLOv5/YOLOv8训练。对做目标检测的工程师来说,它既是练手微调的现成语料,也是验证小目标检测、类别不平衡处理的标准基准。下面按“格式解读→转换划分→训练调参→阈值调优”这条线完整走一遍,每步都给可复用代码。
2. 数据集结构与格式差异:VOC的XML和YOLO的txt差在哪
2.1 4635张、2类别的规模意味着什么
目标检测数据集不是越大越好,关键在于场景覆盖和标注一致性。4635张对“导盲犬+拐杖”这种专用场景属于中等偏上规模,按常见的8:1:1划分大约能得到3700张训练图,370张验证图,463张测试图。训练图够模型见过导盲犬的不同品种体态、拐杖的折叠与倾斜状态,又不至于大到标注噪声失控。两个类别看起来少,但导盲犬和拐杖内部差异极大,导盲犬存在拉布拉多、金毛等不同体型,拐杖有折叠式、盲杖和红白配色之分,实际训练难度接近某些5到10类的工业质检场景。
拿到数据集后第一件事不是解压看图片,而是核对目录结构。VOC风格常见布局是JPEGImages放原图、Annotations放XML、ImageSets/Main放train.txt之类的划分文件;YOLO风格则是images和labels两个平级目录,labels里每个txt文件名与图片文件名一一对应。解压后我一般先跑一遍统计脚本,确认图片数量、标注框数量和类别分布三者对得上,避免训练到一半才发现某类标注缺失。
2.2 VOC格式的XML标注怎么读
VOC标注的核心是XML里每个object节点下的bndbox,提供目标框左上角和右下角坐标,坐标单位是像素。下面这段代码用ElementTree解析XML,统计每个类别的框数量和尺寸分布,处理几百个文件时比逐个打开看快得多。
import xml.etree.ElementTree as ET from pathlib import Path def parse_voc_xml(xml_path): tree = ET.parse(xml_path) root = tree.getroot() objects = [] for obj in root.iter('object'): name = obj.find('name').text box = obj.find('bndbox') xmin = float(box.find('xmin').text) ymin = float(box.find('ymin').text) xmax = float(box.find('xmax').text) ymax = float(box.find('ymax').text) objects.append({ 'name': name, 'bbox': [xmin, ymin, xmax, ymax], 'difficult': obj.find('difficult').text if obj.find('difficult') is not None else '0' }) return root.find('filename').text, objects # 统计用法:遍历标注目录,聚合每个类别的数量和平均宽高 stats = {} for xml_file in Path('Annotations').glob('*.xml'): _, objs = parse_voc_xml(xml_file) for o in objs: name = o['name'] box = o['bbox'] stats.setdefault(name, []).append((box[2]-box[0], box[3]-box[1])) for name, boxes in stats.items(): avg_w = sum(b[0] for b in boxes) / len(boxes) avg_h = sum(b[1] for b in boxes) / len(boxes) print(f'{name}: {len(boxes)} boxes, avg size {avg_w:.1f}x{avg_h:.1f}')脚本里difficult字段值得注意。VOC规范里difficult为1表示该目标难以辨认,YOLO格式没有对应概念,转格式时要么丢弃这些框,要么保留并在训练时降低权重。我的做法是difficult=1的框直接过滤,因为导盲犬被遮挡或拐杖隐藏在身体后时,标注本身已不可靠,强行保留反而干扰损失计算。类别名是字符串,但YOLO只认数字编号,所以下一步必须先建立类别到序号的映射表。
2.3 YOLO格式的txt标注怎么校验
YOLO标注每个txt只有四行数据以外的东西不需要,核心是每行五个数字:类别序号、归一化中心x、归一化中心y、归一化宽度w、归一化高度h。所有的坐标都被除以了图片宽高,数值范围理论在0到1之间。格式转换前要先校验旧有标注有没有越界,因为LabelImg在手动标注时偶尔会拖出图片边界,导致某些框的w或h大于1。
from pathlib import Path def validate_yolo_txt(txt_path, img_w, img_h): errors = [] for line_num, line in enumerate(Path(txt_path).read_text().strip().splitlines(), 1): parts = line.split() if len(parts) != 5: errors.append(f'line {line_num}: expected 5 fields, got {len(parts)}') continue cls, cx, cy, w, h = parts cx, cy, w, h = map(float, (cx, cy, w, h)) if w <= 0 or h <= 0: errors.append(f'line {line_num}: non-positive size {w}x{h}') if not (0 <= cx <= 1 and 0 <= cy <= 1): errors.append(f'line {line_num}: center out of range') if cx + w/2 > 1.0 or cx - w/2 < 0.0: errors.append(f'line {line_num}: bbox crosses left/right border') return errors逻辑说明:这里把txt里的中心坐标和宽高还原成相对于图片的绝对范围,检查中心点是否落在图像内、宽高是否大于0、框是否越出左右边界。参数img_w和img_h来自对应图片的实际尺寸,在校验时可以用cv2.imread或PIL读取,不要在脚本里硬编码,因为数据集里图片尺寸可能不统一。
2.4 7z解压与完整性检查
7z压缩包对目标检测数据集很常见,因为它把小文件碎图的压缩率控制得比tar.gz更好。Linux下先确认装了p7zip,再用t参数测试压缩包完整性,最后解压到指定目录,养成先测试后解压的习惯能省掉不少纠错时间。
sudo apt install p7zip-full 7z t dataset.7z mkdir -p dataset && 7z x dataset.7z -odataset/命令参数说明:7z t只测试不释放文件,输出CRC校验结果,看到“Everything is Ok”才继续;-o后面紧跟输出目录时注意-o和目录之间没有空格,写错了会解压到当前路径。经常遇到的“7z解压到一半报错”大概率是压缩包不完整或下载中断,这时重新转存原包而不是强行修复。如果解压后中文文件名乱码,执行环境加上LANG=zh_CN.UTF-8再跑一次即可。
| 对比项 | VOC格式 | YOLO格式 |
|---|---|---|
| 标注文件后缀 | xml | txt |
| 坐标体系 | 像素绝对坐标(xmin, ymin, xmax, ymax) | 归一化相对坐标(cx, cy, w, h) |
| 类别表示 | object节点下的name字符串 | 每行第一个整数序号 |
| 目标信息 | 有difficult、truncated、pose等字段 | 只保留类别和框 |
| 图片与标注关联 | filename节点或文件名前缀 | 文件名前缀一致 |
3. 把VOC转成YOLO格式并划分训练集
3.1 从XML到txt的坐标换算脚本
VOC转YOLO的公式就是把绝对坐标转成相对坐标。中心点x等于xmin和xmax的平均值再除以图片宽度,框宽等于xmax减xmin再除以图片宽度,y方向同理。写成脚本时不能只处理单张图,而是遍历整个Annotations目录,按图片名找到对应jpg读取宽高,转换成txt写入labels目录。核心转换函数如下。
import xml.etree.ElementTree as ET from pathlib import Path import cv2 class_map = {'guide_dog': 0, 'cane': 1} # 顺序必须固定 def voc_to_yolo(xml_path, images_dir, labels_dir): tree = ET.parse(xml_path) root = tree.getroot() img_name = root.find('filename').text img_path = Path(images_dir) / img_name img = cv2.imread(str(img_path)) h, w = img.shape[:2] lines = [] for obj in root.iter('object'): if obj.find('difficult') is not None and obj.find('difficult').text == '1': continue name = obj.find('name').text if name not in class_map: continue box = obj.find('bndbox') xmin = float(box.find('xmin').text) ymin = float(box.find('ymin').text) xmax = float(box.find('xmax').text) ymax = float(box.find('ymax').text) # 越界像素坐标先裁剪回图像范围内再归一化 xmin = max(0, min(xmin, w - 1)) xmax = max(0, min(xmax, w - 1)) ymin = max(0, min(ymin, h - 1)) ymax = max(0, min(ymax, h - 1)) cx = ((xmin + xmax) / 2) / w cy = ((ymin + ymax) / 2) / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h lines.append(f'{class_map[name]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}') out_path = Path(labels_dir) / (Path(img_name).stem + '.txt') out_path.write_text('\n'.join(lines), encoding='utf-8')逻辑说明:归一化前先做一次像素坐标裁剪,防止标注框超出图片边界的数值参与计算。class_map的编号顺序决定了txt里每个类别的数字,这个映射在后续data.yaml和训练结果里必须保持一致,导盲犬是0、拐杖是1,训练出来的类别输出索引才不会错乱。
3.2 用固定随机种子切分train/val
数据集划分最常见的错误是每次运行随机种子不一致,导致val集泄露到train集,训练指标虚高。固定随机种子的同时,还要保证图片和标注文件一起移动。
import random from pathlib import Path import shutil random.seed(42) images = sorted(Path('JPEGImages').glob('*.jpg')) random.shuffle(images) total = len(images) val_count = int(total * 0.15) test_count = int(total * 0.05) split = {'train': images[:total - val_count - test_count], 'val': images[total - val_count - test_count: total - test_count], 'test': images[total - test_count:]} for split_name, img_paths in split.items(): (Path('dataset') / split_name / 'images').mkdir(parents=True, exist_ok=True) (Path('dataset') / split_name / 'labels').mkdir(parents=True, exist_ok=True) for img_path in img_paths: shutil.copy(img_path, Path('dataset') / split_name / 'images' / img_path.name) label_path = Path('labels') / (img_path.stem + '.txt') if label_path.exists(): shutil.copy(label_path, Path('dataset') / split_name / 'labels' / label_path.name)参数说明:val分配15%约700张,足以评估模型在导盲犬和拐杖上的泛化能力;test只有5%是为了保留独立的最终评测集,避免反复拿val调参过拟合。如果数据集整体只有几千张,建议val不低于300张,太少的验证集会因为某些图片恰好难检测导致mAP波动很大。
3.3 用data.yaml锁住类别顺序
YOLOv8训练时的数据配置写在data.yaml里,path指到数据集根目录,train和val写子目录名,nc是类别数2,names列表的顺序必须和3.1节class_map的编号完全一致。
path: ./dataset train: train/images val: val/images test: test/images nc: 2 names: ['guide_dog', 'cane']yaml的注意事项:names列表不能写成两个类别的随机顺序,因为训练输出结果中的class id直接映射到names下标。训练脚本开始后会打印类似2 nc的模型输出,可以核对类别名是否和预期一致。真的想反向确认转换结果时,把val目录里的txt画到图片上导出,与VOC原XML画出的框逐张对比,个人习惯抽10张就够了,能发现常见的坐标轴颠倒和归一化除以错误边长这类问题。
4. 用YOLOv8训练导盲犬拐杖检测模型
4.1 环境与数据集准备
训练环境用conda建独立虚拟环境,不然torch和CUDA版本冲突会浪费大量时间。ultralytics包同时带训练和推理命令行,一条pip命令装完。
conda create -n yolo python=3.10 -y conda activate yolo pip install ultralytics torch torchvision python -c "import torch; print(torch.cuda.is_available())"最后一行输出True才继续,否则在CPU上训练4635张图的两个类别,yolov8n也要跑十几个小时。GPU显存低于6GB时建议选yolov8n或yolov8s,而不是直接上yolov8x,这个数据规模用不上大模型。
4.2 训练命令和六个关键参数
训练入口是命令行,数据路径指向刚才的data.yaml,权重用yolov8n.pt作为起点做迁移学习。迁移学习对这个数据规模很重要,导盲犬和拐杖的边缘纹理虽然特殊,但通用特征已经在COCO预训练里学过了,从零训练反而容易过拟合。
yolo detect train data=data.yaml model=yolov8n.pt \ epochs=120 imgsz=640 batch=16 patience=30 \ optimizer=AdamW lr0=0.001| 参数 | 建议值 | 说明 |
|---|---|---|
| epochs | 100~150 | 迁移学习下两个类别120轮足够收敛,太多容易在450张验证集上过拟合 |
| imgsz | 640或960 | 拐杖是细长目标,640能平衡速度;如果拐杖在图中占比很小再试960 |
| batch | 显存能承载的最大值 | 16以下BN统计不稳定,两个类别的小数据集尤其明显 |
| patience | 20~30 | 验证集mAP连续30轮不涨就早停,省时间 |
| optimizer | AdamW或SGD | 迁移学习用AdamW收敛稳定,SGD需要更长轮数 |
| lr0 | 0.001~0.01 | 预训练权重下0.001更稳,0.01要配合warmup并观察loss发散风险 |
两类目标在场景中出现的频率可能不均衡,拐杖作为手持物出现次数通常高于导盲犬。训练时可以在criterion策略上做点调整:YOLOv8的损失函数默认对类别和框损失等权处理,如果训练后导盲犬的召回率明显低于拐杖,优先检查是不是类别样本量差异大于3倍。样本悬殊过大时用class_weight结合原数据集补正比盲目改损失权重更直接,两个类别各自计算出现次数,把稀有类别的权重上调。
4.3 从损失曲线到best.pt的取舍
训练结束后看runs/detect/train下的results.png,重点看val/box_loss和val/cls_loss是否同步下降。如果box_loss收敛但cls_loss波动剧烈,说明模型对导盲犬和拐杖的类别区分度不够,优先增加数据增强而不是加深网络。weights目录里best.pt和last.pt的选择,训练被早停时last.pt在验证集上表现不一定比best.pt差,因为早停只参考mAP,last.pt可能正好跳过泛化低谷,我的做法是两个权重都在test集上跑一遍对比,而不是默认取best.pt。
推理验证模型的泛化效果,直接从数据集外取几张真实场景图,用最低成本看模型是否把类似颜色的物体误识别成拐杖。
yolo detect predict model=runs/detect/train/weights/best.pt \ source=./samples/ conf=0.25 iou=0.5参数说明:conf是检测置信度阈值,低于该分数的框全部丢弃,0.25是偏低的,更适合先看模型有哪些候选,再决定业务上线阈值;iou是NMS阶段的交并比阈值,0.5表示两个框重叠超过这个比例就合并。真正部署时要先统计验证集上的置信度分布,再定阈值,不能直接沿用0.25。
5. 置信度阈值调整与单类别误检排查
5.1 用验证集搜索最优conf
YOLO预测接口返回每个框的原始置信度,可以遍历一系列置信度阈值,计算每个阈值下的精确率和召回率,选择F1最高的阈值作为业务默认值。这里用验证集的原因在于test集要留到最后一次评估,常调参会让test集的参考意义贬值。
from ultralytics import YOLO model = YOLO('runs/detect/train/weights/best.pt') results = model.val(data='data.yaml', conf=0.001, iou=0.5) best_f1, best_conf = 0, 0.25 for conf in [0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.5]: precision, recall = compute_metrics(results, conf) f1 = 2 * precision * recall / max(precision + recall, 1e-9) if f1 > best_f1: best_f1, best_conf = f1, conf print(f'best conf: {best_conf}, F1: {best_f1:.3f}')逻辑说明:compute_metrics需要根据标注框和预测框的IoU匹配结果按conf重算TP/FP/FN,ultralytics的val结果包含了所有原始检测框,在循环里只做阈值过滤,不用重复跑前向推理。索引核心参数是conf和iou,前者控制框的置信度门限,后者控制与真实框的匹配程度,业务场景要求漏检率低就选F1偏左的阈值而不是取最大值。
5.2 部署阶段按类别分开设阈值
导盲犬和拐杖的应用目标完全不同:拐杖误检可能只是提示噪音,导盲犬漏检则直接影响安全决策。正式部署时每个类别独立设置置信度阈值比全局统一阈值更合理。做法是根据验证集分别统计两个类别的F1曲线,给拐杖定一个较高的conf下限减少误报,给导盲犬保留更低的conf兜住召回。如果导出为ONNX后推理,后处理脚本里分析model.predict返回的confidences数组,对class id为0和1的检测结果分别过滤,NMS的iou阈值保持0.5不变。最后把验证集上每个类别的精确率和召回率单独打印出来,与训练时打印的mAP做横向对比,确认两个类别的性能差距是否在接受范围内。
本文还有配套的精品资源,点击获取