1

有没有使用 VTK 将 DICOM(ct 扫描)图像转换为点云的方法?

VTK 允许读取 DICOM 和 DICOM 系列以及体积渲染,但是否可以从一系列 DICOM 图像生成点云?

如果在 VTK 中不可能,是否有其他库可以用于此目的?

4

2 回答 2

2

这是一个dicom到点云的演示。Dicom 文件的变化很大,具体取决于图像的收集方式,但这是我们一段时间以来一直用于 CT 扫描的文件。这是“手动版本”,即您需要与终端交互以导航 dicom 目录。可以自动执行此操作,但高度依赖于您的应用程序。

我安装了 pcl 8.0 和 vtkdicom。(我能够在没有 vtkdicom 的情况下进行有限的实现,但它的功能使应用程序在处理各种 dicom 目录结构时更加健壮)。

您需要将 main 中的函数指向计算机上的相应目录(应该是包含 DICOMDIR 文件的文件)。加载 dicom 后,可视化器具有键盘输入 m 和 n 来控制要可视化的强度目标。(您可以轻松更改代码以过滤任何参数:x、y、z、强度)并且可以根据需要更改宽度或步长。

#include <pcl/common/common_headers.h>
#include <pcl/visualization/pcl_visualizer.h>
#include <pcl/filters/passthrough.h>

#include <boost/thread/thread.hpp>

#include <vtkSmartPointer.h>
#include <vtkDICOMImageReader.h>
#include "vtkImageData.h"
#include "vtkDICOMDirectory.h"
#include "vtkDICOMItem.h"
#include "vtkStringArray.h"
#include "vtkIntArray.h"
#include "vtkDICOMReader.h"


bool loadDICOM(pcl::PointCloud<pcl::PointXYZI>::Ptr outCloud, std::string fullPathToDicomDir)
{
    // load DICOM dir file
    vtkSmartPointer<vtkDICOMDirectory> ddir =
        vtkSmartPointer<vtkDICOMDirectory>::New();
    ddir->SetDirectoryName(fullPathToDicomDir.c_str());
    ddir->Update();

    //select patient
    int n = ddir->GetNumberOfPatients();
    int patientSelection = 0;
    if (n > 1)
    {
        std::cout << "Select Patient number, total count: " << n << std::endl;
        std::string userInput;
        std::getline(std::cin, userInput);
        patientSelection = std::stoi(userInput);
    }
    const vtkDICOMItem& patientItem = ddir->GetPatientRecord(patientSelection);
    std::cout << "Patient " << patientSelection << ": " << patientItem.Get(DC::PatientID).AsString() << "\n";

    //select study
    vtkIntArray* studies = ddir->GetStudiesForPatient(patientSelection);
    vtkIdType m = studies->GetMaxId() + 1;
    int studySelection = 0;
    if (m > 1)
    {
        std::cout << "Select study, total count: " << m << std::endl;
        std::string userInput;
        std::getline(std::cin, userInput);
        studySelection = std::stoi(userInput);
    }

    int j = studies->GetValue(studySelection);
    const vtkDICOMItem& studyItem = ddir->GetStudyRecord(j);
    const vtkDICOMItem& studyPItem = ddir->GetPatientRecordForStudy(j);
    cout << " Study " << j << ": \""
        << studyItem.Get(DC::StudyDescription).AsString() << "\" \""
        << studyPItem.Get(DC::PatientName).AsString() << "\" "
        << studyItem.Get(DC::StudyDate).AsString() << "\n";
    int k0 = ddir->GetFirstSeriesForStudy(j);
    int k1 = ddir->GetLastSeriesForStudy(j);

    int seriesSelection;
    std::cout << "Select series, range: " << k0 << " to " << k1 << std::endl;
    for (int i = k0; i <= k1; i++)
    {
        const vtkDICOMItem& seriesItem = ddir->GetSeriesRecord(i);
        vtkStringArray* a = ddir->GetFileNamesForSeries(i);

        cout << "  Series " << i << ": \""
            << seriesItem.Get(DC::SeriesDescription).AsString() << "\" "
            << seriesItem.Get(DC::SeriesNumber).AsString() << " "
            << seriesItem.Get(DC::Modality).AsString() << ", Images: "
            << a->GetNumberOfTuples() << "\n";
    }

    std::string userInput;
    std::getline(std::cin, userInput);
    seriesSelection = std::stoi(userInput);

    const vtkDICOMItem& seriesItem = ddir->GetSeriesRecord(seriesSelection);
    cout << "  Series " << seriesSelection << ": \""
        << seriesItem.Get(DC::SeriesDescription).AsString() << "\" "
        << seriesItem.Get(DC::SeriesNumber).AsString() << " "
        << seriesItem.Get(DC::Modality).AsString() << "\n";

    vtkStringArray* a = ddir->GetFileNamesForSeries(seriesSelection);
    vtkDICOMReader* reader = vtkDICOMReader::New();
    reader->SetFileNames(a);
    reader->Update();



    vtkSmartPointer<vtkImageData> sliceData = reader->GetOutput();

    int numberOfDims = sliceData->GetDataDimension();
    int* dims = sliceData->GetDimensions();
    std::cout << "Cloud dimensions: ";
    int totalPoints = 1;
    for (int i = 0; i < numberOfDims; i++)
    {
        std::cout << dims[i] << " , ";
        totalPoints = totalPoints * dims[i];
    }
    std::cout << std::endl;

    std::cout << "Number of dicom points: " << totalPoints << std::endl;


    //read data into grayCloud

    double* dataRange = sliceData->GetScalarRange();
    double* spacingData = reader->GetDataSpacing();
    std::cout << "Data intensity bounds... min: " << dataRange[0] << ", max: " << dataRange[1] << std::endl;
    if (numberOfDims != 3)
    {
        std::cout << "Incorrect number of dimensions in dicom file, generation failed..." << std::endl;
        return false;
    }
    else
    {
        Eigen::RowVector3f spacing = Eigen::RowVector3f(spacingData[0], spacingData[1], spacingData[2]);
        Eigen::RowVector3i dimensions = Eigen::RowVector3i(dims[0], dims[1], dims[2]);

        outCloud->points.clear();

        std::cout << "x spacing: " << spacing(0) << std::endl;
        std::cout << "y spacing: " << spacing(1) << std::endl;
        std::cout << "z spacing: " << spacing(2) << std::endl;

        for (int z = 0; z < dims[2]; z++)
        {
            if (z % 50 == 0)
            {
                double percentageComplete = (double)z / (double)dims[2];
                std::cout << "Dicom Read Progress: " << (int)(100.0 * percentageComplete) << "%" << std::endl;
            }
            for (int y = 0; y < dims[1]; y++)
            {
                for (int x = 0; x < dims[0]; x++)
                {
                    double tempIntensity = sliceData->GetScalarComponentAsDouble(x, y, z, 0);

                    int tempX = x;

                    pcl::PointXYZI tempPt = pcl::PointXYZI();
                    

                    if (!isinf(tempIntensity) && !isnan(tempIntensity))
                    {
                        //map value into positive realm
                        //tempIntensity = ((tempIntensity - dataRange[0]) / (dataRange[1] - dataRange[0]));
                        if (tempIntensity > SHRT_MAX) { tempIntensity = SHRT_MAX; }
                        else if (tempIntensity < SHRT_MIN) { tempIntensity = SHRT_MIN; }
                    }
                    else
                    {
                        tempIntensity = 0;
                    }

                    tempPt.x = tempX;
                    tempPt.y = y;
                    tempPt.z = z;
                    tempPt.intensity = tempIntensity;
                    outCloud->points.push_back(tempPt);
                }
            }
        }
    }
    std::cout << "Load Dicom Cloud Complete!" << std::endl;
    return true;
}


int indexSlice = 0;
void keyboardEventOccurred(const pcl::visualization::KeyboardEvent& event, void* viewer)
{
    if (event.getKeySym() == "n" && event.keyDown())
    {
        indexSlice -= 1;
    }
    else if (event.getKeySym() == "m" && event.keyDown())
    {
        indexSlice += 1;
    }
}

void displayCloud(pcl::PointCloud<pcl::PointXYZI>::Ptr cloud, std::string field, int step, int width, std::string window_name = "default")
{
    boost::shared_ptr<pcl::visualization::PCLVisualizer> viewer(new pcl::visualization::PCLVisualizer(window_name));

    viewer->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 2, "id");

    viewer->registerKeyboardCallback(keyboardEventOccurred, (void*)viewer.get());

    
    pcl::PointCloud<pcl::PointXYZI>::Ptr tempCloud(new pcl::PointCloud<pcl::PointXYZI>);

    pcl::PassThrough<pcl::PointXYZI> pass;
    pass.setInputCloud(cloud);
    pass.setFilterFieldName(field); //could gate this on intensity if u preferred

    int lastIndex = indexSlice-1; //proc first cycle
    while (!viewer->wasStopped()) {
        
        if (indexSlice != lastIndex)
        {
            int low = step * indexSlice - width / 2;
            int high = step * indexSlice + width / 2;
            pass.setFilterLimits(low, high);
            pass.filter(*tempCloud);
            lastIndex = indexSlice;
            std::cout << field<< " range: " <<low<<" , "<<high<< std::endl;

            viewer->removeAllPointClouds();
            pcl::visualization::PointCloudColorHandlerGenericField<pcl::PointXYZI> point_cloud_color_handler(tempCloud, "intensity");
            viewer->addPointCloud< pcl::PointXYZI >(tempCloud, point_cloud_color_handler, "id");
        }
        
        viewer->spinOnce(50);
    }
    viewer->close();
}

// --------------
// -----Main-----
// --------------
int main(int argc, char** argv)
{
    pcl::PointCloud<pcl::PointXYZI>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZI>);
    loadDICOM(cloud, "C:/Local Software/voyDICOM/resources/DICOM_Samples/2021APR14 MiniAchors_V0");
    displayCloud(cloud,"intensity",100,50);
    return 0;
}

请注意,在大多数情况下,dicom 文件的原始尺寸相对较大,因此我很少(从不?)将整个 dicom 文件加载到点云中(直到此代码)。通常我所做的是以密集格式(短数组)处理它,然后根据从该数据中选择的内容创建云。通过这种方式,您可以在进入稀疏数据集(点云)之前进行某些从锁定数据网格(打开、关闭等)中受益的成像操作,因为这里一切都变得更加昂贵。

它与我的一个调试 dicom 集一起使用的漂亮图片: 在此处输入图像描述

于 2018-01-18T22:30:13.443 回答
-2

毕竟,我想我可能已经找到了一种方法。还没有尝试过,但理论上它应该可以工作。

首先,DICOM 图像需要使用 VTK 转换为 .vtk 格式,一旦将 DICOM 图像转换为 .vtk,就可以使用 PCL(点云库)将它们转换为 .pcd(点云格式)。

于 2017-10-23T14:37:18.923 回答