ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

手术器械语义分割数据集:可视化验证到训练配置全流程解析

手术器械语义分割数据集:可视化验证到训练配置全流程解析 简介这是一套面向医学图像分析与深度学习研究者的手术器械图像语义分割数据集包含约1200张已处理图片及对应标签按32个类别完成语义分割标注并已划分出训练集和验证集适合直接用于分割模型的训练、验证与效果评估。压缩包共2000个文件以png、jpg图像为主其中训练集约860张图片、验证集约370张图片均配有mask模板另含1个txt说明文档和1个py可视化脚本整体约80.79MB。已有104人学习下载。可视化脚本可随机抽取一张样本在同一画面中展示原始图片、GT分割图以及GT在原图上的蒙版叠加结果并自动保存到当前目录便于快速检查标注质量。数据集经过预处理可适配Unet、SwinUnet等主流分割网络也可用于论文实验或算法对比能节省医学图像数据收集与标注的时间成本。1. 手术器械语义分割数据集为什么我先把GT和原图叠在一起看做医学图像分割的同行应该都有这种经历辛辛苦苦下了一个数据集解压完发现一堆jpg和png躺在那儿没有划分文件、没有说明文档光是把文件对应起来就要折腾半天。这份手术器械图像语义分割数据集属于少见的“到手就能用”的类型——约1200张图已经分成860张左右的训练集和370张左右的验证集每张图都有对应的mask模板而且是32个类别的多类别分割。我拿到手第一件事不是急着训模型而是把可视化脚本跑了一遍把原始图、GT图、GT叠在原图上的效果三张图画出来看。这一步帮我避开了后面至少三个坑mask类别标反、某些类别样本极少、灰度图被当成单通道彩色图处理。下面我用这篇文章把这份数据集的真实结构、验证流程、训练配置和踩坑记录都过一遍适合正在找医学分割数据集练手或者准备做手术器械相关课题的从业者。2. 目录结构与32类标签先厘清images和masks的对应关系2.1 拿到手先别训模型花十分钟摸清目录这份数据集的划分已经做好了目录上就是常规的train/valid两级每级下面各挂着images和masks两个兄弟目录。训练集860张左右验证集370张左右加起来约1230张和标题说的1200张基本吻合。images里放的是原始手术视频帧masks里放的是对应的语义分割标签两张图的文件名前缀是一样的只是目录不同比如892.jpg对应的是masks/892.png。我拿到任何图像分割数据集的第一步永远是先确认文件名是否能严格对齐。常见做法是写一个三五行的小脚本遍历images目录取所有文件名然后到masks目录里检查同名文件是否存在一旦发现缺漏立刻记录下来。不要默认“应该都齐了”这类数据集翻车的概率比你想象得高尤其是文件名里面有零填充、后缀不一致这类细节。import os from pathlib import Path img_dir Path(r./train/images) mask_dir Path(r./train/masks) missing [] for img_path in img_dir.glob(*.jpg): mask_path mask_dir / (img_path.stem .png) if not mask_path.exists(): missing.append(img_path.name) print(f总图片数: {len(list(img_dir.glob(*.jpg)))}) print(f缺失mask数: {len(missing)}) for name in missing[:10]: print(f {name} 或对应mask)这段脚本的逻辑很直接用glob扫出所有jpg再用stem取主文件名拼出mask的png路径检查是否存在。实际运行后我发现这份数据集的文件对应关系做得很规整没有出现缺mask的情况不过依然建议每一份新数据集都跑一遍这个检查成本几乎为零。2.2 多类别mask的读取方式和单通道灰度图问题这里有一个很多人第一次接触多类别分割时会踩的坑mask的png不是普通的三通道RGB图而是单通道的调色板索引图或者说是灰度图。如果你直接用cv2.imread默认参数去读得到的是三通道的BGR图虽然看起来也是黑的但后面转成tensor时维度就对不上了。我一般用cv2.imread(path, cv2.IMREAD_GRAYSCALE)或者PIL.Image.open之后转np.array这样拿到的是形状为(H, W)的二维数组数组里的每个像素值就是类别索引范围是0到31。背景通常是0前景手术器械从1开始编号。import cv2 import numpy as np # 关键点IMREAD_GRAYSCALE 保证拿到单通道类别图 mask cv2.imread(r./train/masks/892.png, cv2.IMREAD_GRAYSCALE) print(fmask形状: {mask.shape}) print(f唯一类别值: {np.unique(mask)}) img cv2.imread(r./train/images/892.jpg) print(f原图形状: {img.shape})从输出能看到mask是二维的(H, W)唯一值列表如果只有[0, 1, 2, 3]这类稀疏分布说明当前这张图里只出现了少数几类器械。不要觉得这是异常实际上很多手术帧里同时出现的器械种类并不多可以随便翻几张图验证一下不同类别的分布情况。2.3 32个类别是“总共32类”还是“每张图32类”这里要跟第一次做多类别分割的朋友说清楚32类别指的是整个数据集语义分割的类别总数包含背景类别。一张图里大概率只会出现其中五六个类别而不是每张图都把32个类塞满。训练时计算损失的重点不在于“把32类都预测出来”而在于把当前帧里出现的几个前景类别和背景区分开。类别太多且分布不均衡时常见做法是统计每个类别在所有mask中的像素占比把占比极低的类别标记出来。手术器械数据集里不同器械出现频率差异很大短的止血钳、缝针这类小物件在整张图中的像素占比可能不到千分之一这时候如果直接用常规CrossEntropy训练小物件基本会被模型无视。from collections import Counter mask_dir r./train/masks counter Counter() filenames [f for f in os.listdir(mask_dir) if f.endswith(.png)] for i, fname in enumerate(filenames): m cv2.imread(os.path.join(mask_dir, fname), cv2.IMREAD_GRAYSCALE) uniques np.unique(m) counter.update(uniques.tolist()) if i 300: break print(前300张mask的类别像素出现图数统计:) for cls_id, cnt in counter.most_common(): print(f 类别{cls_id}: {cnt}张图出现)这个统计脚本抽样了前300张足够初判类别分布。如果发现某个类别只有个位数图片出现后面训练时要特别注意要么加大该类别的loss权重要么做针对性数据增强。这类情况在手术器械数据集中很常见尤其是新引入的器械型号标注数据天然偏少。3. 可视化脚本实操一张图看清GT有没有对齐3.1 脚本思路和叠加展示的核心逻辑这份数据集附带了一个可视化脚本随机抽一张图把原图、GT、叠加效果三张图保存到当前目录。很多下载者会跳过这个脚本直接开训但实际上这一步是验证数据质量最快捷的方式。叠加显示的核心逻辑是把mask变成半透明的红色蒙版盖到原图上一眼就能看出类别边界和器械轮廓是否对得上。我自己也经常写这类可视化脚本核心就三个步骤第一原图正常BGR读取转RGB用于展示第二mask用灰度方式读取生成一个全黑的彩色底图把非背景像素在红色通道上置为255第三用cv2.addWeighted做alpha融合。下面是这份数据集配套脚本的等价实现。import cv2 import numpy as np img cv2.imread(r./train/images/892.jpg)[:, :, ::-1] # BGR - RGB mask cv2.imread(r./train/masks/892.png, cv2.IMREAD_GRAYSCALE) # 红色蒙版底图非0像素即前景置为大红色 color_mask np.zeros((*mask.shape, 3), dtypenp.uint8) color_mask[mask 0] (255, 0, 0) # alpha融合原图0.6蒙版0.4肉眼观察边界是否贴合 overlay cv2.addWeighted(img, 0.6, color_mask, 0.4, 0) out np.hstack((img, color_mask, overlay)) cv2.imwrite(visual_check.png, out[:, :, ::-1]) print(已保存 visual_check.png请检查边界是否贴合)如果你跑出来的叠加图出现“蒙版边界比器械大了一圈”或者“蒙版位置和原图器械错开半个身位”的情况那基本可以判定images和masks不是同一帧的图像属于数据集错位问题。这份数据集合成得比较光滑视觉效果基本是像素级的贴合原图和GT的采集分辨率一致不需要额外做坐标变换。3.2 用可视化脚本排查边缘噪声和标注瑕疵多类别医学分割数据集的标注质量不是均匀的边缘部分经常出现“描边出界”或者“内部空洞”的问题。可视化脚本放大看几个典型样本能帮你在训练前就判断这版数据的标注风格是精确到底边界的polygon光栅化结果还是人工涂抹的粗糙标注。建议不要只看一张图而是抽5~10张不同编号的图走一遍脚本输出。具体做法是把上面脚本改成循环按固定步长抽取文件名比如每隔50张抽一张把叠加结果统一存到check_dir目录。手动扫一遍这几张图重点看三类问题小器械边界是否连续、器械互相遮挡时mask是否重叠、暗光区域下器械是否漏标。这些一眼能看到的问题比训练完看loss曲线发现不收敛再回头排查要省时间得多。from pathlib import Path img_dir Path(r./train/images) mask_dir Path(r./train/masks) out_dir Path(r./check_dir) out_dir.mkdir(exist_okTrue) all_imgs sorted(img_dir.glob(*.jpg)) sample_imgs all_imgs[::50] # 每50张抽1张 for img_path in sample_imgs: mask_path mask_dir / (img_path.stem .png) image cv2.imread(str(img_path))[:, :, ::-1] mask cv2.imread(str(mask_path), cv2.IMREAD_GRAYSCALE) color_mask np.zeros((*mask.shape, 3), dtypenp.uint8) color_mask[mask 0] (255, 0, 0) overlay cv2.addWeighted(image, 0.6, color_mask, 0.4, 0) combo np.hstack((image, color_mask, overlay)) out_file out_dir / fcheck_{img_path.stem}.png cv2.imwrite(str(out_file), combo[:, :, ::-1]) print(f已导出 {len(sample_imgs)} 张检查图到 {out_dir})这种抽样检查不需要每张图都看但必须在训练前做一轮。手术器械反光严重金属表面经常出现高光区域如果标注时把高光部分切掉了训练出来网络会对高光区域特别不稳定推理时容易把反光部分划成背景。3.3 从可视化结果反推预处理策略做完叠加可视化之后你还能得到另一个关键信息原图尺寸和器械占比。如果原图是1920x1080这种高清腹腔镜画面而单个器械只占其中一小块那直接resize到512x512会把小器械缩到几乎看不清。常见做法是先用原尺寸做训练数据可视化观察器械占整张图的比例再决定训练尺寸。img cv2.imread(r./train/images/892.jpg) mask cv2.imread(r./train/masks/892.png, cv2.IMREAD_GRAYSCALE) h, w img.shape[:2] total_pixels h * w fore_pixels int((mask 0).sum()) print(f原图尺寸: {w}x{h}) print(f前景像素占比: {fore_pixels / total_pixels * 100:.2f}%)如果前景占比低于5%我的习惯是把训练尺寸从常见的512调到768甚至960虽然训练慢一些但小器械的语义信息不会被彻底丢失。反之如果前景占比在20%以上512完全够用。这份数据集里的手术器械有大有小建议至少多看一眼训练集上的平均前景占比再定分辨率这个决策直接决定小类别能不能学出来。4. 训练配置与损失函数选择多类别分割下的CrossEntropy与Dice组合4.1 数据加载写的四个关键点多类别分割的DataLoader和单类别分割最大的区别在于mask不能做归一化、不能做随机RGB颜色增强、resize时的插值方式必须固定为最近邻。很多第一次跑分割模型的人把mask也做了和原图一样的标准化处理结果mask里的像素值全变成了0模型看到的标签全是背景loss直接不下降或直接报错。import torch from torch.utils.data import Dataset import cv2 import numpy as np class InstrumentSegDataset(Dataset): def __init__(self, img_dir, mask_dir, size(512, 512)): self.img_paths sorted(Path(img_dir).glob(*.jpg)) self.mask_dir Path(mask_dir) self.size size def __len__(self): return len(self.img_paths) def __getitem__(self, idx): img_path self.img_paths[idx] mask_path self.mask_dir / (img_path.stem .png) img cv2.imread(str(img_path)) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask cv2.imread(str(mask_path), cv2.IMREAD_GRAYSCALE) # resize: 原图用双线性mask必须用最近邻 img cv2.resize(img, self.size, interpolationcv2.INTER_LINEAR) mask cv2.resize(mask, self.size, interpolationcv2.INTER_NEAREST) img torch.from_numpy(img.transpose(2, 0, 1)).float() / 255.0 mask torch.from_numpy(mask).long() return img, mask这里最容易出错的一行是cv2.INTER_NEAREST。如果用默认的双线性插值去缩放mask类别边界会混出介于两个整数之间的灰度值比如原类别5和6交界处会多出5.5的像素但像素值必须是整数类别索引这种插值结果会让类别数凭空增多训练时损失函数无法处理。最近邻插值没有这种问题这是多类别分割DataLoader的铁律。4.2 CrossEntropy和Dice Loss搭配的使用场景32类多类别分割直接上CrossEntropy是最稳的起点。原因在于CrossEntropy对每个像素独立做softmax分类考虑的是“这个像素属于哪个类别”的问题它的梯度更新让网络学会全局分类。但它有一个固有缺陷如果某类前景像素占比极低loss对这类样本的贡献也很小网络学完以后这类永远预测成背景。常见的补救方式是接一个Dice Loss或者直接用CrossEntropy加Dice的加权组合前者负责关注边界和小目标。一般的做法是total_loss ce_loss 0.5 * dice_lossdice系数按类别计算后再按前景类别平均。def ce_dice_loss(pred, target, alpha0.5): # pred: [B, C, H, W]target: [B, H, W] ce torch.nn.functional.cross_entropy(pred, target) pred_prob torch.softmax(pred, dim1) # B, C, H, W num_classes pred_prob.shape[1] target_onehot torch.nn.functional.one_hot( target, num_classesnum_classes ).permute(0, 3, 1, 2).float() intersection (pred_prob * target_onehot).sum(dim(2, 3)) union pred_prob.sum(dim(2, 3)) target_onehot.sum(dim(2, 3)) dice (2 * intersection 1e-6) / (union 1e-6) # 跳过背景类和不出现的类别避免拉低整体 dice_score dice[:, 1:].mean() return ce alpha * (1 - dice_score)这段代码里的alpha0.5是一个经验默认值实际工作中我会让dice的权重偏小一点因为在前景占比很低的数据集里dice loss容易让网络陷入“宁可多预测前景也要提高和GT的overlap”的倾向导致误报增多。手术器械分割对误报的容忍度很低预测出本来不在画面中的器械比漏检还麻烦所以权重需要控制在0.3到0.5之间。4.3 类别不平衡的补救手段数据集中如果某个手术器械类别只出现在十几张图中单纯加loss权重效果有限因为每个epoch里这类样本只被看到一两次。常见的另一条路是使用带权重的CrossEntropy统计每个类别的像素占比将占比倒数归一化后作为CE的类别权重。from torch.nn import CrossEntropyLoss # 假设统计得到每个类别像素占比按倒数加权再归一化 class_weights torch.tensor([1.0, 1.2, 1.8, 2.5, 3.0, ...]) criterion CrossEntropyLoss(weightclass_weights)注意权重不要和像素占比严格成反比否则小类别的权重会大到把正常类别的梯度冲掉。我一般会把最大权重限制在5.0以内超过的部分截断因为权重过大会让模型在少数几张图上过拟合验证集iou不升反降。这份数据集整体类别的分布较为集中常规的加权策略就已经够用。5. 常见问题排查mask全黑、类别缺失和显存溢出的三个实操记录5.1 验证集loss一直降但iou不涨训练过程中loss持续下降但验证集的mIoU在一个数值附近震荡这是多类别分割最常见的现象之一。多数情况下不是模型代码写错了而是验证指标计算时把背景类别算进去了。32类里有大量类别在验证集中根本不存在模型把背景预测准确后背景类的iou接近1把整体的平均iou拉高了而实际关心的器械类别iou并不理想。解决方法是排除背景类计算mIoU并且按“仅计算验证集中实际出现的类别”做评估。这样得到的数据才是模型在手术器械上的真实水平。我在工程里习惯同时输出背景类和前景类的分开分数数值一对比就能看出模型到底学没学到器械特征。def compute_iou(pred_mask, gt_mask, num_classes32, ignore_bgTrue): ious [] start 1 if ignore_bg else 0 for cls in range(start, num_classes): pred (pred_mask cls) gt (gt_mask cls) inter (pred gt).sum() union (pred | gt).sum() if union.item() 0: continue # 该类别不在GT中跳过 ious.append(inter.item() / max(union.item(), 1)) return np.mean(ious)这段代码的关键在于continue分支验证集某张图里不存在的类别直接跳过而不是把它的iou算成0拖低整体分数。很多人验证集mIoU偏低就是这里写错了把完全不相关的类别也纳入了平均导致分数失真。5.2 训练时mask全黑loss直接变成常数或不下降如果训练开始后loss完全不变先检查DataLoader里返回的mask张量的取值集合。一个常见的失误是读mask时用了cv2.IMREAD_COLOR拿到三通道图后直接转tensor虽然显示是黑的但实际上每个像素是三维向量而定义的CrossEntropy的target是二维索引图两者形状对不上PyTorch报错或者静默地把类别当作3通道处理。还有一种情况是resize时用了INTER_LINEAR上面已经说过会生成小数像素值。最好在dataloader外面单独写一个测试函数跑一次batch打印mask.unique()确认返回值都在0到31之间。这一步看起来简单但能省下半天排查时间。5.3 显存溢出大批次加大分辨率同时拉满这份数据集的原图如果直接从文件读入而不做resize比如1920x1080的尺寸batch size为8时显存占用轻松超过12GB。很多人在这个环节选择调小batch size结果发现训练不稳定。更好的思路是原图先等比缩放保证短边为512或640再进入网络。这样即便batch size为8到16显存占用也可控。# 统一短边保持纵横比 def resize_short_edge(img, short512): h, w img.shape[:2] scale short / min(h, w) new_w, new_h int(w * scale), int(h * scale) return cv2.resize(img, (new_w, new_h), interpolationcv2.INTER_LINEAR)需要注意的是等比缩放后同一batch内图片尺寸不一致所以我一般在DataLoader里统一再做一次中心裁剪或pad到固定尺寸。实际工程里最省事的做法就是直接resize到(512, 512)因为手术器械位置相对集中在画面中央长宽比变化带来的变形对分割任务影响不大。这一点和自然图像分类任务不同不需要过于在意宽高比失真问题。5.4 可视化脚本生成的三张图里GT颜色全是纯色如果你跑完可视化脚本发现mask整片都是同一个红色完全看不出类别区分这不是脚本坏了而是可视化脚本默认把所有非背景像素渲染成同一种颜色。这份数据集自带的脚本是给人眼做粗略对齐检查用的具体类别分布要看每个类别单独渲染的效果。排查时可以写几行代码把mask中的每个类别分别赋予一个随机RGB颜色再叠加这样一眼就能看出当前图里出现了哪几个手术器械以及它们的边界。colors np.random.randint(0, 255, size(32, 3), dtypenp.uint8) color_mask colors[mask] # mask的每个像素值作为颜色索引 overlay cv2.addWeighted(img, 0.6, color_mask, 0.4, 0)这段代码利用numpy的数组索引把二维类别图变成彩色图比用cv2.applyColorMap更可控。跑完这张图你就能大概预估模型可能在哪几个类别上重度混淆比如两个同形状不同型号的钳子颜色越接近的类别在模型embedding空间里也越容易纠缠。6. 从UNet到SwinUNet小类别mIoU的一个进阶验证思路训练基线模型跑通之后如果发现某些器械类别的mIoU低于10%我的建议是先别急着换网络结构而是把这一类别的样本单独抽出来用第3章的可视化脚本再看一遍标注边界。手术器械之间的视觉差异本来就细微比如直钳和弯钳外形相似度极高人眼有时都难以快速区分模型学不出来完全不稀奇。在标注质量确认无误后再考虑升级网络。用UNet做基线时普通的Encoder-Decoder结构对小目标的敏感度偏低尤其是解码器上采样四次之后小器械的细节信息已经丢失大半。换到SwinUNet或TransUNet这类带Transformer分支的网络优势在于全局上下文建模能力强但代价是显存占用上升和训练时间拉长。我一般验证网络改进是否有效时只看两个指标小类别mIoU是否提升超过3个百分点、整体mIoU有没有下降。如果小类别涨了但其他类别掉得厉害则需要检查是不是引入的全局注意力让模型过度关注背景区域。一个折中方案是采用深监督在解码器的不同层级都接上分割头计算辅助损失让浅层特征也能拿到直接的梯度信号小目标的分割质量通常会有明显改善。# 深监督伪代码辅助输出参与损失计算 out1, out2, out3 model(x) # 不同解码层输出 loss criterion(out1, mask) 0.3 * criterion(out2, mask) 0.1 * criterion(out3, mask)辅助损失系数从大到小递减让主输出主导训练同时浅层特征不至于被完全忽略。实验时可以用这份数据集的验证集快速跑20个epoch做AB对比不需要等完整训练收敛就能看出趋势。从那以后我每次换网络结构都强制走一遍这个流程小类别样本可视化、深监督辅助损失、前景类mIoU单独统计三个步骤缺一不可希望能帮到正在调试手术器械分割模型的你。本文还有配套的精品资源点击获取
返回列表