超声腹部器官2类分割数据集实战:U-Net训练与TTA优化
发布时间:2026/10/2 7:37:02来源:尧图网络
简介这份资源面向医学图像分割方向的研究者、算法工程师与相关专业学生提供超声腹部器官的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_size256, augmentFalse): 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), interpolationT.InterpolationMode.BILINEAR), T.ToTensor(), T.Normalize(mean[0.5], std[0.5]) # 超声单通道常用归一化 ]) self.mask_transform T.Compose([ T.Resize((img_size, img_size), interpolationT.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_size256) loader DataLoader(dataset, batch_size8, shuffleTrue, num_workers4) 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 或 384batch_size8是 8GB 显存下的保守值12GB 以上可以翻倍。num_workers4在 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), dtypenp.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, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), nn.Conv2d(out_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue) ) def forward(self, x): return self.net(x) class UNet(nn.Module): def __init__(self, in_ch1, out_ch1, base64): 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, stride2) self.conv1 DoubleConv(base*8, base*4) self.up2 nn.ConvTranspose2d(base*4, base*2, 2, stride2) self.conv2 DoubleConv(base*4, base*2) self.up3 nn.ConvTranspose2d(base*2, base, 2, stride2) 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], dim1)) u2 self.conv2(torch.cat([self.up2(u1), d2], dim1)) u3 self.conv3(torch.cat([self.up3(u2), d1], dim1)) return self.out(u3) model UNet(in_ch1, out_ch1, base64) print(参数量:, sum(p.numel() for p in model.parameters()))参数说明base64是首层通道数显存不够就降到 32out_ch1对应二分类输出 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, smooth1e-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, threshold0.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()threshold0.5是默认阈值实际部署时可以按验证集 Dice 最大化来调常见在 0.4 到 0.6 之间。smooth1e-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_ch1, out_ch1, base64).to(device) optimizer AdamW(model.parameters(), lr1e-3, weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_max100) 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(fEpoch {epoch}: val Dice{mean_dice:.4f}, best{best_dice:.4f})参数上weight_decay1e-4是常规正则数据量小可以加到 1e-3T_max100要和总轮数匹配。验证集建议从 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 LossDice 权重至少 0.5。学习率从 1e-3 降到 1e-4 再试。还可以用带权重的 BCEpos_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_norm1.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.024600 张的验证集跑一轮几分钟。注意阈值要在验证集上选选完固定住再去测测试集别在测试集上反复调那是自欺欺人。还有一个细节TTA 对小目标分割提升明显但对已经很大的器官可能反而掉点。所以每加一种变换都要在验证集上确认 Dice 是涨的不涨就撤掉。我见过有人无脑堆八种 TTA结果 Dice 掉了三个点血泪经验就是——TTA 不是越多越好得看数据特性。从那以后我每次做完 baseline都强制走一遍“TTA 阈值搜索”的验证流程确认增益是真实的再写进报告。这套流程在这份超声腹部器官 2 类分割数据上通常能把 Dice 从 0.85 抬到 0.87 到 0.88IoU 同步涨两个点左右。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网