尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

指甲病变检测数据集实战:YOLO+VOC格式转换与YOLOv8训练指南

指甲病变检测数据集实战:YOLO+VOC格式转换与YOLOv8训练指南 简介针对指甲病变检测任务的目标检测数据集包含2923张原始JPG图片及对应标注覆盖肢端雀斑样痣黑、甲沟炎、甲弯曲、泰瑞氏甲四类常见指甲病变适合计算机视觉入门者、算法调参人员及医疗影像研究者直接用于YOLO或VOC格式的目标检测训练与验证。压缩包内按JPEGImages、Annotations、labels三个目录分别存放图片、XML标注和TXT标签并额外提供classes.txt用于核对类别顺序整包文件共2000个以XML标注和TXT配置信息为主压缩后容量约64.71MB便于按目录索引。标注统一使用矩形框总框数2930数据未做增强适合在此基础上自主扩展预处理与数据增强策略也可用于评估不同检测模型在指甲病变小样本场景下的表现。目前已有63人学习下载适合作为课题实验或算法对比的起步数据集。1. 2923张指甲病变检测数据集能做什么先看它的真实价值拿到一个叫“4种指甲病变检测数据集2923张YOLOVOC.zip”的压缩包第一反应别急着解压先想清楚这2923张图能帮你解决什么。这个数据集的直接用途是训练一个目标检测模型识别四种指定类型的指甲病变区域比如甲癣、甲沟炎、白甲、甲下出血之类具体类别要看包里的labels命名。它同时给了YOLO和VOC两种标注格式意味着你既可以拿去跑YOLOv8、YOLOv5这类主流检测框架也能用原生VOC工具链做二次标注或转成其他格式不用再人工做标注格式搬运。对做医疗影像辅助诊断、皮肤科信息化、美甲行业图像质检的人来说这个数据集的价值在于“少踩采集和标注的坑”。2923张图不算大但足够做迁移学习起点双格式能直接进训练流程省掉转换时间。适合人群是已经会用目标检测基础流程、想快速在指甲病变场景上跑通模型验证效果的工程师。我先帮你把目录结构、格式转换、训练参数到排错路径完整拆开。2. 拆开zip后的目录结构YOLO和VOC双格式到底怎么编排2.1 解包与目录清单拿到zip先别双击直接拖出来我习惯用命令行解压避免Windows资源管理器碰到权限或文件名乱码。常见做法是建一个专门的工作目录比如nail_dataset然后解压进去。mkdir -p nail_dataset cd nail_dataset unzip ../目标检测-4种指甲病变检测数据集2923张YOLOVOC.zip -d .解压后先看顶层结构通常会有images、Annotations、labels三个核心目录。images里是JPG或PNG原图Annotations里是VOC格式的XML文件labels里是YOLO格式的txt文件。有时会有classes.txt或data.yaml来记录类别名。find . -maxdepth 2 -type d | sort head -5 images/*.jpg 2/dev/null | head -5如果发现Annotations和labels里的文件名不是一一对应先别慌往下看。这种“双格式”数据集最常见的编排是每张图对应一个XML和一个txt文件名用同一个ID例如img_0001.jpg、img_0001.xml、img_0001.txt。但有些发布者会把labels和Annotations分成train/val两个子集所以解压后先统计文件数量对比。2.2 标注文件自检VOC的XML和YOLO的txt字段对照VOC格式的XML核心信息是folder、filename、size以及多个object块每个object里有name、pose、truncated、difficult和bndbox。YOLO格式的txt每行是class_id x_center y_center width height全部是归一化坐标取值范围0到1。看一个典型的XML片段annotation filenameimg_0001.jpg/filename size width1920/width height1080/height depth3/depth /size object namenail_fungus/name bndbox xmin522/xmin ymin310/ymin xmax781/xmax ymax655/ymax /bndbox /object /annotation对于同一张图YOLO txt大概是0 0.3393 0.4468 0.1350 0.3194这个0.3393怎么算用x_center (522 781)/2 / 1920width (781 - 522)/1920同理y和height。转换脚本就是在做这个算术。这里提醒一句如果某个txt里出现大于1的坐标值说明生成脚本没归一化或者原图尺寸读错了这属于训练必炸的脏数据。3. 数据质量校验不看标注就把数据喂给YOLO必翻车3.1 用Python统计类别分布、图片尺寸和标签坐标是否越界拿到双格式数据集第一步不是直接训练而是写脚本核对三件事类别分布是否均匀、图片尺寸是否一致、坐标是否越界。常见局是4种病变里某一种占80%另两种只有几十张这种数据直接训练稀有小类会被模型当成噪声忽略。import os from glob import glob from collections import Counter from PIL import Image img_paths sorted(glob(images/*.jpg)) sorted(glob(images/*.png)) label_paths sorted(glob(labels/*.txt)) print(fimages: {len(img_paths)} labels: {len(label_paths)}) # 类别统计 class_counter Counter() for txt_path in label_paths: with open(txt_path, r) as f: for line in f: parts line.strip().split() if len(parts) 5: class_counter[int(parts[0])] 1 print(class_counter) # 尺寸和越界统计 for img_path in img_paths[:5]: with Image.open(img_path) as im: w, h im.size print(img_path, w, h) for txt_path in label_paths[:5]: with open(txt_path) as f: for line in f: _, cx, cy, bw, bh map(float, line.strip().split()) if cx bw / 2 1.0 or cy bh / 2 1.0: print(f越界: {txt_path} - {line.strip()})这段代码逻辑很简单先统计标签类别ID分布再看几张图尺寸最后检查YOLO归一化坐标是否越界。参数说明里有两个点值得注意类别ID映射必须以classes.txt或data.yaml为准不能想当然认为0对应第一种病越界检查里我用了cx bw/2因为YOLO的cx和bw是中心点加宽高不是左上角坐标。如果你发现某张图片的标注框中心点加半宽大于1.0说明这个框超出图像右边界训练时YOLO会把这个框裁剪或者直接报样本错误最终表现为loss出现NaN或某类AP变成0。3.2 可视化bbox绘制脚本确认xml/txt对得上统计坐标只是第一步真正能暴露问题的是把bbox画回原图上看。常见病是xml和txt虽然文件名一样但对同一目标框的位置差很多或者类别名顺序不一致。这时候写个可视化脚本把两种格式的框分别画出来对比比看数字直观得多。import cv2 import xml.etree.ElementTree as ET def draw_yolo(img, txt_path, class_names): h, w img.shape[:2] with open(txt_path) as f: for line in f: cls, cx, cy, bw, bh map(float, line.strip().split()) x1 int((cx - bw/2) * w) y1 int((cy - bh/2) * h) x2 int((cx bw/2) * w) y2 int((cy bh/2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, class_names[int(cls)], (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) return img def draw_xml(img, xml_path, class_names): root ET.parse(xml_path).getroot() for obj in root.iter(object): name obj.find(name).text box obj.find(bndbox) x1 int(box.find(xmin).text) y1 int(box.find(ymin).text) x2 int(box.find(xmax).text) y2 int(box.find(ymax).text) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText(img, name, (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) return img idx img_0001 img cv2.imread(fimages/{idx}.jpg) img_y draw_yolo(img.copy(), flabels/{idx}.txt, [nail_fungus, nail_psoriasis, leukonychia, subungual_hemorrhage]) img_v draw_xml(img.copy(), fAnnotations/{idx}.xml, None) cv2.imwrite(check_yolo.jpg, img_y) cv2.imwrite(check_voc.jpg, img_v)这段脚本把同一图片的YOLO和VOC框分别画出来绿色是YOLO红色是VOC然后你肉眼对比。参数说明draw_yolo里我把归一化坐标乘图像宽高还原为像素值注意cx - bw/2是左边界加bw/2是右边界draw_xml直接读绝对像素坐标。如果两个图框位置不一致优先查xml里的filename字段是否指向其他图片。有些数据集发布时xml里的filename写成了相对路径或干脆写错导致脚本读错图。检查完成后把这些差异样本单独放一个文件夹后面训练时排除掉。4. 把VOC转成YOLO训练格式训练前的最后一步4.1 转换脚本样例与参数说明VOC格式虽然直观但YOLOv8和YOLOv5训练时直接用txt格式不需要你去改源码。最常见做法是写一个VOC转YOLO的脚本逐行读xml计算归一化中心坐标写入txt。注意这步不是简单格式变化还包含类别映射和忽略difficult样本。import os import xml.etree.ElementTree as ET from glob import glob CLASS_MAP {nail_fungus: 0, nail_psoriasis: 1, leukonychia: 2, subungual_hemorrhage: 3} def voc_to_yolo(xml_path, out_dir, img_base_dir): root ET.parse(xml_path).getroot() img_name root.find(filename).text size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) txt_name os.path.splitext(os.path.basename(img_name))[0] .txt with open(os.path.join(out_dir, txt_name), w) as f: for obj in root.iter(object): name obj.find(name).text if name not in CLASS_MAP: continue # difficult样本跳过避免训练被边缘样本干扰 difficult obj.find(difficult) if difficult is not None and difficult.text.strip() 1: continue box obj.find(bndbox) xmin int(box.find(xmin).text) ymin int(box.find(ymin).text) xmax int(box.find(xmax).text) ymax int(box.find(ymax).text) cx ((xmin xmax) / 2) / img_w cy ((ymin ymax) / 2) / img_h bw (xmax - xmin) / img_w bh (ymax - ymin) / img_h # 坐标截断到[0,1]防止浮点误差导致越界 cx, cy min(max(cx, 0.0), 1.0), min(max(cy, 0.0), 1.0) bw, bh min(bw, 1.0), min(bh, 1.0) f.write(f{CLASS_MAP[name]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}\n) os.makedirs(labels_conv, exist_okTrue) for xml_path in glob(Annotations/*.xml): voc_to_yolo(xml_path, labels_conv, images)这段脚本有两个关键参数CLASS_MAP是类别名到数字ID的映射必须和数据集中classes.txt完全一致difficult样本跳过因为指甲病变边界不清晰的样本如果标成difficultYOLO训练时不会正确处理这个标记。脚本最后输出到labels_conv目录不会覆盖你原本的labels算是给原始数据留了后悔药。如果你的原始标签txt已经存在建议先对比labels_conv和labels里的框数量差异大说明原始VOC和txt不是同一批标注版本以更规整的那份为准。4.2 划分train/val并生成data.yaml训练前要把图片和标签划分成train和val两个集合。常见比例是8:2或9:1但2923张图规模小我习惯用9:1划分验证集保留约292张。注意划分时按类别分布做分层采样避免某类全部跑进验证集导致训练时看不见。mkdir -p dataset/train/images dataset/train/labels mkdir -p dataset/val/images dataset/val/labels写一个简单的Python脚本按比例随机分配用random.seed(42)固定随机种子保证可复现import os, random, shutil from glob import glob random.seed(42) img_paths sorted(glob(images/*.jpg)) sorted(glob(images/*.png)) random.shuffle(img_paths) val_count int(len(img_paths) * 0.1) for i, img_path in enumerate(img_paths): prefix os.path.splitext(os.path.basename(img_path))[0] lbl_path flabels/{prefix}.txt if not os.path.exists(lbl_path): continue if i val_count: dst_img dataset/val/images dst_lbl dataset/val/labels else: dst_img dataset/train/images dst_lbl dataset/train/labels shutil.copy(img_path, dst_img) if os.path.exists(lbl_path): shutil.copy(lbl_path, dst_lbl)随后在dataset/下写data.yamlYOLOv8要求这个配置文件指定train、val路径和类别数、类别名path: ./dataset train: train/images val: val/images nc: 4 names: [nail_fungus, nail_psoriasis, leukonychia, subungual_hemorrhage]这里有个容易踩的坑path字段是相对yaml文件所在目录的路径而不是相对当前终端路径。我见过有人把path写成绝对路径换机器跑直接报找不到图片。建议所有路径全用相对路径并把data.yaml放在dataset目录内部这样整个项目文件夹可以整体拷到别的机器上不用改配置。5. 再用YOLOv8训练时的避坑清单5.1 训练必调参数从imgsz到batch的敏感性用YOLOv8训练指甲病变数据集命令行看起来很简单但参数不能全默认。首推yolov8n.pt或yolov8s.pt作为预训练权重不要从零训练。先确认环境里已经安装ultralytics包并准备好yolo预训练模型下载路径一般首次运行会自动下载到当前项目目录下。yolo detect train \ modelyolov8s.pt \ datadataset/data.yaml \ epochs120 \ imgsz640 \ batch16 \ device0 \ patience20 \ projectrun/nail \ nameexp1参数说明imgsz640是大多数情况的安全值但指甲病变区域在原始照片里可能只占很小比例如果你的图像是1920×1080原图建议用imgsz960或1280配合mosaic0.0否则小目标会被缩得太小。batch16取决于你的GPU显存如果V100 16G可以开32但先小一点跑起来。patience20是早停耐心值意思是20轮验证集指标不涨就自动停。有个容易忽略的点project和name参数能区分多次实验方便后面对比权重不要每次都写到默认的runs/detect下否则日志和权重会被覆盖。5.2 常见问题与排查故障列表训练YOLO最常见的四类问题我都遇到过逐条列一下。第一loss是NaN训练曲线突然掉到负值。现象是训练第几十轮时box_loss变成nan然后整个模型报废。原因一般是标签坐标越界或宽度高为0。排查方法很简单训练前用第3章的越界检查脚本扫一遍labels把越界标签删掉或修正。第二某类AP一直为0。现象是混淆矩阵里某一类完全没预测出来原因有两点这类样本只有几十张或者标注框太小被mosaic增强处理掉。解决方法是给少样本类别做复制增强或者把mosaic关闭、scale调低。第三训练正常但推理时框不准指甲边缘漏检。原因是标注框本身太紧或太松很多医疗数据集的框是标注者手动打的点边缘不统一。解决方法是重新审视标注质量必要时用roboflow这类工具二次微调。第四train和val的类别分布差异大。现象是训练loss很低但val的map只有0.3。原因是划分时没有分层采样。重新用4.2的方法按类别比例划分而不是纯随机。6. 验证模型会看哪几个指标从混淆矩阵到指甲边缘漏检6.1 在测试集上做推理并导出结果训练完成后不要只看最后的mAP还要看每类的precision、recall和PR曲线。YOLOv8会自动生成results.csv你打开就能看到每个类别的指标。更直观的做法是把模型在验证集图片上跑一遍导出带框的图片重点看边缘漏检和误检。yolo detect predict \ modelrun/nail/exp1/weights/best.pt \ sourcedataset/val/images \ conf0.25 \ saveTrue \ save_txtTrue \ projectrun/predict \ namenail_val参数说明conf0.25是置信度阈值指甲病变的框一般比较清晰可以开到0.3减少误报save_txtTrue会保存每张图的检测结果txt方便你做批量指标统计。跑完后去run/predict/nail_val/labels里看检测出的框和真实标签对比。我习惯把真实标签的txt和预测txt合到一个表计算每一类的IoU分布。如果某一类IoU中位数不到0.5说明模型定位能力不行优先怀疑标注边界问题而不是模型结构。6.2 调优技巧小目标、类不平衡和最终剪枝指甲病变的检测场景比一般物体检测难在两点病变区域小且有相似外观。如果第6.1步发现小目标漏检我的做法是把imgsz调到1280同时调高mosaic到1.0增加小目标被拼贴参与训练的概率。如果还是不行换用yolov8m或yolov8l继续训练。但注意这个数据集只有2923张大模型在验证集上很容易过拟合到纹理细节所以每次训练结束要看val和train的loss差值。类不平衡问题用简单的class weights处理。YOLOv8支持在data.yaml里加cls参数比如给样本少的类别加权重或者在训练命令里用loss_grp调整。实操中我更推荐先做离线复制增强把稀有类的小图复制粘贴到其他背景上而不是直接改loss权重因为改权重的副作用是误检增多。最后训练完成后用torch.prune或YOLOv8自带的model.export把模型转成TensorRT或ONNX格式工业落地时推理速度能从60ms降到20ms以下。这一步很玄学同一套模型换不同批次的图片效果差异很大所以我最后习惯把验证集固定为同一份用固定的conf阈值对比每次实验不看到指标涨不动就换数据集。这次从解压到调优最深的教训是“双格式数据集先核对再训练”。我自己第一次拿到类似数据集时看到有VOC就开心地直接转YOLO结果发现xml和txt里框差了20像素白白浪费了一整天训练时间。后来学乖了先写脚本统计分布、画框对比、划分数据集再进入训练。希望这些步骤能帮你少走弯路也希望你跑出来的模型真的能辅助到临床或质检场景。本文还有配套的精品资源点击获取
返回列表