简介:面向医学图像分割学习者的乳腺肿瘤细胞核分割数据集,覆盖256×256与1000×1000两种分辨率,PNG原图与PNG掩膜一一对应,mask采用0/1阈值模板,类别信息可在classes文本中查看。资源内含训练集与测试集,训练集提供66对图像及掩膜,测试集另附对应图像与掩膜,可用于肿瘤区域提取、细胞核分割等模型训练与效果验证。压缩包共167个文件,以165张PNG图像为主,另含1个classes.txt标注说明与1个可视化py脚本;整体大小62.07MB,便于下载与快速上手。可视化脚本可直接运行,随机抽取一张图片并展示原始图、GT掩膜以及GT叠加原图的蒙版效果,省去自行编写展示代码的步骤。该数据集已吸引194人学习浏览,适合医学影像入门、分割算法练习及课程实验使用。
1. 你拿到的不是“图片包”,而是一套能直接训练医学图像分割模型的数据资产
做乳腺肿瘤病理图像研究的人,最怕的不是模型调不好,而是没有带像素级标注的数据。这个医学图像分割数据集把最费时间的环节做掉了:原始病理图像、细胞核级别的标签文件、还有配套的数据可视化代码一起打包,你拿到手就能开始训分割模型,不用自己一张张去画掩膜。所谓“2类”,指的是把每一个像素归到两个类别里——通常就是背景和细胞核前景,也有的是肿瘤细胞核和正常细胞核两类。这套东西最适合三类人:刚入门医学图像分割的研究生、要快速验证 U-Net 系列方案的算法工程师,以及做病理 AI 产品原型的开发者。一句话说清楚价值:图片本身不稀奇,标签文件和可视化代码才是真正替你省下几个星期标注时间的东西。
2. 拆开数据包:2类分割任务到底在分割什么,标签文件长什么样
2.1 类别定义与任务边界:先搞清楚“2类”是哪两个类
乳腺肿瘤细胞核分割最常见的标注形式有两种,你拿到数据集后第一件事不是跑代码,而是打开标签文件确认属于哪一种。第一种是背景加前景的二分类,掩膜里所有细胞核统一标成前景,不管它是肿瘤细胞核还是正常细胞核。第二种是肿瘤细胞核和“其他所有东西”的二分类,正常细胞核被归入背景。两种做法在医学任务里都存在,尤其是当数据来自不同医院或不同染色方案时,标注规范会有差异。
有个细节特别容易被忽略:掩膜的像素值写法。有的数据集把前景标成像素值 255,有的标成 1,有的用 0 和 255 对照。如果你的损失函数把 255 当成类别编号去算交叉熵,模型会直接翻车,因为 logits 输出维度和你标签里的类别值对不上。我一般拿到标签文件会先用 np.unique 看一眼像素值分布,再决定要不要做mask = mask // 255这类映射。这一步只要花两分钟,却能省掉后面训练时好几个小时的排错时间。
| 类别定义方式 | 标签含义 | 常见像素值 | 适用模型输出 |
|---|---|---|---|
| 背景/前景 | 所有细胞核统一为前景 | 0 和 255 或 0 和 1 | 单通道二分类输出 |
| 肿瘤核/其他 | 仅肿瘤细胞核为前景,正常细胞核归入背景 | 0 和 1 | 单通道二分类输出 |
| 三分类扩展 | 背景/正常细胞核/肿瘤细胞核 | 0/1/2 索引图 | 多类别分割,需改输出头 |
如果你的数据集目录里有三个子目录 images、labels、codes,那大概率是第一种或第二种定义。打开标签文件看一眼,比读任何 README 都管用。除了格式,你还要关注染色差异:病理切片的苏木精-伊红染色深浅在不同批次间差异很大,这会让模型在训练集上表现很好、换一批数据就崩。二分类任务对这类差异尤其敏感,因为决策边界完全依赖纹理特征。这一点先记住,后面做数据增强时我们还会提到。
2.2 标签文件格式:用一段代码确认图像和掩膜真的对得上
拿到手的数据包里,图像通常是常见的病理切片格式,掩膜则有三种常见存法:PNG 单通道灰度图、多边形坐标 JSON、以及 COCO 风格的 JSON 标注。PNG 掩膜最省事,读进来就是一个和原图等尺寸的矩阵;JSON 格式则需要先 rasterize 成掩膜才能参与训练。绝大多数情况下你会得到 PNG 掩膜,下面这段代码可以直接用于确认尺寸和像素范围:
import cv2 import numpy as np image_path = "images/sample_001.png" mask_path = "labels/sample_001.png" image = cv2.imread(image_path, cv2.IMREAD_COLOR) mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) print("image shape:", image.shape) # 期望 (H, W, 3) print("mask shape:", mask.shape) # 期望 (H, W) print("mask unique values:", np.unique(mask)) print("mask dtype:", mask.dtype)这段代码里最关键的是cv2.imread的第二个参数。cv2.IMREAD_COLOR强制读成三通道 BGR 图,cv2.IMREAD_GRAYSCALE把掩膜读成单通道灰度图。如果掩膜本身是调色板 PNG,不带这个参数会读出一个三通道的奇怪结果,后面做np.unique时会看到一堆莫名其妙的 RGB 组合,而不是干净的 0 和 1。另外注意 OpenCV 读进来是 BGR 顺序,可视化时想红绿正常显示,得先用cv2.cvtColor(image, cv2.COLOR_BGR2RGB)转一下。
掩膜和原始图对不上的另一个常见原因是尺寸不匹配。有些数据集的掩膜经过了缩放以减小体积,比如原图是 2048×2048,掩膜是 512×512。如果直接拿去训练,模型输出的概率图和标签对不上,损失的梯度会一团糟。遇到这种情况需要等比缩放掩膜,注意插值方式:用cv2.INTER_NEAREST,不要用线性插值,否则标签边界会被插出 0.5 这样的中间值,类别就乱了。
3. 数据可视化代码怎么用起来:从“看得见”到“检查标注质量”
3.1 把掩膜叠到原图上:透明叠加与轮廓画法
医学图像分割的数据可视化代码,本质上就干两件事:让你肉眼判断标注准不准,以及让你在论文里产出一张能说明问题的对比图。先说最常用的透明叠加法。透明叠加适合展示区域重合度,掩膜半透明地盖在原图上,一眼就能看出细胞核边界到底标得准不准。
import cv2 import numpy as np image = cv2.imread("images/sample_001.png") mask = cv2.imread("labels/sample_001.png", cv2.IMREAD_GRAYSCALE) # 把像素值统一为 0/1,避免 255 带来的数值问题 mask_binary = (mask > 127).astype(np.uint8) # 生成绿色叠加层 overlay = np.zeros_like(image) overlay[mask_binary == 1] = (0, 255, 0) # BGR 下的绿色 # alpha 控制透明度:0.5 表示前景和原图各占一半 blended = cv2.addWeighted(image, 0.7, overlay, 0.3, 0) # 再画一圈轮廓,让边界更清晰 contours, _ = cv2.findContours( mask_binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) cv2.drawContours(blended, contours, -1, (0, 0, 255), 2) cv2.imwrite("visualization/sample_001_overlay.png", blended)cv2.addWeighted的四个参数依次是输入图、输入图权重、叠加层、叠加层权重、以及亮度偏置。透明度不是越大越好:alpha 调到 0.5 以上会盖住细胞核本身的纹理,反而看不出标注是否贴边;一般原图权重 0.7、掩膜权重 0.3 比较平衡。轮廓画出来之后,如果你看到轮廓线离细胞核边缘明显有偏移,就要怀疑原始图和掩膜是否来自同一次裁剪,或者存在坐标偏移。
另一个高频需求是把多个样本拼在一张图里对比。常见做法是把原图、掩膜、叠加图按行拼接。注意拼接前所有图要统一尺寸,否则np.hstack会报维度错误。缩放到统一尺寸时同样用最近邻插值,理由和上面提到的一致——保住边界信息。
3.2 类别占比与细胞核数量的一次性体检
拿到数据集直接训练是大忌。有些数据集标注漏得离谱,某些图片里甚至只有背景、一个细胞核都没有。这类样本不加处理直接放进训练集,会让模型学到“输出全黑也能把损失压得很低”的坏习惯。所以训练前先做一次数据体检,统计每一张图的类别占比和连通域数量。
import cv2 import numpy as np from glob import glob mask_files = sorted(glob("labels/*.png")) for mask_file in mask_files: mask = cv2.imread(mask_file, cv2.IMREAD_GRAYSCALE) mask_binary = (mask > 127).astype(np.uint8) foreground_ratio = mask_binary.mean() if foreground_ratio < 0.01: print(f"[warning] {mask_file} foreground ratio = {foreground_ratio:.4f}") # 连通域分析:统计细胞核个数并过滤小噪声 num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats( mask_binary, connectivity=8 ) # labels 从 0 开始,0 是背景,所以实际核数是 num_labels - 1 nucleus_sizes = stats[1:, cv2.CC_STAT_AREA] valid_nuclei = nucleus_sizes[nucleus_sizes > 20] print(f"{mask_file}: {num_labels - 1} nuclei, valid > 20px: {len(valid_nuclei)}")connectedComponentsWithStats的connectivity=8表示八邻域连通,即上下左右再加四个对角方向都算同一个连通区域。细胞核之间往往是挤压在一起的,如果用 4 邻域会把粘连的细胞核拆碎,统计出来的数量比真实值偏大。面积阈值 20 是经验值,具体取决于你的图像分辨率。如果是高倍镜下 2048×2048 的图,单个细胞核通常占几百到上千像素,小于 20 像素的基本是染色杂质或标注毛刺,统计时可以过滤掉。
这套体检能在训练前发现三类问题。第一类是前景占比极端不均衡的样本,少数图片前景占比超过 0.7 或低于 0.01 都会拉动模型偏向背景或前景。第二类是掩膜整体错位,比如某张图的标签明显是上一张图的——看连通域统计看不出来,但叠加可视化能发现。第三类是掩膜里有孤立噪声点,连通域结果里会出现面积只有 1 到 5 像素的小连通域,如果数量特别多,建议用形态学开运算清一遍再做训练数据。
3.3 检查原始图与掩膜对齐的三种常用方法
第一种是肉眼直接看叠加图,最直观但效率低,适合抽检。第二种是只画掩膜轮廓,不填充,分辨率高的时候比透明叠加更容易看清细微偏移。第三种是计算掩膜面积占前景理论面积的比重,比重明显偏离时说明掩膜有系统性缺失。
我常用的做法是随机抽 30 到 50 张样本批量生成叠加图,然后快速翻一遍。注意翻图时重点看细胞密集区域,那里最容易出现掩膜粘连和漏标。如果发现某一张图的掩膜整体偏移了几个像素,但其他图都正常,别急着重新标注,先用相位相关配准试试能不能自动校正偏移。cv2.phaseCorrelate可以算出两张灰度图之间的平移量,对几像素级别的偏移很好使。
4. 从数据集到训练管线:预处理、数据划分与增强参数
4.1 数据集目录怎么组织最省心
很多人在这一步图省事,把预处理脚本、数据原图、临时输出全放在同一条目录下,结果就是一个月后想复现实验,翻了半天不知道哪个版本的数据是干净的。我一般按下面的结构组织:
project/ ├── data/ │ ├── raw/ │ │ ├── images/ │ │ └── masks/ │ ├── processed/ │ │ ├── train/ │ │ ├── val/ │ │ └── test/ │ └── splits.json ├── codes/ │ ├── visualize.py │ ├── preprocess.py │ └── train.py └── outputs/raw目录保持原样不动,所有预处理从raw读取、写入processed。这样做的好处是后悔药随时能吃:就算预处理参数调错了,重新跑一遍脚本就行,不用向别人或自己重新要原始数据。数据划分信息集中在splits.json里,记录每个样本属于哪个集合。按样本 ID 划分而不是按文件路径划分,因为同一张病理大图可能被你裁剪成多个 patch,按 patch 划分会让同一张大图的不同部分同时出现在训练集和验证集里,验证指标虚高。
4.2 裁剪和归一化的参数选择
病理图像普遍很大,2048×2048 甚至更高分辨率的整图直接送进 U-Net 会把显存撑爆。常见做法是随机裁剪固定尺寸的 patch,或者用滑窗裁剪成带重叠的 patch。裁剪参数直接影响最终分割质量,三个参数最值得调:patch 尺寸、重叠率、裁剪数量。
import cv2 import numpy as np class RandomCropPair: def __init__(self, crop_size=(512, 512), foreground_ratio=0.3): self.crop_size = crop_size self.foreground_ratio = foreground_ratio def __call__(self, image, mask): h, w = image.shape[:2] ch, cw = self.crop_size assert h >= ch and w >= cw, "crop size must be smaller than image" best_crop = None # 多尝试几次,找一个前景占比合格且不过于偏边的裁剪 for _ in range(20): y = np.random.randint(0, h - ch + 1) x = np.random.randint(0, w - cw + 1) image_crop = image[y:y + ch, x:x + cw] mask_crop = mask[y:y + ch, x:x + cw] if mask_crop.mean() >= self.foreground_ratio: best_crop = (image_crop, mask_crop) break if best_crop is None: best_crop = (image_crop, mask_crop) return best_crop[0], best_crop[1]这里的np.random.randint是纯随机采样,配合 20 次尝试来筛选前景占比。前景占比阈值foreground_ratio建议设在 0.1 到 0.3 之间。细胞核分割任务里前景占比通常不到 0.3,设得太高会反复随机裁剪直到超时,设得太低会让模型大量看到纯背景 patch。如果数据集中在少数几张超大图上,裁剪时还要注意必须使用同一坐标裁剪 image 和 mask,不能用两套随机数,很多人在这里踩坑。
归一化在医学图像分割里通常不直接用 ImageNet 的均值和标准差,因为病理图的色彩分布和自然图像差得很远。这里提供两个选择:最稳的是只做 0 到 1 缩放,即image = image.astype(np.float32) / 255.0。如果想追求更好的收敛效果,可以先用完整训练集统计每个通道的 mean 和 std,再做标准化。注意用验证集去算 mean 和 std 就是数据泄漏,必须在划分数据集之后只用训练集的统计量。
4.3 数据增强要克制:医学分割里敢用的几个变换
医学图像分割的数据增强和自然图像语义分割不一样,约束特别多。病理图像里的细胞核有明确的形态语义,乱用随机缩放、拉伸、裁剪会把细胞核形状扭曲到不合理,模型学到的是“变形核”的特征。我常用的增强列表如下:
import albumentations as A train_transform = A.Compose([ A.HorizontalFlip(p=0.5), A.VerticalFlip(p=0.5), A.RandomRotate90(p=0.5), A.RandomBrightnessContrast( brightness_limit=0.1, contrast_limit=0.1, p=0.5 ), A.GaussNoise(var_limit=(10.0, 30.0), p=0.2), ])HorizontalFlip和VerticalFlip不会改变细胞核的形态,对病理数据安全。RandomRotate90只允许 90 度的整数倍旋转,不会产生斜向插值模糊。RandomBrightnessContrast用于模拟不同染色深浅,但系数别超过 0.1,超过这个范围细胞核和背景的对比度会被破坏。GaussNoise用来增强模型对染色噪声的鲁棒性,但 var_limit 不宜太大。
两个需要特别谨慎的增强是 ElasticTransform 和 CoarseDropout。弹性形变在自然图像上有奇效,但细胞核是有弹性特征的生物组织,过度形变会让标注失去意义。CoarseDropout 随机挖掉矩形区域,在自然图像里能强制模型关注上下文,但在医学分割里容易让模型学到“看不到就猜一个区域”的错误行为。如果你是新手,第一版管线建议只启用翻转、旋转和亮度对比度,跑通 baseline 之后再慢慢加增强看收益。
5. 训练前必须避开的坑:标签错位、类不均衡与显存不足的 5 个现场
5.1 肉眼看不出来的标签错误:最隐蔽的三个问题
标注看起来没问题,但模型怎么训都训不出来,问题往往出在标签本身。第一个隐蔽问题是掩膜和原图的通道顺序不一致。OpenCV 读进来是 HWC 和 BGR,PyTorch 张量是 CHW,如果你读掩膜之后忘了转通道顺序,模型拿到的输入和标签错位,损失曲线就会一直在高位震荡。解决方法是统一用一套读取约定:所有图像读成 RGB 的(H, W, 3),所有掩膜读成灰度(H, W),在送入模型前再统一转成(C, H, W)。
第二个隐蔽问题是掩膜的类别值不对称。有的掩膜背景是 0、前景是 255,有的反过来。(mask > 127).astype(np.uint8)这种写法在前一种情况可行,在后一种情况下会把背景全变成前景,导致训练时模型疯狂输出全图前景。处理前一定要打印np.unique(mask),确认后再做映射。
第三个隐蔽问题是掩膜边界有 “halo”。部分数据集用多边形标注后做栅格化,会在边界留一圈半透明的过渡带。这会导致边界像素处于两个类别之间,加重分割边界不清晰的问题。解决办法是对掩膜做形态学腐蚀,用cv2.erode去掉一圈边界像素,或者把训练损失聚焦到掩膜内部的像素上。
5.2 训练时的五个高频翻车点
现象一:损失值一直不降,从第一个 epoch 开始就在同一个量级上下抖动。原因通常是前景占比太小,BCE 被大量背景像素支配。细胞核分割任务里前景占比经常不足 0.1,模型只要输出全背景就能把损失压到很低,梯度中来自前景的信息太少。解决方法是换损失函数。推荐用 Dice Loss 或 Focal Loss。Dice Loss 直接优化类别重合度,对前景占比不敏感,是医学分割的默认选项。Focal Loss 的 gamma 参数设 2.0,让模型关注难分像素。
现象二:训练集指标不错,验证集指标暴跌。原因大概率是数据划分泄漏。裁剪 patch 时如果同一张病理大图的不同 patch 同时进了训练集和验证集,模型等于见过了验证集的大部分内容。解决方法是先按图片 ID 划集合,再对每张图做裁剪,并且在进入 dataloader 之前对验证集不做任何随机增强。增强泄漏也是常见的验证指标虚高来源,检查一下验证集的 transform 里有没有翻转和旋转。
现象三:显存直接 OOM。细胞核分割的 patch 尺寸一般 256×256 到 512×512,超过 512 如果不是超大显存,建议别试。显存不足时先检查 batch size,再看混精度。用 PyTorch 自带的混合精度训练可以把显存占用降到原来的六成左右。如果还不行,把 patch 尺寸降一半,同时把 batch size 提高,通常效果比强行跑大 patch 更好。
现象四:预测图上细胞核全部连成一片,边界完全消失。这是细胞核分割的典型问题。细胞核在病理图上天然紧密贴着彼此,模型把邻近细胞核预测成一个整体。后处理阶段用分水岭算法可以拆开,代码逻辑是:先给预测概率图做阈值二值化,再用距离变换找局部极大值作为分水岭种子,最后执行分水岭。这一套后处理在推理阶段很常用,但不要指望它把模型本身的错误全修好,边界质量最终还是要靠训练阶段的数据和损失函数解决。
现象五:训练时随机种子设了等于没设,每次跑出来结果不一样。原因是 PyTorch、NumPy、Python 内置 random、以及 GPU 的 cuDNN 各有各的随机源。运行时只设torch.manual_seed(42)只控制了一部分。完整的配置是在训练脚本开头同时设置四个随机源,并让torch.backends.cudnn.benchmark保持 False 以保证结果可复现。
6. 往验证和落地走一步:用 Dice 与 IoU 评估模型,而不是只看 loss 曲线
模型训练完,你的同事大概率会问一句:你这个分割到底准不准?此时奉上验证脚本就是最有说服力的回答。别拿 loss 曲线说事,loss 是训练过程的中间产物,和最终临床可用的分割质量并不是一回事。我习惯在验证集上计算每个样本的 Dice 系数和 IoU,再取均值作为最终报告指标。
import numpy as np def dice_score(pred_mask, gt_mask, smooth=1e-6): pred_mask = pred_mask.astype(np.uint8) gt_mask = gt_mask.astype(np.uint8) intersection = np.sum(pred_mask * gt_mask) return (2.0 * intersection + smooth) / ( np.sum(pred_mask) + np.sum(gt_mask) + smooth ) def iou_score(pred_mask, gt_mask, smooth=1e-6): pred_mask = pred_mask.astype(np.uint8) gt_mask = gt_mask.astype(np.uint8) intersection = np.sum(pred_mask * gt_mask) union = np.sum(pred_mask) + np.sum(gt_mask) - intersection return (intersection + smooth) / (union + smooth)smooth是一个防止分母为零的小常数,设 1e-6 足够。计算时两个掩膜必须完全二值化,如果你的预测概率输出在 0.4 到 0.6 之间徘徊,直接取 0.5 阈值可能不是最佳选择。我遇到过一次概率图分布整体偏低的情况,那是因为染色的批次差异让模型倾向于保守。用 Otsu 做自适应阈值是个更稳的做法,cv2.threshold(pred, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)会自动找分割点。阈值取完,还要同步做一步面积过滤,删掉小于 20 像素的预测噪声区域,这时再算 Dice 会更贴近病理医生的观感。
评估的最终一步是把预测掩膜叠加到原图上做定性分析,重点看三处:细胞密集区的边界是否清晰,小细胞核是否被漏掉,大细胞核是否被拆成两半。这些在数字指标上表现不明显,但在实际应用中是决定性的。我自己的习惯是维护一张样例表格,把好中差三种样例各挑几张存下来,每次换模型版本时对比一遍。这个习惯帮我发现过两次数据预处理引入的低级错误,一次是掩膜被缩放了 0.5 倍,另一次是训练集和验证集的标签映射写反了,数字指标都看不出异常,可视化一眼就发现问题。希望这套从数据检查到验证评估的流程能帮到你,至少能让你在拿到这套乳腺肿瘤细胞核分割数据时,比当初的自己少走几段弯路。
本文还有配套的精品资源,点击获取