把 spacing 改成 1 mm 并不等于完成重采样。只改元数据,可能让相同数组表示一个尺寸完全不同的物体;真正重采样需要在新的物理网格上重新取值。对于医学影像软件,数组数值与空间元数据必须作为一个整体处理。
**阅读范围:**本文是三维视觉与医学影像软件工程系列的独立技术教程。数据和数值实验均为合成示例,不读取患者数据,不代表实际设备性能或临床精度。文中长度按示例明确的单位解释。资料核对日期:2026 年 10 月 16 日。

图 1:重采样:换一个物理网格重新取值(原创教学配图)
一、图像不只是三维数组
SimpleITK 图像在物理空间中由 size、spacing、origin 和 direction 一起定义。索引 i 对应的物理点是 p=o+D·diag(s)·i,其中 D 的列表示索引轴方向。像素中心与像素覆盖范围也要区分,不能随意把原点当成图像外边界。
SimpleITK 的索引顺序为 x,y,z,GetArrayFromImage 得到的 NumPy 数组通常为 z,y,x。数组转置后若不同时处理方向和 spacing,就会改变物理解释。不能因为两个数组 shape 相同,就认为它们对应同一空间。
二、输出网格是一个明确选择
重采样首先要确定新图像的原点、方向、尺寸和间距。若保持首尾像素中心一致,本篇使用 N_new=2(N_old−1)+1、s_new=s_old/2。这使原来的像素中心恰好落在新网格的偶数索引上。
这个约定保持的是中心到中心的跨度 (N−1)s,不是严格保持半体素外边界的全部覆盖范围。若任务要求保持外边界或采用任意目标 spacing,需要另外制定尺寸取整和原点调整策略,并明确不能同时精确满足的条件。
三、Resample 的变换为什么容易写反?
重采样通常对每个输出位置,寻找它在输入图像中的物理位置,再从输入取值。因此传入的采样变换按输出物理点→输入物理点理解。若掌握的是输入→输出的变换,常需要先求逆。
本文使用恒等物理变换,只改变采样网格,因此可以把坐标问题单独检查清楚。后续加入配准矩阵时,必须明确图像 source/target 与采样 input/output 各自角色,不能看到同一个 4×4 矩阵就直接复用。
四、强度图与分割标签不能用同一种默认插值
连续强度图常使用线性或其他平滑插值,输出像素类型应能够保存小数。分割标签是离散类别,最近邻可避免直接产生不存在的中间类别。把 0、1、2 标签做普通线性插值后四舍五入,可能改变类别边界与语义。
最近邻保留标签集合,并不保证体积、边界或拓扑严格不变。下采样还可能混叠,必要时对连续强度作合适预滤波;离散标签需要任务对应的策略。输出范围之外的填充值也要明确,不能把默认零与真实零强度混淆。
五、可运行的完整合成实验
构造 z,y,x 为 12×20×24 的线性强度数组,设置 spacing=(0.7,0.8,2.0) mm、非零原点和旋转方向矩阵。以一半 spacing 建立新参考网格,用恒等变换重采样,并检查原像素中心取值及标签集合。示例从合成体数据开始,不读取真实 DICOM 序列。
环境说明:代码在本机 Python 3.14 环境中实际执行。为便于复现,请保存为 demo.py,在支持这些库的独立环境中安装依赖;若目标解释器没有兼容 wheel,应先选择受支持的解释器。环境差异可能影响最后几位数值,不影响本文所检查的几何或统计关系。
python -m pip install numpy SimpleITK
python demo.py
import numpy as np
import SimpleITK as sitk
z,y,x=np.indices((12,20,24))
array=(x+2*y+3*z).astype(np.float32)
image=sitk.GetImageFromArray(array)
image.SetSpacing((.7,.8,2.))
image.SetOrigin((10.,20.,30.))
image.SetDirection((0.,-1.,0.,1.,0.,0.,0.,0.,1.))
size=np.array(image.GetSize());spacing=np.array(image.GetSpacing())
new_size=(2*(size-1)+1).astype(int)
reference=sitk.Image([int(v) for v in new_size],sitk.sitkFloat32)
reference.SetOrigin(image.GetOrigin());reference.SetDirection(image.GetDirection())
reference.SetSpacing(tuple(spacing/2))
identity=sitk.Transform(3,sitk.sitkIdentity)
out=sitk.Resample(image,reference,identity,sitk.sitkLinear,-1000.,sitk.sitkFloat32)
np.testing.assert_allclose(sitk.GetArrayFromImage(out)[::2,::2,::2],array,atol=1e-5)
index=(4,5,6)
point=image.TransformIndexToPhysicalPoint(index)
expected=np.array(image.GetOrigin())+np.array(image.GetDirection()).reshape(3,3)@(spacing*np.array(index))
np.testing.assert_allclose(point,expected)
assert out.GetSize()==tuple(int(v) for v in new_size)
labels=sitk.Cast(image>35,sitk.sitkUInt8)
nearest=sitk.Resample(labels,reference,identity,sitk.sitkNearestNeighbor,0,sitk.sitkUInt8)
assert set(np.unique(sitk.GetArrayFromImage(nearest)))<= {0,1}
print('old_size_xyz',image.GetSize(),'new_size_xyz',out.GetSize())
print('index_xyz',index,'physical_point_mm',point)
print('PASS: physical mapping, original-center values, discrete label preservation')
六、运行结果与应该怎样解读
输出 size 从 (24,20,12) 变为 (47,39,23)。索引 (4,5,6) 对应物理点 (6.0,22.8,42.0) mm。检查了新网格偶数索引处恢复原值,并确认最近邻标签只含 0 与 1。

图 2:不同 spacing 的像素中心网格(原创教学配图)
代码中的断言验证了本篇明确描述的性质。配图中标为原理示意的部分用于解释模型,不是实测截图;由公式生成的曲线只在图注与正文列出的假设下成立。不要把合成实验输出当成真实产品的性能报告。
七、常见现象与排查方向
| 现象 | 优先检查 | 处理方向 |
|---|---|---|
| 模型整体缩放 | 只改 spacing 没有重采样 | 建立新网格并重新取值 |
| 左右或轴方向错误 | xyz 与 zyx 混淆 | 核对方向矩阵和已知物理点 |
| 输出大部分为空 | 采样变换方向反了 | 检查输出→输入映射与填充值 |
| 标签出现奇怪类别 | 使用了连续强度插值 | 离散类别采用合适标签插值 |
八、独立验证与适用边界
对角点、中心和已知解剖或合成标志点检查物理坐标,比较重采样前后的真实空间跨度。DICOM 导入还需正确处理序列分组、层面方向和实际层位置,不能用文件名排序代替空间几何。本例没有实现 DICOM 读取,也未处理非规则层间距或组织形变。
建议把原始输入、参数、软件版本、输出结果和失败状态一起保留。若把同一份检查数据反复用于选择参数,它就不再是完全独立的最终验证集。将算法演示接入真实系统时,应另行验证输入质量、参考不确定性和完整处理链路。
参考资料
上述资料用于核对相关接口与基本约定;本文的合成数据、推导示例与代码不声称复刻库内全部实现。
写在最后
"纸上得来终觉浅,绝知此事要躬行。"
------ 陆游,《冬夜读书示子聿》
像素值与物理坐标一起验证,影像处理才能真正保持空间含义。