1、Dicom格式文件解析器 学数字图像与通讯,这里讲的暂不涉及通讯那方面的问题 只讲*.dcm 也就是diocm格式文件的读取,读取本身是没啥难度的 无非就是字节码数据流处理。只不过确实比较繁琐。 分析 整体结构先是128字节所谓的导言部分,说俗点就是没啥意义的破数据 跳过就是了,然后是dataElement依次排列的方式 就是一个dataElement接一个dataElement的方式排到文件结尾 通俗的讲dataElement就是指tag 就是破Dicom标准里定义的数据字典。tag是4个字节表示的 前两字节是组号后两字节是偏移号 比如0008,0018。所有dataElement
2、在文件中都是按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 时元素结构 组号 元素号 值长度 数据元
3、素值 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 = BitCo
4、nverter.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 c
5、ase "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).ToStrin
6、g(); 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 = BitCon
7、verter.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 = BitCon
8、verter.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文件在十六
9、进制编辑器下瞧瞧 给你整明白: 所有dataElement从前到后按tag又可简单分段: 文件元dataElement 不受传输语法影响 总是以显示VR方式表示 因为它里面就定义了传输语法 普通dataElement 受传输语法影响 显示VR表示方式还是隐式VR表示方式 像素数据dataElement 最重要也是最大的一个数据项 其实存储的就是图像数据 几个特殊的tag很重要 前面说过了tag就是dicom里定义的字典。文件元dataElement 和跟像素数据相关的dataElement 都很重要,其他的很多 如果全部照顾完的话估计得写上千行switch语句吧,
10、所以没有必要一般我们一般只抓取关键的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
11、 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 cas
12、e "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
13、 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
14、 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 "
15、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 r
16、eturn "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
17、 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 bre
18、ak; 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
19、"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
20、"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 b
21、reak; 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
22、 case "1.2.840.10008.1.2.1\0"://显示little 4 isLitteEndian = true; 5 isExplicitVR = true; 6 break; 7 case "1.2.840.10008.1.2.2\0"://显示big 8 isLitteEndian = false; 9 isExplicitVR = true; 10 break; 11 case "1.2.840.10008.1.2\0"://隐式l
23、ittle 12 isLitteEndian = true; 13 isExplicitVR = false; 14 break; 15 default: 16 break; 17 } 7fe0,0010 像素数据开始处 整理 根据以上的分析相信解析一个dicom格式文件的过程已经很清晰了吧 第一步:跳过128字节导言部分,并读取"DICM"4个字符 以确认是dicom格式文件 第二步:读 取第一部分 也就是非常重要的文件元dataElement 。读取所有0002开头的tag 并根据0002,00
24、10的值确定传输语法。文件元tag部分的数据元素都是以显示VR的方式表示的 读取它的值 也就是字节码处理 别告诉我说你不会字节码处理哈。传输语法 说得那么官方,你就忽悠吧 其实就确定两个东西而已 1字节序 这个基本上都是little字节序。举个例子吧十进制数 35280 用十六进制表示是0xff00 但是存储到文件中你用十六进制编辑器打开你看到的是这个样子00ff 这就是little字节序。平常我们用的x86PC在windows下都是little字节序 包括AMD的CPU。别太较真 较真的话这个问题又可以写篇博客了。 2确定从0002以后的dataElement的VR是显示还是隐式。说来
25、说去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 上面的那段代码其实
26、就是这个表格的实现,讲到这里你会觉得多么的坑爹啊 是的dicom面向对象的破概念非常烦的。 第三步:读 取普通tag 直到搜寻到7fe0,0010 这个最巨体的存储图像数据的 dataElement 它一个顶别人几十个 上百个。我们在前一步已经把VR是显示还是隐式确定 通过前面的图 ,也就是字节码处理而已无任何压力。显示情况下根据VR 和Len 确定数据类型 跟数据长度直接读取就可以了。隐式情况下这破玩艺儿有点烦,只能根据tag 字典确定它是什么VR再才能读取。关于这个字典也在dicom标准的第六章。上面倒数第二段代码已经把重要的字典都列了出来。 第四步:读 取灰度像素数据并调窗 以GDI
27、的方式显示出来。 说实话开始我还以为dicom这种号称医学什么影像的专家制定出来的标准 读取像素数据应该有难度吧 结果没想到这么的傻瓜。直接按像素从左到右从上到下 一行行依次扫描。两个字节表示1个像素普通Dicom格式存储的是16位的灰度图像,其实有效数据只有12位,除去0 所以最高值是2047。比如CT值 从-1000到+1000,空气的密度为-1000 水的密度为0 金属的密度为+1000 总共的值为2000 调窗技术: 即把12级灰度的数据 通过调节窗宽窗位并让他在RGB模式下显示出来。还技术呢 说实话这个也是没什么技术含量的所谓的技术,两句代码给你整明白。 调 节窗宽窗位到底
28、什么意思,12位的数据那么它总共有2047个等级的灰度 没有显示设备可以体现两千多级的明暗度 就算有我们肉眼也无法分辨更无法诊断。我们要诊断是要提取关键密度值的数据 在医院放射科呆久了你一定经常听医生讲什么骨窗 肺窗 之类的词儿,这就是指的这个“窗”。比如有病人骨折了打了钢板我们想看金属部分来诊断 那么我们应该抓取CT值从800到1000 密度的像素 也就是灰度值 然后把它放到RGB模式下显示,低于800的不论值大小都显示黑色 高于1000的不论值大小都显示白色。 通过以上例子那么这个范围1000-800=200 这个200表示窗宽,800+(200/2)这个表示窗位 一句话,从2047个
29、等级的灰度里选取一个范围放到0~255的灰度环境里显示。
怎样把12位灰度影射到8位灰度显示出来呢,还怎么显示 上面方法都给说明了基本上算半成品了。联想到角度制弧度制,设要求的8位灰度值为x 已知的12位灰度值为y那么:x/255=y/2047 那么x=255y/2047 原理不多讲 等比中项十字相乘法 这个是初中的知识哈。初中没读过的童鞋飘过。。。
原理过程讲完了
代码走起
1 class DicomHandler
2 {
3 string fileName = "";
4 Dictionary 30、ing> tags = new Dictionary 31、 fileHeadLen;//文件头长度
10 long fileHeadOffset;//文件数据开始位置
11 UInt32 pixDatalen;//像素数据长度
12 long pixDataOffset = 0;//像素数据开始位置
13 bool isLitteEndian = true;//是否小字节序(小端在前 、大端在前)
14 bool isExplicitVR = true;//有无VR
15
16 //像素信息
17 int c 32、olors;//颜色数 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 = 33、 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标识头,文件格式错误");
3 34、2 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 35、 textBox1.Text += enor.Key.ToString() + "\r\n";
44 textBox1.Text += enor.Value.ToString().Replace('\0', ' ');
45 }
46 else
47 textBox1.Text += enor.Key.ToString() + enor.Value.ToString().Replace('\0', 36、 ' ') + "\r\n";
48 }
49 dicomFile.Close();
50 }
51 public DicomHandler(string _filename)
52 {
53 fileName = _filename;
54 }
55
56 public void saveAs(string filename)
57 {
58 switch 37、filename.Substring(filename.LastIndexOf('.')))
59 {
60 case ".jpg":
61 gdiImg.Save(filename, System.Drawing.Imaging.ImageFormat.Jpeg);
62 break;
63 case ".bmp":
64 gdiImg.Save(filenam 38、e, System.Drawing.Imaging.ImageFormat.Bmp);
65 break;
66 case ".png":
67 gdiImg.Save(filename, System.Drawing.Imaging.ImageFormat.Png);
68 break;
69 default:
70 break;
71 39、 }
72 }
73 public bool getImg( )//获取图像 在图像数据偏移量已经确定的情况下
74 {
75 if (fileName == string.Empty)
76 return false;
77
78 int dataLen, validLen;//数据长度 有效位
79 int imgNum;//帧数
80
81 rows = in 40、t.Parse(tags["0028,0010"].Substring(5));
82 cols = int.Parse(tags["0028,0011"].Substring(5));
83
84 colors = int.Parse(tags["0028,0002"].Substring(5));
85 dataLen = int.Parse(tags["0028,0100"].Substring(5));
86 validLen = int.Parse(tags["0028 41、0101"].Substring(5));
87
88 gdiImg = new Bitmap(cols, rows);
89
90 BinaryReader dicomFile = new BinaryReader(File.OpenRead(fileName));
91
92 dicomFile.BaseStream.Seek(pixDataOffset, SeekOrigin.Begin);
93
94 long reads = 0;
95 42、 for (int i = 0; i < gdiImg.Height; i++)
96 {
97 for (int j = 0; j < gdiImg.Width; j++)
98 {
99 if (reads >= pixDatalen)
100 break;
101 byte[] pixData = dicomFile.ReadBytes(dataL 43、en / 8 * colors);
102 reads += pixData.Length;
103
104 Color c = Color.Empty;
105 if (colors == 1)
106 {
107 int grayGDI;
108
109 double gray = BitConverter.ToUInt1 44、6(pixData, 0);
110 //调窗代码,就这么几句而已
111 //1先确定窗口范围 2映射到8位灰度
112 int grayStart = (windowCenter - windowWith / 2);
113 int grayEnd = (windowCenter + windowWith / 2);
114
115 if ( 45、gray < grayStart)
116 grayGDI = 0;
117 else if (gray > grayEnd)
118 grayGDI = 255;
119 else
120 {
121 grayGDI = (int)((gray - grayStart) * 46、 255 / windowWith);
122 }
123
124 if (grayGDI > 255)
125 grayGDI = 255;
126 else if (grayGDI < 0)
127 grayGDI = 0;
128 c = Color.FromArgb(gra 47、yGDI, grayGDI, grayGDI);
129 }
130 else if (colors == 3)
131 {
132 c = Color.FromArgb(pixData[0], pixData[1], pixData[2]);
133 }
134
135 gdiImg.SetPixel(j, i, c);
136 48、 }
137 }
138
139 dicomFile.Close();
140 return true;
141 }
142 void tagRead()//不断读取所有tag 及其值 直到碰到图像数据 (7fe0 0010 )
143 {
144 bool enDir = false;
145 int leve = 0;
146 StringBuilder 49、 folderData = new StringBuilder();//该死的文件夹标签
147 string folderTag = "";
148 while (dicomFile.BaseStream.Position + 6 < dicomFile.BaseStream.Length)
149 {
150 //读取tag
151 string tag = dicomFile.ReadUInt16().ToString("x4") + "," 50、
152 dicomFile.ReadUInt16().ToString("x4");
153
154 string VR = string.Empty;
155 UInt32 Len = 0;
156 //读取VR跟Len
157 //对OB OW SQ 要做特殊处理 先置两个字节0 然后4字节值长度
158 //-------------------------------------
©2010-2025 宁波自信网络信息技术有限公司 版权所有
客服电话:4009-655-100 投诉/维权电话:18658249818