news 2026/10/11 20:33:13

颈椎CT骨骼分割数据集实战:三轴2D切片与可视化代码解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
颈椎CT骨骼分割数据集实战:三轴2D切片与可视化代码解析

简介:本资源面向医学图像分割方向的算法工程师、研究生及深度学习爱好者,提供一套完整的人体颈椎CT骨骼分割数据集,可用于训练与验证2D分割模型,也适合作为医学影像入门练手项目。数据按横断面、冠状面、矢状面三个方向切分:x轴为512×512分辨率共3734张图像及对应mask,y轴为512×196分辨率共4252张,z轴为512×196分辨率共2426张,mask灰度值取0、1、2,图像与标签均为png格式,并附show.py可视化脚本便于快速查看标注效果。压缩包为7z格式,共约2000个文件,其中1998个png图像与掩膜、1个txt说明、1个py脚本,整体约348.63MB,目录按切面方向组织清晰。目前已有212人学习下载,配套博文还整理了医学图像分割与分割任务的系列参考内容,便于读者对照理解数据组织方式与训练思路。

1. 颈椎CT骨骼分割数据集:三轴2D切片与可视化代码,拿到手怎么跑

做医学图像分割的同行大概都有过这种体验:算法论文里的 Dice 刷到 0.9,一换到自己的颈椎 CT 数据就翻车,mask 边缘糊成一团,椎体之间粘连分不开。问题往往不在模型,而在数据——颈椎结构细长、椎体间距小、骨组织与周围软组织灰度接近,随便找个通用分割数据集练手,迁移过来基本没法用。这次拆的是一份专门针对颈椎 CT 骨骼分割的三轴 2D 切片数据集,包含横断面、冠状面、矢状面三个方向的切面图像和对应 mask,mask 灰度值为 0/1/2 的阈值图,还附带一个 show.py 可视化脚本。适合正在做脊柱分割、骨科术前规划、CT 骨骼三维重建的从业者,也适合想拿真实医学数据练 2D 分割模型的新手。下面按「数据长什么样 → 怎么读怎么可视化 → 怎么接进训练流程 → 坑在哪」的顺序拆一遍。

2. 三轴切片的组织方式与 mask 编码逻辑

2.1 x/y/z 三轴切片的尺寸与数量差异

这份数据按解剖学三个正交方向切分,每个方向的图像数量和分辨率都不一样,这不是随便定的,而是由颈椎的几何形状决定的。

切面方向分辨率图像数量mask 数量对应解剖视角
x 轴512×51237343734横断面(轴位)
y 轴512×19642524252冠状面
z 轴512×19624262426矢状面

x 轴切面是 512×512 的正方形,因为横断面在人体中本身就是左右对称的宽幅视野,颈椎位于图像中央,周围有气管、食管、血管等结构。y 轴和 z 轴都是 512×196 的长条形,这是因为冠状面和矢状面在前后方向上只需要覆盖颈椎及其邻近区域,196 像素的高度刚好够用,再大就是浪费显存。

图像和 mask 一一对应,命名规则一致,比如HN_P004_265.png对应的 mask 就是同名文件放在 mask 目录下。这种命名方式的好处是写 DataLoader 时直接按文件名配对,不需要额外维护映射表。

注意:三个轴的切片数量不同,说明原始 3D 体数据在不同方向上的层厚和覆盖范围有差异。x 轴切片最多(3734 张),意味着横断面方向的层间距最密,做 2D 训练时这个方向的样本量最大。

2.2 mask 灰度值 0/1/2 的语义与阈值图含义

mask 不是常见的二值图(0/255),而是 0、1、2 三个灰度值的阈值图。这个设计在颈椎分割里很关键:

  • 灰度值 0:背景,包括软组织、空气、非骨骼区域
  • 灰度值 1:颈椎骨骼区域(椎体、椎弓、棘突等骨性结构)
  • 灰度值 2:通常用于标记相邻椎体之间的分界或特定解剖标志

为什么不用 0/255 二值?因为颈椎是多节段结构,C1 到 C7 共 7 节椎体,如果只做前景/背景二分类,模型学不到椎体之间的边界,后处理时无法把粘连的椎体分开。用 0/1/2 三值编码,相当于在像素级别保留了「这是骨骼」和「这是哪一节骨骼的分界」两层信息。

实际训练时怎么处理这个三值 mask?常见做法有两种:

import numpy as np from PIL import Image # 读取 mask mask = np.array(Image.open("mask/HN_P004_265.png")) # 方案一:转为二值分割(骨骼 vs 背景) binary_mask = (mask > 0).astype(np.uint8) # 0 背景,1 骨骼 # 方案二:保留三值做多分类 # 类别 0=背景,1=骨骼,2=分界 multi_mask = mask.copy() # 直接使用原始 0/1/2 # 查看每个类别的像素占比 for cls in [0, 1, 2]: ratio = (mask == cls).sum() / mask.size print(f"类别 {cls} 占比: {ratio:.4f}")

这段代码的逻辑说明:mask > 0把所有非背景像素归为骨骼,适合做二分类分割任务;如果要做多分类,直接保留原始灰度值即可。参数上需要注意的是,PIL 读取 PNG 时默认是 8 位灰度,值域 0-255,但这份数据的 mask 实际只用了 0、1、2 三个值,所以读进来后最大值就是 2,不需要做归一化。如果发现读进来的 mask 最大值是 255,说明文件被重新编码过,需要用mask = mask // 127之类的操作还原。

提示:先跑一遍上面的像素占比统计,如果某个切面的类别 1 占比低于 1%,说明这个切面几乎不含骨骼,训练时可以考虑过滤掉,避免正负样本极度不平衡。

3. 可视化代码 show.py 的拆解与改造

3.1 show.py 的核心逻辑与运行方式

数据集附带的 show.py 是用来快速查看图像和 mask 叠加效果的。虽然只有几十行,但它是验证数据配对是否正确、mask 是否对齐的第一道关卡。常见实现逻辑如下:

import os import numpy as np from PIL import Image import matplotlib.pyplot as plt def show_overlay(img_path, mask_path, alpha=0.5): """ 叠加显示 CT 图像和分割 mask img_path: CT 图像路径 mask_path: mask 路径 alpha: mask 透明度 """ img = np.array(Image.open(img_path).convert("L")) mask = np.array(Image.open(mask_path)) # 创建彩色 mask:骨骼为红色,分界为绿色 color_mask = np.zeros((*mask.shape, 3), dtype=np.uint8) color_mask[mask == 1] = [255, 0, 0] # 骨骼 → 红 color_mask[mask == 2] = [0, 255, 0] # 分界 → 绿 # 叠加 plt.figure(figsize=(10, 5)) plt.subplot(1, 3, 1) plt.imshow(img, cmap="gray") plt.title("CT Image") plt.axis("off") plt.subplot(1, 3, 2) plt.imshow(mask, cmap="jet") plt.title("Mask") plt.axis("off") plt.subplot(1, 3, 3) plt.imshow(img, cmap="gray") plt.imshow(color_mask, alpha=alpha) plt.title("Overlay") plt.axis("off") plt.tight_layout() plt.show() # 使用示例 show_overlay("image/HN_P004_265.png", "mask/HN_P004_265.png")

逻辑说明:先分别读取 CT 和 mask,然后把 mask 的 0/1/2 映射成彩色(红=骨骼,绿=分界),最后用 matplotlib 的 alpha 混合叠加在原图上。参数alpha控制 mask 的透明度,默认 0.5 能同时看清骨骼轮廓和原始灰度。如果叠加后发现 mask 和图像有明显偏移,说明数据配对有问题,需要检查文件名是否一一对应。

3.2 批量可视化与三轴对比的改造

单张查看效率太低,实际调试时我一般会改成批量采样 + 三轴对比。下面这个脚本从 x、y、z 三个方向各随机抽 3 张,拼成 3×3 网格:

import random import glob def batch_show(data_root, n=3): """ 从三个轴各随机抽 n 张做叠加对比 data_root: 数据根目录,下含 x/y/z 三个子目录 """ fig, axes = plt.subplots(3, n, figsize=(4*n, 12)) for row, axis in enumerate(["x", "y", "z"]): img_dir = os.path.join(data_root, axis, "image") mask_dir = os.path.join(data_root, axis, "mask") files = glob.glob(os.path.join(img_dir, "*.png")) samples = random.sample(files, min(n, len(files))) for col, img_path in enumerate(samples): fname = os.path.basename(img_path) mask_path = os.path.join(mask_dir, fname) img = np.array(Image.open(img_path).convert("L")) mask = np.array(Image.open(mask_path)) color_mask = np.zeros((*mask.shape, 3), dtype=np.uint8) color_mask[mask == 1] = [255, 0, 0] color_mask[mask == 2] = [0, 255, 0] axes[row][col].imshow(img, cmap="gray") axes[row][col].imshow(color_mask, alpha=0.4) axes[row][col].set_title(f"{axis}: {fname}") axes[row][col].axis("off") plt.tight_layout() plt.savefig("batch_check.png", dpi=150) print("已保存 batch_check.png") batch_show("./data", n=3)

参数说明:data_root是数据根目录,下面按 x/y/z 分三个子目录,每个子目录里再分 image 和 mask。n控制每个轴抽几张。保存成图片而不是直接 show,是为了在远程服务器上跑的时候能下载下来看。这个脚本跑一遍,基本能确认三件事:图像和 mask 是否对齐、mask 的 0/1/2 编码是否正确、三个轴的解剖视角是否符合预期。

4. 接进 2D 分割训练流程:从 DataLoader 到增强策略

4.1 自定义 Dataset 与文件名配对

这份数据的目录结构如果是x/image/、x/mask/、y/image/、y/mask/这样组织的,写 PyTorch Dataset 时核心就是按文件名配对。下面是一个可直接用的模板:

import torch from torch.utils.data import Dataset, DataLoader import torchvision.transforms as T import os from PIL import Image import numpy as np class CervicalCTDataset(Dataset): def __init__(self, img_dir, mask_dir, transform=None, target_transform=None): self.img_dir = img_dir self.mask_dir = mask_dir self.transform = transform self.target_transform = target_transform # 按文件名排序,确保 image 和 mask 一一对应 self.filenames = sorted([ f for f in os.listdir(img_dir) if f.endswith(".png") ]) def __len__(self): return len(self.filenames) def __getitem__(self, idx): fname = self.filenames[idx] img_path = os.path.join(self.img_dir, fname) mask_path = os.path.join(self.mask_dir, fname) img = Image.open(img_path).convert("L") # 灰度图 mask = Image.open(mask_path) # 0/1/2 阈值图 img = np.array(img, dtype=np.float32) / 255.0 # 归一化到 [0,1] mask = np.array(mask, dtype=np.int64) # 保持整数类别 if self.transform: img = self.transform(img) if self.target_transform: mask = self.target_transform(mask) return img, mask # 使用 train_dataset = CervicalCTDataset( img_dir="./data/x/image", mask_dir="./data/x/mask", transform=T.Compose([ T.ToTensor(), # 转为 [C,H,W],值域 [0,1] ]), target_transform=T.Lambda(lambda x: torch.from_numpy(x).long()) ) train_loader = DataLoader(train_dataset, batch_size=8, shuffle=True, num_workers=4)

逻辑说明:filenames排序后按索引取,保证 image 和 mask 严格配对。图像归一化到 [0,1] 是标准操作,mask 保持整数是因为后面用 CrossEntropyLoss 做多分类。参数上,batch_size设 8 是因为 512×512 的图在 8GB 显存上跑 U-Net 差不多刚好,如果显存小就降到 4。num_workers设 4 是经验值,太多反而会因为 IO 竞争变慢。

4.2 医学图像增强的边界与参数选择

医学图像增强和自然图像不一样,不能随便翻转旋转。颈椎有明确的左右对称性和上下连续性,增强策略要尊重解剖约束:

# 适合颈椎 CT 的增强组合 train_transform = T.Compose([ T.ToTensor(), T.RandomHorizontalFlip(p=0.5), # 左右翻转:颈椎左右对称,安全 T.RandomAffine( degrees=10, # 旋转 ±10°,再大椎体方向就变了 translate=(0.05, 0.05), # 平移 5%,模拟定位偏差 scale=(0.95, 1.05), # 缩放 5%,模拟不同层厚 ), T.RandomAdjustSharpness(sharpness_factor=2, p=0.3), # 锐化,模拟低剂量CT的噪声 ])

参数说明:RandomHorizontalFlip对横断面和冠状面是安全的,因为人体左右对称;但矢状面翻转要谨慎,因为矢状面有前后方向的不对称性。RandomAffine的degrees不要超过 15°,颈椎椎体排列紧密,旋转太大会导致椎体间相对位置失真。RandomAdjustSharpness是模拟低剂量 CT 图像噪声的常用手段,低剂量 CT 在临床上越来越普遍,模型需要对这种噪声有鲁棒性。

注意:不要用 RandomVerticalFlip(上下翻转),颈椎的上下方向有明确的解剖顺序(C1 在上,C7 在下),翻转后语义完全变了。也不要用力过猛的弹性形变,椎体是硬组织,形变不符合物理规律。

5. 避坑与排查:这份数据最容易翻车的五个地方

5.1 mask 读进来全是 0 或全是 255

现象:用 PIL 或 OpenCV 读 mask,发现像素值只有 0 和 255,没有 1 和 2。

原因:PNG 保存时被某些工具重新量化了,或者读取时用了convert("L")之外的模式导致值域被拉伸。

解决:先打印np.unique(mask)看实际值。如果是 0/255,用mask = (mask > 0).astype(np.uint8)还原成 0/1;如果原始就是 0/1/2,确保读取时不经过任何归一化。OpenCV 的cv2.imread(path, cv2.IMREAD_GRAYSCALE)默认保持原始值,比 PIL 更稳妥。

5.2 三个轴的切片数量对不上,怀疑数据缺失

现象:x 轴 3734 张,y 轴 4252 张,z 轴 2426 张,数量差异很大,担心是不是漏了文件。

原因:这不是数据缺失,而是原始 3D 体数据在不同方向上的层厚和覆盖范围不同。x 轴(横断面)层间距最密,所以切片最多;z 轴(矢状面)层间距最疏,切片最少。

解决:不需要补齐。训练时按轴分别建 Dataset,或者把三个轴的数据混合成一个大数据集,让模型同时学到三个视角的特征。如果要做 3D 重建,需要根据原始体数据的 spacing 信息做插值,这份 2D 数据本身不包含 spacing 元信息。

5.3 训练时 loss 不下降,Dice 一直在 0.3 左右

现象:模型训练几个 epoch 后,loss 震荡不降,验证集 Dice 卡在 0.3。

原因:最常见的是类别不平衡——骨骼像素占比可能只有 5% 不到,背景占 95%,模型学会了全预测背景也能拿到 95% 的像素准确率,但 Dice 很低。

解决:换损失函数。把 CrossEntropyLoss 换成 DiceLoss 或 Combined Loss(CE + Dice)。如果做二分类,用BCEWithLogitsLoss(pos_weight=torch.tensor([10.0]))给正样本加权。另外检查一下 mask 的 0/1/2 编码是否被正确转成了训练用的类别数,如果模型输出 2 类但 mask 有 3 个值,会直接报错或静默出错。

5.4 可视化时 mask 和图像错位

现象:show.py 叠加显示时,mask 的骨骼轮廓和 CT 上的骨骼对不上,偏移几个像素。

原因:图像和 mask 在预处理时用了不同的 resize 或 crop 参数,或者文件名配对时用了不稳定的排序(比如os.listdir的顺序在不同系统上不一致)。

解决:确保 image 和 mask 用完全相同的预处理流水线。文件名配对用sorted()固定顺序。如果做了 resize,image 用双线性插值,mask 必须用最近邻插值(Image.NEAREST),否则 mask 的类别值会被插值成小数,破坏 0/1/2 编码。

5.5 显存不够,512×512 的图 batch_size 只能设 2

现象:8GB 显存跑 U-Net,batch_size 设 4 就 OOM。

原因:512×512 的输入在 U-Net 的浅层特征图很大,显存占用主要来自激活值。

解决:三个方向。一是用混合精度训练(torch.cuda.amp),显存直接省 40% 左右;二是把输入 crop 成 384×384 或 256×256,颈椎区域通常在图像中央,crop 掉边缘不影响分割;三是用梯度累积,batch_size 设 2,累积 4 步等效于 batch_size 8。我一般优先上混合精度,改动最小,效果最明显。

6. 进阶技巧:用三轴数据做交叉验证与模型集成

这份数据最大的价值在于三个轴的切片可以互相做交叉验证。同一个颈椎,在横断面上分割好了,投影到冠状面和矢状面应该是一致的。利用这个约束,可以做两件进阶的事。

第一件是三轴一致性校验。训练一个 2D 分割模型后,把同一个病例的 x、y、z 三个轴的预测结果做 3D 重建,检查三个方向的预测是否在空间上对齐。如果横断面预测的椎体边界和矢状面预测的对不上,说明模型在某些视角上过拟合了。具体做法是把三个轴的预测 mask 按原始空间坐标映射回 3D 体数据,然后计算三个方向投影的一致性指标。

第二件是多视角模型集成。分别用 x、y、z 三个轴的数据训练三个独立的 2D 分割模型,推理时对同一个 3D 体数据从三个方向分别预测,再把三个预测结果做投票或平均。常见做法是:

# 伪代码:三轴集成推理 pred_x = model_x(img_x) # 横断面预测 pred_y = model_y(img_y) # 冠状面预测 pred_z = model_z(img_z) # 矢状面预测 # 把三个预测映射回 3D 空间 vol_x = project_to_3d(pred_x, axis="x") vol_y = project_to_3d(pred_y, axis="y") vol_z = project_to_3d(pred_z, axis="z") # 投票集成 final_vol = (vol_x + vol_y + vol_z) / 3 final_mask = (final_vol > 0.5).astype(np.uint8)

这个思路在医学图像分割比赛里很常见,单视角模型的 Dice 可能只有 0.85,三视角集成后能到 0.90 以上。代价是推理时间翻三倍,但颈椎分割对实时性要求不高,术前规划场景下完全可以接受。

还有一个细节:三个轴的图像分辨率不同(x 轴 512×512,y/z 轴 512×196),训练时如果混在一起,要么统一 resize 到相同尺寸,要么分别用不同的输入尺寸。我一般会分别训练,因为 resize 会引入额外的插值误差,而颈椎的精细结构对插值很敏感。

从那以后我每次拿到新的医学分割数据,都强制先跑一遍np.unique(mask)和批量可视化,确认 mask 编码和图像对齐没问题再写训练代码。这个习惯帮我省了至少两次通宵排查数据 bug 的时间。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/11 20:32:57

EDA实战指南:从数据清洗到可视化的完整分析流程

我直接开始写吧。这篇EDA实战文章,我尽量把实操中真正会用到的东西讲透——不是教科书式地罗列函数,而是告诉你拿到一堆杂乱数据后,第一步该看什么、哪些坑必须避开、怎么从图表里读出业务信号。内容会覆盖从数据清洗到可视化分析的完整流程&…

作者头像 李华
网站建设 2026/10/11 20:30:59

Unity-Skills新手实战:一句话让AI帮你建场景、挂脚本、改材质

【免费下载链接】Unity-Skills AI automation skills specifically designed for Unity 项目地址: https://gitcode.com/gh_mirrors/un/Unity-Skills 点击查看 免费下载 Unity-Skills 是一款专为 Unity 打造的 AI 自动化技能工具包,它让 AI 通过本地 RE…

作者头像 李华
网站建设 2026/10/11 20:21:31

YOLOv8自瞄源码实战:从推理链路到坐标映射的避坑指南

简介:这是一份基于YOLOv8实现的AI自瞄项目Python源码与文档说明,面向计算机、人工智能、自动化等专业的在校学生及具备一定Python基础的开发者,可用于毕业设计、课程设计、项目立项演示或自学进阶。资源包共50个文件,以exe可执行程…

作者头像 李华
网站建设 2026/10/11 20:20:54

ac990会计核算软件v8.3实操指南:从账套搭建到结账迁移避坑

简介:AC990会计核算软件V8.3是一套面向企事业单位财务人员、会计信息化学习者和企业信息化实施者的专业会计核算系统,覆盖凭证管理、账簿登记、报表编制、成本计算、资产管理等核心环节,旨在通过自动化处理提高财务效率、降低人为差错。内容围…

作者头像 李华
网站建设 2026/10/11 20:20:38

番茄实例分割数据集实操:从COCO转YOLO-seg到训练避坑指南

简介:这份番茄实例分割数据集面向农业AI、计算机视觉与智能农业开发者,包含1286张真实农业环境采集的JPEG图像,覆盖坏番茄、好番茄、绿番茄和茎部四个类别,每个实例均带精细多边形标注,以YOLO格式组织,可直…

作者头像 李华