☰
111张皮肤镜图像跑YOLO:小样本医学检测的落地实践与避坑指南
2026/10/5 16:39:18 网站建设 项目流程

简介:本资源为面向皮肤疾病检测的YOLO系列目标检测数据集,适用于白癜风、皮炎、黄褐斑三类皮肤病灶的识别任务,可服务于医学图像分析方向的算法学习与模型验证。压缩包共334个文件,包含111张jpg图像、111个txt标签、111个xml标签及1个data.yaml配置文件,整体约4MB,体积轻便便于快速加载。标签同时提供YOLO格式与VOC格式,YOLO格式采用归一化中心点与宽高比例,可直接适配yolov5、yolov8、yolov9、yolov7、yolov10及yolo11等主流算法,数据集已划分完毕,开箱即可投入训练与测试。目前已有160人学习下载,适合希望快速验证皮肤病灶检测方案、对比不同YOLO版本性能或开展小样本医学图像实验的开发者与研究人员,能有效节省数据标注与格式转换的时间成本。

1. 111张皮肤镜图像跑YOLO:小样本医学检测到底能不能落地

手里只有111张带标签的皮肤镜图像,要分白癜风、皮炎、黄褐斑三类,还想用YOLO做检测——这事听起来像拿玩具枪上战场。但我在实际项目里反复验证过一个结论:小样本医学图像做YOLO,关键不在数据量绝对值,而在标签质量、类别平衡和增强策略的组合。111张图如果每张都标得准、三类分布不畸形,配合合理的迁移学习和强增强,完全能跑出一个可用的基线模型,至少能验证技术路线是否值得继续投入。

这个数据集的核心价值在于它覆盖了三种在临床上容易混淆的色素性/炎症性皮肤问题:白癜风是色素脱失、边界清晰;皮炎是红斑鳞屑、边界模糊;黄褐斑是色素沉着、对称分布。用YOLO做检测,目标不是替代医生诊断,而是帮基层筛查场景做初步定位和分类提示。适合谁?适合手里有少量标注数据、想快速验证YOLO在细粒度医学图像上可行性的算法工程师和医学AI方向的研究生。下面我从数据检查、格式转换、训练配置到避坑,把整条链路拆开讲。

2. 111张图先别急着训:数据体检与标签清洗

2.1 三类皮肤病的视觉特征与标注边界

拿到压缩包解压后,第一件事不是写训练脚本,而是把111张图全部过一遍。我一般会按类别建三个文件夹,每类放一个子集,然后用图片查看器快速翻一遍。这一步的目的是建立对数据分布的直觉:白癜风的白斑在伍德灯下可能呈亮蓝白色,普通皮肤镜下边界锐利;皮炎的红斑往往伴随脱屑,标注框容易画大;黄褐斑的色素沉着区域边界渐变,标注框容易画小。

标注边界不一致是小样本医学数据集的头号杀手。同一个皮损,不同标注者可能给出相差20%面积的框。YOLO的损失函数对框的回归很敏感,如果同类目标的框尺度方差过大,模型会学到一个“平均框”,导致定位精度上不去。我的做法是:对每一类,先随机抽10张图,用同一套标准重新检查框的左上角和右下角是否紧贴皮损边缘,发现偏差超过15%的就修正。这一步花不了两小时,但能省掉后面反复调参的玄学时间。

2.2 用脚本做标签分布统计与异常框剔除

人工翻完之后,用脚本做量化体检。下面这段代码读取YOLO格式的标签文件,统计每类的框数量、宽高比和面积占比,帮你发现异常样本。

import os import numpy as np from pathlib import Path from collections import defaultdict # 假设标签目录结构:labels/train/ 下是 .txt 文件 label_dir = Path("labels/train") img_dir = Path("images/train") # 类别映射:0-白癜风 1-皮炎 2-黄褐斑 class_names = {0: "vitiligo", 1: "dermatitis", 2: "melasma"} stats = defaultdict(list) for txt_file in label_dir.glob("*.txt"): img_file = img_dir / (txt_file.stem + ".jpg") if not img_file.exists(): print(f"[警告] 标签无对应图像: {txt_file.name}") continue with open(txt_file, "r") as f: lines = f.readlines() for line in lines: parts = line.strip().split() if len(parts) != 5: print(f"[异常] {txt_file.name} 行格式错误: {line.strip()}") continue cls_id, cx, cy, w, h = int(parts[0]), *map(float, parts[1:]) # 检查归一化坐标是否越界 if not (0 <= cx <= 1 and 0 <= cy <= 1 and 0 < w <= 1 and 0 < h <= 1): print(f"[越界] {txt_file.name} 类别{cls_id} 框: {cx:.3f},{cy:.3f},{w:.3f},{h:.3f}") stats[cls_id].append((w, h, w * h)) # 输出每类统计 for cls_id in sorted(stats.keys()): arr = np.array(stats[cls_id]) print(f"\n类别 {class_names[cls_id]} (id={cls_id}):") print(f" 框数量: {len(arr)}") print(f" 平均宽高: {arr[:,0].mean():.3f} x {arr[:,1].mean():.3f}") print(f" 平均面积占比: {arr[:,2].mean():.4f}") print(f" 面积占比范围: {arr[:,2].min():.4f} ~ {arr[:,2].max():.4f}") # 面积占比超过0.8的框很可能是标注错误 large = arr[arr[:,2] > 0.8] if len(large) > 0: print(f" [注意] 有 {len(large)} 个框面积占比超过80%,建议复查")

这段代码的逻辑很直接:遍历标签文件,检查每行格式是否为5个值,验证归一化坐标是否在0到1之间,然后按类别汇总宽高和面积。参数方面,面积占比超过0.8的框意味着目标几乎占满整张图,在皮肤镜图像里这通常是把整个视野当成了皮损,需要人工确认。如果某类框数量明显少于其他类,比如白癜风只有15个框而皮炎有80个,那就要考虑是否对少数类做过采样或调整损失权重。

2.3 类别不平衡的处理优先级

111张图里三类分布大概率不均匀。假设白癜风40张、皮炎50张、黄褐斑21张,框数量可能更悬殊。处理类别不平衡有几种常见做法,按我的经验排序:第一优先是数据层面,对少数类做旋转、翻转、色彩抖动增强,把有效样本量拉到接近多数类;第二优先是损失层面,在YOLO的分类损失里给少数类更高权重;第三才是采样层面,用WeightedRandomSampler调整DataLoader。不要一上来就改损失函数,先把增强做扎实,因为医学图像的语义信息对几何变换不敏感,旋转90度后的黄褐斑还是黄褐斑。

3. 从VOC到YOLO格式:转换脚本与四个边界坑

3.1 标注格式识别与转换脚本

皮肤疾病数据集常见的标注格式有三种:VOC XML、COCO JSON、以及已经转好的YOLO TXT。压缩包里如果是VOC格式,每个图对应一个XML,里面记录了bounding box的xmin、ymin、xmax、ymax。YOLO需要的是归一化的中心点坐标和宽高,转换公式是:cx = (xmin + xmax) / 2 / img_w,cy = (ymin + ymax) / 2 / img_h,w = (xmax - xmin) / img_w,h = (ymax - ymin) / img_h。

import xml.etree.ElementTree as ET from pathlib import Path from PIL import Image def voc_to_yolo(xml_path, img_path, class_map, output_txt): """将VOC XML转为YOLO TXT格式 class_map: 类别名到id的映射,如 {"vitiligo":0, "dermatitis":1, "melasma":2} """ tree = ET.parse(xml_path) root = tree.getroot() # 获取图像尺寸 img = Image.open(img_path) img_w, img_h = img.size lines = [] for obj in root.findall("object"): cls_name = obj.find("name").text.strip() if cls_name not in class_map: print(f"[跳过] 未知类别 {cls_name} in {xml_path.name}") continue cls_id = class_map[cls_name] bbox = obj.find("bndbox") xmin = float(bbox.find("xmin").text) ymin = float(bbox.find("ymin").text) xmax = float(bbox.find("xmax").text) ymax = float(bbox.find("ymax").text) # 边界裁剪:防止坐标超出图像范围 xmin = max(0, min(xmin, img_w)) ymin = max(0, min(ymin, img_h)) xmax = max(0, min(xmax, img_w)) ymax = max(0, min(ymax, img_h)) # 跳过无效框 if xmax <= xmin or ymax <= ymin: print(f"[无效框] {xml_path.name} 类别{cls_name}") continue 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(output_txt, "w") as f: f.write("\n".join(lines)) return len(lines) # 批量转换 class_map = {"vitiligo": 0, "dermatitis": 1, "melasma": 2} xml_dir = Path("annotations") img_dir = Path("images") out_dir = Path("labels") out_dir.mkdir(exist_ok=True) for xml_file in xml_dir.glob("*.xml"): img_file = img_dir / (xml_file.stem + ".jpg") if not img_file.exists(): print(f"[缺失图像] {xml_file.stem}") continue out_file = out_dir / (xml_file.stem + ".txt") n = voc_to_yolo(xml_file, img_file, class_map, out_file) print(f"{xml_file.name} -> {n} 个框")

这段脚本的关键在于边界裁剪和无效框过滤。医学图像标注时,标注者可能把框拉到图像边缘外,或者因为皮损在图像角落导致xmax小于xmin。不做裁剪的话,归一化坐标会出现负数或大于1的值,YOLO训练时虽然不会报错,但模型会学到错误的定位信号。参数上,class_map必须和你的实际类别名完全一致,大小写敏感,建议先用grep -r "name" annotations/ | sort -u看一下所有类别名。

3.2 四个容易翻车的边界情况

第一个坑是图像尺寸不一致。皮肤镜图像可能来自不同设备,有的1024x1024,有的800x600。转换时每张图都要用PIL读取实际尺寸,不能假设统一大小。第二个坑是类别名有空格或大小写混用,比如"Vitiligo"和"vitiligo"会被当成两个类,转换前统一转小写并去空格。第三个坑是一张图有多个同类目标,YOLO格式允许同一张图有多行相同cls_id,但要注意如果两个框高度重叠(IoU>0.7),可能是重复标注,建议用NMS预处理一下。第四个坑是标签文件编码,Windows下标注工具可能存成GBK,用Python读取时加encoding="utf-8",否则中文类别名会乱码。

转换完成后,用2.2节的统计脚本再跑一遍,确认每类框数量合理、没有越界坐标。这一步做完,数据才算真正准备好。

4. YOLOv8小样本训练配置:参数怎么设才不浪费111张图

4.1 模型选型与迁移学习策略

111张图用YOLOv8n或YOLOv8s就够了,不要上m或l,参数量大了直接过拟合。我的选择逻辑是:如果GPU显存只有8G,选yolov8n.pt预训练权重;如果显存16G以上,可以试yolov8s.pt。预训练权重是在COCO上训的,虽然COCO没有皮肤镜图像,但底层边缘、纹理特征是可迁移的。冻结主干网络前几层,只训检测头,是小样本场景的常规操作。

训练配置用YAML文件管理,下面是一个针对111张图调过的配置:

# skin_yolo.yaml path: ./skin_dataset train: images/train val: images/val names: 0: vitiligo 1: dermatitis 2: melasma # 训练超参 epochs: 200 batch: 8 imgsz: 640 workers: 4 device: 0 optimizer: AdamW lr0: 0.001 lrf: 0.01 momentum: 0.937 weight_decay: 0.0005 warmup_epochs: 5 warmup_momentum: 0.8 # 增强参数(小样本关键) hsv_h: 0.015 hsv_s: 0.7 hsv_v: 0.4 degrees: 15.0 translate: 0.1 scale: 0.5 shear: 5.0 perspective: 0.0 flipud: 0.5 fliplr: 0.5 mosaic: 1.0 mixup: 0.1 copy_paste: 0.1 # 验证与保存 val: true save_period: 20 patience: 50

参数说明:lr0=0.001比默认的0.01小一个量级,因为小样本容易震荡;warmup_epochs=5让学习率从低到高预热,避免初期梯度爆炸;mosaic=1.0和mixup=0.1是强增强组合,能显著提升小样本泛化,但注意mosaic在最后10个epoch要关掉,否则模型学到的目标尺度分布和真实场景偏差太大。patience=50表示50个epoch验证指标不提升就早停,111张图通常100个epoch内就能看到收敛趋势。

4.2 训练命令与关键日志解读

用Ultralytics的YOLOv8命令行启动训练:

yolo detect train \ data=skin_yolo.yaml \ model=yolov8n.pt \ epochs=200 \ batch=8 \ imgsz=640 \ project=skin_runs \ name=exp1 \ exist_ok=True \ pretrained=True \ optimizer=AdamW \ lr0=0.001 \ patience=50

启动后重点看三个指标:box_loss、cls_loss和mAP50。小样本场景下,box_loss在前20个epoch应该快速下降,如果震荡剧烈,把lr0再降到0.0005。cls_loss如果一直不降,检查类别标签是否从0开始连续编号,YOLO要求类别id从0开始且不能跳号。mAP50在验证集上能到0.5以上就算这个数据集有信号,到0.7以上说明标注质量不错。如果mAP50在0.2附近徘徊,大概率是标注框问题,回到第2章重新体检。

4.3 验证集划分与交叉验证的取舍

111张图怎么划分训练验证?常见做法是8:2,即89张训练、22张验证。但小样本下单次划分的方差很大,可能恰好验证集里都是难样本。我的建议是做5折交叉验证,每折用不同的22张做验证,最后取平均mAP。虽然训练时间乘以5,但111张图的训练很快,单折200epoch在单卡上也就十几分钟。交叉验证的结果更能反映模型的真实泛化能力,也方便你判断哪些样本是“顽固错误”,后续针对性补标。

5. 避坑与排查:小样本YOLO训练中最容易翻车的五件事

5.1 现象:训练loss正常下降但验证mAP始终为0

原因通常是验证集的标签路径配置错误。YOLO在验证时会去val指定的目录下找同名的.txt标签文件,如果标签文件名和图像文件名不一致(比如图像是img_001.jpg,标签是img_001_voc.txt),验证时找不到标签,mAP自然为0。解决方法是检查labels/val/下的文件名是否和images/val/一一对应,用ls images/val | sed 's/.jpg/.txt/'和ls labels/val对比一下。

5.2 现象:模型只预测某一类,其他类完全漏检

这是类别不平衡的典型表现。假设皮炎样本占70%,模型会倾向于把所有框都预测成皮炎,因为这样分类损失最小。解决分两步:先在数据增强里对少数类做copy_paste,把少数类目标复制到其他图像上;再在训练配置里加cls_pw参数(YOLOv8支持通过class_weights传权重),给少数类2到3倍的分类损失权重。如果还不行,考虑用focal loss替换默认的BCE loss,但YOLOv8改损失需要动源码,新手不建议。

5.3 现象:验证集mAP波动很大,每次训练结果差10个点以上

小样本训练本身方差就大,但如果波动超过10个点,通常是验证集太小或样本分布不均。22张验证图里如果只有3张黄褐斑,模型在这3张上的表现随机性极强。解决办法是增大验证集比例到30%,或者做交叉验证取平均。另外检查seed参数,YOLO默认随机种子不固定,每次划分和增强的随机性不同,固定seed=42能让结果可复现。

5.4 现象:推理时框的位置偏移明显,分类正确但定位不准

这通常是标注框的宽高比和实际目标不匹配。皮肤镜图像里皮损往往是不规则形状,标注者可能画了正方形框,但实际皮损是细长条。YOLO的回归损失对宽高比敏感,如果训练集里同类目标的宽高比方差大,模型学到的框会偏向平均值。解决方法是回到标注环节,对宽高比异常的框(比如w/h>3或h/w>3)重新检查,必要时用分割掩码代替检测框,但那就变成YOLOv8-seg的任务了。

5.5 现象:训练到后期mAP突然掉点

如果mAP在150epoch左右突然下降,大概率是过拟合。111张图训200epoch,模型可能开始记忆训练样本的噪声。解决方法是早停(patience设30到50),或者在最后50个epoch关闭mosaic增强,让模型在真实分布上微调。另外检查weight_decay,小样本下可以适当增大到0.001,抑制权重过大。

6. 小样本医学检测的进阶技巧:从111张到可用模型的最后一公里

6.1 用测试时增强(TTA)榨干模型性能

训练完之后,推理阶段还能再挤一点性能出来。测试时增强的做法是对同一张测试图做多次变换(水平翻转、多尺度缩放),分别推理后把结果做NMS融合。YOLOv8内置了TTA参数,在predict时加augment=True即可:

yolo detect predict \ model=skin_runs/exp1/weights/best.pt \ source=test_images/ \ augment=True \ conf=0.25 \ iou=0.45 \ save=True \ save_txt=True

augment=True会启用TTA,conf=0.25是置信度阈值,小样本模型建议设低一点,宁可多检一些再人工过滤。iou=0.45是NMS的IoU阈值,皮肤镜图像里皮损之间重叠少,可以设到0.5。TTA通常能带来1到3个点的mAP提升,代价是推理时间乘以3到5倍,看你的场景是否接受。

6.2 错误样本分析与主动学习闭环

跑完验证后,把预测错误的样本挑出来,按错误类型分类:漏检、误检、分类错误、定位偏差。漏检的样本往往是皮损面积小或对比度低,这类样本优先补充标注;误检的样本可能是正常皮肤纹理被误判,需要增加负样本。主动学习的思路是:每轮训练后挑出模型最不确定的样本(置信度在0.3到0.6之间的),人工复核后加入训练集。111张图经过3到5轮主动学习,通常能扩充到200张左右的有效训练集,mAP能有明显提升。

6.3 一个我踩过的坑:别在验证集上调参

最后说一个血泪教训。我刚开始做小样本医学检测时,习惯在验证集上反复调学习率、增强参数,看到mAP涨了就保留。结果模型在验证集上表现很好,但换一批新数据就崩了。后来才明白,验证集被我用成了测试集,模型间接过拟合了验证集。正确做法是:训练集调参,验证集选模型,另外留一个独立的测试集做最终评估。111张图如果实在不够分,至少用交叉验证的折外预测来做模型选择,不要盯着单一验证集的数字调参。

希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询