读取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.PickUpContour16
,PickUpContour16
对图像每个像素计算当前像素周围像素的灰度是否在边缘阈值左右,如果是,那么将二值边缘图像(右图)对应的像素点设置为白点。边缘查找 (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模型的构造界面。