简介:这份资源面向医学图像分割方向的研究者、算法工程师与相关专业学生,提供超声腹部器官的2类别分割数据,类别为背景与腹部器官,可用于训练和评估U-Net等分割网络。包内按训练集与测试集组织,训练集约3700张图像及对应mask,测试集约900张,合计约4600张数据与标签,并附一份可视化脚本,可随机抽取一张图片,将原图、GT图像及GT在原图上的蒙板一并展示并保存到当前目录,便于快速核验标注质量。资源共2000个文件,以1998个png图像与掩膜为主,另含1个txt类别说明和1个py脚本,压缩包约163.36MB,目录结构清晰。已有400人学习,适合需要真实超声数据做分割实验、复现论文或搭建基线模型的读者。配套的医学图像分割网络介绍可帮助理解整体流程。
1. 超声腹部器官分割数据集:4600 张带标签的 2 类分割样本到底能干什么
如果你正在做医学图像分割,尤其是腹部超声方向,大概率会遇到一个很现实的问题:公开数据要么是 CT/MRI,要么类别多到十几类,真正能拿来快速验证模型、跑通训练流程的超声腹部器官 2 类分割数据并不多。这份超声腹部器官图像分割数据集,约 4600 张图像,每张都配了对应的分割标签,类别数为 2,属于典型的二分类像素级分割任务。它适合谁?适合刚入门医学图像分割、想先跑通 U-Net 类模型的人;也适合做超声图像预处理、损失函数对比、小样本策略验证的从业者。超声图像本身噪声大、边界模糊、声影干扰强,拿它当练手数据,比拿干净的 CT 数据更能暴露模型在真实场景下的短板。下面我按“数据长什么样 → 怎么读进来 → 怎么训练 → 坑在哪 → 怎么验证”的顺序,把这份资源拆开讲清楚。
2. 数据组织与读取:从目录结构到张量管道的完整链路
2.1 先搞清楚图像和标签的对应关系
拿到一份分割数据集,第一件事不是急着写模型,而是确认图像和标签是不是一一对应、命名规则是否一致、标签是单通道掩码还是彩色图。超声腹部器官 2 类分割的标签通常是单通道灰度图,背景为 0,目标器官区域为 1,或者背景 0、器官 1、忽略区域 255。常见做法是先用脚本统计一遍标签的像素值分布,确认没有多余的类别索引。
import os import numpy as np from PIL import Image from collections import Counter img_dir = "data/images" mask_dir = "data/masks" img_files = sorted(os.listdir(img_dir)) mask_files = sorted(os.listdir(mask_dir)) print("图像数量:", len(img_files)) print("标签数量:", len(mask_files)) # 检查文件名是否一一对应 img_stems = [os.path.splitext(f)[0] for f in img_files] mask_stems = [os.path.splitext(f)[0] for f in mask_files] mismatch = set(img_stems) ^ set(mask_stems) print("不匹配的文件名数量:", len(mismatch)) # 统计标签像素值分布 counter = Counter() for f in mask_files[:200]: # 先抽样 200 张看分布 mask = np.array(Image.open(os.path.join(mask_dir, f))) counter.update(mask.flatten().tolist()) print("标签像素值分布:", counter.most_common(10))这段脚本做了三件事:确认图像和标签数量是否一致、检查文件名是否严格对应、抽样统计标签像素值。参数上,mask_files[:200]是抽样数量,正式跑之前建议全量统计一次,4600 张的规模全量遍历也就几分钟。如果发现标签里有 2、3 这类不该出现的值,说明标签可能被 JPEG 压缩污染过,需要重新确认数据来源。
2.2 构建 Dataset 和 DataLoader
确认数据干净之后,下一步是把它接进 PyTorch 的训练管道。超声图像常见尺寸不统一,我一般会统一缩放到 256×256 或 320×320,标签用最近邻插值,图像用双线性插值。这里有个细节:图像和标签必须用同一套几何变换,否则增强之后像素对不上,模型直接学废。
import torch from torch.utils.data import Dataset, DataLoader import torchvision.transforms as T from PIL import Image class UltrasoundSegDataset(Dataset): def __init__(self, img_dir, mask_dir, img_size=256, augment=False): self.img_dir = img_dir self.mask_dir = mask_dir self.img_files = sorted(os.listdir(img_dir)) self.img_size = img_size self.augment = augment self.img_transform = T.Compose([ T.Resize((img_size, img_size), interpolation=T.InterpolationMode.BILINEAR), T.ToTensor(), T.Normalize(mean=[0.5], std=[0.5]) # 超声单通道常用归一化 ]) self.mask_transform = T.Compose([ T.Resize((img_size, img_size), interpolation=T.InterpolationMode.NEAREST), T.ToTensor() ]) def __len__(self): return len(self.img_files) def __getitem__(self, idx): name = self.img_files[idx] img = Image.open(os.path.join(self.img_dir, name)).convert("L") mask_name = os.path.splitext(name)[0] + ".png" mask = Image.open(os.path.join(self.mask_dir, mask_name)).convert("L") img = self.img_transform(img) mask = self.mask_transform(mask) mask = (mask > 0.5).float() # 二值化,确保标签是 0/1 return img, mask dataset = UltrasoundSegDataset("data/images", "data/masks", img_size=256) loader = DataLoader(dataset, batch_size=8, shuffle=True, num_workers=4) imgs, masks = next(iter(loader)) print("图像 batch 形状:", imgs.shape) # [8, 1, 256, 256] print("标签 batch 形状:", masks.shape) # [8, 1, 256, 256]逻辑说明:图像用convert("L")转单通道,因为超声原始数据大多是灰度;标签同样转单通道后做二值化,避免插值引入中间值。参数上,img_size决定输入分辨率,256 是速度和精度的折中,显存够可以上 320 或 384;batch_size=8是 8GB 显存下的保守值,12GB 以上可以翻倍。num_workers=4在 Linux 下比较稳,Windows 下如果报错就改成 0。
2.3 标签格式转换的边界情况
有些超声数据集的标签是彩色 PNG,背景黑色、器官红色,直接读进来会变成三通道。这种情况必须做颜色到索引的映射,不能直接转灰度,否则红色和背景的灰度值可能撞车。常见做法是定义一个调色板字典,逐像素查表。
def color_to_index(mask_rgb, palette): """palette: {(r,g,b): index}""" h, w, _ = mask_rgb.shape index_map = np.zeros((h, w), dtype=np.uint8) for color, idx in palette.items(): match = np.all(mask_rgb == color, axis=-1) index_map[match] = idx return index_map palette = {(0, 0, 0): 0, (255, 0, 0): 1} mask_rgb = np.array(Image.open("data/masks/sample.png").convert("RGB")) mask_idx = color_to_index(mask_rgb, palette) print("唯一值:", np.unique(mask_idx))这段代码的关键是np.all(mask_rgb == color, axis=-1),它按通道逐一比对,只有三个通道都相等才算命中。参数palette要根据实际标签颜色调整,不能照抄。如果标签有抗锯齿边缘,颜色比对会漏掉过渡像素,这时候要么用最近颜色距离,要么直接找原始标注文件重新导出。
3. 模型选型与训练:U-Net 在超声二分类分割上的参数配置
3.1 为什么超声腹部器官分割优先选 U-Net 系
超声图像的特点决定了模型选型:边界模糊、斑点噪声强、器官形状变化大。U-Net 的跳跃连接能把浅层纹理和深层语义拼在一起,对小目标和模糊边界比较友好。相比 DeepLab 系列,U-Net 在 4600 张这个量级上更容易训起来,不需要 ImageNet 预训练也能出结果。如果显存紧张,可以把通道数从 64 起步降到 32;如果追求更高精度,可以换 Attention U-Net 或加一个边界损失。我一般先用标准 U-Net 跑一个 baseline,确认数据管道没问题,再动结构。
import torch.nn as nn import torch.nn.functional as F class DoubleConv(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.net = nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding=1), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True), nn.Conv2d(out_ch, out_ch, 3, padding=1), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True) ) def forward(self, x): return self.net(x) class UNet(nn.Module): def __init__(self, in_ch=1, out_ch=1, base=64): super().__init__() self.down1 = DoubleConv(in_ch, base) self.down2 = DoubleConv(base, base*2) self.down3 = DoubleConv(base*2, base*4) self.down4 = DoubleConv(base*4, base*8) self.pool = nn.MaxPool2d(2) self.up1 = nn.ConvTranspose2d(base*8, base*4, 2, stride=2) self.conv1 = DoubleConv(base*8, base*4) self.up2 = nn.ConvTranspose2d(base*4, base*2, 2, stride=2) self.conv2 = DoubleConv(base*4, base*2) self.up3 = nn.ConvTranspose2d(base*2, base, 2, stride=2) self.conv3 = DoubleConv(base*2, base) self.out = nn.Conv2d(base, out_ch, 1) def forward(self, x): d1 = self.down1(x) d2 = self.down2(self.pool(d1)) d3 = self.down3(self.pool(d2)) d4 = self.down4(self.pool(d3)) u1 = self.conv1(torch.cat([self.up1(d4), d3], dim=1)) u2 = self.conv2(torch.cat([self.up2(u1), d2], dim=1)) u3 = self.conv3(torch.cat([self.up3(u2), d1], dim=1)) return self.out(u3) model = UNet(in_ch=1, out_ch=1, base=64) print("参数量:", sum(p.numel() for p in model.parameters()))参数说明:base=64是首层通道数,显存不够就降到 32;out_ch=1对应二分类,输出 logits 后接 sigmoid。参数量大概在 7.8M 左右,4600 张数据够训。如果标签有忽略区域,out_ch要相应调整,损失函数里加 ignore_index。
3.2 损失函数与评估指标的选择
二分类分割最常用的组合是 BCEWithLogitsLoss + Dice Loss。超声图像里目标器官占比通常不大,纯 BCE 会让模型偏向背景,Dice 能拉回一些。我一般用0.5 * BCE + 0.5 * Dice,如果目标特别小,Dice 权重可以提到 0.7。评估指标别只看像素准确率,超声背景占比高,准确率 95% 可能模型什么都没学到,重点看 Dice 和 IoU。
class DiceLoss(nn.Module): def __init__(self, smooth=1e-6): super().__init__() self.smooth = smooth def forward(self, logits, targets): probs = torch.sigmoid(logits) probs = probs.view(-1) targets = targets.view(-1) intersection = (probs * targets).sum() dice = (2. * intersection + self.smooth) / (probs.sum() + targets.sum() + self.smooth) return 1 - dice bce = nn.BCEWithLogitsLoss() dice = DiceLoss() def criterion(logits, targets): return 0.5 * bce(logits, targets) + 0.5 * dice(logits, targets) def compute_metrics(logits, targets, threshold=0.5): probs = torch.sigmoid(logits) preds = (probs > threshold).float() intersection = (preds * targets).sum() dice = (2. * intersection + 1e-6) / (preds.sum() + targets.sum() + 1e-6) union = preds.sum() + targets.sum() - intersection iou = (intersection + 1e-6) / (union + 1e-6) return dice.item(), iou.item()threshold=0.5是默认阈值,实际部署时可以按验证集 Dice 最大化来调,常见在 0.4 到 0.6 之间。smooth=1e-6防止除零。训练时每轮在验证集上算一次 Dice 和 IoU,保存 Dice 最高的权重,别只看 loss。
3.3 训练循环与学习率调度
训练循环本身不复杂,关键是学习率调度和早停。我一般用 AdamW,初始学习率 1e-3,配合 CosineAnnealingLR,跑 80 到 120 轮。4600 张数据,batch size 8,一张 8GB 显存的卡大概每轮 3 到 5 分钟,整体几小时能跑完。
from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = UNet(in_ch=1, out_ch=1, base=64).to(device) optimizer = AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = CosineAnnealingLR(optimizer, T_max=100) best_dice = 0.0 for epoch in range(100): model.train() for imgs, masks in loader: imgs, masks = imgs.to(device), masks.to(device) optimizer.zero_grad() logits = model(imgs) loss = criterion(logits, masks) loss.backward() optimizer.step() scheduler.step() # 验证 model.eval() dice_scores = [] with torch.no_grad(): for imgs, masks in val_loader: imgs, masks = imgs.to(device), masks.to(device) logits = model(imgs) d, _ = compute_metrics(logits, masks) dice_scores.append(d) mean_dice = np.mean(dice_scores) if mean_dice > best_dice: best_dice = mean_dice torch.save(model.state_dict(), "best_unet.pth") print(f"Epoch {epoch}: val Dice={mean_dice:.4f}, best={best_dice:.4f}")参数上,weight_decay=1e-4是常规正则,数据量小可以加到 1e-3;T_max=100要和总轮数匹配。验证集建议从 4600 张里划 10% 到 15%,确保每类都有代表。如果验证 Dice 震荡厉害,先把学习率降到 5e-4 试试。
4. 避坑与排查:超声分割训练里最容易翻车的五个点
4.1 现象:训练 loss 一直降,但验证 Dice 卡在 0.3 不动
原因:最常见的是标签和图像没对齐,或者标签二值化阈值不对。超声标签如果是 0/255,直接除以 255 没问题;但如果是 0/1 又被 JPEG 压缩过,可能出现 0、1、2 混在一起,模型学到的目标区域是错的。另一个原因是数据增强时图像和标签用了不同的插值方式,几何变换后像素错位。
解决:先可视化几张“图像 + 标签叠加图”,肉眼确认边界对得上。然后用np.unique全量统计标签值,确认只有预期类别。增强管道里图像和标签的 Resize、Flip、Rotate 必须同步,用 albumentations 的话要传additional_targets。
4.2 现象:模型把整张图都预测成背景,Dice 接近 0
原因:类别极度不平衡。超声腹部器官在整张图里占比可能只有 5% 到 15%,纯 BCE 下模型发现全预测背景也能拿到很低的 loss,就躺平了。另一个可能是学习率太大,模型一开始就陷入局部最优。
解决:损失函数里加 Dice 或 Focal Loss,Dice 权重至少 0.5。学习率从 1e-3 降到 1e-4 再试。还可以用带权重的 BCE,pos_weight设成背景像素数除以目标像素数,常见在 5 到 20 之间。
4.3 现象:验证集 Dice 很高,但换一批新数据就崩
原因:过拟合。4600 张如果都来自同一台设备、同一批患者,模型可能学到了设备特有的噪声模式,而不是器官的解剖结构。超声图像对探头角度、增益设置非常敏感,数据多样性不够时泛化能力很差。
解决:增强要狠一点,随机亮度对比度、Gamma 校正、弹性形变、随机裁剪都加上。如果条件允许,留出一部分不同来源的数据做外部验证。早停 patience 设 15 到 20 轮,别等 loss 降到零。
4.4 现象:训练到一半 loss 突然变成 NaN
原因:学习率过大导致梯度爆炸,或者 Dice Loss 里除零没处理好。混合精度训练时如果 loss scale 没设对,也容易出 NaN。
解决:加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)。Dice Loss 的 smooth 项别省。用 AMP 的话,GradScaler要正确调用scale和step。如果还是 NaN,把学习率砍半再跑。
4.5 现象:推理时输出全是 0 或全是 1
原因:推理时忘了加 sigmoid,或者阈值设错了。训练时用的是 BCEWithLogitsLoss,输出是 logits,推理必须过 sigmoid 再比阈值。另一个可能是模型保存的是最后轮而不是最佳轮,最后轮已经过拟合了。
解决:推理脚本里明确写probs = torch.sigmoid(logits),阈值先用 0.5,再在验证集上扫一遍找最优。保存权重时按验证 Dice 存 best,别按 epoch 存 last。
5. 进阶技巧:用 TTA 和阈值搜索把 Dice 再抬两个点
baseline 跑通之后,如果想把验证 Dice 从 0.85 推到 0.87 甚至更高,最划算的两个操作是测试时增强(TTA)和阈值搜索。这两个都不需要重新训练,推理阶段就能做,属于典型的“后悔药”式优化。
TTA 的思路是对同一张图做多种变换,分别推理后再把结果平均回去。常见组合是原始、水平翻转、垂直翻转、水平+垂直翻转,四次推理取平均。超声图像里器官左右不对称,水平翻转要谨慎,但垂直翻转通常安全。我一般先试原始+水平翻转,如果 Dice 涨了就再加垂直。
def predict_with_tta(model, img_tensor): """img_tensor: [1, 1, H, W]""" model.eval() with torch.no_grad(): # 原始 p1 = torch.sigmoid(model(img_tensor)) # 水平翻转 p2 = torch.sigmoid(model(torch.flip(img_tensor, dims=[3]))) p2 = torch.flip(p2, dims=[3]) # 垂直翻转 p3 = torch.sigmoid(model(torch.flip(img_tensor, dims=[2]))) p3 = torch.flip(p3, dims=[2]) # 水平+垂直 p4 = torch.sigmoid(model(torch.flip(img_tensor, dims=[2, 3]))) p4 = torch.flip(p4, dims=[2, 3]) return (p1 + p2 + p3 + p4) / 4.0 # 阈值搜索 best_thr, best_dice = 0.5, 0.0 for thr in np.arange(0.3, 0.71, 0.02): dice_scores = [] for imgs, masks in val_loader: imgs, masks = imgs.to(device), masks.to(device) probs = predict_with_tta(model, imgs) preds = (probs > thr).float() intersection = (preds * masks).sum() dice = (2. * intersection + 1e-6) / (preds.sum() + masks.sum() + 1e-6) dice_scores.append(dice.item()) mean_dice = np.mean(dice_scores) if mean_dice > best_dice: best_dice, best_thr = mean_dice, thr print(f"最佳阈值: {best_thr:.2f}, TTA Dice: {best_dice:.4f}")这段代码里,torch.flip的dims参数要跟数据维度对齐,输入是[N, C, H, W],所以水平翻转是dims=[3],垂直是dims=[2]。翻转推理后必须翻回来再平均,否则像素位置对不上。阈值搜索范围我一般从 0.3 扫到 0.7,步长 0.02,4600 张的验证集跑一轮几分钟。注意阈值要在验证集上选,选完固定住再去测测试集,别在测试集上反复调,那是自欺欺人。
还有一个细节:TTA 对小目标分割提升明显,但对已经很大的器官可能反而掉点。所以每加一种变换,都要在验证集上确认 Dice 是涨的,不涨就撤掉。我见过有人无脑堆八种 TTA,结果 Dice 掉了三个点,血泪经验就是——TTA 不是越多越好,得看数据特性。
从那以后我每次做完 baseline,都强制走一遍“TTA + 阈值搜索”的验证流程,确认增益是真实的再写进报告。这套流程在这份超声腹部器官 2 类分割数据上,通常能把 Dice 从 0.85 抬到 0.87 到 0.88,IoU 同步涨两个点左右。希望帮到你。
本文还有配套的精品资源,点击获取