蝶鞍X光分割数据集:从LabelMe标注到U-Net语义分割实战

发布时间:2026/10/19 1:50:46
蝶鞍X光分割数据集:从LabelMe标注到U-Net语义分割实战 简介这是一份针对头颅侧位X光片的蝶鞍形态语义分割数据集面向医学影像研究者、AI算法工程师及内分泌疾病辅助诊断相关项目开发。蝶鞍是脑垂体所在区域其形态异常与多种内分泌疾病相关精准分割是后续定量分析的基础。全部图像经由labelme工具手动标注轮廓边界精细可直接用于训练语义分割模型。压缩包内含1558个文件包括779张jpg原始影像与779个json标注文件覆盖不同个体和拍摄条件下的蝶鞍形态整体大小约671MB。数据组织规整json文件完整记录多边形标注坐标便于转换为mask或直接输入主流网络框架。目前已有123人学习下载。获取后可节省大量人工采集和标注成本尤其适合需要快速搭建医学影像分割基准实验或开展蝶鞍形态学研究的团队也可为临床诊断模型迭代提供可靠训练数据。1. 蝶鞍分割一张头颅侧位片里最先暴露内分泌问题的解剖标记在头颅侧位 X 光片上蝶鞍只是一个位于颅底中央、尺寸通常只有 1 到 2 厘米的小凹陷结构但它恰好是垂体的容身之处。临床上蝶鞍的扩大、变形或骨质破坏往往提示垂体腺瘤、空蝶鞍综合征等内分泌相关问题而这些在侧位片上最直观的体现就是蝶鞍形态的改变。问题是X 光片上蝶鞍与周围颅底骨质重叠严重边界并不像教科书插图那么清晰人工勾画耗时不同医生之间的一致性也不稳定。这个数据集的思路很直接把一批头颅侧位片里的蝶鞍区域用开源标注工具 labelme 手动勾出来形成 JSON 格式的标注文件再配合原始 JPEG 图像一起发布。拿到它之后你可以完成三件事第一直接可视化查看标注质量理解蝶鞍在不同个体上的形态差异第二把 JSON 多边形转换成像素级掩膜喂给语义分割模型训练第三在训练好的模型输出上进一步做形态学测量比如蝶鞍面积、前后径和深度用于辅助临床评估。它的适用人群也很明确正在做医学影像语义分割课题的研究生、需要在项目里快速验证小样本分割效果的算法工程师以及想拿真实标注数据练习数据清洗和训练流程的开发者。这个数据集不是一个大而全的临床数据库它更像是一份“带答案的练习册”核心价值在于标注是人工精修的你可以从中学会怎么把一份手工标注转成能训练模型的数据管线。2. 拆开数据集从 JPEG 文件名到 LabelMe 的 JSON 标注结构2.1 数据集里到底有什么一个文件名对应一份标注下载解压之后你会看到一批以四位数字命名的 JPEG 文件。以示例中的 643.jpg、668.jpg、452.jpg 为例命名没有特殊的语义只是采集时的编号。关键是每个 JPEG 文件旁边会存在一个同名的 .json 文件——这就是 labelme 生成的标注文件。也就是说数据集遵循“一张图像对应一个标注文件”的朴素约定不需要额外的映射表。我用一段简单的 Python 代码就能统计文件数量并核对对应关系import os from pathlib import Path data_dir Path(sella_dataset) images sorted(data_dir.glob(*.jpg)) jsons sorted(data_dir.glob(*.json)) # 检查同名文件是否一一对应 missing [img.stem for img in images if not (data_dir / f{img.stem}.json).exists()] print(f图像数量: {len(images)}, JSON数量: {len(jsons)}) print(f缺少标注的图像: {missing if missing else 无})这段代码用 pathlib 收集所有 jpg 和 json 文件然后按文件名主干stem检查一一对应关系。在准备训练数据时我一般会先跑这一步防止后面训练到一半才发现某个样本没有标注。如果发现 missing 列表不为空就要回去重新下载或者核对数据包完整性不要带着缺失样本硬训练。2.2 LabelMe JSON 的结构shapes 数组、points 点集与 imageData打开任意一个 JSON 文件核心结构是三层shapes 数组存放每个标注对象的标签名和轮廓点imageData 字段存放原始图像的 base64 编码imagePath 字段是原始图像的相对路径。在 labelme 交互界面里当你用多边形工具画完蝶鞍外轮廓后shapes 数组里就会多出一个元素label 字段为“蝶鞍”points 字段为按顺序排列的多边形顶点。{ version: 4.5.10, flags: {}, shapes: [ { label: 蝶鞍, points: [ [512, 331], [523, 320], [540, 315], [570, 322], [588, 332], [601, 350], [615, 372] ], group_id: null, shape_type: polygon, flags: {} } ], imagePath: 643.jpg, imageData: /9j/4AAQSkZJRgABAQAAAQ... }注意 points 里的坐标单位是像素并且是基于原始图像尺寸的绝对坐标不是归一化坐标。如果后续对图像做了缩放或裁剪坐标必须同步换算否则就会出现后面第 5 章里说的“标签错位”问题。另外不同版本的 labelme 生成的 JSON 在字段细节上略有差异比如有些老版本没有 group_id 字段甚至 imageData 可能为空字符串——处理代码只要兼容缺失字段即可不用强求版本完全一致。2.3 快速可视化把标注轮廓画回原图拿到 JSON 之后的第一个动作我建议是先画图看看标注质量。标注是人工做的边缘是否贴合蝶鞍边界、有没有漏标、有没有把周围结构误标进去都要靠肉眼确认。import json import numpy as np import cv2 def draw_label(json_path, img_dir): with open(json_path, r, encodingutf-8) as f: data json.load(f) # 用 JSON 里的 imagePath 定位原始图像 img_path f{img_dir}/{data[imagePath]} img cv2.imread(img_path) for shape in data[shapes]: if shape[shape_type] polygon: pts np.array(shape[points], dtypenp.int32).reshape((-1, 1, 2)) cv2.polylines(img, [pts], isClosedTrue, color(0, 255, 0), thickness2) return img result draw_label(sella_dataset/643.json, sella_dataset) cv2.imwrite(check_643.jpg, result)这段代码把 JSON 里所有 polygon 轮廓画到原图上输出一张可视化的对照图。cv2.polylines 的 isClosed 参数设为 True 会把首尾点连起来因为 labelme 的 polygon 点集默认不重复首尾点这一步很容易漏。颜色选了绿色是因为在灰度 X 光片背景上绿色对比度最好。批量查验的时候我一般会把所有图像合并成一个网格图一次看 9 张或者生成一个 HTML 页面逐张切换。与其在训练之后发现效果奇怪再回头怀疑标注不如在拿到数据的第一天就把每张图的标注质量过一遍——这一步花掉的半小时后面能帮你省下两天的排查时间。3. 把 JSON 多边形转成训练掩膜转换脚本和四个必须确认的参数3.1 为什么不能直接拿 JSON 喂给模型语义分割模型期望的输入是图像矩阵加像素级 ground truth 掩膜也就是和原图尺寸一致的二维数组每个像素取值 0 或 1。而 labelme 的 JSON 是矢量多边形模型读不了这种格式。更直白的说法训练时计算损失函数需要逐像素比较预测值和真值JSON 里的点集必须先栅格化成掩膜才能参与损失计算。这个转换过程在数据准备阶段只做一次但它决定了后续所有训练和评估的下限。如果掩膜转换得不对后面模型练得再好也都是白费因为 ground truth 本身就是错的。3.2 转换脚本从 polygon 到单通道 mask下面是我常用的一段转换代码核心是 cv2.fillPolyimport json import numpy as np import cv2 from pathlib import Path def labelme_json_to_mask(json_path, img_path, out_path, label_name蝶鞍): # 1. 读取原图确定掩膜尺寸 img cv2.imread(str(img_path)) if img is None: raise ValueError(f无法读取图像: {img_path}) h, w img.shape[:2] # 2. 初始化全零单通道掩膜 mask np.zeros((h, w), dtypenp.uint8) # 3. 遍历所有标注对象只保留目标标签 with open(json_path, r, encodingutf-8) as f: data json.load(f) for shape in data[shapes]: if shape[label] label_name and shape[shape_type] polygon: pts np.array(shape[points], dtypenp.int32).reshape((-1, 1, 2)) cv2.fillPoly(mask, [pts], 1) # 前景赋值为 1 # 4. 保存成单通道 PNG 掩膜 cv2.imwrite(str(out_path), mask) return maskfillPoly 的作用是把多边形内部所有像素填充为指定值。这里把前景设为 1、背景保持 0而不是用 0/255因为后续训练时标签通常是类别索引Dice 损失里也习惯直接用 0/1 值。如果你用的是某些可视化库才需要临时换成 0/255 再显示。这段代码里有四个参数值得逐个确认一是 label_name必须和 JSON 里的 label 字段完全一致包括空格和中文标点差一个字符这个样本就会被静默跳过二是 polygon 点集的坐标 dtype必须转成 int32否则 opencv 会直接报类型错误三是 reshape 的格式cv2 的函数要求点集维度是 (-1, 1, 2)写成 (-1, 2) 也能跑但遇到多轮廓时会出问题四是输出掩膜的尺寸一定要以原图读出来的高宽为准不能拿 JSON 里 imageWidth 和 imageHeight 字段去猜因为不同版本的 labelme 对这两个字段的写入并不一致有的甚至根本不写。3.3 类别编码与文件命名规范如果数据集中只有蝶鞍一个前景类掩膜取值就是 0 和 1训练时不需要做额外编码。但如果后续你往数据集里加了其他解剖结构比如蝶窦或颅底就需要把类别数扩展到 0、1、2。常见做法是每种类别用单独数值填充最后在训练代码里做 one-hot 或者直接用带 class weight 的交叉熵这一点要在数据准备阶段就想好否则后面改标注重新生成掩膜的成本不低。文件命名上我习惯把掩膜和原图放在两个目录里但保持同名sella_dataset/ ├── images/ # 原始 JPEG │ ├── 643.jpg │ └── ... └── masks/ # 转换后的 PNG 掩膜 ├── 643.png └── ...这样命名的好处是训练代码里只需要替换目录前缀就可以加载对应的一对文件不需要维护额外的文件名映射表。PNG 是无损格式掩膜用 PNG 保存可以避免 JPEG 压缩在边界处引入的伪影——这块属于细节但会直接影响最终分割的边界精度。3.4 数据集划分训练/验证/测试这个数据集的样本量不大划分策略必须谨慎。常见做法是按 7:2:1 或者 6:2:2 的比例随机划分但要注意医学影像数据常常来自不同个体如果存在同一患者多张相邻切片的情况随机划分会把同一患者的图像同时分到训练和验证集导致验证指标虚高。所以划分的粒度应该是“样本个体”而不是“单张图像”。为了在小样本下尽量稳定我一般会多划分几组固定 split比如 5 个不同随机种子训练 5 次取平均值报告指标。单独一次划分的运气成分太大了特别是样本总量只有几十张的时候某一张图像划到训练集还是验证集对结果的影响比调参大得多。4. 训练蝶鞍语义分割模型选 U-Net 还是 DeepLabV3参数怎么设4.1 模型选型小样本场景下优先考虑浅层 U-Net样本量只有几十张到上百张的情况下直接上大规模预训练模型很容易过拟合。U-Net 这类编码器-解码器结构天然适合医学影像小目标分割编码器逐层下采样提取特征解码器通过跳跃连接恢复空间细节对边界模糊、对比度低的解剖结构表现稳定。如果想用更现代的 DeepLabV3 或者 HRNet也可行但要注意两件事预训练权重从哪里加载、输入分辨率是否匹配。模型参数量小样本表现适合场景浅层 U-Net小稳定不易过拟合单目标、几十样本DeepLabV3大需预训练权重多类别、语义丰富HRNet大需更强数据增强边缘要求高我的建议是先用一个通道数减半的浅层 U-Net把整个训练流程跑通拿到基准指标再考虑换复杂模型。不要一上来就同时调模型结构、损失函数、数据增强三个变量那样翻车了根本定位不到原因。先把一个简单模型跑出稳定指标再逐步换复杂方案。4.2 数据增强旋转、缩放、对比度调整的边界设置数据增强是这个小数据集能训练出可用模型的关键因素。医学影像的增强不能无脑套自然图像的增强方式。旋转范围建议控制在 ±15 度以内因为蝶鞍在侧位片上的朝向相对固定缩放 0.9 到 1.1水平翻转不建议开因为头颅侧位片有左右解剖结构的固定朝向翻转之后解剖位置关系就乱了竖直翻转更不能开那会彻底破坏颅底和颅顶的空间关系。还有一类对 X 光片特别有效的增强是亮度对比度调整。不同设备、不同曝光条件下拍摄的侧位片灰度分布差异很大模拟这种差异能让模型泛化性提升不少。常见实现是随机对图像做亮度偏移和对比度缩放幅度控制在 20% 以内。增强幅度过大会生成解剖结构不自然的样本反而把模型带偏。4.3 训练参数学习率、批大小、损失函数与 epoch基于我的经验小数据集训练的参数设置可以按下面这个配置起步参数建议取值说明输入尺寸512x512原图过大可以先降采样batch size4显卡显存不够就降到 2学习率1e-4Adam 优化器的常用起步值损失函数Dice Loss BCE兼顾区域重叠和像素级精度epoch200配合早停观察验证集验证频率每 5 个 epoch小数据集一次验证很快关于损失函数多说一句纯 Dice Loss 在背景占比极高时容易梯度不稳定纯交叉熵又会被背景像素淹没。常见实践中把 Dice Loss 和 BCE 按 1:1 相加能兼顾两者。Dice Loss 负责优化区域重叠程度BCE 负责稳定梯度信号。起步阶段用相加版本最省心后面如果发现预测边界偏模糊再把 Dice 的权重调高也不迟。学习率的设置值得单独强调。小样本训练时学习率太高很容易在头几个 epoch 就把模型参数推出合理区间后面无论如何降低都拉不回来。我一般从 1e-4 起步观察前 20 个 epoch 的 loss 曲线如果下降太慢再上调到 3e-4如果出现震荡就降到 3e-5。这个数据集上1e-4 配合 Adam 基本是安全选项。4.4 训练过程中的监控指标Dice 与 IoU 怎么看训练时每轮记录 train loss、val loss、Dice 和 IoU 四个指标。Dice 对预测区域和真实区域的重叠更敏感IoU 对误检更敏感两者结合看。val Dice 在 0.8 以上说明模型已基本学到蝶鞍的位置和大致边界val IoU 与 val Dice 差超过 0.1说明边界区域的误分割比较明显预测区域可能比真实区域大一圈或者存在少量碎块状假阳性。此时优先检查数据增强中的缩放范围和后处理中的最小连通域阈值而不是盲目增加训练轮数。还有个容易忽视的点验证集的 Dice 波动。小数据集上单次验证的 Dice 波动很大某一次验证集刚好分到几张边界清晰的图指标就会虚高。所以每 5 个 epoch 验一次的频率比每 1 个 epoch 验一次更合理因为可以看趋势而不是看单点。保存模型时以 val Dice 最高的 epoch 为准而不是最后一个 epoch。5. 避坑与常见问题小样本标注数据集的四个典型翻车现场5.1 标签错位图像尺寸变了polygon 坐标没跟着变现象把掩膜画回原图发现轮廓整体偏移了几个像素或者训练时 loss 居高不下验证 Dice 始终在 0.5 以下。原因labelme 在标注时基于原始高清图后来预处理阶段做了 resize但 polygon 坐标还是原始像素值没有按比例换算。最常见的是把图像从 2000 像素宽等比缩到 512 像素宽但读取 JSON 里的 points 后直接用坐标和图像对不上。解决读取图像后先记录宽高再做任何缩放操作前同步用同比例缩放坐标。最稳妥的做法是先统一所有图像的尺寸再重新生成掩膜不要在训练代码里做“边改尺寸边用旧坐标”的事情。比如原始图 2000x1500缩放后是 512x384那么所有 points 的 x 坐标乘以 512/2000y 坐标乘以 384/1500再转 int32。5.2 类别失衡背景占比过高导致模型学成“全黑”现象训练几个 epoch 之后 val dice 直接降到接近 0预测结果全为背景掩膜上几乎没有前景像素。原因蝶鞍在整张侧位片里占比极小背景像素数远超前景可能达到 100 比 1 甚至更高。模型发现全部预测为背景也能得到极低 loss——如果不使用 Dice Loss纯交叉熵会被背景像素的梯度淹没。解决损失函数用 Dice Loss并配合数据增强。另外可以裁剪出 ROI 再做分割比如固定裁剪图像中心区域把蝶鞍占比提升到 5% 以上这样模型更容易学到正类特征。裁剪时注意不要切掉蝶鞍本身可以先看一下所有标注的中心点分布以统计质心为中心做裁剪。5.3 过拟合验证集 Dice 高、测试集却很差现象训练时 val dice 达到 0.95但换一批新图像测试效果明显变差甚至出现整张图漏检。原因样本量太小模型实际上“背”下了训练图像的纹理特征而不是学会理解“蝶鞍是一个位于特定位置、形态像船的骨质凹陷”这个语义概念。这不是数据集的问题而是任何小样本训练的共性挑战。解决一方面增加数据增强的强度另一方面使用更强的正则化比如 weight decay 调大到 1e-4、Dropout 加到解码器层。还可以用迁移学习初始化时加载在大型 X 光数据集上预训练的编码器权重效果比随机初始化明显更好。最后就是调整预期小样本训练出来的模型指标浮动会比较大单次测试结果不能说明全部问题。5.4 标注文件缺失或损坏训练跑到一半突然报错现象训练到某个 epoch突然报 KeyError 或 json.decoder.JSONDecodeError程序中断而且要花不少时间排查是哪个文件出了问题。原因有些标注文件是人工用低版本 labelme 生成的字段不完全或者文件在拷贝过程中损坏。也有时候是某些 JSON 里 imageData 为空导致后续读取图像时路径解析失败。解决训练前加一个数据完整性校验。写一个循环脚本遍历所有 json 文件逐个尝试 json.load并检查必需的 key 和 shapes 数量是否大于 0。凡是缺失或损坏的文件直接剔除并打印清单。import json from pathlib import Path data_dir Path(sella_dataset) bad_files [] for json_path in data_dir.glob(*.json): try: with open(json_path, r, encodingutf-8) as f: data json.load(f) if shapes not in data or len(data[shapes]) 0: bad_files.append((json_path.name, shapes 为空)) except Exception as e: bad_files.append((json_path.name, str(e))) for name, reason in bad_files: print(f{name}: {reason})这个脚本在几分钟内就能把所有标注文件过一遍跑完之后你就知道哪些样本该被剔除。把这一步加进训练前的数据准备流程从我经手的项目经验看能拦下不少半夜精神状态管理问题。6. 从掩膜到形态学指标让分割结果真正能用起来6.1 从掩膜计算蝶鞍面积、前后径和深度模型输出的二值掩膜不是终点。拿到掩膜后可以提取临床上有意义的形态学参数。蝶鞍的面积是掩膜上非零像素数乘以单像素空间分辨率前后径是掩膜在水平方向上的最大投影长度深度通常是掩膜在垂直方向的最大高度但更准确的测量是在标准侧位片上取特定解剖标志点之间的距离。面积的计算方式很直接import numpy as np def sella_metrics(mask, pixel_spacing_mm0.1): # 掩膜中非零像素个数乘以单像素面积(mm^2) area np.sum(mask 0) * (pixel_spacing_mm ** 2) # 前后径水平方向最大宽度 fwd_back np.any(mask 0, axis0).sum() * pixel_spacing_mm # 深度垂直方向最大高度 depth np.any(mask 0, axis1).sum() * pixel_spacing_mm return area, fwd_back, depth需要注意 pixel_spacing 是物理像素间距毫米单位这个值通常从 DICOM 头文件里读取如果只有 JPEG 没有 DICOM 就得估算或由临床研究者提供。不同设备拍摄的片子pixel_spacing 不同直接比较像素面积没有意义。6.2 后处理去掉孤立噪点与边缘平滑模型输出的掩膜通常带有零星噪点直接提取轮廓做测量会受到干扰。一种简单有效的处理方式是去掉面积过小的连通域再用形态学闭运算填平边界上的小凹槽。import cv2 import numpy as np def clean_mask(mask, min_area50): # 找到所有连通域去掉面积小于阈值的 cnts, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cleaned np.zeros_like(mask) for c in cnts: area cv2.contourArea(c) if area min_area: cv2.drawContours(cleaned, [c], -1, 1, thickness-1) # 用 3x3 核做一次闭运算填平边缘凹槽 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) cleaned cv2.morphologyEx(cleaned, cv2.MORPH_CLOSE, kernel) return cleaned后处理的核心是一遍连通域过滤加一遍闭运算。min_area 的取值取决于图像分辨率512x512 输入下取 50 个像素比较合适如果是原图尺寸就要相应调大。这个步骤对最终测量指标的稳定性的影响很大至少能降低不少因为模型预测碎块导致的数值波动。6.3 与标注对比做一致性校验模型训练结束后用干净数据分隔出一批验证图像逐个计算 Dice 和中心点坐标偏差输出一份质量报告。当出现单张 Dice 极低时把那几张单独找出来与标注图对比检查是数据本身标注质量参差还是模型在特定形态的蝶鞍上失手。这个一致性校验脚本虽然简单但很有价值。它会强制你把每张样本的输出打开看一遍而不是只看一个漂亮的平均指标。平均 Dice 0.9 的背后可能藏着 5 张完全漏检的图像这种分布差异在临床辅助场景下是致命的。自从在这个数据集上踩过一次“平均指标达标、单张样本全错”的坑之后我每次训练完都会强制自己过一遍逐样本指标报告先看每个样本的 dice 排序再重点检查排名靠后的 5 张。这个习惯后来也帮我养成了对所有医学影像分割项目的标准验收方式——指标均值只是下限边界失败率才决定模型能不能落地。希望这份拆解能让你拿到数据后少走几段弯路。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询