转自:http://www.cnblogs.com/assassinx/archive/2013/01/09/dicomViewer.html
Dicom全称是医学数字图像与通讯,这里讲的暂不涉及通讯那方面的问题 只讲*.dcm 也就是diocm格式文件的读取,读取本身是没啥难度的 无非就是字节码数据流处理。只不过确实比较繁琐。
分析:
整体结构先是128字节所谓的导言部分,说俗点就是没啥意义的破数据 跳过就是了,然后是dataElement依次排列的方式 就是一个dataElement接一个dataElement的方式排到文件结尾 通俗的讲dataElement就是指tag。tag就是破Dicom标准里定义的数据字典。tag是4个字节表示的 前两字节是组号,后两字节是偏移号。比如0008,0018。所有dataElement在文件中都是按tag排序的 比如0002,0001 0002,0002 0003,0011
文件整体结构如下:
单个dataElement的结构如下:
显示VR:VR为OB OW OF UT SQ UN的元素结构
组号 |
元素号 |
VR |
预留 |
值长度 |
数据元素值 |
2 |
2 |
2 |
2(0x00,0x00) |
4 |
由数据长度决定 |
显示VR:VR为普通类型时元素结构(少了预留那一行)
组号 |
元素号 |
VR |
值长度 |
数据元素值 |
2 |
2 |
2 |
4 |
由数据长度决定 |
隐式VR 时元素结构
组号 |
元素号 |
值长度 |
数据元素值 |
2 |
2 |
4 |
由数据长度决定 |
要问VR是啥东东 ,值表示法 啥叫值表示法啊 俺不懂 int string short ushort 懂不 就是这个意思,Dicom标准真坑爹 非要整个怪怪的概念。
VR总共27个 跟c#值类型对应关系我都写好了:
1 string getVF(string VR, byte[] VF)
2 {
3 string VFStr = string.Empty;
4 switch (VR)
5 {
6 case "SS":
7 VFStr = BitConverter.ToInt16(VF, 0).ToString();
8 break;
9 case "US":
10 VFStr = BitConverter.ToUInt16(VF, 0).ToString();
11
12 break;
13 case "SL":
14 VFStr = BitConverter.ToInt32(VF, 0).ToString();
15
16 break;
17 case "UL":
18 VFStr = BitConverter.ToUInt32(VF, 0).ToString();
19
20 break;
21 case "AT":
22 VFStr = BitConverter.ToUInt16(VF, 0).ToString();
23
24 break;
25 case "FL":
26 VFStr = BitConverter.ToSingle(VF, 0).ToString();
27
28 break;
29 case "FD":
30 VFStr = BitConverter.ToDouble(VF, 0).ToString();
31
32 break;
33 case "OB":
34 VFStr = BitConverter.ToString(VF, 0);
35 break;
36 case "OW":
37 VFStr = BitConverter.ToString(VF, 0);
38 break;
39 case "SQ":
40 VFStr = BitConverter.ToString(VF, 0);
41 break;
42 case "OF":
43 VFStr = BitConverter.ToString(VF, 0);
44 break;
45 case "UT":
46 VFStr = BitConverter.ToString(VF, 0);
47 break;
48 case "UN":
49 VFStr = Encoding.Default.GetString(VF);
50 break;
51 default:
52 VFStr = Encoding.Default.GetString(VF);
53 break;
54 }
55 return VFStr;
56 }
找个dicom文件在十六进制编辑器下瞧瞧 给你整明白:
所有dataElement从前到后按tag又可简单分段:
文件元dataElement | 不受传输语法影响 总是以显示VR方式表示 因为它里面就定义了传输语法 |
普通dataElement | 受传输语法影响 显示VR表示方式还是隐式VR表示方式 |
像素数据dataElement | 最重要也是最大的一个数据项 其实存储的就是图像数据 |
几个特殊的tag很重要 前面说过了tag就是dicom里定义的字典。文件元dataElement 和跟像素数据相关的dataElement 都很重要,其他的很多 如果全部照顾完的话估计得写上千行switch语句吧,所以没有必要一般我们一般只抓取关键的tag。并且在隐式语法下要确定VR也必须根据字典来确定
关键的tag如下:
1 string getVR(string tag)
2 {
3 switch (tag)
4 {
5 case "0002,0000"://文件元信息长度
6 return "UL";
7 break;
8 case "0002,0010"://传输语法
9 return "UI";
10 break;
11 case "0002,0013"://文件生成程序的标题
12 return "SH";
13 break;
14 case "0008,0005"://文本编码
15 return "CS";
16 break;
17 case "0008,0008":
18 return "CS";
19 break;
20 case "0008,1032"://成像时间
21 return "SQ";
22 break;
23 case "0008,1111":
24 return "SQ";
25 break;
26 case "0008,0020"://检查日期
27 return "DA";
28 break;
29 case "0008,0060"://成像仪器
30 return "CS";
31 break;
32 case "0008,0070"://成像仪厂商
33 return "LO";
34 break;
35 case "0008,0080":
36 return "LO";
37 break;
38 case "0010,0010"://病人姓名
39 return "PN";
40 break;
41 case "0010,0020"://病人id
42 return "LO";
43 break;
44 case "0010,0030"://病人生日
45 return "DA";
46 break;
47 case "0018,0060"://电压
48 return "DS";
49 break;
50 case "0018,1030"://协议名
51 return "LO";
52 break;
53 case "0018,1151":
54 return "IS";
55 break;
56 case "0020,0010"://检查ID
57 return "SH";
58 break;
59 case "0020,0011"://序列
60 return "IS";
61 break;
62 case "0020,0012"://成像编号
63 return "IS";
64 break;
65 case "0020,0013"://影像编号
66 return "IS";
67 break;
68 case "0028,0002"://像素采样1为灰度3为彩色
69 return "US";
70 break;
71 case "0028,0004"://图像模式MONOCHROME2为灰度
72 return "CS";
73 break;
74 case "0028,0010"://row高
75 return "US";
76 break;
77 case "0028,0011"://col宽
78 return "US";
79 break;
80 case "0028,0100"://单个采样数据长度
81 return "US";
82 break;
83 case "0028,0101"://实际长度
84 return "US";
85 break;
86 case "0028,0102"://采样最大值
87 return "US";
88 break;
89 case "0028,1050"://窗位
90 return "DS";
91 break;
92 case "0028,1051"://窗宽
93 return "DS";
94 break;
95 case "0028,1052":
96 return "DS";
97 break;
98 case "0028,1053":
99 return "DS";
100 break;
101 case "0040,0008"://文件夹标签
102 return "SQ";
103 break;
104 case "0040,0260"://文件夹标签
105 return "SQ";
106 break;
107 case "0040,0275"://文件夹标签
108 return "SQ";
109 break;
110 case "7fe0,0010"://像素数据开始处
111 return "OW";
112 break;
113 default:
114 return "UN";
115 break;
116 }
117 }
最关键的两个tag:
0002,0010
普通tag的读取方式 little字节序还是big字节序 隐式VR还是显示VR。由它的值决定
1 switch (VFStr)
2 {
3 case "1.2.840.10008.1.2.1 "://显示little
4 isLitteEndian = true;
5 isExplicitVR = true;
6 break;
7 case "1.2.840.10008.1.2.2 "://显示big
8 isLitteEndian = false;
9 isExplicitVR = true;
10 break;
11 case "1.2.840.10008.1.2 "://隐式little
12 isLitteEndian = true;
13 isExplicitVR = false;
14 break;
15 default:
16 break;
17 }
7fe0,0010
像素数据开始处
整理
根据以上的分析相信解析一个dicom格式文件的过程已经很清晰了吧
第一步:跳过128字节导言部分,并读取"DICM"4个字符 以确认是dicom格式文件
第二步:读取第一部分 也就是非常重要的文件元dataElement 。读取所有0002开头的tag 并根据0002,0010的值确定传输语法。文件元tag部分的数据元素都是以显示VR的方式表示的 读取它的值 也就是字节码处理 别告诉我说你不会字节码处理哈。传输语法 说得那么官方,你就忽悠吧 其实就确定两个东西而已
1字节序 这个基本上都是little字节序。举个例子吧十进制数 35280 用十六进制表示是0xff00 但是存储到文件中你用十六进制编辑器打开你看到的是这个样子00ff 这就是little字节序。平常我们用的x86PC在windows下都是little字节序 包括AMD的CPU。别太较真 较真的话这个问题又可以写篇博客了。
2确定从0002以后的dataElement的VR是显示还是隐式。说来说去0002,0010的值就 那么固定几个 并且只能是那么几个 这些都在那个北美放射学会定义的dicom标准的第六章 有说明 :
1.2.840.10008.1.2 | Implicit VR Little Endian: Default Transfer Syntax for DICOM | Transfer Syntax |
1.2.840.10008.1.2.1 | Explicit VR Little Endian | Transfer Syntax |
1.2.840.10008.1.2.2 | Explicit VR Big Endian | Transfer Syntax |
上面的那段代码其实就是这个表格的实现,讲到这里你会觉得多么的坑爹啊 是的dicom面向对象的破概念非常烦的。
第三步:读取普通tag 直到搜寻到7fe0,0010 这个最巨体的存储图像数据的 dataElement 它一个顶别人几十个 上百个。我们在前一步已经把VR是显示还是隐式确定 通过前面的图 ,也就是字节码处理而已无任何压力。显示情况下根据VR 和Len 确定数据类型 跟数据长度直接读取就可以了。隐式情况下这破玩艺儿有点烦,只能根据tag 字典确定它是什么VR再才能读取。关于这个字典也在dicom标准的第六章。上面倒数第二段代码已经把重要的字典都列了出来。
第四步:读取灰度像素数据并调窗 以GDI的方式显示出来。 说实话开始我还以为dicom这种号称医学什么影像的专家制定出来的标准 读取像素数据应该有难度吧 结果没想到这么的傻瓜。直接按像素从左到右从上到下 一行行依次扫描。两个字节表示1个像素普通Dicom格式存储的是16位的灰度图像,其实有效数据只有12位,除去0 所以最高值是2047。比如CT值 从-1000到+1000,空气的密度为-1000 水的密度为0 金属的密度为+1000 总共的值为2000
调窗技术:
即把12级灰度的数据 通过调节窗宽窗位并让他在RGB模式下显示出来。还技术呢 说实话这个也是没什么技术含量的所谓的技术,两句代码给你整明白。
调节窗宽窗位到底什么意思,12位的数据那么它总共有2047个等级的灰度 没有显示设备可以体现两千多级的明暗度 就算有我们肉眼也无法分辨更无法诊断。我们要诊断是要提取关键密度值的数据 在医院放射科呆久了你一定经常听医生讲什么骨窗 肺窗 之类的词儿,这就是指的这个“窗”。比如有病人骨折了打了钢板我们想看金属部分来诊断 那么我们应该抓取CT值从800到1000 密度的像素 也就是灰度值 然后把它放到RGB模式下显示,低于800的不论值大小都显示黑色 高于1000的不论值大小都显示白色。
通过以上例子那么这个范围1000-800=200 这个200表示窗宽,800+(200/2)这个表示窗位
一句话,从2047个等级的灰度里选取一个范围放到0~255的灰度环境里显示。
怎样把12位灰度影射到8位灰度显示出来呢,还怎么显示 上面方法都给说明了基本上算半成品了。联想到角度制弧度制,设要求的8位灰度值为x 已知的12位灰度值为y那么:x/255=y/2047 那么x=255y/2047 原理不多讲 等比中项十字相乘法 这个是初中的知识哈。初中没读过的童鞋飘过。。。
代码走起
1 class DicomHandler
2 {
3 string fileName = "";
4 Dictionary<string, string> tags = new Dictionary<string, string>();//dicom文件中的标签
5 BinaryReader dicomFile;//dicom文件流
6
7 //文件元信息
8 public Bitmap gdiImg;//转换后的gdi图像
9 UInt32 fileHeadLen;//文件头长度
10 long fileHeadOffset;//文件数据开始位置
11 UInt32 pixDatalen;//像素数据长度
12 long pixDataOffset = 0;//像素数据开始位置
13 bool isLitteEndian = true;//是否小字节序(小端在前 、大端在前)
14 bool isExplicitVR = true;//有无VR
15
16 //像素信息
17 int colors;//颜色数 RGB为3 黑白为1
18 public int windowWith = 2048, windowCenter = 2048 / 2;//窗宽窗位
19 int rows, cols;
20 public void readAndShow(TextBox textBox1)
21 {
22 if (fileName == string.Empty)
23 return;
24 dicomFile = new BinaryReader(File.OpenRead(fileName));
25
26 //跳过128字节导言部分
27 dicomFile.BaseStream.Seek(128, SeekOrigin.Begin);
28
29 if (new string(dicomFile.ReadChars(4)) != "DICM")
30 {
31 MessageBox.Show("没有dicom标识头,文件格式错误");
32 return;
33 }
34
35
36 tagRead();
37
38 IDictionaryEnumerator enor = tags.GetEnumerator();
39 while (enor.MoveNext())
40 {
41 if (enor.Key.ToString().Length > 9)
42 {
43 textBox1.Text += enor.Key.ToString() + "
";
44 textBox1.Text += enor.Value.ToString().Replace('