
简介这份头颅侧位片蝶鞍形态数据集面向医学影像分析与语义分割方向的研究者和开发者尤其适合从事内分泌疾病辅助诊断、X光片解剖结构分割的初中级算法实践者。蝶鞍作为脑垂体的容纳区域其形态异常常与内分泌疾病相关精确分割对临床诊断与治疗评估具有实际意义。资源包共1558个文件包含779个json标注文件与779个jpg图像文件json由labelme手动标注生成记录蝶鞍区域的轮廓信息jpg为对应的头颅侧位X光原图压缩包整体约671.16MB下载后可直接用于语义分割模型的训练与验证。目前已有123人学习下载。手工标注方式能保留蝶鞍区域的细微特征帮助模型学习区分蝶鞍与周围组织读者可据此搭建训练流程、验证分割精度并在此基础上探索形态异常识别等应用省去繁琐的数据采集与标注环节。1. 头颅侧位片蝶鞍形态数据集从 labelme 标注到语义分割训练的第一公里如果你正在做口腔正畸、颅颌面影像分析或者单纯想找一个结构边界清晰、标注成本可控的医学语义分割练手数据头颅侧位片里的蝶鞍sella turcica是个被低估的切入点。它位于蝶骨体上方垂体窝所在形态在侧位片上呈一个相对封闭的凹陷轮廓边界对比度好不像软组织那样糊成一团。这个数据集的核心价值在于它用 labelme 手动标注了蝶鞍区域导出后可直接喂给语义分割模型训练省掉了从零标注的几百小时。适合三类人想入门医学图像分割的学生、需要蝶鞍形态量化做研究的临床方向从业者、以及想验证自己分割 pipeline 是否靠谱的工程师。下载即可获取意味着你拿到的是标注文件加原图不是一堆需要自己清洗的原始 DICOM。但别急着跑模型先搞清楚标注格式和坐标系否则第一轮训练就会翻车。2. 蝶鞍语义分割的数据集结构与 labelme 标注格式拆解2.1 蝶鞍在头颅侧位片上的解剖边界与标注粒度蝶鞍的语义分割不是把整个蝶骨都框进去。临床上关注的蝶鞍形态通常指垂体窝的骨性轮廓前界为鞍结节后界为鞍背上界为鞍膈所在的水平线下界为蝶骨体底壁。在头颅侧位片上这个区域表现为一个近似椭圆形或 J 形的低密度影周围是致密骨白线。标注时如果粒度太粗把鞍背和鞍结节都算进去分割结果会偏大后续测量蝶鞍面积、深度、直径时误差会累积。常见做法是只标注蝶鞍的骨性内缘轮廓即垂体窝的边界不包含鞍背外侧的骨质。labelme 的多边形标注适合这种不规则闭合轮廓比矩形框精确得多。一个容易忽略的点是头颅侧位片本身有放大率不同设备的放大系数不同标注时不需要校正但训练前要确认所有图像的像素间距一致否则模型学到的边界尺度会混乱。2.2 labelme 标注文件的 JSON 结构与字段含义labelme 标注后每个图像对应一个同名 JSON 文件核心字段是shapes、imagePath、imageData、imageHeight、imageWidth。shapes是一个列表每个元素包含label、points、shape_type、flags。对于蝶鞍语义分割label通常统一写成sella或sella_turcicashape_type是polygonpoints是归一化前的像素坐标列表格式为[[x1, y1], [x2, y2], ...]。imageData字段在保存时如果勾选了「保存图像数据」会嵌入 base64 编码的原图这会让 JSON 文件体积暴增建议标注时取消勾选只保留imagePath引用。下面是一个典型的蝶鞍标注 JSON 片段{ version: 5.3.1, flags: {}, shapes: [ { label: sella, points: [[312.4, 208.7], [318.9, 205.2], [326.1, 204.8], [333.5, 207.3], [337.2, 212.6], [335.8, 218.4], [329.3, 221.9], [321.7, 222.1], [314.6, 219.5], [310.8, 214.2]], group_id: null, shape_type: polygon, flags: {} } ], imagePath: ceph_001.png, imageData: null, imageHeight: 512, imageWidth: 512 }这段 JSON 里points是 10 个顶点的闭合多边形顺序是顺时针或逆时针都行但必须首尾不重复labelme 会自动闭合。imageHeight和imageWidth是原图尺寸后续转 mask 时要用到。如果imageData不为 null说明图像被嵌入了训练时读取原图会多一步解码建议统一置为 null。2.3 从 labelme JSON 到语义分割 mask 的转换脚本拿到标注后不能直接训练需要把 JSON 转成单通道的 PNG mask像素值 0 表示背景1 表示蝶鞍。下面这个脚本批量处理一个目录下的所有 JSON输出同名的_mask.pngimport json import os import numpy as np from PIL import Image, ImageDraw def json_to_mask(json_path, output_dir): with open(json_path, r, encodingutf-8) as f: data json.load(f) h data[imageHeight] w data[imageWidth] mask Image.new(L, (w, h), 0) draw ImageDraw.Draw(mask) for shape in data[shapes]: if shape[shape_type] ! polygon: continue label shape[label] if label not in [sella, sella_turcica]: continue points [tuple(p) for p in shape[points]] draw.polygon(points, outline1, fill1) base os.path.splitext(os.path.basename(json_path))[0] mask.save(os.path.join(output_dir, base _mask.png)) if __name__ __main__: json_dir ./annotations out_dir ./masks os.makedirs(out_dir, exist_okTrue) for fn in os.listdir(json_dir): if fn.endswith(.json): json_to_mask(os.path.join(json_dir, fn), out_dir)逻辑说明Image.new(L, (w, h), 0)创建单通道黑底draw.polygon用填充值 1 画出蝶鞍区域。参数上outline1和fill1保证边界和内部都是前景避免出现空心轮廓。如果标注里有多个类别把label判断改成字典映射即可。注意points是浮点数PIL 会自动取整对于 512 尺寸的侧位片亚像素误差可以忽略。转换后建议随机抽几张 mask 叠加到原图上目视检查确认没有偏移或漏标。2.4 数据集划分与训练前的目录组织语义分割训练通常按 7:2:1 划分训练、验证、测试。目录结构建议如下dataset/ images/ train/ val/ test/ masks/ train/ val/ test/图像和 mask 文件名必须一一对应比如images/train/ceph_001.png对应masks/train/ceph_001_mask.png。如果原图是 DICOM 转出的 PNG确认位深是 8 位16 位图像直接训练会导致归一化后数值范围异常。另外头颅侧位片常有黑边或准直器遮挡建议在训练前统一裁剪到有效视野否则模型会学到大量无意义的背景。这一步没有固定脚本用 OpenCV 的findContours找最大轮廓再裁剪即可但要注意别把蝶鞍区域裁掉。3. 用 U-Net 跑通蝶鞍语义分割的最小训练流程3.1 为什么蝶鞍分割优先选 U-Net 而不是 DeepLabV3蝶鞍在侧位片里只占几百个像素属于小目标分割。U-Net 的跳跃连接能把浅层的高分辨率特征直接传到解码器对细小边界很友好。DeepLabV3 依赖空洞卷积和 ASPP感受野大适合大目标或复杂场景但在小目标上容易过度平滑边界。我一般会先用 U-Net 跑 baseline如果边界 Dice 低于 0.85 再考虑加注意力模块或换 DeepLabV3。ResNet34 作为编码器 backbone 是个稳妥选择比 VGG 轻比 MobileNet 表达能力强预训练权重在 ImageNet 上容易获取。注意医学图像和自然图像分布差异大编码器可以冻结前几层也可以全量微调数据量小于 500 张时建议冻结到 stage2。3.2 数据加载与增强针对侧位片的几个关键参数侧位片的增强不能照搬自然图像。水平翻转会改变解剖左右关系蝶鞍本身近似对称翻转影响不大但如果你同时做左右侧位片分类翻转会引入标签错误。随机旋转建议限制在 ±10 度因为头颅位置在拍摄时已经标准化大角度旋转不符合真实分布。亮度对比度抖动可以加模拟不同曝光。下面是一个基于 PyTorch 的 Dataset 片段import torch from torch.utils.data import Dataset from PIL import Image import numpy as np import albumentations as A class SellaDataset(Dataset): def __init__(self, img_dir, mask_dir, filenames, transformNone): self.img_dir img_dir self.mask_dir mask_dir self.filenames filenames self.transform transform def __len__(self): return len(self.filenames) def __getitem__(self, idx): name self.filenames[idx] img np.array(Image.open(f{self.img_dir}/{name}).convert(L)) mask np.array(Image.open(f{self.mask_dir}/{name.replace(.png, _mask.png)}).convert(L)) mask (mask 0).astype(np.float32) if self.transform: augmented self.transform(imageimg, maskmask) img, mask augmented[image], augmented[mask] img img.astype(np.float32) / 255.0 img np.stack([img] * 3, axis0) # 单通道转三通道适配预训练编码器 mask np.expand_dims(mask, axis0) return torch.from_numpy(img), torch.from_numpy(mask) train_transform A.Compose([ A.Resize(256, 256), A.Rotate(limit10, p0.5), A.RandomBrightnessContrast(p0.3), A.HorizontalFlip(p0.5), ])逻辑说明convert(L)确保灰度读取mask 0把任意非零值统一为 1。np.stack([img]*3)是为了适配 ResNet34 预训练权重的三通道输入虽然侧位片是灰度但复制三通道比改第一层卷积更省事。Resize(256, 256)是权衡显存和精度的常用尺寸如果显存够可以上 512。参数上Rotate limit10是经验值再大边界会失真HorizontalFlip p0.5在蝶鞍任务里可以保留因为蝶鞍形态左右差异不显著。3.3 损失函数选择Dice BCE 的组合与权重蝶鞍区域小背景像素占 95% 以上纯 BCE 会让模型倾向于全预测背景。Dice Loss 直接优化重叠度对小目标更敏感。常见做法是Loss BCE Dice权重各 0.5。如果边界精度不够可以把 Dice 权重提到 0.7。下面是一个实现import torch.nn as nn import torch class DiceBCELoss(nn.Module): def __init__(self, dice_weight0.5): super().__init__() self.dice_weight dice_weight self.bce nn.BCEWithLogitsLoss() def forward(self, pred, target): bce_loss self.bce(pred, target) pred_sigmoid torch.sigmoid(pred) intersection (pred_sigmoid * target).sum() dice_loss 1 - (2. * intersection 1e-6) / (pred_sigmoid.sum() target.sum() 1e-6) return (1 - self.dice_weight) * bce_loss self.dice_weight * dice_loss参数说明1e-6是平滑项防止分母为零。dice_weight默认 0.5如果验证集 Dice 震荡大降到 0.3 让 BCE 主导稳定梯度。注意BCEWithLogitsLoss内部带 sigmoid所以 pred 不要提前过 sigmoid。3.4 训练循环与验证指标Dice 和 IoU 的实时监控训练循环里每轮验证后计算 Dice 和 IoU保存最佳权重。下面是一个精简的训练骨架import torch from torch.optim import Adam from torch.optim.lr_scheduler import ReduceLROnPlateau def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss 0 for img, mask in loader: img, mask img.to(device), mask.to(device) optimizer.zero_grad() pred model(img) loss criterion(pred, mask) loss.backward() optimizer.step() total_loss loss.item() return total_loss / len(loader) def validate(model, loader, device): model.eval() dice_sum, iou_sum, count 0, 0, 0 with torch.no_grad(): for img, mask in loader: img, mask img.to(device), mask.to(device) pred torch.sigmoid(model(img)) pred_bin (pred 0.5).float() intersection (pred_bin * mask).sum() dice (2. * intersection 1e-6) / (pred_bin.sum() mask.sum() 1e-6) union pred_bin.sum() mask.sum() - intersection iou (intersection 1e-6) / (union 1e-6) dice_sum dice.item() iou_sum iou.item() count 1 return dice_sum / count, iou_sum / count model ... # U-Net with ResNet34 encoder optimizer Adam(model.parameters(), lr1e-4) scheduler ReduceLROnPlateau(optimizer, modemax, patience5) criterion DiceBCELoss(dice_weight0.5)逻辑说明验证时pred 0.5是常用阈值如果边界偏保守可以降到 0.4。ReduceLROnPlateau监控 Dice5 轮不升就降学习率。初始 lr 1e-4 对 Adam 是安全值如果 loss 不降先检查数据归一化和 mask 对齐。训练轮数一般 50 到 100早停看验证 Dice。4. 蝶鞍分割训练中的避坑与排查清单4.1 现象训练 loss 正常下降但验证 Dice 始终低于 0.5原因通常是图像和 mask 没有对齐。labelme 标注时如果图像被缩放或旋转过JSON 里的imageHeight和imageWidth可能和实际训练图像不一致。解决用脚本检查每对图像和 mask 的尺寸确保Image.open(img).size Image.open(mask).size。另外如果 mask 转换时用了convert(L)但原图是 RGB尺寸对但通道数不同不影响对齐但建议统一转灰度。4.2 现象模型把所有像素预测为背景Dice 为 0这是类别极度不平衡的典型表现。蝶鞍区域可能只占图像的 1% 到 3%BCE 的梯度被背景淹没。解决把损失函数换成 Dice 主导dice_weight提到 0.8或者在数据加载时对包含蝶鞍的 patch 做过采样。另一个原因是学习率太大模型直接塌缩到平凡解把 lr 降到 1e-5 再试。4.3 现象分割边界比标注大一圈或小一圈边界偏移通常和损失函数、阈值有关。Dice Loss 对边界敏感但 BCE 会拉大预测区域。如果预测偏大把验证阈值从 0.5 提到 0.6如果偏小降到 0.4。另外检查 mask 转换时draw.polygon的outline和fill是否都为 1如果只设了outline1而fill0mask 会是空心轮廓模型学到的边界会内缩。4.4 现象训练集 Dice 很高但测试集骤降过拟合。蝶鞍数据集通常只有几百张U-Net 参数量在 20M 以上很容易记住训练集。解决加数据增强旋转、亮度、弹性变形加 Dropout 或 Weight Decay冻结编码器前几层。如果测试集和训练集来自不同设备还要做直方图匹配或 CLAHE 预处理消除设备间的灰度差异。4.5 现象labelme 打开 JSON 后多边形错位labelme 的坐标是相对于原图左上角的像素坐标如果标注时图像被外部工具裁剪过但 JSON 没更新打开就会错位。解决标注前统一把所有图像裁剪到相同尺寸标注后不要再用其他工具修改原图。如果已经错位用imageHeight和imageWidth反推缩放比例批量修正points。5. 蝶鞍形态量化从分割 mask 到临床可用的测量指标分割只是手段蝶鞍形态量化才是目的。拿到二值 mask 后可以计算面积、周长、最大深度、最大直径、圆形度。面积就是 mask 像素数乘以像素物理面积如果知道像素间距。周长用cv2.findContours加cv2.arcLength。最大深度是 mask 垂直方向的最大跨度最大直径是水平方向最大跨度。圆形度4πA/P²反映蝶鞍的规则程度值越接近 1 越圆。下面是一个测量脚本import cv2 import numpy as np def measure_sella(mask_path, pixel_spacing0.1): mask cv2.imread(mask_path, 0) _, binary cv2.threshold(mask, 127, 255, cv2.THRESH_BINARY) contours, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return None cnt max(contours, keycv2.contourArea) area_px cv2.contourArea(cnt) perimeter_px cv2.arcLength(cnt, True) x, y, w, h cv2.boundingRect(cnt) area_mm2 area_px * (pixel_spacing ** 2) perimeter_mm perimeter_px * pixel_spacing depth_mm h * pixel_spacing diameter_mm w * pixel_spacing circularity 4 * np.pi * area_px / (perimeter_px ** 2 1e-6) return { area_mm2: round(area_mm2, 2), perimeter_mm: round(perimeter_mm, 2), depth_mm: round(depth_mm, 2), diameter_mm: round(diameter_mm, 2), circularity: round(circularity, 3) }参数说明pixel_spacing是每个像素对应的物理尺寸头颅侧位片通常有 DICOM 里的PixelSpacing标签转 PNG 时容易丢失需要从原始 DICOM 里抄下来。如果没有可以用已知尺寸的标尺片估算。cv2.RETR_EXTERNAL只取外轮廓避免内部孔洞干扰。circularity对边界噪声敏感建议先对 mask 做一次形态学闭运算再测量。验证分割质量对测量的影响如果 Dice 从 0.9 降到 0.8面积误差可能到 10% 以上深度误差相对小。所以临床研究里报告蝶鞍测量值前先报告分割 Dice 和 IoU。我一般会保留原始 mask 和测量脚本方便复现。另外蝶鞍形态和年龄、性别、种族有关做组间比较时记得校正这些混杂因素。这套流程跑通后你可以把 labelme 标注扩展到鞍背、鞍结节、蝶窦等结构做多类别语义分割数据集的复用价值就上来了。希望帮到你。本文还有配套的精品资源点击获取