简介:本资源为ISIC皮肤病变图像分割数据集,面向医学图像分割方向的算法工程师、研究生及竞赛选手,可用于细粒度分割模型的训练与验证。数据图像分辨率在1000至2000之间,原图为jpg格式,mask标签为png格式,标签中0代表背景、255代表皮肤病,具体类别可在classes文本中查看,并附带show脚本可同时可视化图像与标签。压缩包共约2000个文件,以1279个png标签、719个jpg图像为主,另含1个txt类别说明与1个py可视化脚本,整体约819.28MB。数据集划分为训练集与验证集,训练集含900张图像及900张对应mask,验证集含379张图像及379张对应mask,目录结构清晰,便于直接接入分割网络。目前已有797人学习下载,适合需要快速搭建皮肤病变分割实验、验证模型效果或进行细粒度分割研究的读者参考使用。
1. ISIC 皮肤病变分割数据集:从一堆图片到能训的掩码,中间差了什么
皮肤病变分割这个方向,很多人第一次上手就卡在数据上。ISIC 档案库里的原始图像是临床或皮肤镜拍摄的彩色照片,分辨率、光照、毛发遮挡、病灶边界模糊程度差异极大,而模型要学的是「哪里是病变、哪里是正常皮肤」这种像素级判断。ISIC 皮肤病变图像分割数据集之所以被反复提起,是因为它把原始图像、对应的二值掩码、以及病灶类别标注打包在一起,让分割任务有了可监督的信号。它适合两类人:一类是想验证 U-Net、DeepLab、SegFormer 这类分割网络在医学场景下表现的算法工程师,另一类是皮肤科方向的研究者,需要一套公开、可复现的数据来对比不同方法。但拿到压缩包不等于能直接开训,图像与掩码的命名对应、掩码的像素值约定、类别信息的读取方式,每一步都有坑。这篇笔记就按我实际处理这套数据的顺序,把从解压到跑通第一个 epoch 的路径拆开讲。
2. 先搞清楚 ISIC 数据集的目录结构与标注约定
2.1 图像、掩码、类别信息三者的对应关系
ISIC 数据集的常见组织方式是按任务分目录。分割任务下通常有两个平行文件夹:一个放原始图像,扩展名多为.jpg或.png;另一个放对应的分割掩码,扩展名多为.png。掩码文件名的主干部分与图像文件名一致,这是建立对应关系的唯一依据。类别信息一般单独放在一个 CSV 或 JSON 文件里,字段包含图像文件名、病灶类型(如黑色素瘤、基底细胞癌、良性痣等)、以及可能的其他元数据。
这里第一个容易翻车的地方是:图像文件名可能带ISIC_前缀和一串数字,而掩码文件名有时会多一个_segmentation后缀。如果你直接按os.path.splitext去配对,就会漏掉一批。我一般会先写一段脚本把两个目录的文件名主干提取出来,做集合运算,确认交集大小是否等于图像总数。
import os img_dir = "data/ISIC/images" mask_dir = "data/ISIC/masks" def stem(name): # 去掉扩展名,再去掉常见的 _segmentation 后缀 base = os.path.splitext(name)[0] if base.endswith("_segmentation"): base = base[:-len("_segmentation")] return base img_stems = {stem(f) for f in os.listdir(img_dir) if f.lower().endswith((".jpg", ".png"))} mask_stems = {stem(f) for f in os.listdir(mask_dir) if f.lower().endswith(".png")} print("图像数:", len(img_stems)) print("掩码数:", len(mask_stems)) print("交集:", len(img_stems & mask_stems)) print("有图无掩码:", len(img_stems - mask_stems)) print("有掩码无图:", len(mask_stems - img_stems))这段代码的逻辑是先统一文件名主干,再比较两个集合。参数上,img_dir和mask_dir要换成你本地解压后的实际路径。如果「有图无掩码」不为零,说明要么掩码缺失,要么命名规则比预想的更复杂,需要打印几个具体文件名人工核对。这一步不做,后面 DataLoader 里会随机报FileNotFoundError,而且很难定位是哪个样本。
2.2 掩码像素值的二值化处理
ISIC 掩码常见的是 8 位灰度 PNG,病灶区域像素值可能是 255,背景是 0;但也有的版本用 1 表示病灶、0 表示背景,甚至出现过 128 作为前景的情况。如果你不确认就直接除以 255,模型学到的可能是全零或全一。我习惯在写 Dataset 之前,先统计一批掩码的像素值分布。
import numpy as np from PIL import Image import os mask_dir = "data/ISIC/masks" sample_masks = os.listdir(mask_dir)[:20] for m in sample_masks: arr = np.array(Image.open(os.path.join(mask_dir, m))) unique, counts = np.unique(arr, return_counts=True) print(m, dict(zip(unique.tolist(), counts.tolist())))如果输出里只有 0 和 255,那二值化就是arr > 127。如果出现 1 和 0,就直接用。如果出现 128 或其他中间值,说明掩码可能经过压缩或有抗锯齿边缘,需要先做阈值化再转成 0/1。这个检查花两分钟,能省掉后面调半天 loss 不下降的玄学时间。
2.3 类别信息的读取与过滤
类别信息文件通常是 CSV,字段名可能是image、diagnosis、benign_malignant等。不同年份的 ISIC 档案库字段名有差异,不要硬编码列名,先读表头。如果你只做二分类分割(病灶 vs 背景),类别信息主要用来做数据分层或后续分类头;如果你要做多类别分割,那类别信息就直接决定掩码的通道数。常见做法是先用 pandas 读进来,看diagnosis的取值分布,再决定是否合并稀有类别。
import pandas as pd meta = pd.read_csv("data/ISIC/metadata.csv") print(meta.columns.tolist()) print(meta["diagnosis"].value_counts().head(10))参数上,metadata.csv的路径按实际调整。如果diagnosis列里出现unknown或空值,要么剔除这些样本,要么单独归为一类。我一般会保留但标记,训练时用ignore_index跳过,避免噪声标签把模型带偏。
3. 把 ISIC 数据喂给分割网络:Dataset 与增强的落地写法
3.1 自定义 Dataset 的最小实现
PyTorch 的Dataset是绕不开的。核心就三件事:读图、读掩码、做同步增强。图像和掩码必须用同一组随机参数做几何变换,否则掩码对不上图像,模型直接学废。下面是一个最小可运行的实现,假设掩码已经是 0/255,图像是 RGB。
import os import numpy as np import torch from torch.utils.data import Dataset from PIL import Image import torchvision.transforms.functional as TF import random class ISICDataset(Dataset): def __init__(self, img_dir, mask_dir, img_size=256, train=True): self.img_dir = img_dir self.mask_dir = mask_dir self.img_size = img_size self.train = train self.names = sorted([ f for f in os.listdir(img_dir) if f.lower().endswith((".jpg", ".png")) ]) def __len__(self): return len(self.names) def __getitem__(self, idx): name = self.names[idx] stem = os.path.splitext(name)[0] img_path = os.path.join(self.img_dir, name) mask_path = os.path.join(self.mask_dir, stem + ".png") if not os.path.exists(mask_path): mask_path = os.path.join(self.mask_dir, stem + "_segmentation.png") img = Image.open(img_path).convert("RGB") mask = Image.open(mask_path).convert("L") # 统一尺寸 img = TF.resize(img, [self.img_size, self.img_size]) mask = TF.resize(mask, [self.img_size, self.img_size], interpolation=TF.InterpolationMode.NEAREST) # 同步随机翻转 if self.train and random.random() > 0.5: img = TF.hflip(img) mask = TF.hflip(mask) img = TF.to_tensor(img) # [0,1] mask = (TF.to_tensor(mask) > 0.5).float() # 二值化到 0/1 return img, mask逻辑说明:__getitem__里先按主干名找掩码,兼容两种命名。TF.resize对掩码用最近邻插值,避免产生中间灰度值。翻转用同一个随机判断,保证图像和掩码同步。最后掩码转成 0/1 的 float 张量,配合BCEWithLogitsLoss使用。参数上,img_size我一般从 256 起步,显存够再上 384 或 512;ISIC 图像原始分辨率差异大,统一缩放是必要的,但要注意病灶占比小的样本缩放后可能只剩几十个像素,这时候可以考虑随机裁剪而不是直接缩放。
3.2 医学图像增强的边界:哪些能加,哪些别碰
通用增强里,水平翻转、垂直翻转、小角度旋转(±15°)、亮度对比度微调,在皮肤病变分割里都是安全的。但颜色抖动要克制,因为病灶的颜色是重要判别特征,大幅改变色调可能让标签语义漂移。Cutout 和 CutMix 在自然图像里好用,在医学分割里要慎用,尤其是 CutMix 把两块不同病灶拼在一起,掩码边界会变得没有临床意义。
我一般会加一个 ElasticTransform,模拟皮肤形变,但参数要小,alpha控制在 30 以内,sigma控制在 5 左右。再大就会把病灶形状扭曲得不像真实病变。另外,ISIC 图像里常见毛发遮挡,有人会专门做毛发去除预处理,但我的经验是,如果训练集里毛发样本足够多,模型自己能学到一定鲁棒性,强行去除反而可能引入伪影。如果要做,用 DullRazor 这类经典方法,别用太激进的形态学操作。
3.3 损失函数与评价指标的选择
二值分割最常用的是BCEWithLogitsLoss加DiceLoss的组合。ISIC 数据里病灶区域通常只占图像的一小部分,纯 BCE 会被背景主导,Dice 能拉回一些。我一般用0.5 * BCE + 0.5 * Dice,如果小病灶漏检严重,把 Dice 权重提到 0.7。
评价指标别只看像素准确率。背景占 90% 以上时,全预测背景也能拿高准确率,但 Dice 是 0。所以报告里至少要有 Dice 系数和 IoU。计算 Dice 时,预测值要先过 sigmoid 再阈值化到 0/1,阈值一般取 0.5,但如果你发现模型偏向保守,可以扫一下 0.3 到 0.7 看哪个 Dice 最高。
def dice_coeff(pred, target, threshold=0.5, eps=1e-6): pred = (torch.sigmoid(pred) > threshold).float() inter = (pred * target).sum(dim=(1, 2, 3)) union = pred.sum(dim=(1, 2, 3)) + target.sum(dim=(1, 2, 3)) dice = (2 * inter + eps) / (union + eps) return dice.mean().item()参数上,eps防止除零,threshold按验证集表现调。注意pred和target的 shape 要一致,都是[B, 1, H, W]。
4. 训练 ISIC 分割模型时最容易踩的五个坑
4.1 掩码和图像尺寸不一致导致广播错误
现象:DataLoader 返回的img是[3, 256, 256],mask是[1, 256, 256],但拼接 batch 时报错说尺寸不匹配。原因:某张掩码原始尺寸和图像不同,TF.resize只对图像做了,掩码漏了,或者掩码是单通道但被读成了三通道。解决:在__getitem__里对掩码强制convert("L"),并且 resize 时显式指定[img_size, img_size],不要依赖默认行为。另外,可以在 Dataset 初始化时先遍历一遍所有样本,检查图像和掩码的原始尺寸比例,把异常样本列出来。
4.2 掩码像素值不是 0/255 导致 loss 不收敛
现象:训练几个 epoch,loss 一直在 0.69 附近晃,Dice 接近 0。原因:掩码里前景像素值是 1,但代码里用> 127二值化,结果全变成 0,模型学到的全是背景。解决:先跑 2.2 节的像素值统计,确认前景值。如果是 1,就用> 0.5;如果是 255,就用> 127。更稳妥的做法是先把掩码归一化到 [0,1],再统一用> 0.5。
4.3 类别信息里的文件名和图像文件名对不上
现象:想按类别分层采样,结果metadata.csv里的image列是ISIC_0000001,而图像文件名是ISIC_0000001.jpg,直接 merge 全变 NaN。原因:CSV 里存的是不带扩展名的主干,而文件系统里带扩展名。解决:在 pandas 里先对image列做str.replace去掉扩展名,或者给图像文件名也生成一个主干列,两边对齐后再 merge。如果还有_segmentation后缀差异,一并处理。
4.4 验证集 Dice 高但测试集崩了
现象:验证集 Dice 0.85,换一批外部数据或留出测试集,Dice 掉到 0.6。原因:ISIC 数据里不同来源的子集分布差异大,比如皮肤镜图像和临床照片的色调、分辨率、病灶占比都不同。如果验证集和训练集来自同一子集,指标会虚高。解决:划分数据时按diagnosis或数据来源做分层,确保验证集覆盖各类。另外,增强不要只加在训练集,验证集至少做 resize 和归一化,保持和训练一致的预处理。
4.5 显存不够时盲目降分辨率
现象:把img_size从 512 降到 128,显存够了,但小病灶分割 Dice 掉得厉害。原因:ISIC 里有些病灶只占图像很小一块,降分辨率后只剩几个像素,模型根本学不到边界。解决:优先用梯度累积或混合精度,而不是直接砍分辨率。如果必须降,先统计病灶面积分布,确保降采样后前景像素不少于 100 个。另一个办法是随机裁剪出病灶周围区域,而不是整图缩放。
5. 用 ISIC 数据做迁移与验证的进阶技巧
5.1 预训练权重的选择与微调策略
ISIC 数据量不算大,从头训分割网络容易过拟合。常见做法是加载 ImageNet 预训练的编码器,比如 ResNet34 或 EfficientNet-B0,解码器随机初始化。微调时,前几个 epoch 冻结编码器,只训解码器,等 loss 稳定后再解冻全部,用更小的学习率。我一般用1e-3训解码器,解冻后降到1e-4。如果显存允许,用 SegFormer 的 MIT-B0 主干,在 ISIC 上收敛比 U-Net 快,但要注意它的输入归一化参数和 ImageNet 不同,别直接套。
5.2 用交叉验证代替单次划分
ISIC 数据里类别不平衡,单次划分可能让某些稀有类别全落在训练集或验证集。5 折分层交叉验证更稳。每折按diagnosis分层,确保每类在每折里的比例接近全局。跑完 5 折后,报告 Dice 的均值和标准差,而不是只报最好那一折。如果标准差超过 0.05,说明模型对数据划分敏感,需要检查是不是某些折里小病灶样本过多。
5.3 可视化验证:别只看数字
训练完一定要把预测掩码叠回原图看。我习惯每跑完一个 epoch,从验证集里抽 8 张,画成 4 行 2 列:原图、真实掩码、预测掩码、叠加图。叠加图里真值用绿色轮廓,预测用红色轮廓,重合部分变黄。这样一眼能看出是漏检、过分割还是边界偏移。数字指标只能告诉你「差多少」,可视化告诉你「差在哪」。如果发现模型在毛发多的样本上边界特别碎,就回去加毛发相关的增强;如果发现小病灶整块漏,就调 Dice 权重或加 focal loss。
5.4 一个我常犯的错误
早期我总想把所有能加的增强都堆上去,结果训练集和验证集的分布被拉得太开,验证 Dice 反而降了。后来我给自己定了个规矩:每加一种增强,先单独跑一轮,看验证 Dice 有没有提升,没有就撤掉。ISIC 数据本身多样性已经够强,增强的目的是补足短板,不是炫技。另外,掩码的边界像素在训练时不要忽略,有些实现会把边界像素的 loss 权重调低,但在皮肤病变分割里,边界恰恰是最难也最重要的部分,我一般保持等权重,或者用边界加权的 Dice。
希望帮到你。
本文还有配套的精品资源,点击获取