ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

导盲犬拐杖检测数据集与YOLOv8训练实战:从VOC转YOLO到阈值调优

导盲犬拐杖检测数据集与YOLOv8训练实战:从VOC转YOLO到阈值调优 简介导盲犬拐杖检测数据集配套标注资源面向目标检测学习者和开发者聚焦“导盲犬”与“白色拐杖”两类目标。数据基于LabelImg绘制矩形框VOC与YOLO格式均可用于常见检测框架适合进行模型训练、验证或算法对比。包内文件总计2000个其中包含1999个XML标注文件与1个使用说明TXT压缩包大小约286.53MB。标注覆盖两类物体导盲犬框数1620白色拐杖框数4430共计6050个标注框类别信息明确。需要留意数据集中部分图片为增强图片也有来自视频连续截取场景的样本使用前建议逐类核查。目前已有89人学习下载可用作迁移学习、数据增强实验或检测效果评估的参考数据集。包内仅提供准确合理的标注文件不承诺训练精度也不包含模型权重。1. 导盲犬拐杖检测数据集两个类别撑起的专用目标检测任务导盲犬拐杖检测数据集把目标锁定在视障人士出行最常见的两个参与方导盲犬和拐杖白杖。在通用行人检测里这两个目标通常被归入行人或直接当背景导致辅助出行设备在路口、地铁站这类场景下经常漏检。这份VOCYOLO格式的双标注数据集提供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格式没有对应概念转格式时要么丢弃这些框要么保留并在训练时降低权重。我的做法是difficult1的框直接过滤因为导盲犬被遮挡或拐杖隐藏在身体后时标注本身已不可靠强行保留反而干扰损失计算。类别名是字符串但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(fline {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(fline {line_num}: non-positive size {w}x{h}) if not (0 cx 1 and 0 cy 1): errors.append(fline {line_num}: center out of range) if cx w/2 1.0 or cx - w/2 0.0: errors.append(fline {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解压到一半报错”大概率是压缩包不完整或下载中断这时重新转存原包而不是强行修复。如果解压后中文文件名乱码执行环境加上LANGzh_CN.UTF-8再跑一次即可。对比项VOC格式YOLO格式标注文件后缀xmltxt坐标体系像素绝对坐标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), encodingutf-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(parentsTrue, exist_okTrue) (Path(dataset) / split_name / labels).mkdir(parentsTrue, exist_okTrue) 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是类别数2names列表的顺序必须和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 python3.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 datadata.yaml modelyolov8n.pt \ epochs120 imgsz640 batch16 patience30 \ optimizerAdamW lr00.001参数建议值说明epochs100150迁移学习下两个类别120轮足够收敛太多容易在450张验证集上过拟合imgsz640或960拐杖是细长目标640能平衡速度如果拐杖在图中占比很小再试960batch显存能承载的最大值16以下BN统计不稳定两个类别的小数据集尤其明显patience2030验证集mAP连续30轮不涨就早停省时间optimizerAdamW或SGD迁移学习用AdamW收敛稳定SGD需要更长轮数lr00.0010.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差因为早停只参考mAPlast.pt可能正好跳过泛化低谷我的做法是两个权重都在test集上跑一遍对比而不是默认取best.pt。推理验证模型的泛化效果直接从数据集外取几张真实场景图用最低成本看模型是否把类似颜色的物体误识别成拐杖。yolo detect predict modelruns/detect/train/weights/best.pt \ source./samples/ conf0.25 iou0.5参数说明conf是检测置信度阈值低于该分数的框全部丢弃0.25是偏低的更适合先看模型有哪些候选再决定业务上线阈值iou是NMS阶段的交并比阈值0.5表示两个框重叠超过这个比例就合并。真正部署时要先统计验证集上的置信度分布再定阈值不能直接沿用0.25。5. 置信度阈值调整与单类别误检排查5.1 用验证集搜索最优confYOLO预测接口返回每个框的原始置信度可以遍历一系列置信度阈值计算每个阈值下的精确率和召回率选择F1最高的阈值作为业务默认值。这里用验证集的原因在于test集要留到最后一次评估常调参会让test集的参考意义贬值。from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) results model.val(datadata.yaml, conf0.001, iou0.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(fbest conf: {best_conf}, F1: {best_f1:.3f})逻辑说明compute_metrics需要根据标注框和预测框的IoU匹配结果按conf重算TP/FP/FNultralytics的val结果包含了所有原始检测框在循环里只做阈值过滤不用重复跑前向推理。索引核心参数是conf和iou前者控制框的置信度门限后者控制与真实框的匹配程度业务场景要求漏检率低就选F1偏左的阈值而不是取最大值。5.2 部署阶段按类别分开设阈值导盲犬和拐杖的应用目标完全不同拐杖误检可能只是提示噪音导盲犬漏检则直接影响安全决策。正式部署时每个类别独立设置置信度阈值比全局统一阈值更合理。做法是根据验证集分别统计两个类别的F1曲线给拐杖定一个较高的conf下限减少误报给导盲犬保留更低的conf兜住召回。如果导出为ONNX后推理后处理脚本里分析model.predict返回的confidences数组对class id为0和1的检测结果分别过滤NMS的iou阈值保持0.5不变。最后把验证集上每个类别的精确率和召回率单独打印出来与训练时打印的mAP做横向对比确认两个类别的性能差距是否在接受范围内。本文还有配套的精品资源点击获取
返回列表