dicom读取方法资料.docx
《dicom读取方法资料.docx》由会员分享,可在线阅读,更多相关《dicom读取方法资料.docx(25页珍藏版)》请在冰豆网上搜索。
![dicom读取方法资料.docx](https://file1.bdocx.com/fileroot1/2023-2/13/ec2a6c08-7b8b-410d-95c1-a9187b87e658/ec2a6c08-7b8b-410d-95c1-a9187b87e6581.gif)
dicom读取方法资料
Dicom格式文件解析器
学数字图像与通讯,这里讲的暂不涉及通讯那方面的问题只讲*.dcm也就是diocm格式文件的读取,读取本身是没啥难度的无非就是字节码数据流处理。
只不过确实比较繁琐。
分析
整体结构先是128字节所谓的导言部分,说俗点就是没啥意义的破数据跳过就是了,然后是dataElement依次排列的方式就是一个dataElement接一个dataElement的方式排到文件结尾通俗的讲dataElement就是指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是啥东东,值表示法啥叫值表示法啊俺不懂intstringshortushort懂不就是这个意思,Dicom标准真坑爹非要整个怪怪的概念。
VR总共27个跟c#值类型对应关系我都写好了:
1stringgetVF(stringVR,byte[]VF)
2{
3stringVFStr=string.Empty;
4switch(VR)
5{
6case"SS":
7VFStr=BitConverter.ToInt16(VF,0).ToString();
8break;
9case"US":
10VFStr=BitConverter.ToUInt16(VF,0).ToString();
11
12break;
13case"SL":
14VFStr=BitConverter.ToInt32(VF,0).ToString();
15
16break;
17case"UL":
18VFStr=BitConverter.ToUInt32(VF,0).ToString();
19
20break;
21case"AT":
22VFStr=BitConverter.ToUInt16(VF,0).ToString();
23
24break;
25case"FL":
26VFStr=BitConverter.ToSingle(VF,0).ToString();
27
28break;
29case"FD":
30VFStr=BitConverter.ToDouble(VF,0).ToString();
31
32break;
33case"OB":
34VFStr=BitConverter.ToString(VF,0);
35break;
36case"OW":
37VFStr=BitConverter.ToString(VF,0);
38break;
39case"SQ":
40VFStr=BitConverter.ToString(VF,0);
41break;
42case"OF":
43VFStr=BitConverter.ToString(VF,0);
44break;
45case"UT":
46VFStr=BitConverter.ToString(VF,0);
47break;
48case"UN":
49VFStr=Encoding.Default.GetString(VF);
50break;
51default:
52VFStr=Encoding.Default.GetString(VF);
53break;
54}
55returnVFStr;
56}
找个dicom文件在十六进制编辑器下瞧瞧给你整明白:
所有dataElement从前到后按tag又可简单分段:
文件元dataElement
不受传输语法影响总是以显示VR方式表示 因为它里面就定义了传输语法
普通dataElement
受传输语法影响显示VR表示方式还是隐式VR表示方式
像素数据dataElement
最重要也是最大的一个数据项其实存储的就是图像数据
几个特殊的tag很重要前面说过了tag就是dicom里定义的字典。
文件元dataElement和跟像素数据相关的dataElement都很重要,其他的很多如果全部照顾完的话估计得写上千行switch语句吧,所以没有必要一般我们一般只抓取关键的tag。
并且在隐式语法下要确定VR也必须根据字典来确定
关键的tag如下:
1stringgetVR(stringtag)
2{
3switch(tag)
4{
5case"0002,0000":
//文件元信息长度
6return"UL";
7break;
8case"0002,0010":
//传输语法
9return"UI";
10break;
11case"0002,0013":
//文件生成程序的标题
12return"SH";
13break;
14case"0008,0005":
//文本编码
15return"CS";
16break;
17case"0008,0008":
18return"CS";
19break;
20case"0008,1032":
//成像时间
21return"SQ";
22break;
23case"0008,1111":
24return"SQ";
25break;
26case"0008,0020":
//检查日期
27return"DA";
28break;
29case"0008,0060":
//成像仪器
30return"CS";
31break;
32case"0008,0070":
//成像仪厂商
33return"LO";
34break;
35case"0008,0080":
36return"LO";
37break;
38case"0010,0010":
//病人姓名
39return"PN";
40break;
41case"0010,0020":
//病人id
42return"LO";
43break;
44case"0010,0030":
//病人生日
45return"DA";
46break;
47case"0018,0060":
//电压
48return"DS";
49break;
50case"0018,1030":
//协议名
51return"LO";
52break;
53case"0018,1151":
54return"IS";
55break;
56case"0020,0010":
//检查ID
57return"SH";
58break;
59case"0020,0011":
//序列
60return"IS";
61break;
62case"0020,0012":
//成像编号
63return"IS";
64break;
65case"0020,0013":
//影像编号
66return"IS";
67break;
68case"0028,0002":
//像素采样1为灰度3为彩色
69return"US";
70break;
71case"0028,0004":
//图像模式MONOCHROME2为灰度
72return"CS";
73break;
74case"0028,0010":
//row高
75return"US";
76break;
77case"0028,0011":
//col宽
78return"US";
79break;
80case"0028,0100":
//单个采样数据长度
81return"US";
82break;
83case"0028,0101":
//实际长度
84return"US";
85break;
86case"0028,0102":
//采样最大值
87return"US";
88break;
89case"0028,1050":
//窗位
90return"DS";
91break;
92case"0028,1051":
//窗宽
93return"DS";
94break;
95case"0028,1052":
96return"DS";
97break;
98case"0028,1053":
99return"DS";
100break;
101case"0040,0008":
//文件夹标签
102return"SQ";
103break;
104case"0040,0260":
//文件夹标签
105return"SQ";
106break;
107case"0040,0275":
//文件夹标签
108return"SQ";
109break;
110case"7fe0,0010":
//像素数据开始处
111return"OW";
112break;
113default:
114return"UN";
115break;
116}
117}
最关键的两个tag:
0002,0010
普通tag的读取方式little字节序还是big字节序 隐式VR还是显示VR。
由它的值决定
1switch(VFStr)
2{
3case"1.2.840.10008.1.2.1\0":
//显示little
4isLitteEndian=true;
5isExplicitVR=true;
6break;
7case"1.2.840.10008.1.2.2\0":
//显示big
8isLitteEndian=false;
9isExplicitVR=true;
10break;
11case"1.2.840.10008.1.2\0":
//隐式little
12isLitteEndian=true;
13isExplicitVR=false;
14break;
15default:
16break;
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
ImplicitVRLittleEndian:
DefaultTransferSyntaxforDICOM
TransferSyntax
1.2.840.10008.1.2.1
ExplicitVRLittleEndian
TransferSyntax
1.2.840.10008.1.2.2
ExplicitVRBigEndian
TransferSyntax
上面的那段代码其实就是这个表格的实现,讲到这里你会觉得多么的坑爹啊是的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原理不多讲等比中项十字相乘法这个是初中的知识哈。
初中没读过的童鞋飘过。
。
。
原理过程讲完了
代码走起
1classDicomHandler
2{
3stringfileName="";
4Dictionarytags=newDictionary();//dicom文件中的标签
5BinaryReaderdicomFile;//dicom文件流
6
7//文件元信息
////usingSystem.Drawing;
////usingSystem.Drawing.Imaging;
/////usingSystem.Drawing.Drawing2D;
8publicBitmapgdiImg;//转换后的gdi图像
9UInt32fileHeadLen;//文件头长度
10longfileHeadOffset;//文件数据开始位置
11UInt32pixDatalen;//像素数据长度
12longpixDataOffset=0;//像素数据开始位置
13boolisLitteEndian=true;//是否小字节序(小端在前、大端在前)
14boolisExplicitVR=true;//有无VR
15
16//像素信息
17intcolors;//颜色数RGB为3黑白为1
18publicintwindowWith=2048,windowCenter=2048/2;//窗宽窗位
19introws,cols;
20publicvoidreadAndShow(TextBoxtextBox1)
21{
22if(fileName==string.Empty)
23return;
24dicomFile=newBinaryReader(File.OpenRead(fileName));
25
26//跳过128字节导言部分
27dicomFile.BaseStream.Seek(128,SeekOrigin.Begin);
28
29if(newstring(dicomFile.ReadChars(4))!
="DICM")
30{
31MessageBox.Show("没有dicom标识头,文件格式错误");
32return;
33}
34
35
36tagRead();
37
38IDictionaryEnumeratorenor=tags.GetEnumerator();
39while(enor.MoveNext())
40{
41if(enor.Key.ToString().Length>9)
42{
43textBox1.Text+=enor.Key.ToString()+"\r\n";
44textBox1.Text+=enor.Value.ToString().Replace('\0','');
45}
46else
47textBox1.Text+=enor.Key.ToString()+enor.Value.ToString().Replace('\0','')+"\r\n";
48}
49dicomFile.Close();
50}
51publicDicomHandler(string_filename)
52{
53fileName=_filename;
54}
55
56publicvoidsaveAs(stringfilename)
57{
58switch(filename.Substring(filename.LastIndexOf('.')))
59{
60case".jpg":
61gdiImg.Save(filename,System.Drawing.Imaging.ImageFormat.Jpeg);
62break;
63case".bmp":
64gdiImg.Save(filename,System.Drawing.Imaging.ImageFormat.Bmp);
65break;
66case".png":
67gdiImg.Save(filename,System.Drawing.Imaging.ImageFormat.Png);
68break;
69default:
70break;
71}
72}
73publicboolgetImg()//获取图像在图像数据偏移量已经确定的情况下
74{
75if(fileName==string.Empty)
76returnfalse;
77
78intdataLen,validLen;//数据长度有效位
79intimgNum;//帧数
80
81rows=int.Parse(tags["0028,0010"].Substring(5));
82cols=int.Parse(tags["0028,0011"].Substring(5));
83
84colors=int.Parse(tags["0028,0002"].Substring(5));
85dataLen=int.Parse(tags["0028,0100"].Substring(5));
86validLen=int.Parse(tags["0028,0101"].Substring(5));
87
88gdiImg=newBitmap(cols,rows);
89
90BinaryReaderdicomFile=newBinaryReader(File.OpenRead(fileName));
91
92dicomFile.BaseStream.Seek(pixDataOffset,SeekOrigin.Begin);
93
94longreads=0;
95for(inti=0;i96{
97for(intj=0;j98{
99if(reads>=pixDatalen)
100break;
101byte[]pixData=dicomFile.ReadBytes(dataLen/8*colors);
102reads+=pixData.Length;
103
104Colorc=Color.Empty;
105if(colors==1)
106{
107intgrayGDI;
108
109doublegray=BitConverter.To