内容时间:2024.9.2-2024.9.10
上周我们确定了技术栈和需要开发的功能,这周开始开发最常见的工作流:DICOM读取+MPR渲染。
DICOM 读取
vtk提供了vtkDICOMImageReader读取dicom文件。调用SetFileName读取单个文件,得到的是一个2D数据;调用SetDirectoryName读取文件夹,得到的是一个3D数据。
运行vtk example ReadDICOMSeries 查看效果,其结果很像MPR的横截面,如果将相同的数据导入3D slicer还会发现两者的顺利是相反的。slicer取最小值时,vtkDICOMImageReader取的是靠近脑袋的那张图,3D slicer取的是靠近脚的那张图。
究其原因,还是坐标系的不同,查看vtkDICOMImageReader源码。Reader会使用DICOMAppHelper根据image position patient按照降序排列数据,然后将数据存为一维数组。
/* Get the filenames for a series ordered by image position
patient. This is the most reliable way to order the images in a
series. Use the first series by default. */
void GetImagePositionPatientFilenamePairs(
std::vector<std::pair<float, std::string>>& v, bool ascending = true);
this->AppHelper->GetImagePositionPatientFilenamePairs(sortedFiles, false);
---
// DICOM stores the upper left pixel as the first pixel in an
// image. VTK stores the lower left pixel as the first pixel in
// an image. Need to flip the data.
vtkIdType rowLength;
rowLength = this->DataIncrements[1];
unsigned char* b = (unsigned char*)buffer;
unsigned char* iData = (unsigned char*)imgData;
iData += (imageDataLengthInBytes - rowLength); // beginning of last row
for (int i = 0; i < this->AppHelper->GetHeight(); ++i)
{
memcpy(b, iData, rowLength);
b += rowLength;
iData -= rowLength;
}
buffer = ((char*)buffer) + imageDataLengthInBytes;
3D Slicer则是依据dicom提供的meta信息,将数据转化到RAS坐标系中。dicom meta里面存储了IJK坐标系转换到LPS坐标系的必要信息。值得一提的是,想要支持不同存储标准,肯定要加一层将不同的坐标系转换到统一的坐标系下进行处理。3D slicer是转换到RAS坐标系之后在进行MPR。
通过简单阅读ReadDICOMSeries的源码,发现vtk在dicom读取这块只做了最简单的支持,并没有提供将dicom series转换到RAS坐标系的方法。虽然3D slicer的源码可以作为参考,但还是准备找一个专业的库支持坐标系转换,最好还能支持其他标准,但这部分不着急,ReadDICOMSeries暂时够用。
MPR渲染
vtk提供了vtkResliceImageViewer来实现MPR渲染,通过设置SetSliceOrientationToXY,XZ,YZ我们可以得到横切面,矢状面和冠状面。但体验下来这个类并不太好用,并且这个类把vtk的mapper,actor,renderer,renderWindow,interactor封装到了一起,不太适合学习MPR渲染原理,开源的实现上也没有直接使用这个类的。
MPR相关的内容本周只了解到这,所以下周继续深入这块。
下周计划
- 调查MPR其他实现方式
- 参考3D slicer搭建初版UI