简介:本资源为面向医学图像分割方向的学习者与研究人员整理的手术器械语义分割数据集,适用于深度学习分割模型的训练、验证与算法对比实验,尤其适合正在实践U-Net、SwinUnet、TransUnet等网络改进的读者。数据集已按训练集与验证集划分完毕:训练集包含约860张图像及对应mask标签,验证集约370张,整体约1200张样本,覆盖32个分割类别,可直接投入训练流程。压缩包共约2000个文件,以png与jpg图像为主,另含1个txt说明与1个py脚本,包体约80.79MB。其中可视化脚本可随机抽取一张图片,展示原始图像、GT图像及GT在原图上的蒙板效果,并自动保存至当前目录,便于快速核验标注质量与分割结果。目前已有104人学习,适合需要现成多类别手术器械分割数据、希望减少数据清洗与划分成本的中高级读者参考使用。
1. 手术器械语义分割数据集:1200 张标注图能撑起一个可用的分割模型吗
拿到一个医学图像分割数据集,第一反应往往不是「太好了有数据了」,而是「这 1200 张到底能干什么」。手术器械图像语义分割数据集,顾名思义,标注的是手术场景里各类器械的像素级区域——钳子、镊子、剪刀、持针器、吸引器这些,每张图对应一张 mask,多类别,已经处理过,格式统一。它解决的核心问题是:在手术视野里把器械从背景组织、纱布、血渍中精确抠出来,输出每个像素的类别归属。
这件事的受众很明确。做手术机器人视觉引导的、做术中器械计数与清点的、做手术视频结构化分析的、以及拿医学图像分割当课题的研究生,都会需要这类数据。1200 张不算大,但语义分割本来就不是靠堆量取胜的任务,标注质量、类别平衡、场景多样性才是决定模型能不能落地的关键。下面我按「先判断这批数据值不值得投入 → 怎么跑通第一条基线 → 参数怎么调 → 坑在哪 → 怎么把效果再往上抬」的顺序讲清楚。
2. 先判断数据能不能用:类别分布、mask 格式与划分策略
2.1 语义分割和实例分割别混,先确认你的任务边界
热词里有人搜「yolo26 中实例分割与语义分割的区别」,这个问题在手术器械场景里特别要命。语义分割输出的是「这个像素属于哪一类」,同一类器械挨在一起会连成一片;实例分割输出的是「这个像素属于哪一个具体个体」,两把钳子即使重叠也能分开。手术器械清点要的是实例级,器械区域粗定位用语义级就够。
拿到这个数据集,先确认它的 mask 是单通道索引图(每类一个整数值)还是多通道 one-hot。单通道索引图是语义分割的标准形态,用 PIL 读进来是mode='P'或mode='L',转成训练标签时要做一次映射。如果发现同一张 mask 里两把同类器械被标成了同一个值,那它就是纯语义分割,别指望直接拿去做实例计数。
from PIL import Image import numpy as np mask = Image.open("label_0001.png") print(mask.mode, mask.size) # 常见输出:P (512, 512) 或 L arr = np.array(mask) print(np.unique(arr)) # 看类别索引,比如 [0 1 2 3 4 5]这段代码的作用是快速摸清标签的存储形态。mode='P'说明是调色板索引图,np.unique打印出的整数就是类别编号。参数上重点看两点:唯一值数量是否等于「背景 + 器械类别数」,以及有没有出现 255 这种边界忽略值。如果唯一值里冒出 200 多的大数,多半是可视化用的彩色图被误当标签,得先做颜色到索引的反查。
2.2 类别不平衡是这类数据集的默认状态
手术场景里背景(组织、纱布、视野边缘)占比通常超过 70%,小器械比如缝针、镊子尖可能只占几十个像素。直接上交叉熵,模型会学会「全预测背景」也能拿到高准确率,这就是典型的翻车现场。先统计每个类别的像素占比,再决定损失函数和采样策略。
| 统计项 | 含义 | 处理动作 |
|---|---|---|
| 每类像素占比 | 判断长尾程度 | 占比 < 1% 的类要加权 |
| 每图类别数 | 单图器械种类 | 少于 2 类的图考虑降采样 |
| 空 mask 图 | 无器械帧 | 直接剔除,别喂给模型 |
| 尺寸分布 | 是否统一 | 不统一就统一 resize 到 512 |
统计脚本不用复杂,遍历一遍标签目录,把每张 mask 的np.bincount累加即可。我一般会把结果存成 CSV,后面调class_weight直接读。这里有个血泪经验:别用训练集统计出来的权重去评估验证集,划分之前先做全量统计,再按类别分层切分,否则验证集的类别分布和训练集对不上,指标会虚高。
2.3 划分策略:按视频帧划分,别按图片随机划分
手术数据很多是从视频抽帧来的,相邻帧几乎一样。如果你随机按图片 8:1:1 划分,训练集和验证集里会出现同一台手术、同一时刻的近似重复帧,验证指标会好看得离谱,一上真实视频就崩。正确做法是按手术序列或视频片段划分:同一台手术的帧只能进同一个集合。
import os, random from collections import defaultdict # 假设文件名里带手术编号,如 S03_frame_0121.png groups = defaultdict(list) for f in os.listdir("images"): surgery_id = f.split("_")[0] groups[surgery_id].append(f) surgery_ids = list(groups.keys()) random.seed(42) random.shuffle(surgery_ids) n = len(surgery_ids) train_ids = surgery_ids[:int(n*0.7)] val_ids = surgery_ids[int(n*0.7):int(n*0.85)] test_ids = surgery_ids[int(n*0.85):]逻辑很直白:先按手术编号分组,再对组做随机划分,保证同一台手术不跨集合。参数上random.seed必须固定,方便复现;比例 7:1.5:1.5 是我在 1200 张量级下常用的,验证集太小指标抖动大,太大会挤占训练数据。如果手术编号在文件名里没有,就得靠时间戳或目录结构反推,这一步偷懒后面一定还债。
3. 跑通第一条基线:从索引 mask 到 U-Net 训练循环
3.1 把索引 mask 转成训练可用的标签
多数分割框架要的是类别索引图,但有的损失函数要 one-hot。我一般统一在 Dataset 里输出 long 类型的索引图,损失函数用CrossEntropyLoss或DiceLoss,需要 one-hot 时在 loss 内部转。这样数据管道最轻。
import torch from torch.utils.data import Dataset from PIL import Image import numpy as np class SurgicalSeg(Dataset): def __init__(self, img_dir, mask_dir, filenames, size=512): self.img_dir, self.mask_dir = img_dir, mask_dir self.filenames = filenames self.size = size def __len__(self): return len(self.filenames) def __getitem__(self, idx): name = self.filenames[idx] img = Image.open(f"{self.img_dir}/{name}").convert("RGB") mask = Image.open(f"{self.mask_dir}/{name}").convert("L") img = img.resize((self.size, self.size)) mask = mask.resize((self.size, self.size), Image.NEAREST) img = np.array(img, dtype=np.float32) / 255.0 img = torch.from_numpy(img).permute(2, 0, 1) mask = torch.from_numpy(np.array(mask, dtype=np.int64)) return img, mask关键点在mask.resize用了Image.NEAREST,最近邻插值保证类别索引不被插值成小数;图像用默认双线性即可。convert("L")把调色板图转成灰度索引,前提是原图确实是索引存储。如果原 mask 是彩色可视化图,这一步会得到错误的灰度值,必须换成颜色反查表。归一化用/255.0是最简方案,想更稳可以换成 ImageNet 均值方差。
3.2 损失函数:交叉熵打底,Dice 补小目标
单用交叉熵,小器械类会被淹没。我常用的组合是0.5 * CE + 0.5 * Dice,CE 负责像素级稳定梯度,Dice 负责拉高小目标的重叠度。类别权重通过weight参数传进 CE,权重值取该类像素占比的倒数再归一化。
import torch.nn as nn import torch.nn.functional as F class ComboLoss(nn.Module): def __init__(self, class_weights, num_classes): super().__init__() self.ce = nn.CrossEntropyLoss(weight=class_weights) self.num_classes = num_classes def dice_loss(self, logits, target): probs = F.softmax(logits, dim=1) target_oh = F.one_hot(target, self.num_classes).permute(0,3,1,2).float() dims = (0, 2, 3) inter = (probs * target_oh).sum(dims) union = probs.sum(dims) + target_oh.sum(dims) return 1 - ((2*inter + 1e-6) / (union + 1e-6)).mean() def forward(self, logits, target): return 0.5 * self.ce(logits, target) + 0.5 * self.dice_loss(logits, target)class_weights是长度为类别数的张量,背景类权重通常设小一点,器械类按占比倒数放大。1e-6是防止除零的后悔药,别省。Dice 在 batch 内按类别求平均,如果某些类在当前 batch 没出现,union为 0,加平滑项后该项趋近 1,相当于不贡献梯度,这是可接受的。
3.3 训练循环里必须盯的三个量
跑起来之后别只看 loss。我固定盯三个量:每类的 Dice、学习率、以及验证集上「全背景预测」的比例。第三个量能提前预警模型塌缩。
for epoch in range(EPOCHS): model.train() for img, mask in train_loader: img, mask = img.to(dev), mask.to(dev) logits = model(img) loss = criterion(logits, mask) opt.zero_grad(); loss.backward(); opt.step() model.eval() with torch.no_grad(): for img, mask in val_loader: logits = model(img.to(dev)) pred = logits.argmax(1) # 统计 pred 全为 0 的样本比例 all_bg = (pred == 0).all(dim=(1,2)).float().mean().item() scheduler.step()argmax(1)取通道维最大值得到预测类别图。all_bg超过 0.3 就说明模型在偷懒,先查类别权重是不是没生效,再查学习率是不是太大导致早期塌缩。学习率我一般从 1e-3 起步,配CosineAnnealingLR,1200 张数据 80 到 120 个 epoch 基本收敛。
4. 参数怎么设:输入尺寸、batch、增强与评估口径
4.1 输入尺寸别盲目上 1024
手术器械里小目标多,直觉上想用大分辨率。但 1200 张数据配大分辨率,显存吃紧、batch 变小、BN 统计不稳,反而掉点。我的经验是 512×512 起步,显存允许再上 768。如果器械普遍很小,与其加分辨率,不如加一个高分辨率分支做特征融合,或者用带空洞卷积的主干保留感受野。
| 输入尺寸 | 显存占用(batch=8) | 小目标 Dice | 建议场景 |
|---|---|---|---|
| 384×384 | 低 | 偏低 | 快速验证管道 |
| 512×512 | 中 | 均衡 | 默认起点 |
| 768×768 | 高 | 略升 | 小器械为主 |
| 1024×1024 | 很高 | 不稳定 | 数据量需 >5k |
4.2 增强要贴合手术场景,别照搬自然图像
翻转、旋转、颜色抖动是标配,但手术图像有它的特殊性。左右翻转对器械类别通常安全,上下翻转要谨慎,因为手术视野有明确的方向语义。颜色抖动幅度别太大,血渍和组织的颜色本身就是判别线索,抖过头等于制造噪声。我一般加随机亮度 ±0.2、对比度 ±0.2,再加轻微的弹性形变模拟组织形变。
import albumentations as A train_tf = A.Compose([ A.HorizontalFlip(p=0.5), A.RandomBrightnessContrast(0.2, 0.2, p=0.5), A.ShiftScaleRotate(shift_limit=0.05, scale_limit=0.1, rotate_limit=15, p=0.5), A.ElasticTransform(alpha=1, sigma=50, p=0.2), ])ShiftScaleRotate的旋转限制在 15 度以内,手术视野不会大幅旋转。ElasticTransform概率压到 0.2,太频繁会让边界标签失真。所有几何变换必须同步作用到 mask 上,albumentations 默认会处理,但如果你自己写管道,记得 mask 用最近邻。
4.3 评估口径:mIoU 之外一定要看每类 Dice
mIoU 会被大类主导,小器械类的好坏被平均掉。报告结果时我固定给一张每类 Dice 表,外加混淆矩阵。如果某个器械类的 Dice 长期低于 0.4,先别调模型,回去看这一类在训练集里有多少张图、多少像素,很可能是样本太少,得靠过采样或专门的数据补充。
提示:验证集评估时把预处理和训练时对齐,尤其是归一化参数。训练用 ImageNet 均值方差,验证却用 /255,指标会莫名其妙掉一截。
5. 避坑与排查:1200 张数据最容易踩的五个坑
5.1 现象:训练 loss 正常下降,验证 mIoU 卡在 0.3 不动
原因:按图片随机划分导致训练验证泄漏,或者验证集里全是简单帧,模型没学到东西但指标虚高后回落。更常见的是类别权重没传进损失,模型塌缩到背景类。
解决:改成按手术序列划分,重新统计类别权重并确认weight参数真的进了CrossEntropyLoss。打印一个 batch 的预测类别分布,如果 95% 以上是背景,就是权重问题。
5.2 现象:mask 读进来全是 0 或全是 255
原因:标签是彩色可视化图,convert("L")把 RGB 压成灰度后数值全乱;或者标签是 16 位图,PIL 默认读成mode='I',转L时截断。
解决:先print(mask.mode),如果是RGB或P且np.unique结果异常,用颜色反查表映射回索引。16 位图用np.array(mask)后手动做//257或按实际位深缩放。
5.3 现象:小器械类 Dice 始终为 0
原因:该类像素占比极低,Dice 平滑项让梯度几乎消失;或者该类在划分后训练集里只剩个位数样本。
解决:对含小器械的图做过采样,损失里给该类更高权重,必要时单独训一个二分类检测头先定位再分割。别指望一个统一模型解决所有长尾。
5.4 现象:显存溢出,batch 降到 2 还是 OOM
原因:输入尺寸 1024 加 U-Net 深层特征图,激活值占用远超预期;或者 dataloader 的num_workers开太大,每个 worker 都复制了一份数据。
解决:先降输入到 512,用混合精度训练,num_workers设成 4 到 8 之间试。还不行就换轻量主干,比如 MobileNet 或 EfficientNet-B0,别硬扛。
5.5 现象:推理时单张图要好几秒
原因:模型没切 eval 模式,BN 还在更新;或者没加torch.no_grad(),计算图一直建。
解决:推理前model.eval(),外面套with torch.no_grad():,再导出 ONNX 或 TensorRT 做部署加速。手术场景对延迟敏感,这一步迟早要做。
6. 把 1200 张用出 5000 张的效果:半监督与伪标签的实操技巧
数据量固定的时候,提升空间在「怎么榨干未标注数据」和「怎么让已有标注更值钱」。我常用的组合是:先用 1200 张训一个基线,对无标注的手术视频帧做推理,挑置信度高的伪标签加入训练集,迭代两到三轮。关键是置信度阈值和类别平衡,阈值太低会引入噪声,太高则伪标签数量不够。
model.eval() pseudo = [] with torch.no_grad(): for img, name in unlabeled_loader: logits = model(img.to(dev)) prob = torch.softmax(logits, dim=1) conf, pred = prob.max(1) # 只保留平均置信度 > 0.9 且前景占比合理的帧 fg_ratio = (pred > 0).float().mean().item() if conf.mean().item() > 0.9 and 0.02 < fg_ratio < 0.6: pseudo.append((name, pred.cpu()))conf.mean() > 0.9是整体置信度门槛,fg_ratio过滤掉全背景和全前景的极端帧。伪标签加入后,损失里给伪标签样本一个小于 1 的权重,比如 0.5,避免噪声主导。迭代两轮后如果验证 Dice 不再涨,就停,继续加只会过拟合伪标签的偏差。
另一个技巧是测试时增强(TTA):推理时对同一张图做水平翻转和轻微缩放,把多次预测的概率图平均后再取 argmax。这个几乎零成本,Dice 通常能涨 1 到 2 个点,代价是推理时间翻几倍。手术离线分析可以上,实时场景要权衡。
我自己的习惯是:每拿到一个新数据集,先花半天做数据体检,把类别分布、mask 格式、划分方式全部落成脚本,再动模型。这半天省下来的,是后面反复调参、怀疑人生的一周。手术器械分割这类任务,数据质量的天花板远低于模型架构的差异,把标注用对、把评估口径对齐,比换十个主干都管用。希望帮到你。
本文还有配套的精品资源,点击获取