简介:这是一份面向医学图像分析与深度学习研究者的超声乳腺良性图像分割数据集,按背景与良性乳腺两类进行像素级标注,classes文件详细说明类别信息。训练集约300对图像与mask,测试集约130对图像与mask,目录以images与masks分开存放,便于直接接入U-Net、DeepLabV3+等常见分割模型训练与验证。包内另附Python可视化脚本,可随机抽取样本,将原始超声图、GT掩膜图及GT在原图上的叠加效果保存到当前目录,方便快速核对数据质量与标注效果。压缩包为7z格式,共877个文件,主体为875张png图像,另含类别说明txt与可视化脚本py,大小88.01MB。这份数据集已有958人学习,对开展乳腺超声病灶分割实验、构建基准数据集或入门医学图像分割的读者来说,可直接用这套数据划分与配套工具完成模型训练与结果可视化。
1. 医学图像分割数据集上手:这份超声乳腺良性图像资源能直接用来训练
影像组学方向的从业者应该都有同感:医学图像分割最耗时间的往往不是调模型,而是找一份干净、带标签、划分好训练测试集的数据。看到这份“超声乳腺良性图像分割数据集”时,我的第一反应是终于不用自己从 PACS 里翻病例、找医生标注、再手工划分数据了。它把训练集、测试集和对应的 mask 标签都打包好,还附带一个可视化脚本,拿到手先跑一遍脚本就能确认图像和标签是否对齐,再进入训练流程。适合正在做医学图像分割、乳腺超声相关课题的学生和工程师,也适合想快速验证 U-Net 等分割网络效果的从业者。下面从数据本身开始拆。
2. 先摸清家底:images/masks 目录结构、2 类别标签与训练测试集划分
2.1 目录结构与文件命名
这份数据集的目录划分很直接:训练集和测试集各自拥有 images 与 masks 两个子目录。images 放原始超声图像,masks 放对应的分割标签。从项目正文给出的文件名来看,图像以“benign (编号).png”的方式命名,括号里是序号,说明数据在采集时做了匿名化处理,不携带患者信息,这对后续公开发表实验结果是友好的。
我用常见的目录树展示一下内部结构:
dataset/ ├── train/ │ ├── images/ │ │ ├── benign (1).png │ │ ├── benign (2).png │ │ └── ... │ └── masks/ │ ├── benign (1).png │ ├── benign (2).png │ └── ... └── test/ ├── images/ │ ├── benign (300).png │ └── ... └── masks/ ├── benign (300).png └── ...这里的命名规律有一个隐藏信息:同一张图的 image 和 mask 共享文件名,只是放在不同目录下。这意味着训练时可以用文件名做关联,而不是依赖目录顺序,后续做数据划分或剔除坏样本时也不容易错位。
2.2 “2 类别分割”到底怎么理解
摘要里写的是“2 类别的分割,背景、良性乳腺,具体的查看 classes 文件”。很多初学者看到“2 类”会以为是“良性和恶性”二分类,实际上这里的分割任务是像素级的:每个像素要么属于背景,要么属于良性乳腺区域。mask 图像中灰度值为 0 的区域代表背景,灰度值为 255(或 1)的白色区域代表乳腺区域。
这一点在开始训练前一定要确认。常见做法是写一段脚本统计 mask 的像素取值分布,避免出现标签值和你预期不一致的情况。我一般会先打印出 mask 中出现的灰度值集合:
from PIL import Image import numpy as np mask = np.array(Image.open("train/masks/benign (1).png").convert("L")) print("mask shape:", mask.shape) print("unique pixel values:", np.unique(mask))逻辑说明:把 mask 读成单通道灰度图,用 np.unique 列出所有出现的像素值。正常情况应该只输出 0 和 255 两类,如果出现其他数值,说明标签图里有噪点或标记遗留。参数说明:convert("L") 强制转灰度,可以避免 PNG 带 alpha 通道时读到四通道数据的干扰;也可以用 np.bincount 统计各类别占比,进一步确认类别是否均衡。
2.3 训练集和测试集划分的合理性
摘要说明训练集约 300 张、测试集约 130 张,比例接近 7:3。在医学图像分割项目里,这个划分比例算是可接受的。需要提醒的是,这份数据集的划分是作者预先做好的,不代表所有超声乳腺数据都适合直接套用这个比例。如果你的场景里数据量更少,建议用 K 折交叉验证而不是固定划分。
我第一次跑这份数据集时走了个弯路:直接用训练集训练,测试集评估,然后发现指标不错,后来换到另一批超声图像上效果明显下降。回头排查才发现超声图像的探头型号、增益设置、图像尺寸都会影响模型泛化。所以拿到任何数据集,第一步都应该检查训练集和测试集是否存在明显的分布差异,比如平均亮度、对比度、图像尺寸是否一致。
import cv2 import glob train_images = glob.glob("train/images/*.png") test_images = glob.glob("test/images/*.png") train_mean = [cv2.imread(p, 0).mean() for p in train_images[:50]] test_mean = [cv2.imread(p, 0).mean() for p in test_images[:50]] print("train mean intensity:", sum(train_mean) / len(train_mean)) print("test mean intensity:", sum(test_mean) / len(test_mean))逻辑说明:用灰度模式读取图像,计算平均像素强度。如果训练集和测试集均值差异超过 15~20,建议考虑归一化方式或加入亮度扰动增强。参数说明:这里只取了前 50 张做快速抽查,如果时间充裕可以全量统计;灰度读取用 0 参数,cv2.imread 的第二个参数传 0 表示 IMREAD_GRAYSCALE。
3. 从数据到模型:U-Net 训练这批乳腺超声图像的整体流程
3.1 为什么首选 U-Net 而不是先做目标检测或分类
乳腺超声图像中的良性区域形状不规则、边界模糊,和背景的灰度对比度有时很低。如果用目标检测框来做,框很难贴合不规则病灶边缘;如果直接做分类,又丢掉了病灶位置信息。U-Net 的编码器-解码器结构天然适合这种像素级预测任务,跳连(skip connection)能把浅层的边缘细节传递到深层特征,在小样本医学图像上表现稳定。
我一般不会上来就换 Attention U-Net 或 TransUNet,而是先用标准 U-Net 跑通流程。原因很简单:标准 U-Net 训练稳定、显存占用适中、代码资料多,出了任何问题都容易排查。等你确认数据和标签没问题,再往上加注意力模块才有意义。
3.2 数据加载与预处理:把 PNG 图像和 mask 对齐喂给网络
医学图像分割的数据加载有几个关键点:图像可能被存储为三通道 RGB 但内容实际是灰度,mask 必须是单通道且像素值为 0 和 255(或 0 和 1),图像尺寸需要统一。以下是我常用的 DataLoader 写法:
import torch from torch.utils.data import Dataset, DataLoader from PIL import Image import numpy as np import glob class BreastUltrasoundDataset(Dataset): def __init__(self, image_dir, mask_dir, img_size=(256, 256)): self.image_paths = sorted(glob.glob(image_dir + "/*.png")) self.mask_paths = sorted(glob.glob(mask_dir + "/*.png")) self.img_size = img_size def __len__(self): return len(self.image_paths) def __getitem__(self, idx): img = Image.open(self.image_paths[idx]).convert("RGB") mask = Image.open(self.mask_paths[idx]).convert("L") img = img.resize(self.img_size, Image.BILINEAR) mask = mask.resize(self.img_size, Image.NEAREST) img_np = np.array(img) / 255.0 mask_np = np.array(mask) / 255.0 mask_np = (mask_np > 0.5).astype(np.float32) img_tensor = torch.from_numpy(img_np).permute(2, 0, 1).float() mask_tensor = torch.from_numpy(mask_np).unsqueeze(0).float() return img_tensor, mask_tensor dataset = BreastUltrasoundDataset("train/images", "train/masks", (256, 256)) dataloader = DataLoader(dataset, batch_size=8, shuffle=True, num_workers=4)逻辑说明:图像统一转 RGB 并归一化到 0~1,mask 转灰度后同样归一化,再通过阈值判断强制转为 0 和 1 二值标签。mask 的 resize 必须用 NEAREST 插值,因为线性插值会在边界产生灰色过渡值,破坏分割标签的硬边界。参数说明:img_size 选 256×256 是速度和安全性的折中,如果你的显存足够,可以改成 512×512 保留更多超声纹理细节;batch_size 在显存 8GB 左右时设 8 比较稳妥。
3.3 损失函数选择:BCE 和 Dice 的配合逻辑
训练分割模型时最常踩的坑是类别不平衡。超声图像里背景区域往往远大于乳腺区域,如果只用二值交叉熵(BCE),模型会倾向于把所有像素预测为背景,因为这样 loss 已经很低。解决方式是引入 Dice Loss,按像素预测的类别区域重叠度计算损失,对小目标更敏感。
常见做法是把 BCE 和 Dice Loss 按权重相加,比如loss = 0.5 * bce_loss + dice_loss。Dice Loss 的计算方式如下:
def dice_loss(pred, target, smooth=1.0): pred = torch.sigmoid(pred) intersection = (pred * target).sum() dice = (2.0 * intersection + smooth) / (pred.sum() + target.sum() + smooth) return 1.0 - dice逻辑说明:smooth 参数防止分子分母都为 0 时出现除零错误。可以看出 Dice Loss 不关心像素绝对数量,只关心预测区域和目标区域的重叠程度,这对小目标分割非常关键。参数说明:smooth 通常取 1.0,可以理解为拉普拉斯平滑;如果你发现训练后期 loss 震荡,可以尝试把 smooth 降到 0.1 让梯度更稳定。
3.4 训练参数建议与完整训练循环
对于约 300 张训练图的规模,我建议用 AdamW 优化器,初始学习率 1e-4,配合余弦退火学习率调度。batch size 根据显存调节,训练 100 epoch 左右。训练循环里记录每个 epoch 的 loss 和在验证集上的 Dice,方便后续回溯。
import torch.optim as optim model = UNet(in_channels=3, out_channels=1) optimizer = optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-5) scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=100) for epoch in range(100): model.train() total_loss = 0.0 for images, masks in dataloader: optimizer.zero_grad() outputs = model(images) loss = 0.5 * torch.nn.functional.binary_cross_entropy_with_logits(outputs, masks) loss = loss + dice_loss(outputs, masks) loss.backward() optimizer.step() total_loss += loss.item() scheduler.step() print(f"Epoch {epoch+1}, Loss: {total_loss / len(dataloader):.4f}")逻辑说明:BCEWithLogits 内部已经包含 sigmoid,所以 Dice Loss 里再手动做一次 sigmoid 不会冲突。两个损失相加后梯度会同时回传到模型,训练过程中如果发现 loss 在某个值附近不再下降,优先检查学习率是否过大,或者 mask 标签是否出现错位。参数说明:weight_decay 设为 1e-5 做轻微正则,防止过拟合;T_max 和 epoch 数保持一致。
4. 效果怎么看:自带可视化脚本、Dice/IoU 指标与 bad case 复盘
4.1 数据集自带可视化脚本的用法
这份数据集附带了一个图像分割可视化脚本,功能是随机抽取一张图,把原始图像、GT mask、GT 叠加在原图上的效果拼在一起展示并保存。这个脚本虽然简单,但对验证数据质量很有价值。我第一次拿到数据集时先跑这个脚本,发现有一张图的 mask 明显比原图中的病灶区域大一圈,这种情况如果直接拿去训练,模型会学到错误的边界。
脚本的核心逻辑可以用下面的代码概括:
import matplotlib.pyplot as plt import random from PIL import Image import numpy as np idx = random.randint(0, len(image_paths) - 1) img = np.array(Image.open(image_paths[idx]).convert("RGB")) mask = np.array(Image.open(mask_paths[idx]).convert("L")) overlay = img.copy() overlay[:, :, 0] = np.where(mask > 0, 255, overlay[:, :, 0]) fig, axes = plt.subplots(1, 3, figsize=(15, 5)) axes[0].imshow(img); axes[0].set_title("Original") axes[1].imshow(mask, cmap="gray"); axes[1].set_title("GT Mask") axes[2].imshow(overlay); axes[2].set_title("Overlay") plt.savefig("visual_check.png", dpi=150, bbox_inches="tight")逻辑说明:overlay 把 mask 区域在原图上标红,便于肉眼观察标注边界是否贴合真实病灶。这段逻辑和项目自带脚本的思路一致,你可以直接运行原脚本,也可以按自己的需求改输出尺寸和颜色。参数说明:dpi=150保证保存的图片足够清晰,方便放大看边缘细节;随机抽取的方式决定了每次运行结果不同,如果想复现某次结果,需要固定随机种子。
4.2 用 Dice 和 IoU 量化分割质量
肉眼查看可视化结果只能定性判断,要说服自己或别人模型有效,还得用数值指标。分割任务最常用的两个指标是 Dice 系数和 IoU。Dice 衡量预测区域与真实区域的重叠程度,IoU 更严格,对过分割或欠分割更敏感。计算方式如下:
def calculate_metrics(pred_mask, gt_mask): intersection = (pred_mask & gt_mask).sum() union = (pred_mask | gt_mask).sum() dice = (2.0 * intersection) / (pred_mask.sum() + gt_mask.sum()) iou = intersection / union return dice, iou逻辑说明:pred_mask 和 gt_mask 都是布尔数组。我一般会在测试集上逐张计算再取平均,同时记录每张图的分项指标,方便定位到是哪一类图像拉低了整体分数。参数说明:注意 pred_mask 和 gt_mask 的数据类型必须一致,否则位运算会出问题。
4.3 边缘质量与失败样例:过分割和欠分割怎么区分
指标高不代表模型一定好,尤其是医学图像分割,边界质量比整体重叠度更重要。我复盘 bad case 时通常会看两类错误:过分割(预测区域比真实病灶大,边缘向外膨胀)和欠分割(预测区域比真实病灶小,边缘收缩)。
过分割常见的原因是模型无法区分病灶边界附近的低回声区域,把正常组织也划进了预测区域;欠分割则可能是因为病灶中心和背景灰度差异太小,模型只分割出了高对比度部分。解决办法各不相同:过分割可以尝试给损失函数加边界惩罚项,或者在数据增强里加入更强的对比度变换;欠分割可以尝试加深网络、增大输入分辨率,或使用多尺度特征融合。
我习惯在测试集上把所有预测结果导出成一张 montage,按 Dice 从低到高排列,优先看分数最低的十几张:
import torchvision grid = torchvision.utils.make_grid(pred_images, nrow=5, padding=10) grid_np = grid.numpy().transpose((1, 2, 0)) plt.imsave("bad_case_montage.png", grid_np)逻辑说明:把一批预测结果拼成网格图,方便快速浏览。结合每张图对应的 Dice 分数,可以迅速发现哪些样例是模型系统性失败的,而不是随机噪声。参数说明:nrow=5 表示每行放 5 张,padding=10 控制图像间距,避免拼在一起难以分辨边界。
5. 超声乳腺分割避坑指南:五条踩过的坑与解法
5.1 文件名带空格和括号:glob 排序错位
这份数据集的图像文件名是“benign (117).png”这种格式,空格和括号都在文件名里。直接使用命令行按文件名拼接字符串或者写 shell 脚本时,很容易因为空格没转义导致路径拼接错误。更隐蔽的问题是排序错位:如果按字符串排序,“benign (9)”会排在“benign (11)”后面,因为字符顺序是逐位比较的。在 PyTorch 的 DataLoader 里用 sorted(glob.glob(...)) 就会得到错误的顺序,导致 image 和 mask 对不上。
解决方式是用自然排序,或者用带零填充的数字序号重新命名。我通常的做法是在数据预处理阶段把所有文件统一重命名为固定数字格式:
for file in train/images/*.png; do num=$(echo "$file" | grep -oP '\(\d+\)' | tr -d '()') mv "$file" "train/images/img_$(printf '%03d' "$num").png" done逻辑说明:用正则提取括号里的数字序号,printf 补零到三位数,确保排序和视图都对齐。这个脚本同时处理 image 和 mask 两份目录,跑完后目录会清爽很多。注意:建议保留原始文件备份,以防重命名后发现问题需要回溯。
5.2 超声图像三通道但内容灰度:通道数搞错
让刚接触医学图像的同学最容易翻车的一点:很多超声 PNG 是作为 RGB 三通道存储的,但三个通道的内容几乎一样。如果不做处理直接喂给网络,模型要额外学习“三个通道其实是同一个灰度图”这件事,浪费参数量且容易过拟合。更隐蔽的是某些 PNG 带 alpha 通道,直接读出来是四通道。
解决方式是读图时统一转成灰度,再复制三通道,或者直接在输入层用 1 通道模型。我建议显存充足时还是保留三通道输入,因为后续如果想换用 ImageNet 预训练模型,三通道输入是必须的。核心逻辑就是保证喂给模型的数据,通道含义完全一致。
5.3 mask 是一种像素标签,不是三种颜色的“涂色图”
有同事把 mask 当成 RGB 图像读取,直接plt.imshow(mask)发现显示出来是绿色的,因为 matplotlib 对单通道图像默认用 colormap 渲染。这只是显示问题,真正的坑在于把它当真值算损失时,数值范围变成了 0~255 而不是 0~1。Dice Loss 在这种数值错位下会计算出毫无意义的梯度。
统一的做法是在 dataset 读取时强制convert("L"),然后除以 255 并做阈值处理。千万不要省这一步。如果发现训练时 loss 一直在 0.7 附近不动,先检查 mask 的数值范围是不是 0~255。
5.4 背景和病灶类别严重不平衡:BCE 单独撑不住
超声图像里背景占比经常超过 90%,良性区域可能只占图像的 5% 到 10%。BCE Loss 是逐像素独立计算的,模型只要把所有像素都预测为背景,loss 就已经很低了。最终的结果就是模型输出全黑的掩膜,Dice 直接为 0。
我一般用 BCE + Dice 组合损失,必要时还可以给前景像素加权。如果你发现模型在验证集上全输出 0,先检查是不是 loss 里缺少 Dice 项。这个数据类型下,单独用 BCE 训练基本等于自杀。
5.5 训练集和测试集图像尺寸不一致:resize 后掩膜错位
超声设备不同,导出的图像尺寸可能不同。这份数据集看起来都是正方形的超声图像,但训练和测试集的平均尺寸可能会有微小差异。直接 resize 到同一尺寸后,病灶区域的比例关系理论上不会变,但如果使用 padding 而不是 resize,掩膜和原图的坐标就完全对不上了。
我踩过这个坑:原图是 800×600,我 pad 成 800×800 再 resize 到 256×256,结果 mask 也被 pad 了,白色区域的位置整体偏移,训练出来的模型边界全部错位。后来改成统一 resize,不用 padding,问题才解决。原因很简单:resize 会按比例缩放内容,而 padding 会改变内容在原图中的绝对位置。
6. 多模型怎么选:同一个测试集上做 Dice 对比的小设置
当你用这份数据集把手头的 U-Net 跑通之后,自然会想试试改进版本,比如 Attention U-Net、DeepLabV3+,或者加入预训练编码器。这个时候最容易犯的错误是每换一个模型就改一批超参数,最后指标变了也不知道是模型改进带来的还是参数调整带来的。
我常用的做法是固定一组“公共实验设置”,让所有模型在这组设置下比赛。具体来说,固定训练轮数 100、初始学习率 1e-4、损失函数 0.5 * BCE + Dice、batch size 8、输入尺寸 256×256,其他一切保持不变。每个模型只在结构上做变化,这样才能公平对比。
完整流程我一般会这么组织:先准备好一个 baseline 模型(标准 U-Net),跑完一轮记录最测试集 Dice;再跑目标模型(比如加入了注意力模块的 U-Net),用完全相同的随机种子和数据增强策略。数据增强我只做水平翻转、垂直翻转和随机旋转,旋转角度限定在 20 度以内,这样不会改变乳腺病灶的基本形态。
此外我还会把每轮的预测蒙版都保存下来,最后挑一个中间 epoch 和一个最终 epoch 的预测结果做对比。中间 epoch 的预测如果比最终 epoch 还好,说明模型开始过拟合,训练轮数需要缩减或者加正则化。这一轮对比做完,你基本能确定这个数据集的“标准答案”:什么模型、什么参数、什么增强策略下能得到最好的分割效果。
从做这个实验之后,我每次拿到新的医学分割数据集都会强制走一遍这个流程:先跑自带可视化脚本检查数据,固定一组公共超参数跑通 baseline,再逐个替换结构做对比。虽然麻烦,但你会发现很多“换了模型涨了 3 个点”的结论,其实换个随机种子就消失了。希望帮到你。
本文还有配套的精品资源,点击获取