☰
乳腺细胞癌症分割数据集实战:从标签质检到训练避坑指南
2026/10/8 1:12:59 网站建设 项目流程

简介:本资源面向医学图像分割方向的初学者与算法实践者,提供一套乳腺细胞癌症分割的二分类数据集,可直接用于训练与验证分割网络。数据图像与mask均为512×512的png格式,mask采用0与255的阈值标注,0为背景、255为癌症细胞,具体类别可在classes文本中查看。包内共103个文件,以101个png图像与标签为主,另含1个txt类别说明和1个py可视化脚本,压缩包约23.91MB,分为训练集40张图像及对应mask、测试集10张图像及对应mask。附带的可视化脚本无需修改即可运行,随机抽取一张图片,将原图、GT图像及GT在原图上的蒙板结果一并展示并保存到当前目录,便于快速核对标注质量。目前已有208人学习,适合希望上手医学图像分割、搭建基线并观察掩膜效果的中级读者参考。

1. 乳腺细胞癌症分割数据集:从病理切片到二分类掩码,这套数据能省掉多少标注成本

病理切片里找癌细胞,最耗人的不是模型调参,是标注。一张 H&E 染色的乳腺组织切片,放大到 40 倍后视野里全是密密麻麻的细胞核,哪些是恶性、哪些是良性、边界在哪,全靠病理医生一格格画。我见过一个三人标注小组,两周才啃完八十来张图,最后还因为边界标准不统一返工。所以当我第一次拿到「乳腺细胞癌症分割数据集(2类)」这种打包好的资源时,第一反应不是兴奋,是去核对它的掩码质量——因为医学图像分割数据集的价值,八成压在标签文件上,剩下两成才是数据可视化代码好不好用。

这套数据面向的任务很明确:二类语义分割,把乳腺细胞区域分成背景和癌变细胞(或按数据集约定分成正常/恶性两类),输出和原图同尺寸的掩码。它适合三类人:想入门医学图像分割但拿不到合规数据的算法工程师、做病理 AI 辅助诊断需要快速搭基线的研究者、以及带学生做课程设计但不想在标注上耗掉半学期的老师。热搜里 yolov8训练自己的数据集、mmrotate训练dota数据集 这些词天天有人搜,说明大家缺的从来不是模型,是能直接喂进去的干净数据。这套东西解决的正是这个卡点:图、标签、可视化脚本一次给齐,你拿到手当天就能跑通第一个 IoU 数字。

2. 拆开这套数据集:目录结构、标签格式与二分类掩码的读法

2.1 先搞清楚 2 类到底指哪两类

医学分割里说「2类」,不同数据集口径不一样,必须先确认再动手。常见的有三种约定:第一种是前景/背景二值,掩码里 0 是背景、1 是癌变细胞区域;第二种是正常细胞/癌变细胞,背景被并入其中一类;第三种是癌变/非癌变组织,粒度更粗。这套数据按标题「乳腺细胞癌症分割」的措辞,我一般默认它是前景背景二值加细胞级标注,也就是掩码里非零像素就是癌变细胞。但你拿到手第一件事不是信我,是打开一张掩码做直方图,看看到底有几个唯一值。

import numpy as np from PIL import Image # 读一张掩码,统计唯一像素值,确认类别约定 mask = np.array(Image.open("masks/patient_001_mask.png")) unique, counts = np.unique(mask, return_counts=True) for u, c in zip(unique, counts): print(f"像素值 {u}: {c} 个 ({c / mask.size * 100:.2f}%)")

这段代码的逻辑很直白:把掩码转成 numpy 数组后统计唯一值。如果输出只有 0 和 1,就是标准二值;如果出现 0、1、2 甚至 255,说明标注用了多值编码或者调色板 PNG,你得先做映射再训练。参数上唯一要注意的是Image.open读调色板 PNG 时可能返回 P 模式,稳妥做法是加Image.open(...).convert("L")强制转灰度,否则np.array出来的可能是索引而不是真实类别值。这一步不做,后面算出来的 IoU 全是错的,属于典型的黑匣子式翻车。

2.2 目录结构与文件命名约定

一个规整的医学分割数据集,目录通常长这样:images 放原图,masks 放标签,可能还有一份 splits 记录训练验证划分。文件名必须一一对应,否则你写 DataLoader 时对不上号。我见过最坑的一种情况是原图叫001.jpg、掩码叫001_mask.png,后缀还不一样,加载时得靠字符串替换。所以拿到数据先跑一遍配对检查,别等训练到一半才发现有图没标签。

import os img_dir, mask_dir = "images", "masks" imgs = {os.path.splitext(f)[0] for f in os.listdir(img_dir)} masks = {os.path.splitext(f)[0].replace("_mask", "") for f in os.listdir(mask_dir)} print("原图数量:", len(imgs)) print("掩码数量:", len(masks)) print("有图无标签:", imgs - masks) print("有标签无图:", masks - imgs)

逻辑说明:用集合做差集,一次性暴露所有不配对的文件。参数上replace("_mask", "")是针对常见命名习惯,如果你的数据掩码没有这个后缀,把这行去掉即可。这一步花三十秒,能省掉后面几小时的排查。配对干净之后,再去看图像尺寸是否统一——医学数据经常混着不同扫描分辨率,尺寸不一致就得在预处理阶段统一 resize 或 padding,这个决定会直接影响小目标细胞的召回。

2.3 标签文件的三种常见格式与转换

标签文件不一定都是 PNG 掩码。有些数据集给的是 JSON(多边形坐标)、有些给的是 COCO 格式的 annotations,还有些直接给 numpy 的.npy。这套数据标题里写了「标签文件」,大概率是掩码图,但你要做好转换准备。多边形转掩码是最常见的一种,用opencv或PIL.ImageDraw都能做。

import json import numpy as np import cv2 # 假设标签是 COCO 风格的多边形,转成二值掩码 with open("annotations.json") as f: ann = json.load(f) h, w = ann["imageHeight"], ann["imageWidth"] mask = np.zeros((h, w), dtype=np.uint8) for seg in ann["shapes"]: pts = np.array(seg["points"], dtype=np.int32) cv2.fillPoly(mask, [pts], color=1) # 前景填 1 cv2.imwrite("converted_mask.png", mask)

逻辑说明:fillPoly把多边形内部填成 1,多个多边形叠加就得到完整前景。参数上color=1决定了类别值,如果你要区分正常/癌变两类,就按多边形所属类别分别填 1 和 2。注意np.int32这个类型不能省,fillPoly对点坐标类型敏感,传 float 会报错。转换完一定要可视化抽查几张,多边形标注最容易出现自相交或点序错乱,转出来的掩码会缺一块,这种错误肉眼一看便知,但纯看数字看不出来。

3. 用可视化代码把数据看明白:叠加、直方图与边界检查

3.1 原图与掩码叠加,一眼看出标注质量

数据可视化代码是这套资源里最容易被低估的部分。很多人拿到数据直接开训,结果 loss 不降,回头才发现掩码整体偏移了几个像素。叠加显示是最快的质检手段:把掩码做成半透明红色盖在原图上,边界对不对、有没有漏标,一眼就清楚。

import numpy as np from PIL import Image img = np.array(Image.open("images/patient_001.png").convert("RGB")) mask = np.array(Image.open("masks/patient_001_mask.png").convert("L")) # 掩码二值化后叠加红色 overlay = img.copy() overlay[mask > 0] = [255, 0, 0] blended = (img * 0.6 + overlay * 0.4).astype(np.uint8) Image.fromarray(blended).save("check_overlay.png")

逻辑说明:先复制原图,把掩码非零区域涂红,再按 6:4 混合,既保留组织纹理又能看清标注范围。参数上mask > 0这个阈值对二值掩码够用,如果掩码是多值编码,改成mask == 1更精确。混合系数 0.6/0.4 是我常用的,红色太浓会盖住细胞形态,太淡又看不清边界,你可以按自己屏幕调。这一步建议至少抽查二十张,覆盖不同患者、不同染色批次,因为病理数据批次间色差很大,标注质量也常常参差不齐。

3.2 类别分布直方图,判断有没有严重不平衡

医学分割几乎必然遇到类别不平衡,癌变区域往往只占整张图的百分之几。训练前先统计前景像素占比,能帮你决定要不要上 Dice Loss 或者加权交叉熵。如果前景占比低于 5%,纯交叉熵大概率会让模型学会「全预测背景」这种偷懒解。

import numpy as np from PIL import Image import os ratios = [] for f in os.listdir("masks"): m = np.array(Image.open(os.path.join("masks", f)).convert("L")) ratios.append((m > 0).mean()) ratios = np.array(ratios) print(f"前景占比 均值 {ratios.mean():.4f} 最小 {ratios.min():.4f} 最大 {ratios.max():.4f}")

逻辑说明:逐张算前景像素比例,最后看均值和极值。参数上(m > 0).mean()直接给出比例,不用手动除。如果均值在 0.1 到 0.3 之间,普通交叉熵还能用;低于 0.05 就建议换 Dice 或 Tversky Loss。极值也值得看,如果某张图前景占比接近 1,那多半是标注把整张图都涂了,属于脏数据,得单独拎出来复核。这个统计跑一遍几分钟,但能帮你避开训练几天后才发现模型摆烂的坑。

3.3 边界带可视化,检查标注一致性

分割任务里最难的从来不是大块区域,是边界。不同标注者对癌变细胞边界的判断能差出十几个像素,这种不一致会直接拉低模型上限。把掩码做形态学腐蚀和膨胀,取差集就是边界带,单独可视化出来能看出标注风格是否统一。

import cv2 import numpy as np from PIL import Image mask = np.array(Image.open("masks/patient_001_mask.png").convert("L")) kernel = np.ones((5, 5), np.uint8) boundary = cv2.dilate(mask, kernel) - cv2.erode(mask, kernel) img = np.array(Image.open("images/patient_001.png").convert("RGB")) img[boundary > 0] = [0, 255, 0] Image.fromarray(img).save("check_boundary.png")

逻辑说明:膨胀减腐蚀得到边界带,涂成绿色叠在原图上。参数上kernel大小决定边界带宽度,5×5 适合细胞级标注,如果目标很大可以调到 9×9。看的时候重点观察边界是否平滑、有没有锯齿状突变,锯齿往往意味着标注是快速涂抹而非精细勾画。这套数据如果边界普遍粗糙,你就别指望模型能学到精细分割,评估指标定太高只会自欺欺人。

4. 训练前的预处理与增强:乳腺病理切片不能照搬自然图像那套

4.1 染色归一化,病理数据的头号预处理

自然图像增强那套(随机裁剪、翻转、色彩抖动)搬到病理切片上,色彩抖动这一条要慎用。H&E 染色在不同实验室、不同扫描仪下色差极大,同一张切片在不同设备上能偏成粉紫或蓝紫。直接做色彩抖动会放大这种域偏移,模型学到的可能是染色风格而不是细胞形态。正确做法是先做染色归一化,把颜色分布对齐到统一参考,再做几何增强。

import numpy as np import cv2 def stain_normalize(img, target_mean=(0.8, 0.7, 0.75)): # 简化版:按通道做均值方差对齐,工程上常用 Macenko 或 Reinhard img = img.astype(np.float32) / 255.0 mean, std = img.mean(axis=(0, 1)), img.std(axis=(0, 1)) out = (img - mean) / (std + 1e-6) out = out * 0.2 + np.array(target_mean) return np.clip(out * 255, 0, 255).astype(np.uint8)

逻辑说明:这是 Reinhard 归一化的简化实现,按通道把图像拉回统一均值方差。参数上target_mean是参考图的通道均值,工程里更稳的做法是选一张标注质量高的图当参考,用 Macenko 方法做 SVD 分解。这里给简化版是为了让你先跑通流程,真要上生产建议换tiatoolbox或staintools这类成熟库。注意归一化必须对原图和掩码同步处理时只作用于原图,掩码不能动,否则标签就废了。

4.2 几何增强与类别平衡采样

几何增强对病理切片是安全的:水平翻转、垂直翻转、90 度旋转都不会改变细胞语义,因为切片本身没有固定方向。但要注意,如果你的数据里细胞有方向性分布(比如某些组织切面),过度旋转可能引入不真实样本。我一般用翻转加小角度旋转(±15 度),再配合随机裁剪到固定尺寸。

import albumentations as A transform = A.Compose([ A.HorizontalFlip(p=0.5), A.VerticalFlip(p=0.5), A.RandomRotate90(p=0.5), A.ShiftScaleRotate(shift_limit=0.05, scale_limit=0.1, rotate_limit=15, p=0.5), A.RandomCrop(height=512, width=512, p=1.0), ]) # 注意:掩码用最近邻插值,原图用双线性 aug = transform(image=img, mask=mask)

逻辑说明:albumentations的 Compose 会同步处理图和掩码,这是它比手写增强靠谱的地方。参数上RandomCrop设成 512 是因为病理切片原图往往几千像素,直接训练显存吃不消,切块是标准做法。关键细节是掩码插值方式,albumentations默认对掩码用最近邻,但如果你手动改过配置,一定要确认没被改成双线性,否则掩码会出现 0.5 这种非整数类别值,训练直接崩。类别平衡采样则是针对前景少的图提高采样权重,用WeightedRandomSampler按前景占比倒数设权重即可。

4.3 训练集验证集划分的坑

医学数据划分不能纯随机。同一患者的多个切片如果被分到训练和验证两边,验证指标会虚高,因为模型见过这个患者的组织特征。正确做法是按患者划分,保证验证集里的患者训练时完全没见过。这套数据如果文件名带患者 ID,就按 ID 分组划分;如果没有,至少按图像来源或批次划分。

import os from sklearn.model_selection import GroupShuffleSplit files = sorted(os.listdir("images")) groups = [f.split("_")[0] for f in files] # 假设文件名前缀是患者 ID gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, val_idx = next(gss.split(files, groups=groups)) print("训练集:", len(train_idx), "验证集:", len(val_idx))

逻辑说明:GroupShuffleSplit保证同一 group 不会同时出现在两边。参数上groups的提取方式取决于你的命名规则,如果文件名没有患者信息,就得靠外部元数据表。这一步做错,你看到的验证 Dice 0.9 可能实际只有 0.7,上线就露馅。我一般会在划分后再核对一遍两边的患者 ID 集合有没有交集,多花一分钟,买个安心。

5. 避坑与排查:这套数据最容易翻车的五个地方

5.1 掩码像素值不是 0/1,训练 loss 直接 NaN

现象:模型跑第一个 epoch,loss 就变成 nan 或者大得离谱。原因:掩码是调色板 PNG,np.array读出来是 0 到 255 的索引值,或者标注用了 255 表示前景。交叉熵拿到 255 这个类别号直接越界。解决:加载掩码时统一做映射,mask = (mask > 0).astype(np.uint8)或者按实际编码表转换,转换后再统计一次唯一值确认只剩 0 和 1。

5.2 原图和掩码尺寸不一致,resize 后标签错位

现象:叠加可视化时掩码整体偏移或缩放不对。原因:原图是 1024×1024,掩码是 512×512,或者长宽比不同被强行拉伸。解决:加载时先断言尺寸一致,不一致就同步 resize,且掩码必须用最近邻插值。更稳的做法是统一 padding 到相同尺寸再裁剪,避免拉伸引入形变。

5.3 前景占比过低,模型学会全预测背景

现象:训练 loss 稳定下降,但验证 IoU 接近 0,可视化出来全是黑。原因:癌变区域占比太小,交叉熵被背景主导。解决:换 Dice Loss 或 Focal Loss,或者在采样阶段提高前景图权重。判断依据就是第 3 章那个直方图,前景均值低于 0.05 就必须处理。

5.4 验证集指标虚高,上线效果断崖

现象:本地验证 Dice 0.9,换一批数据掉到 0.5。原因:划分时同一患者数据泄漏到两边,或者染色归一化只对训练集做了、验证集没做。解决:按患者分组划分,预处理流程封装成统一函数,训练和推理走同一套。这个坑最隐蔽,因为指标好看的时候没人会怀疑。

5.5 可视化代码路径写死,换目录就报错

现象:跑可视化脚本报 FileNotFoundError。原因:代码里路径是绝对路径或者写死了作者本机的目录结构。解决:把所有路径改成相对路径或命令行参数,用argparse传数据根目录。这套资源里的可视化代码如果路径写死,第一件事就是改成可配置,否则你换个环境就得重写。

6. 从跑通到跑好:评估指标选择与一个提升边界的小技巧

二分类分割的评估,IoU 和 Dice 是最常用的两个,但它们在类别极不平衡时会有欺骗性。一张前景占比 2% 的图,模型全预测背景,IoU 也能有 0.9 以上,因为背景占了绝大多数像素。所以我一般会额外看前景单独的 IoU(也就是把背景排除,只算前景区域的交并比),以及边界带的 F1。前者反映模型有没有真的找到癌细胞,后者反映边界画得准不准。这三个指标一起看,才不会被平均数骗过去。

import numpy as np def metrics(pred, gt): pred, gt = pred > 0.5, gt > 0 inter = (pred & gt).sum() union = (pred | gt).sum() iou = inter / (union + 1e-6) dice = 2 * inter / (pred.sum() + gt.sum() + 1e-6) # 只算前景的 IoU,排除背景主导 fg_iou = inter / (gt.sum() + (pred & ~gt).sum() + 1e-6) return iou, dice, fg_iou

逻辑说明:fg_iou的分母是真实前景加上误检的前景,不包含真阴性背景,所以更能反映前景分割质量。参数上 0.5 是阈值,实际评估时建议扫一遍 0.3 到 0.7,看哪个阈值下 fg_iou 最高,那个才是你部署时该用的。很多论文只报一个 Dice,你复现时如果发现 Dice 高但 fg_iou 低,说明模型在偷懒。

提升边界精度有个便宜好用的技巧:在损失函数里给边界带加权。用第 3 章那个膨胀减腐蚀的方法生成边界掩码,算 loss 时边界像素权重乘 3 到 5 倍。这样模型会把更多注意力放在难分的边界上,前景 IoU 通常能涨两三个点,代价只是多几行代码。我试过在几个病理数据集上,这个技巧对细胞级分割的边界提升比换 backbone 还明显,而且不增加推理成本。

最后说个我自己的习惯:拿到任何医学分割数据集,先花半天只做质检和可视化,不碰模型。这半天里把掩码唯一值、尺寸配对、前景占比、边界质量全过一遍,写成一个检查脚本存下来。后面换数据集、换标注版本,重跑一遍脚本就知道数据有没有变。这个习惯帮我省掉的返工时间,远比那半天多。希望帮到你。

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

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

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

立即咨询