大部分医学领域数据存储的都是dicom
格式,但是对于CT
等一类的序号图像,就需要多个dicom
文件独立存储,最终构成一个序列series,这样存储就太过于复杂了。
nifti(Neuroimaging Informatics Technology Initiative,神经影像信息技术倡议)
格式,是一种用于神经影像学数据存储和交换的标准格式。它的设计旨在简化神经影像学数据的处理、分析和共享,并且广泛应用于医学影像、脑成像和神经科学研究领域。
NIfTI
数据格式基于扩展名为“.nii”
或“.nii.gz”
的文件。可以直接在itk-snap
软件中打开,直接拖动就可以了。这块我也在我博客的其他文章进行了展示,感兴趣的可以直接去我的主页查看。
这里我们就简单介绍下,nii
格式文件的读取和保存,目前发现有很多方法,反倒是有些乱了。整理汇总下,方便有这部分需求的伙伴。
注意:如果图像是用作AI
训练,普通的png
图像和原始的nii
存储的3维转2维数据都是可以使用的。一般都会在前处理阶段对数据做归一化操作,所以,我们验证的结果来看,影响不大。
一、文件读取与存储
1.1、读取nii
1.1.1、nibabel库读取
NiBabel
提供对一些常见医学和神经影像文件格式的读/写访问,包括ANALYZE(plain,SPM99,SPM2及更高版本),GIFTI,NIfTI1,NIfTI2,CIFTI-2,MINC1,MINC2,AFNI BRIK/HEAD,MGH和ECAT以及Philips PAR/REC
。该库可以完全或选择性地访问各种图像格式的元数据,可以通过 NumPy
数组访问图像数据,对DICOM
的支持非常有限,也是PyNIfTI
第三方库的继任者。
nibabel
图像由三个方面组成
- hdr: 描述图像的图像元数据(关于数据的数据),通常以图像头部的形式, header;
- ext: 自己可以随意定义数据的部分。dicom2nii后的文件,存储的是一个告知图像数组数据在引用空间中的位置的仿射数组, affine;
- img: 存储 3D 或 4D图像数据数组, get_fdata
用下面代码,将nii的三个部分打印出来查看,如下:
import nibabel as nib
img = nib.load(r"./data/DET0000101.nii.gz")
# Convert them to numpy format,
data = img.get_fdata()
affine = img.affine
head = img.header
print("数组大小为\n{}".format(data.shape))
print("仿射变换矩阵为\n{}".format(affine))
print("数组头为\n{}".format(head))
打印内容如下:
数组大小为
(128, 128, 96)
仿射变换矩阵为
[[-1. 0. 0. -0.]
[ 0. -1. 0. -0.]
[ 0. 0. 1. 0.]
[ 0. 0. 0. 1.]]
数组头为
<class 'nibabel.nifti1.Nifti1Header'> object, endian='<'
sizeof_hdr : 348
data_type : b''
db_name : b''
extents : 0
session_error : 0
regular : b'r'
dim_info : 0
dim : [ 3 128 128 96 1 1 1 1]
intent_p1 : 0.0
intent_p2 : 0.0
intent_p3 : 0.0
intent_code : none
datatype : int32
bitpix : 32
slice_start : 0
pixdim : [1. 1. 1. 1. 0. 0. 0. 0.]
vox_offset : 0.0
scl_slope : nan
scl_inter : nan
slice_end : 0
slice_code : unknown
xyzt_units : 2
cal_max : 0.0
cal_min : 0.0
slice_duration : 0.0
toffset : 0.0
glmax : 0
glmin : 0
descrip : b''
aux_file : b''
qform_code : scanner
sform_code : scanner
quatern_b : 0.0
quatern_c : 0.0
quatern_d : 1.0
qoffset_x : -0.0
qoffset_y : -0.0
qoffset_z : 0.0
srow_x : [-1. 0. 0. -0.]
srow_y : [ 0. -1. 0. -0.]
srow_z : [0. 0. 1. 0.]
intent_name : b''
magic : b'n+1'
更多了解的,可以查询下Analyze
格式和NIFTI
格式,以及dicom
数据和nii
格式定义的方向,是不同的。这部分感兴趣的可以自己查询,可能在你的项目中会遇到这个情况,造成困惑。
1.1.2、itk库读取
ITK(The Insight Toolkit)
是一个开源的跨平台图像处理库,提供了强大了图像处理和分析能力。其主要用于医学图像处理和计算机辅助诊断领域。但是也可以应用于洽谈领域的图像处理任务。
ITK
是由美国国家生物医学图像库和美国国家癌症研究所公共开发的开源软件。采用C++
编写,提供python
接口,具有丰富的图像处理算法和工具,包括图像滤波,分割,配准和重建。
(原本以为只能打开nii
的文件呢,原来他的功能这么强大,NB了)
读取nii
文件:
import itk
nii_path = os.path.join(dir, 'sample.nii')
imgs = itk.array_from_image(itk.imread(nii_path))
实现中值滤波:
image = itk.imread(input_filename)
median = itk.MedianImageFilter.New(image, Radius = 2)
itk.imwrite(median, output_filename)
- 官方文档地址:itk documentation
- 更多关于
itk
库的图像操作,可以去官网学习,点击直达:Quick_start_guide
1.1.3、SimpleITK库读取
SimpleITK
是专门处理医学影像的软件,是 ITK
的简化接口,使用起来非常便捷。SimpleITK
支持 8 种编程语言,包括c++、Python、R、Java、c#、Lua、Ruby 和 TCL
。
读取nii
文件:
nii_path = os.path.join(path, filename)
image = sitk.ReadImage(nii_path)
print('image size:', image.GetSize())
更多关于simple itk
库的图像操作,可以去官网学习,点击直达:Quick_start_guide
1.2、存储 nii
存储nii文件就比较的直接,将数组直接存储为nii格式的文件,或者nii.gz的文件。但是呢,如果header和ext的信息,记得一并写入进去,否则在一些数据处理的情况下,会存在不一致的问题。
1.2.1、nibabel 库存储
# 创建一个新的Nifti对象,使用之前的header和修改后的数组
new_image_obj = nib.Nifti1Image(image, image_obj.affine, header=image_obj.header)
# 保存为nii.gz文件
nib.save(new_image_obj, os.path.join(save_dir, filename))
1.2.2、itk 库存储
ct = itk.image_from_array(array)
itk.imwrite(ct, os.path.join(save_path, e+'.nii')) # 保存nii文件
1.2.3、SimpleITK 库存储
sitk.WriteImage(image_arr, os.path.join(save_dir, filename))
1.2.4、保存nii文件过程中,header问题
上述这些方法都是可以对数组,存储为nii
或nii.gz
文件,但是,对于nii
文件内的header
,可能会被修改,与之前的header
信息不一致。这部分信息是非常重要的,所以,希望希望对数组修改后存储的文件,能够沿用之前的header
。
这是原始的volume
的nii
文件内,header
记录的信息,如下:
sizeof_hdr 348
data_type b''
db_name b''
extents 0
session_error 0
regular b'r'
dim_info 0
dim [ 3 512 512 333 1 1 1 1]
intent_p1 0.0
intent_p2 0.0
intent_p3 0.0
intent_code 0
datatype 8
bitpix 32
slice_start 0
pixdim [1. 0.826172 0.826172 1.25 0. 0. 0. 0. ]
vox_offset 0.0
scl_slope nan
scl_inter nan
slice_end 0
slice_code 0
xyzt_units 2
cal_max 0.0
cal_min 0.0
slice_duration 0.0
toffset 0.0
glmax 0
glmin 0
descrip b''
aux_file b''
qform_code 1
sform_code 0
quatern_b 0.0
quatern_c 0.0
quatern_d 1.0
qoffset_x 204.014
qoffset_y 211.5
qoffset_z -431.974
srow_x [0. 0. 0. 0.]
srow_y [0. 0. 0. 0.]
srow_z [0. 0. 0. 0.]
intent_name b''
magic b'n+1'
如果采用nibabel
进行保存,则需要改写为下面这种形式,如下:
nii_path = os.path.join(data_dir, filename)
image_obj = nib.load(nii_path)
image = image_obj.get_fdata()
header = image_obj.header
# print(np.max(image))
# print(image.shape)
# print(header, type(header))
dim = header['dim']
pixdim = header['pixdim']
shape = dim * pixdim
shape_z = shape[3]
if shape_z <200:
new_z = round(320.0/shape_z)
print(filename, shape_z, new_z)
print(dim)
pixdim[3] = new_z
print(pixdim)
n+=1
image_obj.header['pixdim'] = pixdim
# 创建一个新的Nifti对象,使用之前的header和修改后的数组
new_image_obj = nib.Nifti1Image(image, image_obj.affine, header=image_obj.header)
# 保存为nii.gz文件
nib.save(new_image_obj, os.path.join(save_dir, filename))
1.3、小汇总
import itk
import SimpleITK as sitk
import nibabel as nib
def read_nii_itk(nii_path):
image = itk.array_from_image(itk.imread(nii_path))
return image
def read_nii_nib(nii_path):
I = nib.load(nii_path)
I_affine = I.affine
I = I.get_fdata()
return I, I_affine
def save_nii_itk(array_data, save_path):
image_data = itk.image_from_array(array_data)
itk.imwrite(image_data, save_path) # 保存nii文件
print('save end')
def save_nii_nib(array_data, I_affine, save_path):
nib.Nifti1Image(array_data, I_affine).to_filename(save_path)
print('save ends')
二、SimpleITK 和 itk 库数组的差异
在日常操作中,发现上述两个库都是可以对nii
文件进行操作的,但是两个库读取后的数组,存在差异,具体我们可以通过下面的介绍,进行理解。
先对两个库读取同一个nii
文件如下:
import os
import SimpleITK as sitk
import itk
nii_path = os.path.join(r'./images', 'brain.nii.gz')
image = sitk.ReadImage(nii_path)
print('image size:', image.GetSize()) # depth、height、width
imgs = itk.array_from_image(itk.imread(nii_path))
print('img shape:', imgs.shape) # width、height、depth
打印结果:
image size: (224, 256, 192)
img shape: (192, 256, 224)
为什么两个库读取的结果,会有差异呢?
- 在
nii
文件中,像素的排列顺序是从头到尾按顺序排列的,即最外层是z轴方向,然后是y轴方向,最内层是x轴方向。 - 但是在读取
nii
文件时,有些库会将这个排列顺序调整为从内到外的顺序,即最内层是z轴方向,然后是y轴方向,最外层是x轴方向。 - 这是因为不同的库对于数组的存储方式不同,一些库使用
C
语言的方式进行存储,而C
语言中数组的存储方式是从内到外的。
itk
和SimpleITK
读取时候存在差异吗?
- 在
itk
中,采用的是C
语言的方式进行存储,因此读取出来的数组顺序是从外到内的。 - 而在
SimpleITK
中,采用的是C++
的方式进行存储,因此读取出来的数组顺序是从内到外的。
这个顺序的不同可能会导致一些问题,因此在使用不同的库读取nii
文件时,需要注意数据的存储顺序。
三、nii 文件软件查看
nii
文件,可以采用itk-snap直接打开查看。如下展示的就是原始图像的nii
文件,和标注后的mask
文件,同时在itk-snap
打开后的样子。
四、总结
nii
文件格式,在对医疗数据处理的过程中,无处不在,是一个最最经常会遇到的。了解和操作nii
文件,对其中可能遇到的问题有一些基础的了解,至关重要。但是,现在我更喜欢nrrd
文件了,不知道你是否知道nrrd
文件。