3D Slicer 术语表精读:坐标系、体数据与分割的那些概念
3D Slicer 术语表精读:坐标系、体数据与分割的那些概念
为什么值得专门整理这一套术语
3D Slicer 是医学影像领域用得最广的开源平台之一,它的价值不只在于软件本身好用。它背后那套概念模型——数据怎么组织、坐标系怎么定义、显示和数据怎么分离——被大量商业和自研影像软件直接或间接借鉴过。
换句话说,看懂 Slicer 的术语,相当于拿到了理解同类软件设计的一把钥匙。你自己做的三维重建模块、别人家的规划软件、开源库里的某个数据结构,多半都能在这套词汇里找到对应。
下面按四组整理。解释都用我自己的话重写,方便对照理解;权威定义请以官方文档为准,链接在文末。
一、坐标系与空间
这一组是最容易出错、后果也最严重的一组。
| 术语 | 中文 | 说明 |
|---|---|---|
| Coordinate system | 坐标系/参考系 | 由原点位置、各轴方向和长度单位共同确定。Slicer 里的坐标系一律是右手系 |
| LPS | 左-后-上坐标系 | Left-Posterior-Superior,医学影像计算中最通用的解剖坐标系。Slicer 存盘时统一用它 |
| RAS | 右-前-上坐标系 | Right-Anterior-Superior,Slicer 内部运算使用。与 LPS 相差前两个轴取反 |
| IJK | 体素坐标 | 数组索引坐标,整数值对应体素中心。按 VTK 约定,I 索引列、J 索引行、K 索引层 |
| Bounds | 包围盒 | 对象在三个轴上的范围,用六个浮点数表示:各轴的最小值与最大值 |
| Extent | 整数范围 | 三轴上的整数索引范围,同样六个值,首尾都包含在内。因此某轴长度是 max − min + 1 |
| Spacing | 体素间距 | 体素的物理尺寸,通常以毫米计 |
| Resolution | 分辨率 | 同样指体素尺寸,但这个词语义容易误导,后文单独说 |
| Geometry | 几何 | 对象在三维空间中的位置与形状 |
| Transform | 变换 | 把对象从一个坐标系换算到另一个。刚性变换只改变位置和方位;线性变换还能缩放、镜像、错切;非线性变换可以任意扭曲空间 |
| Registration | 配准 | 让两个对象在空间上对齐的过程。结果是一个变换,把”浮动”对象换算到”固定”对象所在的空间 |
| Reference | 参考 | 没有固定含义,通常指某个操作的次要输入,可能是数据、坐标系或几何 |
关于 Transform,有一个设计上的细节值得留意:要在世界坐标系里显示一幅体数据,需要把体数据重采样,因此需要的是从世界坐标系到体数据的变换;而要把其他类型的对象换算到世界坐标系,则需要正方向的变换。Slicer 的变换节点两个方向都能提供,存一个、另一个实时算出来。
二、数据对象
| 术语 | 中文 | 说明 |
|---|---|---|
| Scene | 场景 | 承载当前载入的全部数据,以及它们该如何显示、如何使用的信息。这个词源自计算机图形学 |
| Node | 节点 | 场景中的一个数据对象。可以是数据本身,也可以是描述显示方式、存储方式、空间变换的对象 |
| MRML | 医学现实标记语言 | Slicer 用来存储、可视化和处理医学信息对象的底层库,节点体系就建立在它之上 |
| Volume | 体数据 | 存放三维体素数组的节点。几何由原点、间距和轴方向三者确定。二维图像被当作只有一层的体数据 |
| Voxel | 体素 | 体数据的一个元素,形状是长方体,坐标指的是它的中心点。值可以是标量,也可以是向量 |
| Labelmap | 标签图 | 体素值为离散整数的体数据,每个整数代表一个结构或区域。优点是能用一个数组紧凑地表示互不重叠的多个区域 |
| Segmentation | 分割 | 在图像中勾画三维结构的过程,也指这个过程产出的节点 |
| Segment | 分段 | 分割结果中的单个结构 |
| Model | 模型 | 存放表面网格(三角形、多边形)或体网格(四面体、楔形)的节点 |
| Cell | 单元 | 网格的基本拓扑元素,线、多边形、四面体都属于此类 |
| Markups | 标注 | 用户在视图中放置的简单几何对象和测量,包括点列表、线、曲线、平面、ROI 等类型 |
| Fiducial | 基准点 | 三维空间中的一个点。这个词来自影像引导手术领域,那里用”基准标记物”标定点的位置 |
| ROI | 感兴趣区域 | 三维中的一个盒状区域,可用于裁剪体数据、剪切模型 |
三、显示与强度映射
| 术语 | 中文 | 说明 |
|---|---|---|
| Image intensity | 图像强度 | 通常就是体素值。最终显示出来的亮度和颜色,是它经过窗宽窗位和颜色查找表算出来的 |
| Window/level | 窗宽/窗位 | 体素值到显示亮度的线性映射。窗宽是被映射到完整显示范围的那段强度区间的大小,窗位是被映射到显示范围中心的那个体素值 |
| Brightness/contrast | 亮度/对比度 | 描述同一个线性映射的另一套参数:亮度是偏移量,对比度是乘数。医学影像里更常用窗宽窗位 |
| Color legend | 颜色图例/色标 | 叠加在切片或三维视图上的控件,说明各种颜色代表什么 |
| Orientation marker | 方位标记 | 箭头、方块或人形标记,用来指示视图里的轴方向 |
| Slice | 切片 | 三维对象与一个平面的交面 |
| Slice view annotations | 切片视图标注 | 切片视图角落显示的文字,含体数据名称和选定的 DICOM 标签 |
| Ruler | 标尺 | 有两个意思:视图中作为尺寸参照的叠加线;或标注模块里的距离测量线 |
| VR | 体绘制/虚拟现实 | 这个缩写有歧义,建议写全称,或在上下文里明确定义 |
窗宽窗位和亮度对比度是同一件事的两种参数化方式,这一点值得注意。如果你的软件同时提供两套调节方式,内部要保证它们指向同一个映射,否则用户会看到两个滑块互相”打架”。
四、软件架构
| 术语 | 中文 | 说明 |
|---|---|---|
| Module | 模块 | 由界面、逻辑算法组成的软件组件。可以提供新的节点类型、显示管理器、读写插件等。模块之间通常相互独立,靠修改节点来通信 |
| Extension | 扩展 | 未随主程序打包、可另行下载安装的模块集合 |
| Extensions manager | 扩展管理器 | 在应用内浏览、安装、卸载扩展的组件 |
| Extensions index | 扩展索引 | 存放各扩展描述信息的仓库,扩展目录由它构建 |
| VTK | 可视化工具包 | Slicer 用于数据表示与可视化的底层库。Slicer 大量类派生自 VTK,因此也沿用了它的编码风格和接口约定 |
| ITK | 洞察工具包 | Slicer 用于大部分图像处理运算的底层库 |
| Source volume | 源体数据 | 分割时那些依赖底层图像强度的效果所取用的体数据 |
| Scalar value | 标量值 | 一个简单数值,通常是浮点数 |
| Scalar component | 标量分量 | 向量中的一个元素。分量数就是向量长度 |
| Frame | 帧 | 时间序列中的一个时间点。为避免歧义,这个词不用来指体数据的某一层 |
模块之间只通过节点通信这个设计,值得自研软件借鉴。它意味着任何一个模块都可以被替换或单独测试,只要它对节点的读写约定不变。这对做单元测试和接口测试都是好事。
易错点一:RAS 与 LPS 相差前两轴取反
这是整份术语表里最该记牢的一条。
DICOM 标准用的是 LPS,Slicer 内部运算用的是 RAS,两者只差前两个轴的方向取反。转换本身只是几个符号的事,但出错的后果很不讲道理:
图像看起来完全正常,只是左右镜像了。
人体在解剖上是近似左右对称的,一幅镜像过的 CT 或 X 光片,从画面上几乎看不出异常。骨骼形态、组织灰度、图像质量全都正常。你只有在对照解剖标志、或者和另一套没出错的数据比对时才会发现。
对手术导航这类应用,这个 bug 的严重程度不需要多解释。
实际做测试时,建议专门设计一组验证方位的用例:
- 用带明确左右不对称标志的数据(比如已知病灶在某一侧)
- 或者在体模上做一个非对称的标记特征,专门用来验证方位
- 覆盖全部的数据导入路径——不同来源、不同格式的数据,转换代码可能不是同一份
- 任何涉及坐标系转换的代码改动,这组用例都要回归
不要指望靠”看图”发现这类问题。
易错点二:IJK 与 numpy 的索引顺序相反
VTK 的约定是 I 索引列、J 索引行、K 索引层,按 [I][J][K] 的顺序理解。而 numpy 数组的排布是 a[K][J][I],正好反过来。另一种说法是:I 是变化最快的索引,K 是变化最慢的。
这个顺序搞反了不会抛异常,程序照常跑完,只是结果被转置了。如果转置发生在中间某一步,后续的处理会得到一堆看似合理实则错乱的数据。
做影像相关的 Python 脚本时,建议养成两个习惯:
取到数组后先打印 shape。 确认各维度的长度和你预期的层数、行数、列数对得上。如果各维度长度恰好相同(比如 512×512×512 的立方体数据),这个检查就失效了,这时更要小心。
在数据边界处写明约定。 函数的文档字符串里写清楚参数是 IJK 顺序还是 KJI 顺序,比靠记忆可靠。
易错点三:Resolution 这个词语义误导
Resolution 在 Slicer 里指的是体素尺寸,单位是毫米每像素。问题在于日常语感和它是反的:
数值大 = 间距大 = 图像粗糙。
说”这套数据分辨率是 1.0”,听起来像是挺高,实际上比 0.5 的那套粗一倍。正因为容易误解,Slicer 界面上很少直接用这个词,而是用 Spacing(间距)。
自研软件可以直接学这个做法:界面上写”体素间距”,不写”分辨率”。用户看到”间距 0.5mm”不会产生歧义,看到”分辨率 0.5”就会犹豫一下。
Segmentation 与 Labelmap:为什么要分成两个概念
大多数影像软件用一个 labelmap 来存分割结果——一个三维整数数组,值为 1 的体素属于结构 A,值为 2 的属于结构 B,以此类推。这个做法简单紧凑,但有两个硬限制。
第一,分段不能重叠。 一个体素只能有一个值,也就只能属于一个结构。但实际需求里重叠是常见的:肿瘤和它所在的器官、血管和它穿过的组织、规划中的安全边界和实际靶区。用单个 labelmap 表达不了。
第二,只有一种表示形式。 labelmap 是体素形式,适合做运算和统计,但不适合快速三维显示。要在三维视图里流畅地转动一个分割结果,需要的是表面网格。每次显示都现场做一次等值面提取,交互会很卡。
Slicer 的做法是引入一个专门的 segmentation 节点,它既不是 labelmap 节点也不是 model 节点,而是一个可以同时持有多种表示形式的容器:
- 用多个二值 labelmap 分别存放各个分段,从而允许重叠
- 同时保留封闭表面表示,供三维视图直接使用
- 各种表示之间可以按需相互转换,且保持同步
这个设计的核心思想是:把”数据是什么”和”数据以什么形式存储”解耦开。一个分割结果在概念上是一组结构,至于它眼下以体素还是网格形式存在,是实现细节,调用方不必关心。
做自研软件时,如果你的分割模块现在是直接操作一个 labelmap 数组,不妨评估一下将来会不会遇到重叠需求或显示性能问题。这两个需求一旦出现,改起来的成本比一开始就把结构分开要高很多。
这套概念对自研影像软件的借鉴意义
把上面的内容收拢一下,有四点是可以直接拿来用的。
数据与显示分离。 Slicer 把”数据节点”和”显示节点”分成两类对象,同一份数据可以有多套显示配置,在不同视图里呈现成不同样子。这比把颜色、透明度直接塞进数据对象要灵活得多。
坐标系约定要显式化,而且只定一次。 Slicer 内部统一 RAS、存盘统一 LPS,转换集中在读写边界上完成。这种做法的好处是转换逻辑只有一处,出问题只需要查一个地方。最怕的是各个模块各自转换,转多了转少了都很难查。
多表示形式的容器设计。 前面讲的 segmentation 节点是一个典型例子。凡是同一份数据存在多种存储形式、而调用方只关心语义的场景,都可以套用这个模式。
模块间只通过数据通信。 模块彼此独立、只靠读写节点交互,这让单元测试和替换实现都变得容易。自研软件如果模块之间互相直接调用方法,耦合会越积越深。
术语内容参考自 3D Slicer 官方文档的 Glossary 章节,解释部分为本文作者重写。原文见 https://slicer.readthedocs.io/。