资讯动态

蝶鞍分割小样本实战:从labelme标注到U-Net训练全流程解析

发布时间:2026/10/19 1:50:41 来源:尧图企业网站定制
简介头颅侧位片蝶鞍形态数据集是一套面向医学影像语义分割任务的高质量标注资源适合医学图像处理研究者、算法工程师及内分泌疾病辅助诊断相关项目开发者使用可用于蝶鞍区域分割模型的训练与验证。数据集中所有图像均通过Labelme工具进行细致的逐像素手动标注能够精准勾勒蝶鞍区域轮廓尤其适用于蝶鞍这类结构较小、边界复杂的解剖区域为训练高精度语义分割模型提供可靠的监督信息。整个压缩包共1558个文件包含779张JPG原图与779个对应JSON标注文件文件结构简洁清晰便于按图索骥完成数据划分与模型迭代包体大小约为671.16MB。标注数据与原始图像一一对应无需额外整理即可开始训练。目前已有123人学习/下载对于希望快速获取医学标注数据、开展蝶鞍形态分析或探索医学分割算法的读者而言这份数据集可省去繁琐的数据采集与标注成本直接用于模型训练、算法对比及科研验证具有明确的实用价值。1. 头颅侧位片蝶鞍形态数据集十张标注图能做出一套完整的分割流程蝶鞍是脑垂体所在的那个小小骨性凹陷。头颅侧位X光片上它的轮廓往往只有一小块夹在颅底骨影之间很容易被忽略。这份头颅侧位片蝶鞍形态数据集正是对着这个区域做的 labelme 手工标注下载后你会得到一批 jpg 原图和同名 JSON 文件里面没有现成的分割结果真正的产出需要你自己从 JSON 解析、Mask 生成一直到语义分割模型训练一步步打通。我一开始没把这份数据当回事觉得图片量太小可能一次就过拟合但真正跑下来发现关键不在数量而在流程是否闭合。对想上手医学影像语义分割的人来说这是少有的能直接跑通的资源不用翻标注规范不用找脱敏数据打开就能看到“原图 X光片灰度特征 蝶鞍小目标”这三个必踩的坎。2. labelme标注解析把JSON里保存的蝶鞍多边形变成可用于训练的Mask解压数据集后目录里是 jpg 和 JSON 标注文件成对出现。labelme 生成的 JSON 保存的是标注工程的原始信息类别名、坐标、形状类型都在里面但它不是语义分割训练直接要的 Mask。每张图对应一个 JSON、一个蝶鞍轮廓你需要先把这些坐标还原成一幅 0/1 的二值图。这步听起来简单实际做的时候有几个细节容易埋雷比如多边形首尾不闭合、形状是凸的还是凹的、图像灰度范围不统一都会让转换结果和真实轮廓对不上。2.1 labelme的JSON结构从imagePath到shapes里的那串pointslabelme 的 JSON 结构比较固定核心就几个字段。我一般会先写个小脚本把整批 JSON 扫一遍确认 annotation 的 shape_type 都是 polygon、label 名称统一避免训练到一半发现有的标注是矩形或者出现了两个类别名。字段作用训练前要确认的点version标注器版本不用管imagePath原始图片相对路径确认原图还在别依赖内嵌数据imageHeight / imageWidth原图像素尺寸记下来后面统一 resize 时用shapes标注区域数组每个元素里有 label、points、shape_typeimageDatabase64 内嵌图数据这份数据集大图场景通常为空字符串检查脚本可以这样写import json, glob for jf in glob.glob(*.json): with open(jf, encodingutf-8) as f: data json.load(f) labels {} for s in data[shapes]: labels[s[label]] labels.get(s[label], 0) 1 print(jf, data[imageWidth], data[imageHeight], labels)逻辑不复杂遍历所有 JSON统计每张图的标注类别和数量。参数上要注意 imageWidth 和 imageHeight 是原始像素尺寸后续转掩膜时如果设定了输出尺寸会以这个尺寸作为参考如果这批数据的 label 不止一种shape_type 也出现混用那转换脚本就要做分支而不是直接填多边形。2.2 写转换脚本闭合检查、多边形填充与Mask输出拿到标注坐标后最直接的工具是labelme_json_to_dataset命令它能一次性生成 img.png、label.png 和 label_names.txt。但我更推荐自己写转换脚本原因是蝶鞍这种不规则小结构经常出现凹多边形而系统中自带的转换流程不见得会帮你处理首尾不闭合的问题后续排查更麻烦。控制权拿在自己手里出了问题才知道去哪看。一个稳定的转换函数大概长这样import json import numpy as np import cv2 def labelme_json_to_mask(json_path, out_sizeNone): with open(json_path, r, encodingutf-8) as f: data json.load(f) h data[imageHeight] w data[imageWidth] mask np.zeros((h, w), dtypenp.uint8) for shape in data[shapes]: if shape.get(shape_type, polygon) ! polygon: continue pts np.array(shape[points], dtypenp.float32) label shape[label] # 首尾闭合检查labelme 的 polygon 通常不重复记录第一个点 if not np.allclose(pts[0], pts[-1]): pts np.vstack([pts, pts[0]]) # 蝶鞍轮廓不规则必须用 fillPoly不能只用 fillConvexPoly cv2.fillPoly(mask, [np.int32(pts)], color1) if out_size is not None: mask cv2.resize(mask, (out_size, out_size), interpolationcv2.INTER_NEAREST) return mask代码逻辑分三段第一段读出原始宽高并初始化全零 Mask第二段遍历 shapes 里的每个标注把首尾点补上后调用cv2.fillPoly填充第三段是可选 resize。参数说明out_size我建议默认不传按原图尺寸先生成高清 Mask等到训练加载时再做缩放。这样原图坐标系里始终保留一份无损标注最后验证分割结果时还能把它映射回原始 X 光片去检查轮廓对齐度。填充颜色写成 1 而不是 255是为了让 Mask 保持 0/1 二值后面计算 Dice Loss 不用再归一化。2.3 X光片读图与归一化先解决灰度范围不一致再谈训练头颅侧位 X 光片不是自然图像不同设备、不同曝光条件下同一解剖结构的像素灰度差异可能非常大。直接用原始像素值喂给模型模型会把“亮度”当成特征来学而不是学“形状”。读图时用 OpenCV 灰度模式读取最稳然后做一遍归一化。我一般用百分位截断而不是简单的标准化import cv2 import numpy as np def normalize_xray(img): # 先转灰度百分位截断能抑制单张图的极亮和极暗噪点 if img.ndim 3: img cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) p_low, p_high np.percentile(img, (2, 98)) img (img - p_low) / (p_high - p_low 1e-6) return np.clip(img, 0, 1).astype(np.float32)逻辑说明取 2% 和 98% 分位数作为截断上下界把灰度线性拉伸到 01 区间能避免单一像素过亮或过暗主导整张图的对比度。参数上要注意如果图像黑色背景占比太高2% 分位可能还是背景截断后前景对比度反而被拉平。遇到这种情况我一般先用一个固定灰度阈值找出头部包围盒只在包围盒内做分位数统计再把整张图映射到同一区间。X 光片预处理这一步不做扎实后面的模型预测就会忽暗忽亮。3. 蝶鞍语义分割模型训练U-Net、损失函数与迁移学习的关键参数Mask 准备好了现在进入训练环节。蝶鞍分割本质上是单通道、单类别的二分类语义分割任务。模型结构层面U-Net 这一类编码器-解码器结构是这个小样本场景下最稳的起手式真正决定十张图能不能训成的是损失函数、输入尺寸和数据划分策略。先讲选型再给超参数最后说迁移学习怎么接进来。3.1 模型选型U-Net为什么比DeepLabV3更稳起初我也考虑过 DeepLabV3觉得空洞卷积能扩大感受野可能对颅底这种复杂背景更友好。实际对比之后还是回到了 U-Net。蝶鞍在整张侧位片里占比很小分割这种小结构靠的是边缘细节恢复能力而 DeepLab 的重点是上下文语义边界处的浅层信息经过多层空洞卷积后已经被稀释。U-Net 的跳跃连接把编码器浅层边缘特征直接传给解码器对这种小目标来说收益远大于感受野扩大。对比下来大致是这样结构技术特点蝶鞍分割表现U-Net跳跃连接、逐级上采样边界完整小结构不容易丢DeepLabV3空洞卷积、ASPP上下文强但小目标边界偏粗SegNet池化索引上采样细节恢复弱不推荐小结构实际代码里我一般用带 ResNet encoder 的 U-Net而不是原始四层卷积版本因为 ResNet 作为 encoder 能直接兼容 ImageNet 预训练权重Decoder 部分保持 U-Net 的上采样结构两边拼接特征。3.2 训练配置输入尺寸、Batch Size与Loss组合这块是我调整最多的部分。输入尺寸选 256 是因为蝶鞍本身只占图像两三个百分点更高的分辨率放大小结构细节的同时也会放大增强带来的伪影而十张图的训练量扛不住太多噪声。Batch Size 4 是显存和 BN 稳定性的折中太小的话 BatchNorm 统计不稳太大会在小样本下迅速过拟合。超参数取值说明输入尺寸256 x 256显存允许可试 512但小样本下提升有限Batch Size4BN 稳定与显存占用的折中LossBCEWithLogitsLoss DiceLoss小目标必须配 DiceOptimizerAdamWlr1e-4weight_decay0.01比纯 Adam 更容易收敛SchedulerReduceLROnPlateau验证 Dice 停滞时降学习率Loss 组合的写法如下import torch import torch.nn as nn def dice_loss(prob, target, smooth1.0): # prob 为 sigmoid 之后的概率target 为 0/1 Mask intersection (prob * target).sum() return 1 - (2.0 * intersection smooth) / (prob.sum() target.sum() smooth) # 训练循环内 logits model(images) loss_bce nn.functional.binary_cross_entropy_with_logits(logits, masks) prob torch.sigmoid(logits) loss loss_bce dice_loss(prob, masks)只靠 BCE 在蝶鞍这种正样本占比不到 3% 的数据上会迅速收敛到“全背景”的局部最优Dice Loss 通过整体区域的交并比约束让模型必须把蝶鞍内部也预测对。smooth 参数默认 1.0 是防止分母为零数据量小的时候 smooth 不要设太大否则梯度会被稀释。二者相加时权重是 1:1如果发现预测偏保守可以把 Dice 权重提到 1.5。3.3 迁移学习ImageNet预训练在X光片上还能用吗这是个老争论。X 光片和 ImageNet 自然图像差异很大但预训练模型前半段学到的基本边缘、纹理、角点特征依然有迁移价值尤其在小样本场景下至少不用从零学边缘检测。我的做法是encoder 加载预训练权重第一层卷积输入通道从 3 改成 1取预训练三个通道权重均值作为单通道初始化。from torchvision.models import resnet34, ResNet34_Weights encoder resnet34(weightsResNet34_Weights.IMAGENET1K_V1) encoder.conv1 nn.Conv2d(1, 64, kernel_size7, stride2, padding3, biasFalse) with torch.no_grad(): # 三通道权重取平均让第一层输出分布尽量接近预训练时的状态 encoder.conv1.weight.data encoder.conv1.weight.data.mean(dim1, keepdimTrue)参数说明mean(dim1, keepdimTrue)是把预训练卷积核的 RGB 三通道压成单通道保留维度以便后面加载。Decoder 部分必须随机初始化因为预训练任务没有对应输出。如果加载预训练后训练反而震荡就切回随机初始化并把数据增强强度调高一档这在十张图的数据规模下也是常见操作。4. 小样本蝶鞍分割常见问题五个标注入坑与训练陷阱排查十张图的小样本分割看起来很容易跑飞下面五个坑都是训练和标注过程中真实会遇到的每条按现象、原因、解决来写。排查顺序建议从标注数据本身开始再到预处理和训练策略。4.1 训练半天Mask全黑背景占比超过98%时BCE失效现象训练了五六个 epochloss 在下降但验证集预测结果全是黑色Dice 为 0。原因蝶鞍正样本像素在整张 256 x 256 图像里可能只有几百像素背景占比超过 98%。BCE 对小类别不敏感模型发现全部预测为背景也能拿到很低的 loss于是收敛到了这种“偷懒解”。解决把损失函数从纯 BCE 切换成 BCE Dice 组合Dice Loss 天然按区域重叠比例计算背景占比再高只要预测全黑Dice 就是 0梯度信号立刻拉满。排查时不要只看 loss 曲线第一轮训练就要输出验证集 Dice 和预测图。4.2 验证集Dice很高但新图一塌糊涂灰度归一化漏了一环现象训练集和验证集指标都正常但拿一张没见过的头颅侧位片去推理蝶鞍轮廓明显偏移边缘多了一圈或缺了一块。原因训练时每张图都做了百分位截断归一化但新图如果曝光偏暗或偏亮输入的灰度分布和训练分布不一致模型学到的对比对就失效了。X 光片之间灰度范围差异极大这一步不做统一模型分不清“边缘”和“亮度带”。解决推理时对每一张新图执行与训练数据完全一致的normalize_xray注意是百分位截断还是 z-score 要做成固定函数不能训练用一套、推理用另一套。我一般还会在推理前打印新图归一化前后的灰度分布和训练数据的分布对一下眼。4.3 随机划分让结果忽高忽低小样本需要K折而不是留出法现象同样的模型和超参换一个随机种子留出两张图做验证Dice 从 0.85 跌到 0.65。原因十张图里不同个体的蝶鞍形态差异很大随机留出的两张如果恰好都是形态偏大或骨影叠加复杂的验证集指标就失真了。固定验证集在这种样本量下不够可靠讨论模型好坏也缺乏依据。解决用 5 折交叉验证代替固定划分每一折单独训练、早停、记录 Best Dice最后把五折均值当作模型真实水平的估计。这会让训练次数从 1 次变成 5 次但小样本下这是唯一不容易自欺的评估方式。4.4 旋转增强把蝶鞍转出图像设置rotate_limit前先想边界现象开启数据增强后有一类训练样本的 Mask 变成全零或者边界缺失一块模型训练很快崩掉。原因旋转角度设置太大比如 rotate_limit30图像旋出边界后四周被黑色填充而蝶鞍如果恰好靠近图像边缘旋转后就被裁剪到黑边外变成无效训练对。解决几何增强要克制。旋转角度控制在 ±15 度以内缩放控制在 0.05 以内位移控制在 2% 以内。同时在增强管线里加一个 Mask 非零面积检查发现某张训练样本增强后正样本像素低于阈值就重新采样一次这个防御性检查能避免“脏样本”进到训练循环。4.5 fillPoly填充出大色块标注多边形自交更隐蔽现象某张图转换出来的 Mask 明显大于实际蝶鞍区域覆盖到了周围颅底骨影和 JSON 里的坐标点位对不上。原因labelme 手工标注时如果打点顺序出现交叉多边形会变得自交cv2.fillPoly按扫描线规则填充时会产生意外的大面积区域。人工标注很难避免这种问题尤其蝶鞍轮廓又小又曲折。解决转换脚本里对每个 polygon 做一次简单校验计算多边形面积和所有相邻边夹角出现明显异常就单独打印文件名。更稳妥的做法是转完所有 Mask 后把原图、标注轮廓、Mask 三张图拼在一起逐张过一遍。这类标注脏数据靠随机抽检是发现不了的。5. 让十张图发挥极限增强组合、K折交叉验证与掩膜后处理小样本训练时数据增强是必需品但增强参数选不对会加速过拟合甚至直接破坏标注。这一章讲我实际用的增强组合、交叉验证评估方式以及预测完之后的掩膜后处理流程。5.1 增强组合怎么选几何变换克制光度变换放开我的增强管线固定用下面这组配置import albumentations as A train_transform A.Compose([ A.RandomBrightnessContrast( brightness_limit0.2, contrast_limit0.2, p0.5 ), A.ShiftScaleRotate( shift_limit0.02, scale_limit0.05, rotate_limit15, border_mode0, p0.8 ), A.HorizontalFlip(p0.5), A.Resize(256, 256), ])几个参数要特别说明brightness_limit 和 contrast_limit 可以放开到 0.2模拟不同 X 光机的曝光差异这对医学影像泛化帮助最大。ShiftScaleRotate 里 shift_limit 只给 0.02、rotate_limit 只给 15因为蝶鞍区域太小几何变换一大就移出边界。border_mode 默认是 0也就是黑边填充如果改成cv2.BORDER_REFLECT_101能减少黑边干扰但这会让图像边缘像素被反射X 光片上等于造出了不存在的高亮伪影所以我一般还是保持黑边配合 4.4 的 Mask 非零检查。flip 在这里是合理的头颅侧位片左右翻转后蝶鞍依然是蝶鞍解剖结构对称性足够。不要在增强里加 CoarseDropout 或随机遮挡蝶鞍本身太小随机遮挡很容易把唯一的正样本区域抹掉。5.2 用5折交叉验证替代固定验证集评估策略直接影响你最终敢不敢把这个模型用到新片子上。十张数据做 10 折留一法每一折只有一张验证图Dice 方差大到你无法判断模型到底行不行。5 折是更折中的选择。from sklearn.model_selection import KFold import numpy as np kf KFold(n_splits5, shuffleTrue, random_state42) dice_scores [] for fold, (train_idx, val_idx) in enumerate(kf.split(images)): best_dice train_one_fold( images[train_idx], masks[train_idx], images[val_idx], masks[val_idx] ) dice_scores.append(best_dice) print(fmean dice: {np.mean(dice_scores):.3f} ± {np.std(dice_scores):.3f})逻辑说明train_one_fold内部就是前面第 3 章的训练循环返回模型在该折验证集上的 Best Dice。KFold 的 shuffleTrue 确保图像顺序被打散random_state 固定方便复现。五折均值反映模型平均水平标准差反映不同个体形态差异带来的波动。如果标准差超过 0.1说明蝶鞍形态个体差异太大需要更多样本来压住方差而不是继续调超参。5.3 分割后处理连通域过滤与形态学闭合模型输出的概率图直接二值化通常会有两种问题蝶鞍内部因为骨骼纹理被预测成背景出现小孔洞背景里出现零星的假阳性碎片。手动处理时我会按面积过滤掉小连通域再做一次形态学闭运算。def postprocess(pred, min_area50): pred (pred 0.5).astype(np.uint8) # 只保留面积最大的连通域蝶鞍应该是图像里唯一的掩膜组件 num, labels, stats, _ cv2.connectedComponentsWithStats(pred, connectivity8) if num 1: return np.zeros_like(pred) largest 1 int(np.argmax(stats[1:, cv2.CC_STAT_AREA])) pred np.where(labels largest, 1, 0).astype(np.uint8) kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) pred cv2.morphologyEx(pred, cv2.MORPH_CLOSE, kernel) return pred参数说明min_area50表示小于 50 像素的碎片直接忽略这个阈值要跟着 Mask 分辨率走如果模型输出是 64 x 64 的小掩膜50 像素可能把真实蝶鞍也删掉。MORPH_CLOSE的核用椭圆是为了贴合蝶鞍本身的圆润边界正方形核容易留下棱角。后处理只影响最终指标计算不参与训练梯度所以参数可以根据验证集可视化结果反复调。6. 验证与量化把蝶鞍分割结果放回原图坐标再看一遍模型训好后验证不是只看一个 Dice 数字就结束。蝶鞍分割的最终价值在于让医生快速抓住形态变化面积增大、前后径变长、轮廓变深是临床上常见的关注方向而这些都要从 Mask 里量出来。我习惯做两步一是把预测掩膜映射回原始 X 光片尺寸看轮廓对齐二是从掩膜里计算出蝶鞍的形态指标。# 训练时输入是256x256预测完必须映射回原图尺寸 pred_orig cv2.resize(pred_256, (orig_w, orig_h), interpolationcv2.INTER_NEAREST) xray_bgr cv2.cvtColor(xray_orig, cv2.COLOR_GRAY2BGR) contours, _ cv2.findContours(pred_orig, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cv2.drawContours(xray_bgr, contours, -1, (0, 0, 255), 2)这一步很容易被跳过但非常关键因为训练时 256 x 256 分辨率下的边界偏移可能只有两三个像素映射回原始高清 X 光片上就变成了二三十像素的偏差临床位置上完全不可接受。轮廓叠回原图后肉眼能直接判断模型有没有把蝶鞍下缘和颅底骨影混淆。def sella_metrics(mask): contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return None cnt max(contours, keycv2.contourArea) x, y, w, h cv2.boundingRect(cnt) area cv2.contourArea(cnt) return {pixel_area: area, bbox_w: w, bbox_h: h, aspect_ratio: w / h}pixel_area对应蝶鞍整体大小aspect_ratio对应前后径与上下径的比值变化。有一点要提醒不同 X 光片的空间分辨率可能不一样直接用像素面积横向对比之前必须先确认图像里有没有标尺信息否则像素面积换算成毫米面积时会失真。我在一次复盘里发现某张新片预测出来的 Dice 很好看但把掩膜映射回原图后轮廓中心偏了快半个蝶鞍。原因就是我只在 256 分辨率下看图边界上的小偏移被缩放掩盖了。从那以后每次做医学影像小结构分割都会强制走一遍“resize 回原图、轮廓叠加、形态参数输出”这套检查训练指标和实际位置都对得上才算真正完成一份可交付的分割结果。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑