读取Dicom图像文件

  系统运行前,要求用户输入骨骼模型的Dicom序列图,并在此基础上进行3D骨骼的建模。Dicom序列文件的解析,夏旸师兄写了两种方法,一种是Unity的伪线程IEnumerator,另一种是C#的多线程,实际采用的是C#的多线程。

  多线程读取Dicom文件 (DisplayTunning.cs):

public void LoadDicomFileMultiThread(List<string> listFileName)
{
    Clear();
    this.ListDicomDecoder.Clear();
    this.listFileName = listFileName;
    this.listIsRead = new ArrayList(CollectionUtil.GetList<bool>(false, listFileName.Count));
    Debug.Log(Time.realtimeSinceStartup);
    Thread[] threads = new Thread[SystemInfo.processorCount]; // 创建线程数组
    for (int i = 0; i < SystemInfo.processorCount; i++)
    {
        threads[i] = new Thread(new ThreadStart(ThreadReadDicomFile));   // 开始解析
        threads[i].Start();
    }
    while (true)
    {
        if (ListDicomDecoder.Count == listFileName.Count)
        {
            break;
        }
        Thread.Sleep(0);
    }
    Debug.Log(Time.realtimeSinceStartup);

    dicomController.FinishReadFile();     // 通知主界面文件读取完毕
    ListDicomDecoder.Sort();              // 对dicom数组进行排序
    InitControlArg();                     // 计算基本信息,包括图像的显示长宽、层间距、网格缩放因子、映射表
    InitSliderProperty();                 // 初始化主界面的3个滑动条,用于调整CT图像的窗位、窗宽、边缘阈值
    UpdateTexture();                      // 更新主界面的图片预览

    for (int i = 0; i < SystemInfo.processorCount; i++)
    {
        threads[i].Abort();
    }
}

public void ThreadReadDicomFile()
{
    while (!isFinishRead)
    {
        DicomDecoder dicomDecoder = new DicomDecoder();  // DicomDecoder负责文件解析
        string file = GetUnReadFileName();               // 读取在等待队列中的文件
        if (file != "")
        {
            dicomDecoder.DicomFileName = file;
        }
        if (dicomDecoder.dicmFound)
        {
            ListDicomDecoder.Add(dicomDecoder);
        }
        Thread.Sleep(0);
    }
}

  解析的实际过程由对象dicomDecoder负责,是第三方算法,解析后的数据也存储在dicomDecoder中。每1张Dicom图像对应1个dicomDecoder对象,因此文件解析的结果是1个List<DicomDecoder>

  当文件读取完毕后,主界面控制类ConstructController显示出图片序列的滚动条,通过滚动条可以查看所有的CT图像:

  另外,文件读取类DisplayTunning会在界面顶部的工具栏里显示出3个滚动条,分别负责调整CT图像的边缘阈值、窗宽和窗位。

  • 边缘阈值。

    右图为边缘阈值的呈像图,凡涉及到图像边缘采点都由单独的类IsoLine负责。这里,当阈值滚动条发生改变时,会调用函数IsoLine.PickUpContour16PickUpContour16对图像每个像素计算当前像素周围像素的灰度是否在边缘阈值左右,如果是,那么将二值边缘图像(右图)对应的像素点设置为白点。

    边缘查找 (IsoLine.cs):

public static void PickUpContour16(ref Texture2D texture, List<ushort> listPix, byte[] lut16, byte contourValue, int step)
{
    int[] X = new int[5];
    int[] Y = new int[5];
    float[] value = new float[5];
    int width = texture.width;
    int height = texture.height;

    Color[] blacks = CollectionUtil.GetArray<Color>(Color.black, step * step);
    Color[] transparents = CollectionUtil.GetArray<Color>(Color.white, step * step);

    // 遍历像素点
    for (int y = 0; y < height; y += step)
    {
        for (int x = 0; x < width; x += step)
        {
            if (y == 0 || y == texture.height - step ||
                x == 0 || x == texture.width - step)
            {
                texture.SetPixels(x, y, step, step, blacks);   // 图像边缘像素值为0
            }
            else
            {
                texture.SetPixels(x, y, step, step, blacks);
                //texture.SetPixels(x, y, step, step, transparents);

                int index = y * width + x;
                X[0] = x; Y[0] = y; value[0] = lut16[listPix[index]];
                X[1] = x + step; Y[1] = y; value[1] = lut16[listPix[index + step]];
                X[2] = x + step; Y[2] = y + step; value[2] = lut16[listPix[index + step * width + step]];
                X[3] = x; Y[3] = y + step; value[3] = lut16[listPix[index + step * width]];

                X[4] = (X[0] + X[1]) / 2;         // 中心X
                Y[4] = (Y[1] + Y[2]) / 2;         // 中心Y
                value[4] = 0.25F * (value[0] + value[1] + value[2] + value[3]);

                int v1, v2, v3, small, medium, large;
                v3 = 4;
                for (v1 = 0; v1 < 4; v1++)
                {
                    v2 = v1 + 1;
                    if (v1 == 3) v2 = 0;

                    int temp;
                    large = v1;
                    medium = v2;
                    small = v3;  // v3表示中心像素点

                    ...          // 此处省略,过程是交换small、medium、large的位置以满足大小关系

                    if (value[small] < contourValue && contourValue < value[large])
                    {
                        texture.SetPixels(x, y, step, step, transparents);   // 边缘点像素值为255
                        break;
                    }
                }
            }
        }
    }
    texture.Apply();
}
  • 窗宽。 左图为窗宽和窗位的呈像图,在这里窗宽是CT图像的专用术语,表示器官对射线的反应数值范围。缩小窗宽范围会显示更少的内容,由于骨骼对射线的反应波段比较长,所以窗框即使很小也能看见骨骼,而放大窗宽则可以看见更多的细节,比如肌肉和脂肪。有点类似于滤波器。

  • 窗位。 窗位负责移动窗宽的显示位置,假设原图窗口数值总长度为0~2677,窗宽为10,那么窗位可以是0~2667。

  调整好这3个数值,可以在左图看见清晰的大腿骨骼组织,并且在右图可以看见通过边缘检测得到的骨骼轮廓。至此,图像的读取工作就已经完成,我们获得的数据有:图像集的原始数据List<DicomDecoder>,边缘阈值ContourValue,窗宽位置pixMin~pixMax

  接下来,我们可以通过点击左上角的三维重建进入3D模型的构造界面。

results matching ""

    No results matching ""