首页 / 资讯中心 / 文章详情

脸部皮肤病检测数据集:YOLO与VOC格式转换及训练全流程

脸部皮肤病检测数据集:YOLO与VOC格式转换及训练全流程 ★ FEATURED ARTICLE
简介这份脸部皮肤病检测数据集面向计算机视觉入门与目标检测实践者尤其适合正在做YOLO或VOC格式训练、需要真实医学图像练手的学习者。数据集聚焦粉刺、丘疹、结节、脓疱四类皮肤病灶可用于目标检测模型的训练与验证帮助理解多类别小目标标注与数据组织方式。压缩包共约2000个文件以1590个xml标注文件和410个txt标签文件为主另含对应jpg图片整体约35.93MB目录按图片、xml、txt分文件夹存放结构清晰便于直接接入YOLO或VOC训练流程。标签框总数为8827个其中粉刺3875、丘疹3041、脓疱1297、结节614图片清晰且未做增强标注均为矩形框。目前已有103人学习适合作为个人学习项目的数据基础但资源不对模型精度作保证且仅限学习交流请勿用于商业用途。1. 脸部皮肤病检测数据集从标注格式到训练落地的完整路径拿到一份脸部皮肤病检测数据集第一反应往往不是“数据够不够”而是“这标注到底能不能直接喂给模型”。YOLO 格式和 VOC 格式同时存在意味着你既可以走 Ultralytics 那条极简训练链路也可以回到 torchvision 的 VOC 生态做细粒度改造。但真正动手时很多人卡在第一步XML 里的bndbox和 TXT 里的class x_center y_center w h到底怎么无损互转类别映射表放哪图像尺寸不一致时归一化会不会把框带偏。这篇笔记围绕“脸部皮肤病检测数据集YOLOVOC格式”这个具体对象把格式解析、转换脚本、训练参数、评估口径和踩坑记录一次讲透。适合已经跑通过通用目标检测、准备把模型往皮肤病灶检测上迁移的工程师也适合手里有一批标注数据、但不确定该用哪套格式落地的团队。2. 先搞清楚 YOLO 与 VOC 在皮肤病检测里的分工2.1 两种格式的坐标体系差异VOC 格式的核心是 XML 文件每个目标对应一个object节点里面包含name和bndbox。bndbox用的是绝对像素坐标xmin、ymin、xmax、ymax原点在图像左上角。YOLO 格式则是每张图一个 TXT每行一个目标格式为class_id x_center y_center width height四个坐标全部除以图像宽高做归一化取值在 0 到 1 之间。这个差异在脸部皮肤病检测里特别要命。皮肤病灶的边界往往不是硬边缘标注员在 XML 里画框时习惯贴着肉眼可见的皮损外沿转成 YOLO 归一化后如果图像被 resize 到 640×640框会跟着缩放但归一化坐标本身不变。问题出在有些转换脚本先 resize 再算归一化有些先算归一化再 resize两者在长宽比不一致时结果不同。我一般会坚持“先读原图尺寸算归一化再统一 resize 训练”这样 TXT 里的值始终对应原图比例换输入分辨率时不用重新转标注。2.2 类别映射表为什么不能省皮肤病检测的类别通常包括痤疮、湿疹、银屑病、玫瑰痤疮、荨麻疹等不同数据集给的类别名可能是中文、英文缩写或拼音。VOC 的name是字符串YOLO 的class_id是整数中间必须有一张固定的映射表。常见做法是建一个classes.txt每行一个类别名行号从 0 开始就是class_id。转换时用字典查表查不到就报错并记录文件名不要静默跳过。提示类别名里不要出现空格和特殊符号YOLO 训练时data.yaml的names列表如果和 TXT 里的 id 对不上模型会把所有类当成背景loss 降不下去但 mAP 一直是 0。2.3 数据集划分的坑按图分还是按病灶分脸部皮肤病有个特点同一个人可能有多张不同角度的照片同一个病灶可能出现在多张图里。如果按图随机划分训练集和验证集同一病灶的不同角度可能同时出现在两边验证集精度会虚高。我一般会先按患者 ID 分组再在组内划分确保同一个人的数据只出现在一个集合里。如果数据集没给患者 ID至少按图像相似度做聚类把高度相似的图分到同一侧。3. 把 VOC 转成 YOLO转换脚本与四个边界坑3.1 转换脚本的完整实现下面这个脚本处理 VOC 的Annotations和JPEGImages目录输出 YOLO 的images和labels目录同时生成classes.txt。脚本假设 XML 里的类别名已经统一不做自动纠错。import os import xml.etree.ElementTree as ET from PIL import Image # 类别映射按实际数据集修改 CLASS_MAP { acne: 0, eczema: 1, psoriasis: 2, rosacea: 3, urticaria: 4, } def convert_bbox(size, box): 将 VOC 绝对坐标转为 YOLO 归一化坐标 dw 1.0 / size[0] dh 1.0 / size[1] x_center (box[0] box[1]) / 2.0 y_center (box[2] box[3]) / 2.0 w box[1] - box[0] h box[3] - box[2] return (x_center * dw, y_center * dh, w * dw, h * dh) def convert_annotation(xml_path, img_path, out_label_path): tree ET.parse(xml_path) root tree.getroot() size root.find(size) w int(size.find(width).text) h int(size.find(height).text) # 用 PIL 再读一次实际尺寸防止 XML 里写错 with Image.open(img_path) as im: real_w, real_h im.size if (w, h) ! (real_w, real_h): print(f[WARN] size mismatch: {xml_path}, xml({w},{h}), real({real_w},{real_h})) w, h real_w, real_h lines [] for obj in root.iter(object): name obj.find(name).text.strip() if name not in CLASS_MAP: print(f[SKIP] unknown class {name} in {xml_path}) continue cls_id CLASS_MAP[name] 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 - 1)) ymin max(0, min(ymin, h - 1)) xmax max(0, min(xmax, w - 1)) ymax max(0, min(ymax, h - 1)) if xmax xmin or ymax ymin: print(f[SKIP] invalid box in {xml_path}) continue bb convert_bbox((w, h), (xmin, xmax, ymin, ymax)) lines.append(f{cls_id} .join([f{v:.6f} for v in bb])) with open(out_label_path, w) as f: f.write(\n.join(lines)) def main(anno_dir, img_dir, out_img_dir, out_lbl_dir): os.makedirs(out_img_dir, exist_okTrue) os.makedirs(out_lbl_dir, exist_okTrue) for xml_file in os.listdir(anno_dir): if not xml_file.endswith(.xml): continue stem os.path.splitext(xml_file)[0] xml_path os.path.join(anno_dir, xml_file) img_path os.path.join(img_dir, stem .jpg) if not os.path.exists(img_path): print(f[MISS] image not found for {xml_file}) continue out_lbl_path os.path.join(out_lbl_dir, stem .txt) convert_annotation(xml_path, img_path, out_lbl_path) # 复制图像到输出目录保持同名 import shutil shutil.copy(img_path, os.path.join(out_img_dir, stem .jpg)) if __name__ __main__: main( anno_dirVOC/Annotations, img_dirVOC/JPEGImages, out_img_dirYOLO/images, out_lbl_dirYOLO/labels, )逻辑说明convert_bbox接收的是(xmin, xmax, ymin, ymax)顺序先算中心点再算宽高最后乘缩放因子。convert_annotation里用 PIL 重新读图像尺寸是因为部分 VOC 数据集的 XML 里size字段是标注工具默认值和实际图不一致。边界裁剪和无效框跳过是必须的皮肤病标注里经常出现框贴边甚至超出 1 像素的情况不裁剪会导致归一化坐标小于 0 或大于 1训练时被 Ultralytics 直接过滤掉。参数说明CLASS_MAP必须和data.yaml里的names顺序完全一致。f{v:.6f}保留 6 位小数足够 YOLO 训练精度再多了文件体积大且无意义。shutil.copy可以换成软链接或直接移动看磁盘空间。3.2 边界坑一XML 里 size 字段不可信现象转换后的 TXT 里某些框的归一化坐标大于 1 或小于 0训练时这些框被静默丢弃对应图像变成负样本。原因XML 的size写的是 500×500实际图像是 1920×1080标注员在 500×500 画布上标的框直接按 500 归一化就错了。解决脚本里用 PIL 读实际尺寸发现不一致时以实际尺寸为准并打印警告。如果数据集里这种不一致很多建议先批量检查一遍。3.3 边界坑二类别名大小写和空格现象CLASS_MAP里写的是AcneXML 里是acne转换时全部跳过TXT 为空。原因字符串匹配区分大小写且标注工具可能带尾部空格。解决在name obj.find(name).text.strip()之后统一转小写CLASS_MAP的键也用小写。如果类别名有中文建议先映射成英文再转避免编码问题。3.4 边界坑三图像格式不是 JPG现象img_path拼出来是.jpg但实际图像是.png或.jpeg脚本报[MISS]。原因VOC 数据集常见混合格式。解决先扫描JPEGImages目录建立stem - 实际文件名的字典转换时按字典取。或者用glob匹配stem .*取第一个存在的文件。3.5 边界坑四空标注文件现象某张图没有任何目标转换后生成一个空 TXT。YOLO 训练时空 TXT 是合法的负样本但如果数据集中负样本比例过高模型会偏向预测背景。原因皮肤病数据集里正常皮肤照片被误放进来了。解决统计空 TXT 比例超过 10% 就考虑剔除一部分或者在data.yaml里单独设一个background类。我一般会保留少量负样本但不超过总图数的 5%。4. 用 YOLO 训练脸部皮肤病检测模型的参数怎么设4.1 data.yaml 的写法与路径陷阱YOLO 训练依赖一个data.yaml里面指定训练、验证、测试集的路径和类别名。路径可以是绝对路径也可以是相对data.yaml所在目录的相对路径。我一般用绝对路径避免在ultralytics不同版本里相对路径解析不一致。path: /data/skin_detection train: images/train val: images/val test: images/test nc: 5 names: 0: acne 1: eczema 2: psoriasis 3: rosacea 4: urticarianc必须等于names的长度且names的键从 0 开始连续。如果 TXT 里出现了class_id5但nc5训练时直接报索引越界。train和val指向的是图像目录YOLO 会自动去找同级的labels目录所以目录结构必须是images/train和labels/train并列。4.2 输入分辨率与病灶尺寸的匹配脸部皮肤病病灶在整张脸的照片里占比可能很小比如一颗痤疮直径只有 30 像素。YOLO 默认imgsz640下采样 32 倍后特征图只有 20×20小目标很容易丢。我一般会把imgsz提到 1024 或 1280同时把batch降到 8 或 4显存不够就开amp混合精度。如果病灶普遍偏小还可以在data.yaml里加rect: True做矩形训练减少 padding 带来的无效计算。4.3 学习率与 warmup 的设置皮肤病数据集通常不大几千张图量级。从头训练容易过拟合常见做法是加载 COCO 预训练权重冻结 backbone 前几层用较小学习率微调。我一般设lr00.001lrf0.01warmup_epochs3warmup_momentum0.8。如果验证集 loss 震荡把lr0降到 0.0005cos_lrTrue开余弦退火。patience20早停避免无效训练。yolo detect train \ data/data/skin_detection/data.yaml \ modelyolov8m.pt \ imgsz1024 \ epochs200 \ batch8 \ lr00.001 \ lrf0.01 \ warmup_epochs3 \ cos_lrTrue \ patience20 \ ampTrue \ projectruns/skin \ nameexp01参数说明modelyolov8m.pt是中等规模显存 12G 左右能跑imgsz1024。ampTrue混合精度速度提升约 30%精度损失可忽略。project和name决定输出目录方便对比不同实验。训练完在runs/skin/exp01/weights/best.pt拿最优权重。4.4 数据增强的取舍YOLO 默认开启mosaic、hsv、flip等增强。皮肤病检测里flipud垂直翻转要慎用因为脸部上下翻转后病灶位置不符合解剖结构可能引入噪声。mosaic把四张图拼成一张对小目标检测有帮助但皮肤病病灶颜色和纹理是重要特征拼接后边界处可能出现不自然的肤色过渡。我一般保留mosaic1.0但把close_mosaic10设成最后 10 个 epoch 关闭让模型在真实分布上收尾。hsv_h0.015、hsv_s0.7、hsv_v0.4是默认值如果数据集本身色偏严重可以适当降低饱和度增强幅度。5. 评估与排查mAP 上不去时先看什么5.1 指标口径mAP50 和 mAP50-95 的差距YOLO 验证输出mAP50和mAP50-95。皮肤病病灶边界模糊IoU 从 0.5 提到 0.95 时精度下降很快所以mAP50-95通常比mAP50低 20 到 30 个点。如果mAP50正常但mAP50-95极低说明框的位置不够准可能是标注框偏大或偏小。我一般会抽 20 张验证图把预测框和 GT 框画在一起看偏得多的类别单独调。5.2 混淆矩阵里看类别混淆训练完在runs/skin/exp01/下有confusion_matrix.png。皮肤病类别之间外观相似比如湿疹和银屑病都可能表现为红斑鳞屑模型容易混。如果混淆矩阵显示 A 类大量被预测成 B 类先检查标注里这两类是否定义清晰再考虑加类别权重或补充难例。YOLO 不直接支持类别权重但可以通过复制少数类图像来平衡。5.3 排查清单loss 不降的五个原因现象训练几个 epoch 后box_loss和cls_loss都不降。原因一data.yaml路径错模型读不到图全部当背景。原因二TXT 里class_id超出nc范围被过滤。原因三归一化坐标算错框全在图像外。原因四学习率太大loss 震荡不收敛。原因五预训练权重和类别数不匹配modelyolov8m.pt加载时nc被覆盖。解决先跑yolo detect train一个 epoch看输出里train和val的图片数是否正常再抽一张图用yolo detect predict看预测框位置。5.4 验证集指标虚高的排查如果验证集mAP50到 0.9 但实际测试效果差先查数据泄露。按患者 ID 分组划分的验证集指标通常比随机划分低 5 到 10 个点但更真实。另外检查验证集里是否有训练集图像的增强版本YOLO 默认不做跨集增强但手动复制数据时容易混。我一般会在划分后算一次训练集和验证集的图像哈希重复的直接剔除。6. 从检测框到病灶计数一个后处理技巧训练完模型只是第一步实际业务里经常需要统计每张脸的病灶数量。YOLO 输出的框会有重叠尤其是同一病灶被多个 anchor 预测。conf0.25、iou0.45是默认 NMS 参数但皮肤病病灶密集时NMS 会把相邻的真实病灶误删。我一般把iou提到 0.6再对同一类别的框做一次基于距离的合并如果两个框中心点距离小于较小框宽度的 0.5 倍且类别相同就合并成一个。import numpy as np def merge_boxes(boxes, labels, scores, dist_ratio0.5): 合并中心点过近的同类别框boxes 格式 xyxy keep [] used np.zeros(len(boxes), dtypebool) order np.argsort(scores)[::-1] for i in order: if used[i]: continue keep.append(i) used[i] True for j in order: if used[j] or labels[j] ! labels[i]: continue ci ((boxes[i][0] boxes[i][2]) / 2, (boxes[i][1] boxes[i][3]) / 2) cj ((boxes[j][0] boxes[j][2]) / 2, (boxes[j][1] boxes[j][3]) / 2) wi boxes[i][2] - boxes[i][0] wj boxes[j][2] - boxes[j][0] min_w min(wi, wj) dist ((ci[0] - cj[0]) ** 2 (ci[1] - cj[1]) ** 2) ** 0.5 if dist min_w * dist_ratio: used[j] True return [boxes[i] for i in keep], [labels[i] for i in keep], [scores[i] for i in keep]逻辑说明先按置信度降序排列保留最高分框然后遍历其余框如果和已保留框类别相同且中心点距离小于较小框宽度的dist_ratio倍就标记为已用。dist_ratio0.5是经验值病灶密集时调到 0.3稀疏时调到 0.7。这个后处理在计数场景下比单纯 NMS 更稳但会牺牲一点定位精度适合只需要数量的业务。参数说明boxes是xyxy格式的 numpy 数组labels是整数类别scores是浮点置信度。合并后返回三个列表可以直接画图或写报告。如果同一病灶被分成两个不同类别这个逻辑不会合并需要额外做类别优先级判断。我自己的习惯是每次换数据集先跑一遍转换脚本用yolo detect train一个 epoch 看 loss 是否正常下降再抽 10 张图做预测可视化。确认标注和训练链路没问题后再调imgsz和增强参数。这套流程在脸部皮肤病检测上帮我省了很多反复排查的时间希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站