赞
踩
研究DICOM坐标系转换,查阅网上资料时,发现大多自用文章存在术语不统一,概念混淆的情况,这里进行简单的总结,如有错误望请指出
下面我将简单介绍常用的医学影像文件格式。
DICOM(Digital Imaging and Communications in Medicine)是一种用于医学图像处理的国际标准。它定义了一种格式,用于存储、传输、共享和打印医学图像信息。DICOM不仅包含图像数据,还包含了与图像相关的患者信息、设备信息和诊断信息。
这种数据结构使得DICOM可以表示各种复杂的医学图像数据,包括2D图像、3D体积数据、时间序列数据等。同时,通过使用元数据,DICOM还可以包含与图像相关的丰富的上下文信息,如患者信息、设备信息、诊断信息等。
Group | Element | Tag Description | 中文解释 | VR |
---|---|---|---|---|
0008 | 0060 | Modality | 检查模态(MRI/CT/CR/DR) | CS |
0020 | 0032 | Image Position (Patient) | 图像位置 | DS |
0020 | 0037 | Image Orientation (Patient) | 图像方位 | DS |
0020 | 1041 | Slice Location | 切片在某轴上的位置(通常是Z轴) | DS |
0018 | 0050 | Slice Thickness | 切片厚度(mm) | DS |
0018 | 0088 | Spacing Between Slices | 切片间距(mm) | DS |
0028 | 0030 | Pixel Spacing | 图像中每个像素的物理尺寸 | DS |
通过 Group & Element 的组合,确定了唯一的Tag,我们可以通过该Tag获取该类型数据元素中的信息。举例的Tag,后面我们会详细介绍,将用于坐标系的转换。
NIfTI(Neuroimaging Informatics Technology Initiative)格式是一种用于存储医学图像数据的文件格式,特别是用于神经影像学的数据。它是由NIH(美国国立卫生研究院)的NIfTI项目组开发的,目的是提供一种简单、灵活且方便的方式来存储和共享大量的神经影像学数据。
NIfTI格式通常有两种文件扩展名:.nii和.nii.gz。.nii文件是未压缩的NIfTI文件,而.nii.gz文件是gzip压缩后的NIfTI文件。
数据主要由两部分组成:头部(header)和数据部分(data)。
NIfTI格式还支持存储多个数据集在一个文件中,这通过在头部中指定维度信息来实现。例如,一个四维的NIfTI文件可以存储一个随时间变化的3D体积数据序列。
// NIFTI-1 头文件存储信息(顺序存放) /* NIFTI 第一版的数据格式的头文件大小必须为348字节,这是为了兼容新,和旧的analyze格式保持一致。就是sizeof_hdr保存的数据大小。 */ // //OFFSET SIZE Description int sizeof_hdr; //0B 4B Size of the header.Must be 348 (bytes). char data_type[10]; //4B 10B Not used; compatibility with analyze. char db_name[18]; //14B 18B Not used; compatibility with analyze. int extents; //32B 4B Not used; compatibility with analyze. short session_error; //36B 2B Not used; compatibility with analyze. char regular; //38B 1B Not used; compatibility with analyze. char dim_info; //39B 1B Encoding directions(phase, frequency, slice). short dim[8]; //40B 16B Data array dimensions. float intent_p1; //56B 4B 1st intent parameter. float intent_p2; //60B 4B 2nd intent parameter. float intent_p3; //64B 4B 3rd intent parameter. short intent_code; //68B 2B nifti intent. short datatype; //70B 2B Data type. short bitpix; //72B 2B Number of bits per voxel. short slice_start; //74B 2B First slice index. float pixdim[8]; //76B 32B Grid spacings(unit per dimension). float vox_offset; //108B 4B Offset into a.nii file! start point. float scl_slope; //112B 4B Data scaling, slope. float scl_inter; //116B 4B Data scaling, offset. short slice_end; //120B 2B Last slice index. char slice_code; //122B 1B Slice timing order. char xyzt_units; //123B 1B Units of pixdim[1..4]. float cal_max; //124B 4B Maximum display intensity. float cal_min; //128B 4B Minimum display intensity. float slice_duration; //132B 4B Time for one slice. float toffset; //136B 4B Time axis shift. int glmax; //140B 4B Not used; compatibility with analyze. int glmin; //144B 4B Not used; compatibility with analyze. char descrip[80]; //148B 80B Any text. char aux_file[24]; //228B 24B Auxiliary filename. short qform_code; //252B 2B Use the quaternion fields. short sform_code; //254B 2B Use of the affine fields. float quatern_b; //256B 4B Quaternion b parameter. float quatern_c; //260B 4B Quaternion c parameter. float quatern_d; //264B 4B Quaternion d parameter. float qoffset_x; //268B 4B Quaternion x shift. float qoffset_y; //272B 4B Quaternion y shift. float qoffset_z; //276B 4B Quaternion z shift. float srow_x[4]; //280B 16B 1st row affine transform float srow_y[4]; //296B 16B 2nd row affine transform. float srow_z[4]; //312B 16B 3rd row affine transform. char intent_name[16]; //328B 16B Name or meaning of the data. char magic[4]; //344B 4B Magic string.
上面介绍的是NIfTI-1,NIfTI-2是NIfTI-1的拓展版本,不做过多介绍。 以下NIfTI-2的头文件参考,如果magic字段的值为"n+2\0",则文件是NIfTI-2格式。 // NIfTI-2 header typedef struct { int sizeof_hdr; // 0B 8B Size of the header. Must be 540 (bytes). char magic[8]; // 8B 8B Magic string. int16_t datatype; // 16B 2B Data type. int16_t bitpix; // 18B 2B Number of bits per voxel. int64_t dim[8]; // 20B 64B Data array dimensions. double intent_p1; // 84B 8B 1st intent parameter. double intent_p2; // 92B 8B 2nd intent parameter. double intent_p3; // 100B 8B 3rd intent parameter. double pixdim[8]; // 108B 64B Grid spacings (unit per dimension). double vox_offset; // 172B 8B Offset into .nii file (start point of data). double scl_slope; // 180B 8B Data scaling, slope. double scl_inter; // 188B 8B Data scaling, offset. double cal_max; // 196B 8B Maximum display intensity. double cal_min; // 204B 8B Minimum display intensity. double slice_duration; // 212B 8B Time for one slice. double toffset; // 220B 8B Time axis shift. int64_t slice_start; // 228B 8B First slice index. int64_t slice_end; // 236B 8B Last slice index. char descrip[80]; // 244B 80B Any text. char aux_file[24]; // 324B 24B Auxiliary filename. int qform_code; // 348B 4B Use the quaternion fields. int sform_code; // 352B 4B Use of the affine fields. double quatern_b; // 356B 8B Quaternion b parameter. double quatern_c; // 364B 8B Quaternion c parameter. double quatern_d; // 372B 8B Quaternion d parameter. double qoffset_x; // 380B 8B Quaternion x shift. double qoffset_y; // 388B 8B Quaternion y shift. double qoffset_z; // 396B 8B Quaternion z shift. double srow_x[4]; // 404B 32B 1st row affine transform. double srow_y[4]; // 436B 32B 2nd row affine transform. double srow_z[4]; // 468B 32B 3rd row affine transform. int slice_code; // 500B 4B Slice timing order. int xyzt_units; // 504B 4B Units of pixdim[1..4]. int intent_code; // 508B 4B nifti intent. char intent_name[16]; // 512B 16B Name or meaning of the data. char dim_info; // 528B 1B Encoding directions (phase, frequency, slice). char unused_str[15]; // 529B 15B Unused, filled with zeroes. } nifti_2_header; // Total: 544B
NIfTI和DICOM都是医学影像领域常用的文件格式,但它们有一些重要的区别:
近乎原始光栅数据(Nearly Raw Raster Data,NRRD)是一种用于表示n维光栅数据的文件格式。它由Gordon Kindlmann在开发Teem库时设计,用于存储和处理医学影像数据和体渲染数据,被广泛用于医学影像处理和体渲染软件中,如3D Slicer、MITK等。
NRRD文件通常包括一个头文件和一个数据文件。头文件是纯文本文件,包含了元数据和数据文件的路径。数据文件包含了实际的光栅数据,可以是二进制格式,也可以是文本格式。此外,NRRD也支持将头文件和数据文件合并为一个文件,这种情况下,头部信息和光栅数据之间由一个空行分隔。
dimension
:数据的维度,例如2D图像的维度为2,3D图像的维度为3。sizes
:每个维度的大小,例如一个256x256的2D图像的sizes为256 256。type
:数据类型,例如unsigned char、short、float等。endian
:数据的字节序,例如little或big。encoding
:数据的编码方式,例如raw、ascii、gzip等。space
:空间坐标系,例如left-posterior-superior (LPS)或right-anterior-superior (RAS)。space directions
:每个维度的方向向量。space origin
:空间坐标系的原点。dimension
和sizes
字段决定。在处理医学图像数据时,通常会首先将数据从像素坐标系转换到患者坐标系,然后再转换到世界坐标系。这样可以更方便地描述和分析患者的解剖结构,同时也可以方便地与其他设备或系统交互。
存在一个场景,当我们需要描述其中绝对位置和方向时,约定一个以点O为世界原点,于O点彼此正交的三个单位轴形成的笛卡尔坐标系为这个空间的世界坐标系。世界坐标系只是无数可能的坐标系之一,但是我们需要一个坐标系来测量默认情况下的所有事物,所以我们创建了它并给它起了“世界坐标系”这个特殊名称。
医学影像以患者为主体,因此解剖学坐标系/患者坐标系极其重要。
首先我们需要了解医学上的六个指向和三个观察位:
六个指向:
三个观察位:
所谓的解剖坐标系,这是一个相对于特定患者的坐标系,通常用于描述患者身体内部结构的位置和方向。在患者坐标系中,原点通常被定义为患者身体的某个特定点,方向通常取上述六个方向其三。如常见的LPS坐标系,RAS坐标系。
这是一个相对于特定图像的坐标系,通常用于描述图像中像素的位置。在像素坐标系中,原点通常被定义为图像的左上角,X轴从左到右,Y轴从上到下。(很多文章将像素坐标系与图像坐标系混淆,请注意)
3D Slicer: 默认使用RAS(右前上)坐标系。这是一种常见的神经影像坐标系,其中X轴指向右,Y轴指向前,Z轴指向上。
MITK (Medical Imaging Interaction Toolkit): MITK默认使用LPS(左后上)坐标系,这是DICOM标准中定义的坐标系,其中X轴指向左,Y轴指向后,Z轴指向上。
FSL (FMRIB Software Library): FSL默认使用RAS(右前上)坐标系。
SPM (Statistical Parametric Mapping): SPM默认使用RAS(右前上)坐标系。
OsiriX: OsiriX默认使用LPS(左后上)坐标系。
请注意,虽然这些软件有默认的坐标系,但大多数都支持在不同的坐标系之间转换,以便处理来自不同设备和软件的图像数据。
在医学图像处理中,我们通常会设定一个世界坐标系来描述物体的绝对位置和方向。这个世界坐标系通常会选择一个与患者身体结构相关的坐标系,如LPS(左后上)或RAS(右前上)坐标系,因为这样可以更直观地理解和解释图像数据。因此,我们可以说,在医学图像处理的上下文中,解剖学坐标系(也就是LPS或RAS坐标系)实际上就是一种世界坐标系。而在DICOM中,被称为参考坐标系(Reference Coordinate System)的世界坐标系以LPS为标准。
当我们进行坐标系的变换时,所涉及的旋转、缩放、翻转、平移等操作,都称之为仿射变换,变换使用的矩阵称为仿射矩阵。在DICOM中,我们可以通过以下步骤构建从像素坐标系转到物理坐标系所要用的仿射矩阵:
以下是最终的矩阵:
row_vector[0] | column_vector[0] | slice_vector[0] | image_position[0] |
---|---|---|---|
row_vector[1] | column_vector[1] | slice_vector[1] | image_position[1] |
row_vector[2] | column_vector[2] | slice_vector[2] | image_position[2] |
0 | 0 | 0 | 1 |
这个仿射矩阵可以将像素坐标系中的坐标转换到物理坐标系中。例如,如果我们有一个像素坐标P(x, y, z),我们可以通过将P乘以这个仿射矩阵,得到物理坐标系中的坐标。
在C++中,我们可以使用VTK库与ITK库来处理DICOM文件并计算像素坐标系到物理坐标系的转换。以下是一个示例代码:
#include <itkImageFileReader.h> #include <itkGDCMImageIO.h> #include <itkSpatialOrientationAdapter.h> #include <vtkMatrix4x4.h> #include <vtkMath.h> using ImageType = itk::Image<unsigned short, 3>; using ReaderType = itk::ImageFileReader<ImageType>; // 创建变换矩阵的函数 vtkSmartPointer<vtkMatrix4x4> CreateTransformMatrix(const ReaderType::Pointer &reader); // 获取图像方向的函数 itk::SpatialOrientation::ValidCoordinateOrientationFlags GetImageOrientation(const ReaderType::Pointer &reader); // 如果方向不是LPS,转换矩阵的函数 vtkSmartPointer<vtkMatrix4x4> TransformMatrixIfNotLPS(vtkSmartPointer<vtkMatrix4x4> matrix, itk::SpatialOrientation::ValidCoordinateOrientationFlags orientationFlag); int main(int argc, char* argv[]) { // 检查命令行参数 if (argc < 2) { std::cerr << "Usage: " << argv[0] << " <DICOM file>" << std::endl; return 1; } // 创建ITK读取器并读取图像 ReaderType::Pointer reader = ReaderType::New(); reader->SetFileName(argv[1]); reader->Update(); // 根据源数据获取变换矩阵 vtkSmartPointer<vtkMatrix4x4> transformMatrix = CreateTransformMatrix(reader); // 获取图像坐标系 itk::SpatialOrientation::ValidCoordinateOrientationFlags orientationFlag = GetImageOrientation(reader); // 如果坐标系不是LPS,进行矩阵变换计算 if (orientationFlag != itk::SpatialOrientation::ITK_COORDINATE_ORIENTATION_LPS) { transformMatrix = TransformMatrixIfNotLPS(transformMatrix, orientationFlag); } // 打印变换矩阵 transformMatrix->Print(std::cout); // 使用变换矩阵从像素坐标系转到LPS坐标系 // double pixelPoint[4] = {row, column, slice, 1}; // double lpsPoint[4]; // transformMatrix->MultiplyPoint(pixelPoint, lpsPoint); return 0; } vtkSmartPointer<vtkMatrix4x4> CreateTransformMatrix(const ReaderType::Pointer &reader) { if (!reader) { std::cout << "Invalid reader pointer!" << std::endl; return nullptr; } // 获取图像位置、方向和间距 ImageType::PointType imagePosition = reader->GetOutput()->GetOrigin(); ImageType::DirectionType imageOrientation = reader->GetOutput()->GetDirection(); ImageType::SpacingType pixelSpacing = reader->GetOutput()->GetSpacing(); // 创建变换矩阵 vtkSmartPointer<vtkMatrix4x4> transformMatrix = vtkSmartPointer<vtkMatrix4x4>::New(); // 计算行向量和列向量 double rowVector[3], columnVector[3]; for (int i = 0; i < 3; ++i) { rowVector[i] = imageOrientation[i][0] * pixelSpacing[0]; columnVector[i] = imageOrientation[i][1] * pixelSpacing[1]; } // 在矩阵中设置行向量、列向量和图像位置 for (int i = 0; i < 3; ++i) { transformMatrix->SetElement(i, 0, rowVector[i]); transformMatrix->SetElement(i, 1, columnVector[i]); transformMatrix->SetElement(i, 3, imagePosition[i]); } // 计算行向量和列向量的叉积以获取切片方向 double sliceDirection[3]; vtkMath::Cross(rowVector, columnVector, sliceDirection); // 在矩阵中设置切片方向 for (int i = 0; i < 3; ++i) { transformMatrix->SetElement(i, 2, sliceDirection[i]); } // 设置矩阵的最后一个元素 transformMatrix->SetElement(3, 0, 0); transformMatrix->SetElement(3, 1, 0); transformMatrix->SetElement(3, 2, 0); transformMatrix->SetElement(3, 3, 1); return transformMatrix; } itk::SpatialOrientation::ValidCoordinateOrientationFlags GetImageOrientation(ReaderType::Pointer reader) { if (!reader) { std::cout << "Invalid reader pointer!" << std::endl; return itk::SpatialOrientation::ITK_COORDINATE_ORIENTATION_INVALID; } // 获取图像方向 ImageType::DirectionType direction = reader->GetOutput()->GetDirection(); // 将图像方向转换为itk::SpatialOrientation::ValidCoordinateOrientationFlags值 itk::SpatialOrientationAdapter adapter; itk::SpatialOrientation::ValidCoordinateOrientationFlags orientationFlag = adapter.FromDirectionCosines(direction); return orientationFlag; } vtkSmartPointer<vtkMatrix4x4> TransformMatrixIfNotLPS(vtkSmartPointer<vtkMatrix4x4> matrix, itk::SpatialOrientation::ValidCoordinateOrientationFlags orientationFlag) { if (!matrix) { std::cout << "Invalid matrix pointer!" << std::endl; return nullptr; } // 使用itk::SpatialOrientationAdapter将orientationFlag转换为方向余弦矩阵 itk::SpatialOrientationAdapter adapter; itk::SpatialOrientation::ValidCoordinateOrientationFlags lpsFlag = itk::SpatialOrientation::ITK_COORDINATE_ORIENTATION_LPS; itk::SpatialOrientationAdapter::DirectionType directionCosines = adapter.ToDirectionCosines(lpsFlag); // 直接修改原始矩阵的元素,将其转换为LPS坐标系 for (int i = 0; i < 3; ++i) { for (int j = 0; j < 3; ++j) { matrix->SetElement(i, j, directionCosines[i][j]); } } return matrix; }
请注意,我们不是总能从DICOM获取到想要的Tag,当有些Tag设备无法提供的时候,我们可以根据其他Tag进行origin、spacing、orientation的计算。这需要我们对于具体数据具体分析。
因为我的软件都是将NIfTI 与Nrrd转换为DICOM再使用,关于这两种格式文件的解析如果以后有时间我再补充。
最后,因为个人时间与水平有限,部分知识、代码未进行完整的验证与测试,请大家酌情参考使用。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。