简介面向目标检测与医学影像识别场景这份数据集整理了2923张指甲图像覆盖肢端雀斑样痣黑、甲沟炎、甲弯曲、泰瑞氏甲4类病变共2930个矩形标注框其中泰瑞氏甲871框、甲弯曲711框、甲沟炎700框、肢端雀斑样痣黑648框类别分布相对均衡整体数据规模适中。图像同时提供VOCxml和YOLOtxt两种标注格式分别存放于JPEGImages、Annotations、labels三个文件夹可直接接入YOLO、Faster R-CNN等常见目标检测训练流程适合学生、研究者快速开展指甲病变检测实验便于快速验证模型效果。压缩包共2000个文件以xml标注文件为主并含数据集说明txt整体约64.71MB已有64人浏览学习。图片清晰度整体一般且未做增强少量略模糊仍可作为初始训练集用于算法验证、格式转换练习与特征分析也可作为毕设或课程设计的数据支撑为后续优化检测精度提供参考。1. 指甲病变检测数据集到手之后先用账本精神把它拆干净指甲病变检测这类项目最不缺的是模型最缺的是能直接喂进训练脚本的干净数据。这份 2923 张、四类病变、VOC 和 YOLO 双格式的指甲病变检测数据集覆盖肢端雀斑样痣黑、甲沟炎反甲、甲弯曲、泰瑞氏甲四种典型表现总框数 2930适合用来做 YOLO 系模型的训练验证、VOC 转 YOLO 流程的实验以及皮肤科图像筛查 Demo 的前期选型。它的直接价值在于图片、XML、TXT 三种文件一一对应省掉最痛苦的标注格式整理环节。但前提是你得先把类别顺序、坐标归一化、文件匹配这些细节吃透否则大概率会在训练阶段连续翻车。下面按我实际拆包的顺序把数据结构和每个坑都过一遍。2. 数据盘点4 类病变、2923 张图、2930 个框先算清这笔账2.1 四个类别在检测任务里到底长什么样拿到数据集先别急着训练把类别名称和临床外观对齐是第一件事。这个数据集里的四类指甲病变外观差异其实比预想中大这对目标检测来说反而是好事特征区分度越高模型越容易收敛。类别名标签原文中文常用名框数典型外观特征Acral Lentiginous Melanoma肢端雀斑样痣黑648甲下或甲周不规则色素沉着边界模糊颜色深浅不均Koilonychia甲沟炎 / 反甲700甲板中央凹陷、边缘翘起呈匙状Onychogryphosis甲弯曲711甲板增厚、弯曲变形颜色偏黄偏暗Terry-s nail泰瑞氏甲871甲近端大片白色浑浊远端残留窄条粉红带从检测角度看泰瑞氏甲和反甲的画面表现相对“整块”框通常大而稳定肢端雀斑样痣黑和甲弯曲则容易出现边界不清晰的情况需要标注框把病灶完整包住。我拿到数据后习惯做的事是先把四类各抽 20 张图快速翻一遍确认类别名和画面内容对得上。因为这种公开资源里偶尔会出现标签名写得很规范、但内容归类混乱的情况这个数据集相对干净但抽查依然值得做。2.2 图片数量和框数为什么对不上“JPEGImages 2923 张、Annotations 2923 个、labels 2923 个总框数却是 2930”第一次看到这个数字组合的人容易懵以为数据有缺失。其实这是目标检测数据的常态一张图可以同时出现多个病灶也可以多个指甲各框一个区域。用一个简单的减法就能估算多框占比2930 减去 2923差额只有 7。这意味着数据集里绝大多数图片是单框单目标只有少数图片包含两个或以上的框。千万别把“每张图恰好一个框”当成默认假设训练之前仍然要逐张核对否则后面统计 mAP 时会被多框样本干扰。另外注意一个细节labels 文件夹里是 2923 个 TXT 文件如果某张图没有任何标注工具通常会生成空 TXT但这份数据里没有出现空标签问题说明作者在导出时做了过滤这一点值得其它数据集参考。2.3 用脚本核对三件套文件与类别分布拆包之后第一件事就是把三个目录里的文件名对齐。我给 A 同学演示过一段脚本它能在 30 秒内找出缺失、多出和错位的文件import os base_dir 指甲病变数据集 # 改成你的解压路径 dirs { imgs: os.path.join(base_dir, JPEGImages), xmls: os.path.join(base_dir, Annotations), txts: os.path.join(base_dir, labels), } files {} for key, path in dirs.items(): files[key] set(os.listdir(path)) img_names {f.split(.)[0] for f in files[imgs]} xml_names {f.split(.)[0] for f in files[xmls]} txt_names {f.split(.)[0] for f in files[txts]} print(图片数量:, len(img_names)) print(XML数量:, len(xml_names)) print(TXT数量:, len(txt_names)) print(三者完全一致:, img_names xml_names txt_names) print(缺少标注的图片:, img_names - xml_names - txt_names) print(无对应图片的标注:, xml_names - img_names)这段代码的逻辑很直接把三个目录的文件主名分别取出来转成集合然后做差集和交集比较。参数上要注意base_dir必须用绝对路径或确认相对路径正确最稳妥的方式是直接把解压后的文件夹拖到终端里拿到完整路径。如果输出“三者完全一致”为 True再往下走不一致则先用这个差集定位缺失文件别急着进训练。3. 从 VOC 到 YOLO目录结构、classes.txt 顺序与坐标转换脚本3.1 压缩包里的三层目录第一眼最容易看走眼这个数据集的物理结构很典型解压后你会看到三个同级文件夹JPEGImages、Annotations、labels分别存放 JPG 图片、XML 标注、YOLO 格式 TXT 标注。很多新手拿到后习惯直接改路径开始训练结果要么找错目录层级要么把 JPEGImages 记成 images到训练时报“文件不存在”才回头查。文件夹内容文件数说明JPEGImages原始图片2923全部为 .jpg分辨率中等小部分模糊AnnotationsVOC 格式标注2923包含对象类别与 bndbox 坐标labelsYOLO 格式标注2923每行一个目标格式为 class cx cy w h这里有个隐藏信息TXT 文件与 JPEG 的对应关系靠文件名主键关联比如xyxr_images_nail2583.jpg对应xyxr_images_nail2583.txt。如果你要合入自己的数据命名规则必须保持完全一致否则 YOLO 训练时会出现数据加载但框为空或找不到图的情况。3.2 VOC 的 XML 里有效信息只有三个节点VOC 标注看着字段多训练时真正会被读取的只有三个节点filename、object/name、object/bndbox。其它像 folder、source、segmented 这些字段在转换时通常直接忽略。下面是一段简化后的 XML 结构和这个数据集的格式基本一致annotation folderJPEGImages/folder filenamexyxr_images_nail2583.jpg/filename size width640/width height480/height depth3/depth /size object nameKoilonychia/name bndbox xmin112/xmin ymin98/ymin xmax356/xmax ymax402/ymax /bndbox /object /annotation注意size里的 width 和 height 必须真实对应图片分辨率它直接影响坐标归一化结果。有些数据集里 XML 的 width/height 写错转出来的 YOLO 坐标越界模型要么训练报错要么收敛后检测框复位。拿到手后建议随机抽 5 个 XML 和对应图片比对尺寸。3.3 坐标转换脚本从绝对坐标到归一化中心点VOC 的 bndbox 是绝对像素坐标YOLO 要求的是归一化中心坐标转换公式是cx (xmin xmax) / 2 / widthcy (ymin ymax) / 2 / heightw (xmax - xmin) / widthh (ymax - ymin) / height下面是一段可复用的批量转换脚本我拆这个数据集时就用它重新生成了一份 TXT确保万无一失import xml.etree.ElementTree as ET import os CLASSES [ Acral Lentiginous Melanoma, Koilonychia, Onychogryphosis, Terry-s nail ] # 顺序必须与 labels/classes.txt 一致 xml_dir Annotations txt_dir labels os.makedirs(txt_dir, exist_okTrue) for xml_name in os.listdir(xml_dir): if not xml_name.endswith(.xml): continue xml_path os.path.join(xml_dir, xml_name) txt_name xml_name.replace(.xml, .txt) tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) lines [] for obj in root.iter(object): name obj.find(name).text.strip() if name not in CLASSES: continue cls_id CLASSES.index(name) 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) cx (xmin xmax) / 2.0 / img_w cy (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) with open(os.path.join(txt_dir, txt_name), w, encodingutf-8) as f: f.write(\n.join(lines))这段脚本的核心是CLASSES.index(name)这一步它决定了类别 ID 的映射基准。如果 CLASSES 顺序和 labels 文件夹里的 classes.txt 不一致生成的 TXT 会把 A 类当 B 类训练时表现为 loss 正常下降但混淆矩阵异常。参数上cx/cy/w/h保留 6 位小数足够过长的浮点数只会增加文件体积对精度没有实质帮助。3.4 类别顺序classes.txt 是唯一基准不是 XML 里的顺序资源描述里有一句很容易被忽略的提醒YOLO 格式类别顺序和标注导出时的顺序不对应要以 labels 文件夹下 classes.txt 为准。这句话的意思是不要用 XML 里标签出现的先后顺序去推断类别 ID也不要因为某个 TXT 第一行是数字 2 就觉得它是第三类。正确做法是打开 labels/classes.txt逐行读取每行索引就是类别 ID。上面脚本里的 CLASSES 列表也要手动与之一致。我遇到过不止一次因为把 COCO 的类别顺序习惯性套到新数据集上导致整个训练白跑的情况这类错误最气人的地方在于训练过程完全正常直到算 mAP 时才发现预测类别全都是镜像错位的。4. 训练前校验缺图、坐标越界、空标签一次性揪出来4.1 文件匹配检查先跑一遍“三件套对齐”上一步文件数检查确认了数量但还没检查到具体文件名。更稳的做法是逐张检查每张 JPG 都有对应 XML 和 TXT并且 XML 里 filename 指向的文件真实存在。下面这个脚本就是干这件事的import os jpg_dir JPEGImages xml_dir Annotations txt_dir labels jpgs [f for f in os.listdir(jpg_dir) if f.lower().endswith((.jpg, .jpeg))] missing [] for jpg in jpgs: stem os.path.splitext(jpg)[0] xml_path os.path.join(xml_dir, stem .xml) txt_path os.path.join(txt_dir, stem .txt) if not (os.path.exists(xml_path) and os.path.exists(txt_path)): missing.append(jpg) print(图片总数:, len(jpgs)) print(缺失标注的图片:, len(missing)) for img in missing[:10]: print( -, img)这里有个小技巧用f.lower().endswith((.jpg, .jpeg))而不是直接判断.jpg可以有效规避部分图片扩展名是大写.JPG的情况。在 Windows 上解压的文件可能看不出问题但拷贝到 Linux 服务器上训练时大小写不敏感的文件系统会立刻露馅。如果你要往自有数据里补样本也建议统一转为小写扩展名再入库。4.2 坐标合法性归一化坐标越界是最常被漏掉的坑YOLO 训练框架对坐标越界比较敏感。有的版本会直接报错有的版本会静默丢弃越界框导致实际参与训练的框变少。因此把全部 TXT 扫一遍抛出越界坐标是非常值得做的一件事import os txt_dir labels bad_files [] for txt_name in os.listdir(txt_dir): if not txt_name.endswith(.txt): continue path os.path.join(txt_dir, txt_name) with open(path, r) as f: for line_no, line in enumerate(f, 1): parts line.strip().split() if len(parts) ! 5: bad_files.append((txt_name, line_no, 列数不为5)) continue try: cls_id, cx, cy, w, h int(parts[0]), float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4]) except ValueError: bad_files.append((txt_name, line_no, 解析失败)) continue if not (0 cx 1 and 0 cy 1 and 0 w 1 and 0 h 1): bad_files.append((txt_name, line_no, 坐标越界)) print(异常标注数量:, len(bad_files)) for item in bad_files[:20]: print(item)坐标检查的参数边界不是严格的 0~1。以 w 为例理论上单目标框不能超过图片宽度但如果病灶延伸到图片边缘xmin 为 0、xmax 等于图片宽度归一化后 w 恰好是 1.0这在数学上合法。所以判断条件建议用w 1.0 or w 0而不是w 1.0直接判非法。遇到刚好多于 1.0 的情况多半是 XML 里 xmax 写超了图片宽度这时要回到 XML 查原始坐标。4.3 可视化复核把 GT 框画出来比看任何表格都直接脚本检查能过滤语法问题但过滤不了语义问题框没有包住病灶、框太小、框只框住半个指甲这些只能靠人眼确认。所以我每次拆数据集一定会跑一段可视化脚本把标注框画在图上导出成一个 review 文件夹import cv2 import os jpg_dir JPEGImages txt_dir labels out_dir review os.makedirs(out_dir, exist_okTrue) CLASSES [ Acral Lentiginous Melanoma, Koilonychia, Onychogryphosis, Terry-s nail ] for txt_name in os.listdir(txt_dir): stem txt_name.split(.)[0] img_path os.path.join(jpg_dir, stem .jpg) if not os.path.exists(img_path): continue img cv2.imread(img_path) h, w img.shape[:2] with open(os.path.join(txt_dir, txt_name), r) as f: for line in f: parts line.strip().split() cls_id, cx, cy, bw, bh int(parts[0]), float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4]) x1 int((cx - bw / 2) * w) y1 int((cy - bh / 2) * h) x2 int((cx bw / 2) * w) y2 int((cy bh / 2) * h) color (0, 255, 0) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, CLASSES[cls_id], (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1) cv2.imwrite(os.path.join(out_dir, stem _review.jpg), img)我在参数上习惯把框线厚度设为 2类别文字字号设为 0.5这样在 640×480 分辨率的图上刚好能看清又不会挡住病灶区域。可视化抽检的比例不需要 100%但至少每类要抽 30 张重点看多框样本和边界处样本。这一步虽然花十分钟却能省掉后面调模型调半天发现是数据问题的尴尬。5. 避坑与排查5 个常见翻车点按现象、原因、解决拆给你看5.1 文件明明存在训练却说找不到图片现象YOLO 训练启动后报错提示某张 jpg 图片不存在但打开 JPEGImages 文件夹文件就在眼前。原因大多数情况下是系统大小写敏感造成的。XML 里 filename 写的是xyxr_images_nail2583.jpg但实际文件是xyxr_images_nail2583.JPGWindows 下解压软件不会纠正这类差异拷到 Linux 服务器就露馅。解决跑 4.1 的文件匹配脚本把所有 XML 里的 filename 与磁盘实际文件名逐一比对不一致的直接重命名统一为小写 .jpg 扩展名。从那以后我每次都会在解压后先跑一遍find . -name *.JPG -exec rename s/\.JPG$/.jpg/ {} \;这类命令把扩展名归一化再进训练。5.2 训练正常收敛但预测类别整体错位现象loss 曲线一路下降验证集 mAP 看起来也不差但把预测结果画出来发现框的位置基本对类别全乱了比如把泰瑞氏甲全预测成甲弯曲。原因TXT 第一列的类别 ID 与 classes.txt 顺序不一致。VOC 导出成 YOLO 时类别 ID 由生成脚本决定如果生成时按 XML 里字母序排序而训练时按 classes.txt 里的实际顺序读取两者错位模型就等于在拿错误的标签做监督。解决以 labels/classes.txt 为准重建 CLASSES 列表重新跑 3.3 的转换脚本生成 TXT再抽查三五个 TXT 的第一列。最简单的方法是打开一个 TXT把第一列数字对应到 classes.txt 的同一行看类别名是否与图片内容吻合。数据集的类别名称里有一个带连字符的Terry-s nail个别解析脚本会把连字符当特殊字符处理尽量保持和 classes.txt 完全一致的写法不要擅自改名。5.3 TXT 里出现 0 宽度或者 0 高度的框现象训练到一半报 Assertion 错误提示某个 box 的 w 或 h 为 0或者标注框面积为零。原因转换脚本里 xmax 与 xmin 相等或者 ymax 与 ymin 相等也有可能是原标注里出现了坐标整行相同的脏数据。解决在转换脚本里加一个过滤条件把w 0 or h 0的框直接丢弃并打印对应文件名和坐标方便回溯 XML。这类框即使不报错参与训练也只会贡献噪声。数据集的脏数据比例不高但这类过滤逻辑应该固化到自己的转换脚本里不要每次手工删。5.4 统计框数时发现某类框数和描述对不上现象按描述Acral Lentiginous Melanoma 是 648 框Koilonychia 是 700 框但自己统计出来数字不一样。原因统计基线不同。直接从 XML 统计 name 节点得到的框数和从 TXT 第一列统计得到的框数会因为类别顺序映射错误而不同另外如果解压过程中有文件被覆盖或重复也会导致计数漂移。解决写一个独立统计脚本分别按 XML 和 TXT 统计输出两张分类计数表。如果两者一致说明数据是干净的双格式如果不一致优先相信 XML因为它是原标注。这里需要特别说明框数以 Annotations 为准而不是以 labels 为准因为 labels 可能由第三方脚本生成。5.5 部分图像模糊模型在小目标上的召回率低现象整体 mAP 尚可但肢端雀斑样痣黑这类边界模糊的病灶recall 明显低于其它类别。原因数据集本身标注时就存在小部分模糊图片边界不清晰导致检测框不稳定再加上没有图像增强模型对模糊特征的鲁棒性不足。解决训练时打开轻微的马赛克增强和随机亮度对比度调整验证时关闭增强评估阶段把模糊样本单独抽出来统计一次看看是标注边界问题还是模型问题。如果时间充裕可以把模糊样本单独复制出来做一次二次标注能显著提升这类数据在边缘场景下的表现。6. 进阶用法把类别不平衡和模糊短板写进训练计划四类框数在 648 到 871 之间浮动最大差 223比例并不悬殊但训练计划里仍然建议做两步处理。第一步是切分数据集时用随机种子打乱并做分层抽样保证每个子集里四类比例接近全局分布第二步是设置训练轮数时以框数最少的类别为基准观察它的 loss 曲线是否先进入平台期。我习惯在数据切分后追加一组快速实验先用默认参数训练 30 轮单独打印每个类别的验证集召回率哪个类别的 AP 最低就针对哪个类别增加训练图片增强强度或者单独调整检测框置信度阈值。泰瑞氏甲这类背景对比明显、框容易学习肢端雀斑样痣黑这种边界模糊的类别可能要把置信度阈值从默认的 0.25 往下调到 0.15才能把边缘检测框捡回来。这个调参动作不是玄学它是在和数据集本身的模糊度做妥协。验证阶段也值得加一道“稳定度测试”把同一张图写个脚本水平翻转、亮度调暗 20%、轻微平移 5 个像素再看模型输出的框是否稳定落在病灶区域。这个数据集标注框本身是矩形框对旋转敏感度较高如果要做旋转增强角度控制建议不超过 15 度否则框和病灶的真实方向会产生明显偏差。做过这批实验你会发现数据集的短板其实不在数量而在模糊样本覆盖。最后一次提醒四类英文标签里有一处连字符写法Terry-s nail如果框架里对类别名做正则解析注意别让它吞掉字符。不要修改 labels/classes.txt 里的原始写法否则类别 ID 映射会崩掉。我从那次类别错位的翻车之后养成了一个习惯不管数据集描述写得多详细拿到手必做三件事先数文件再查坐标最后抽查类别索引。这套流程五分钟跑完真的省下后面好几天调模型的精力。希望帮到你。本文还有配套的精品资源点击获取