博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Dicom格式文件解析器[转]
阅读量:5957 次
发布时间:2019-06-19

本文共 20452 字,大约阅读时间需要 68 分钟。

 

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是啥东东 ,值表示法 啥叫值表示法啊 俺不懂 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\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"://隐式little12         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
tags = new Dictionary
();//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() + "\r\n"; 44 textBox1.Text += enor.Value.ToString().Replace('\0', ' '); 45 } 46 else 47 textBox1.Text += enor.Key.ToString() + enor.Value.ToString().Replace('\0', ' ') + "\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 (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(filename, 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 } 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 = int.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,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 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(dataLen / 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.ToUInt16(pixData, 0);110 //调窗代码,就这么几句而已 111 //1先确定窗口范围 2映射到8位灰度112 int grayStart = (windowCenter - windowWith / 2);113 int grayEnd = (windowCenter + windowWith / 2);114 115 if (gray < grayStart)116 grayGDI = 0;117 else if (gray > grayEnd)118 grayGDI = 255;119 else120 {121 grayGDI = (int)((gray - grayStart) * 255 / windowWith);122 }123 124 if (grayGDI > 255)125 grayGDI = 255;126 else if (grayGDI < 0)127 grayGDI = 0;128 c = Color.FromArgb(grayGDI, 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 }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 folderData = new StringBuilder();//该死的文件夹标签147 string folderTag = "";148 while (dicomFile.BaseStream.Position + 6 < dicomFile.BaseStream.Length)149 {150 //读取tag151 string tag = dicomFile.ReadUInt16().ToString("x4") + "," +152 dicomFile.ReadUInt16().ToString("x4");153 154 string VR = string.Empty;155 UInt32 Len = 0;156 //读取VR跟Len157 //对OB OW SQ 要做特殊处理 先置两个字节0 然后4字节值长度158 //------------------------------------------------------这些都是在读取VR一步被阻断的情况159 if (tag.Substring(0, 4) == "0002")//文件头 特殊情况160 {161 VR = new string(dicomFile.ReadChars(2));162 163 if (VR == "OB" || VR == "OW" || VR == "SQ" || VR == "OF" || VR == "UT" || VR == "UN")164 {165 dicomFile.BaseStream.Seek(2, SeekOrigin.Current);166 Len = dicomFile.ReadUInt32();167 }168 else169 Len = dicomFile.ReadUInt16();170 }171 else if (tag == "fffe,e000" || tag == "fffe,e00d" || tag == "fffe,e0dd")//文件夹标签172 {173 VR = "**";174 Len = dicomFile.ReadUInt32();175 }176 else if (isExplicitVR == true)//有无VR的情况177 {178 VR = new string(dicomFile.ReadChars(2));179 180 if (VR == "OB" || VR == "OW" || VR == "SQ" || VR == "OF" || VR == "UT" || VR == "UN")181 {182 dicomFile.BaseStream.Seek(2, SeekOrigin.Current);183 Len = dicomFile.ReadUInt32();184 }185 else186 Len = dicomFile.ReadUInt16();187 }188 else if (isExplicitVR == false)189 {190 VR = getVR(tag);//无显示VR时根据tag一个一个去找 真烦啊。191 Len = dicomFile.ReadUInt32();192 }193 //判断是否应该读取VF 以何种方式读取VF194 //-------------------------------------------------------这些都是在读取VF一步被阻断的情况195 byte[] VF = { 0x00 };196 197 if (tag == "7fe0,0010")//图像数据开始了198 {199 pixDatalen = Len;200 pixDataOffset = dicomFile.BaseStream.Position;201 dicomFile.BaseStream.Seek(Len, SeekOrigin.Current);202 VR = "UL";203 VF = BitConverter.GetBytes(Len);204 }205 else if ((VR == "SQ" && Len == UInt32.MaxValue) || (tag == "fffe,e000" && Len == UInt32.MaxValue))//靠 遇到文件夹开始标签了206 {207 if (enDir == false)208 {209 enDir = true;210 folderData.Remove(0, folderData.Length);211 folderTag = tag;212 }213 else214 {215 leve++;//VF不赋值216 }217 }218 else if ((tag == "fffe,e00d" && Len == UInt32.MinValue) || (tag == "fffe,e0dd" && Len == UInt32.MinValue))//文件夹结束标签219 {220 if (enDir == true)221 {222 enDir = false;223 }224 else225 {226 leve--;227 }228 }229 else230 VF = dicomFile.ReadBytes((int)Len);231 232 string VFStr;233 234 VFStr = getVF(VR, VF);235 236 //----------------------------------------------------------------针对特殊的tag的值的处理237 //特别针对文件头信息处理238 if (tag == "0002,0000")239 {240 fileHeadLen = Len;241 fileHeadOffset = dicomFile.BaseStream.Position;242 }243 else if (tag == "0002,0010")//传输语法 关系到后面的数据读取244 {245 switch (VFStr)246 {247 case "1.2.840.10008.1.2.1\0"://显示little248 isLitteEndian = true;249 isExplicitVR = true;250 break;251 case "1.2.840.10008.1.2.2\0"://显示big252 isLitteEndian = false;253 isExplicitVR = true;254 break;255 case "1.2.840.10008.1.2\0"://隐式little256 isLitteEndian = true;257 isExplicitVR = false;258 break;259 default:260 break;261 }262 }263 for (int i = 1; i <= leve; i++)264 tag = "--" + tag;265 //------------------------------------数据搜集代码266 if ((VR == "SQ" && Len == UInt32.MaxValue) || (tag == "fffe,e000" && Len == UInt32.MaxValue) || leve > 0)//文件夹标签代码267 {268 folderData.AppendLine(tag + "(" + VR + "):" + VFStr);269 }270 else if (((tag == "fffe,e00d" && Len == UInt32.MinValue) || (tag == "fffe,e0dd" && Len == UInt32.MinValue)) && leve == 0)//文件夹结束标签271 {272 folderData.AppendLine(tag + "(" + VR + "):" + VFStr);273 tags.Add(folderTag + "SQ", folderData.ToString());274 }275 else276 tags.Add(tag, "(" + VR + "):" + VFStr);277 }278 }279 }

好了收工。

测试下成果

1 if (openFileDialog1.ShowDialog() != DialogResult.OK) 2     return; 3  4 string fileName = openFileDialog1.FileName; 5  6 handler = new DicomHandler(fileName); 7  8 handler.readAndShow(textBox1); 9 10 this.Text = "DicomViewer-" + openFileDialog1.FileName;11 12 13 backgroundWorker1.RunWorkerAsync();

这里处理gdi位图的时候直接用的setPix 处理速度比较慢所以用了backgroundWorker,实际应用中请使用内存缓冲跟指针的方式

否则效率低了是得不到客户的认可的哦,gdi位图操作可使用lockBits加指针的方式 ,12位的灰度像素数据可以第一次读取后缓存到内存中 以方便后面调窗的快速读取
优化这点代码也不难哈 对指针什么的熟点就行了,前几章都有。

这是ezDicom 经过公认测试的软件 我们来跟他对比一下,打开 

调窗测试,我们注意到两个东西 在没有窗宽窗位时 默认窗宽是2047+1即2048  窗位是2048/2即1024
直观的感受是调窗宽像在调图像对比度 ,调窗位像在调图像亮度。
窗宽为255的时候图像是最瑞丽的 因为255其实就是8位图像的默认窗宽。
注意窗位那里有小小区别,ez窗位显示的是根据1024那里为0开始偏移 而我的程序是根据窗宽中间值没有偏移
没有偏移的情况稍微符合逻辑点吧。
但是可以看到原理是一样的 结果是一样的。
源码下载测试dcm文件: 
最近也没有以前写的文章那么欢乐了 不知道为什么,长大了 没有以前开心了 呵呵 。
筒子们2013年新年快乐。
这篇文章发布很久了 感谢朋友们的关注,分析讲解跟代码有点混乱 感觉有点敷衍了事纯粹赚人气的感觉 对不住大家了。另外本文的调窗代码是有问题的 升级版本请看《》一文中的改进代码。

转载于:https://www.cnblogs.com/fx2008/p/5508620.html

你可能感兴趣的文章
C++: find()函数的注意事项
查看>>
js的事件学习笔记
查看>>
leetcode 【 Add Two Numbers 】 python 实现
查看>>
Android接收系统广播
查看>>
将网络中的图片存为NSData并保存到sqlite的BLOB字段中
查看>>
Cocos2d-js-v3.2 在 mac 上配置环境以及编译到 Andorid 的注意事项(转)
查看>>
iOS用三种途径实现一方法有多个返回值
查看>>
python--class test
查看>>
从零开始理解JAVA事件处理机制(3)
查看>>
HttpURLConnection类的使用
查看>>
linux命令分析---SED (二)
查看>>
[INS-32025] 所选安装与指定 Oracle 主目录中已安装的软件冲突。
查看>>
py2与py3差别
查看>>
欧洲语言框架A1到C2,法语等级 A1、A2、B1、B2、C1、C2
查看>>
c语言中以追加只写方式打开文本文件,C语言中打开文件读取,写入的操作
查看>>
c语言编程 企业发放,求c语言编程企业员工全年销售额统计及奖金发放系..._统计师_帮考网...
查看>>
C语言编辑中午和英语库,懂英语和C语言的来
查看>>
c语言cabd快速查询的方法,滨州医学院 数据结构C语言版习题精选
查看>>
Event loop事件循环
查看>>
开始使用新 Confluence 6 站点
查看>>