尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

医学图像分割实践:CTSpine1K三切面脊椎分割与U-Net适配

发布时间:2026/9/23 21:54:46

资讯中心
01
ARTICLE

医学图像分割实践:CTSpine1K三切面脊椎分割与U-Net适配

医学图像分割实践:CTSpine1K三切面脊椎分割与U-Net适配
简介面向医学图像分割研究与算法训练的骶骨脊椎CT分割数据集源于CTSpine1K覆盖轴位面、冠状面、矢状面三个切面含5类语义标签。切片阶段剔除了ROI占比不足3%的低信息量图像并使用医学影像常用窗宽窗位做对比度增强统一缩放为512×512原图以jpg保存标签为8bit灰度png背景0、前景非0附classes.txt对照类别。压缩包约472.47MB共2000个文件含1998个png图像与掩膜、1个txt类别说明、1个Python可视化脚本show.py打开即可直接查看分割掩膜效果。三个切面分别提供2113、2272、6404组配对样本总计超一万张便于多角度训练与评估。已有224人学习下载适合作为脊椎分割、医学图像预处理及模型性能验证的基准数据。1. 医学图像分割数据集骶骨腰痛脊椎分割先把三个切面玩明白搞医学图像分割的人拿到一个数据集第一反应往往是先看标签长什么样而不是急着跑模型。这份骶骨腰痛脊椎分割数据集核心是CTSpine1K的2D切片版本从轴位面横断面、冠状面、矢状面三个方向切出2D图像共5个类别标签是8bit灰度png非0为前景、0为背景。它的价值在于把3D CT的脊椎分割问题拆成了三个独立的2D任务方便直接用U-Net、DeepLabV3这类成熟模型去训练也适合做三切面融合的对比实验。适合的人群很明确正在做医学图像分割、想找带标签的脊椎数据练手、或者需要做2D分割基线实验的从业者。我拿到手先做的事不是训练而是把三个切面的数据规模、类别分布、标签合法性全部摸了一遍这一遍下来后面跑模型基本没走过弯路。2. 解剖CTSpine1K三个切面的划分逻辑与五类别标签体系2.1 x、y、z轴切面数据规模与划分逻辑项目正文里写得很清楚x轴2113对图像和masky轴2272对z轴6404对。这里先解释一个容易混淆的点——x、y、z轴并不直接对应解剖学上的轴位面、冠状面、矢状面而是对应CT体数据在三个正交方向上的切片索引。常见做法是把体数据按三个方向各切一遍然后以切片所在轴命名。实践中z轴通常是扫描的层方向切片数最多所以z轴样本量最大是符合预期的。切片时项目做了两个关键处理一是去掉了ROI区域不足3%的切片这个比例很讲究。如果保留全零背景切片模型会学到直接输出背景这种偷懒解尤其是对小目标分割任务背景占比过高的训练集会让损失函数被背景类主导。二是统一重采样到512×512。原始CT的层内分辨率通常在0.5~1.0mm但层间距可能到1~2.5mm直接拿原始分辨率训练会导致各向异性问题。缩放到512×512成了2D分割的标准输入尺寸也方便和torchvision、MMSegmentation里的预训练骨干网络对接。文件命名也能看出切片来源像liver_33_260.png这种命名前面是脏器名中间是层号最后一个是切片索引。虽然这份数据主体是脊椎分割但这些文件名说明原始数据来自更广泛的腹部CT数据集处理时没有做脏器过滤只是按ROI占比筛选。你拿到手后不要被文件名里的liver干扰直接按目录结构用就行。2.2 五类别标签与classes.txt的映射关系mask是8bit灰度pnguint8格式0是背景非0是前景。具体类别要看classes.txt这是整个数据集里最不能跳过的文件。常见做法是先读classes.txt把类别名和像素值建立映射再决定训练时是单通道多类还是one-hot编码。这里有个容易踩的坑8bit灰度png的像素值范围是0~255但类别数只有5个意味着像素值不一定是连续的1~5可能有跳号。打开png检查一下实际像素值分布这是拿到任何分割数据集后的第一件事。如果某个类别在某个切面里完全没有出现而你没检查就直接训练多分类交叉熵会报类别索引越界或者模型永远学不会那个类别。另一个坑是png的调色板模式有些工具保存的png带调色板直接cv2.imread读出来是单通道索引图没问题但如果用PIL打开且转RGB就会把调色板颜色当成真实像素导致类别信息完全错乱。所以统一用cv2.imread(..., cv2.IMREAD_GRAYSCALE)读取最稳妥。2.3 windowing预处理为什么对比度增强非做不可项目里提到做了医学影像常用的windowing对比度增强。CT值的原始单位是HUHounsfield Unit范围从-1000到3000以上但骨骼、软组织的CT值区间很窄。如果不做windowing直接归一化大部分像素值会挤在很窄的灰度范围内对比度极低模型很难学到边缘特征。windowing的本质是做一个线性映射把指定窗宽WW和窗位WL范围内的CT值映射到0~255范围外的截断。对于脊椎分割骨骼的CT值一般在200~1000HU附近窗位设400左右、窗宽设1000~1500是常见做法。这份数据集已经做过windowing并重缩放成512×512所以你在训练时不需要再做一次但要知道这个前提——数据已经是处理好的状态。如果你拿其他CT数据做迁移学习必须先做同样的windowing否则分布差异会让模型效果断崖式下跌。提示训练集和验证集必须使用完全相同的windowing参数。如果一张图窗宽窗位不同灰度分布就不同模型在验证集上的表现会失真。3. 从零跑通标签可视化show.py的使用与参数说明3.1 先确认目录结构与数据格式拿到压缩包后第一件事不是直接跑show.py而是先看目录结构。标准结构应该是这样的CTSpine1K_2D/ ├── x_axis/ │ ├── images/ # 2113张jpg │ └── masks/ # 2113张png ├── y_axis/ │ ├── images/ # 2272张jpg │ └── masks/ # 2272张png ├── z_axis/ │ ├── images/ # 6404张jpg │ └── masks/ # 6404张png ├── classes.txt └── show.py这段目录结构是这份资源落地的骨架。注意images里是jpg、masks里是png这种混合格式是刻意的——jpg压缩率高适合存自然图像png无损适合存标签。你后面写数据加载器时要分别处理两种格式不要统一用cv2.imread后强制转格式。检查完目录后用一行命令验证图片和mask数量是否一一对应for d in x_axis y_axis z_axis; do echo $d: $(ls $d/images | wc -l) images, $(ls $d/masks | wc -l) masks; done这段bash命令用循环遍历三个轴目录分别统计images和masks下的文件数。ls | wc -l统计文件数量如果两边数字不一致说明数据不完整后面训练时DataLoader会报索引越界。我在实际项目里发现过两次masks比images多的情况都是因为有人手动删了部分坏图但没同步删对应的mask所以这一步别跳过。3.2 运行show.py的正确参数show.py的作用是把原图和mask叠加显示方便你肉眼确认分割标注质量。直接跑python show.py之前先打开脚本看一眼默认路径参数。常见做法是脚本支持命令行参数传入数据根目录和切面类型python show.py --data_root ./CTSpine1K_2D --axis z --index 100--data_root指定数据集根目录--axis选择切面方向x/y/z--index指定显示第几张图。脚本逻辑一般是读images/index.jpg和masks/index.png然后把mask用半透明红色叠加到原图上。如果脚本里没有做半透明叠加而是直接并排显示也没关系只是观感问题。跑通之后我一般会改一下脚本把叠加显示的mask改成按类别染不同颜色这样能一眼看出五个类别的边界是否贴合解剖结构。具体做法是用cv2.applyColorMap给每个像素值映射一个颜色再和原图加权融合。这一步不是必须的但对检查粗标注特别有效。3.3 批量导出对比图不逐个翻图一张张看太慢我通常会把show.py改造成批量导出模式把前50张图的对比结果拼成一张大图import cv2 import numpy as np import glob, os def blend_and_export(img_path, mask_path, out_path): img cv2.imread(img_path, cv2.IMREAD_COLOR) mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) color_mask cv2.applyColorMap((mask * 40).astype(np.uint8), cv2.COLORMAP_JET) blended cv2.addWeighted(img, 0.7, color_mask, 0.3, 0) cv2.imwrite(out_path, blended) images sorted(glob.glob(./z_axis/images/*.jpg))[:50] for idx, img_path in enumerate(images): name os.path.basename(img_path).replace(.jpg, .png) mask_path f./z_axis/masks/{name} blend_and_export(img_path, mask_path, f./vis_{idx:03d}.png)这段脚本的核心逻辑分三步先用cv2.imread分别读入原图和灰度mask然后用applyColorMap把mask的像素值映射成彩色最后用addWeighted把彩色mask按0.3权重叠加到原图上。这里的mask * 40是为了把0~5的类别值放大到0~200的可见范围因为直接拿像素值1和2去映射颜色在JET色表上几乎分不出来。批量导出后快速扫一遍缩略图就能判断标注质量。如果某个mask和原图的脊椎轮廓明显错位那张图就可以从训练集里剔除。4. 把数据集喂进分割模型适配U-Net与YOLOv8的完整流程4.1 标签合法性校验先剔除垃圾样本再训练医学数据集的标注质量参差不齐尤其是多类别分割经常出现mask是纯黑全背景但原图有清晰脊椎的情况这是因为切片时ROI不足3%被剔除了但个别文件可能漏处理。训练前必须做一次标签合法性校验剔除三类问题样本全零mask、类别数超过预期、图像尺寸不等于512×512。import cv2 import numpy as np import os valid_classes {0, 1, 2, 3, 4, 5} # 0是背景1~5是5个类别 removed [] for axis in [x_axis, y_axis, z_axis]: img_dir f./{axis}/images mask_dir f./{axis}/masks for mask_name in os.listdir(mask_dir): mask_path os.path.join(mask_dir, mask_name) mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) if mask is None: removed.append((mask_path, unreadable)) continue unique_vals np.unique(mask) unexpected set(unique_vals) - valid_classes if unexpected: removed.append((mask_path, funexpected_values:{unexpected})) elif mask.shape ! (512, 512): removed.append((mask_path, fbad_shape:{mask.shape})) elif mask.sum() 0: removed.append((mask_path, empty_mask)) print(f共发现 {len(removed)} 个问题样本:) for path, reason in removed[:20]: print(f {path}: {reason})这段校验脚本先遍历三种切面的mask目录对每张mask读取灰度图然后依次检查三个条件能否正常解码、像素值是否在合法类别集合内、尺寸是否为512×512。发现问题就把路径和原因记录到removed列表。mask.sum() 0是判断全零mask的快捷方式因为背景像素值为0全零mask的和就是0。运行完看输出如果问题样本超过总数的1%就要回到数据源头检查预处理流程是否有bug。还有一个容易被忽略的点jpg是压缩格式对同一张图反复保存会累积压缩伪影掩膜如果也存成jpg就彻底废了。这份数据images用jpg没问题但你自己做数据增强时增强后的图要用png或npy保存尤其是mask任何有损压缩都会让类别边缘出现噪点。4.2 按切面划分训练集与验证集不要随机打乱三个切面的数据特征差异极大z轴样本是x轴的3倍如果全部混在一起随机划分模型会倾向学好样本量大的z轴切面x轴和y轴的效果就会变差。更合理的做法是按切面分别划分每个切面内部用相同比例切分import os import random from collections import defaultdict random.seed(42) train_ratio 0.8 splits {train: [], val: []} for axis in [x_axis, y_axis, z_axis]: img_dir f./{axis}/images names sorted(os.listdir(img_dir)) n_val int(len(names) * (1 - train_ratio)) random.shuffle(names) val_names set(names[:n_val]) for name in names: entry f{axis}/{name} if name in val_names: splits[val].append(entry) else: splits[train].append(entry) print(ftrain: {len(splits[train])}, val: {len(splits[val])})这段代码按切面分组后在每个组内以相同比例随机采样验证集而不是把所有文件混在一起打乱。random.seed(42)保证每次运行划分结果一致方便复现实验。输出文件列表后我会把splits保存成json或txt训练时直接读这个文件列表而不是每次运行时重新划分。这样还有一个好处后面做三切面对比实验时每个模型的训练集和验证集完全一致消融实验的差异只能来自模型本身。4.3 U-Net输入的标准化配置与常见报错2D U-Net的输入通常是N×C×H×W的tensorC在灰度图场景下是1但很多人直接用cv2.imread读图默认得到三通道BGR导致通道数对不上。正确做法是读灰度图后扩展维度import cv2 import torch from torch.utils.data import Dataset class SpineDataset(Dataset): def __init__(self, file_list, img_dir./, size512): self.items file_list self.img_dir img_dir self.size size def __getitem__(self, idx): rel_path self.items[idx] axis rel_path.split(/)[0] name rel_path.split(/)[-1] img_path f{self.img_dir}/{axis}/images/{name} mask_path f{self.img_dir}/{axis}/masks/{name.replace(.jpg, .png)} img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) img img.astype(np.float32) / 255.0 mask mask.astype(np.int64) img_tensor torch.from_numpy(img).unsqueeze(0) mask_tensor torch.from_numpy(mask) return img_tensor, mask_tensor def __len__(self): return len(self.items)这里有两个关键设计一是unsqueeze(0)把H×W的灰度图变成1×H×W匹配U-Net的输入格式二是mask转成int64而不是float32因为PyTorch里的交叉熵损失要求target是LongTensor。读取时用的通道数和格式一定要在训练前打印出来确认我用这招排查过好几次损失不下降的问题最后发现是mask里出现了类别值6而模型输出通道数只有5。如果是YOLOv8做分割还需要把png转成YOLO的txt格式每个类别一个分割多边形。这一步可以用OpenCV的findContours提取每个类别的轮廓点再归一化坐标pip install ultralytics opencv-python安装YOLOv8和OpenCV环境后用脚本读取png对每个非零类别提取轮廓并保存为txt。注意YOLO分割格式和检测格式的坐标归一化方式相同轮廓点坐标要除以图像宽高。脊椎的mask边缘比较复杂一个类别可能出现多个连通域每个连通域都要单独写一行。5. 避坑指南CTSpine1K最常翻车的六个问题与排查方法5.1 现象mask读出来是全黑或者颜色异常原因用PIL的Image.open().convert(RGB)读取8bit灰度png把灰度值当成了调色板索引或者用cv2.imread默认的IMREAD_COLOR模式读取mask被强行转成三通道。解决统一用cv2.imread(path, cv2.IMREAD_GRAYSCALE)读取并在读取后检查np.unique(mask)的像素值是否在0~5范围内。这个坑在Windows和Linux上的表现还不一样Windows下imread对路径中的中文敏感建议数据集路径全用英文。5.2 现象训练时损失函数报错index out of range原因classes.txt里定义了类别1~5但实际某个mask里出现了像素值255或者类别跳号。常见来源是标注工具导出的png自动填充了255作为ignore区域。解决在数据加载器里做像素值重映射把不合法像素全部置0并记录日志。代码逻辑是mask[~np.isin(mask, valid_classes)] 0这样即使有脏数据也不会让训练崩掉但要把出现次数打印出来因为它说明标注过程有系统性问题。5.3 现象验证集mIoU很高但实际预测效果一塌糊涂原因三个切面的类别分布不一样如果把三组数据混合后随机划分验证集里很可能和训练集来自同一个病例的相邻切片信息泄漏leakage导致评估虚高。解决划分数据集时按切面分层并且在论文或实验记录里注明验证集构成。我在z轴数据上遇到过mIoU从0.71掉到0.53的情况就是因为我后来意识到验证集和训练集切片相邻几乎等于让模型做记忆填空。5.4 现象模型预测结果有大量椒盐噪点原因训练时用了默认的交叉熵损失没有考虑脊椎边缘像素在整张图里占比极低模型为了降低损失会选择忽略边缘细节。解决换用Dice Loss或Focal Loss。Dice Loss对小目标更友好但训练初期容易振荡常见做法是前20个epoch用交叉熵之后切换Dice Loss或者两者加权求和。5.5 现象图像增强后mask和原图错位原因对原图做了随机旋转、裁剪但没有对mask做完全相同的变换。解决用albumentations库的Compose统一处理它保证对image和mask施加同一组变换参数import albumentations as A transform A.Compose([ A.RandomRotate90(p0.5), A.HorizontalFlip(p0.5), A.ShiftScaleRotate(shift_limit0.1, scale_limit0.1, rotate_limit15, p0.5), ]) augmented transform(imageimg, maskmask) img_aug augmented[image] mask_aug augmented[mask]albumentations的设计就是传字典同时返回增强后的图像和mask它的RandomRotate90、ShiftScaleRotate内部会同步处理像素坐标不会出现错位。如果自己手写transform记住所有几何变换都要用同一个随机种子。5.6 现象训练时显存爆满却不知道是哪一步的问题原因512×512输入在batch_size16时单卡8G显存肯定是不够的。解决先确认模型输入是否有意外的高分辨率再看DataLoader的num_workers是不是开太多导致CPU瓶颈然后逐步缩小batch_size从16到8到4。还有一个隐蔽问题很多人的模型把mask当one-hot处理输出5个通道的softmax结果每个通道都存一份float32的feature map显存占用会比预期多5倍。改用logits直接算loss可以省这一块。6. 进阶验证用类别比例与切片连续性反向检验数据质量这组数据的三切面结构给了你一个普通2D分割数据集没有的验证维度同一病例在z轴上相邻切片应该有相似的解剖结构类别面积占比应该平滑变化。如果某个切片突然出现大面积类别消失那大概率是标注遗漏而不是解剖变异。我写过一个小脚本统计每个切片的类别像素占比然后按切片索引排序画曲线用来快速定位标注异常的区域比你一张张翻图快得多。统计类别占比的代码很短import cv2 import numpy as np import os for mask_name in sorted(os.listdir(./z_axis/masks))[:200]: mask cv2.imread(f./z_axis/masks/{mask_name}, cv2.IMREAD_GRAYSCALE) total mask.size ratios [] for c in range(1, 6): ratios.append((mask c).sum() / total) idx mask_name.replace(.png, ).split(_)[-1] print(f{idx}: , .join([f{r:.4f} for r in ratios]))这段脚本逐张统计每个类别的像素占比输出数值序列。正常数据里相邻切片的类别占比是连续变化的如果出现占比从0.2突然跳到0.0再跳回来那个位置就有问题。这比只看mIoU有意义得多它告诉你的是标注的一致性。类别不平衡一定会存在。骶骨、腰椎在不同切面的出现频率差别很大x轴切面上某些椎体类别可能只占不到1%的像素。如果你不做处理模型就会在测试集上表现出大类别mIoU高、小类别mIoU极低的典型症状。解决方式是给Dice Loss加类别权重权重设为1 - 类别频率让稀有类别的梯度贡献变大。我跑下来加权重后小类别的Dice能从0.23提高到0.41尽管整体mIoU只有小幅上涨但解剖结构的完整性明显变好。从那以后我每次拿到医学分割数据集都强制自己先做三件事验证标签合法性、按解剖结构分层划分、统计类别频率曲线。这三步做完后面模型表现出的问题基本都能定位到模型本身而不是数据。希望这份CTSpine1K三切面数据集的使用经验能帮到你少走我走过的弯路。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。