开发者社区> 问答> 正文

关于android上解析dicom文件 的问题

android上解析dicom文件的问题,
我们做的是一个医疗项目,有一个观片功能,需要解析dicom,并显示详细信息,还要可测量等。我搞了两天没一点思路,以前从没接触过这方面。
谁知道啊?

展开
收起
爵霸 2016-03-18 14:26:18 3283 0
1 条回答
写回答
取消 提交回答
  • 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.10"://显示little
    4 isLitteEndian = true;
    5 isExplicitVR = true;
    6 break;
    7 case "1.2.840.10008.1.2.20"://显示big
    8 isLitteEndian = false;
    9 isExplicitVR = true;
    10 break;
    11 case "1.2.840.10008.1.20"://隐式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 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() + "rn";
    44 textBox1.Text += enor.Value.ToString().Replace('0', ' ');
    45 }
    46 else
    47 textBox1.Text += enor.Key.ToString() + enor.Value.ToString().Replace('0', ' ') + "rn";
    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 else
    120 {
    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 //读取tag
    151 string tag = dicomFile.ReadUInt16().ToString("x4") + "," +
    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 //------------------------------------------------------这些都是在读取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 else
    169 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 else
    186 Len = dicomFile.ReadUInt16();
    187 }
    188 else if (isExplicitVR == false)
    189 {
    190 VR = getVR(tag);//无显示VR时根据tag一个一个去找 真烦啊。
    191 Len = dicomFile.ReadUInt32();
    192 }
    193 //判断是否应该读取VF 以何种方式读取VF
    194 //-------------------------------------------------------这些都是在读取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 else
    214 {
    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 else
    225 {
    226 leve--;
    227 }
    228 }
    229 else
    230 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.10"://显示little
    248 isLitteEndian = true;
    249 isExplicitVR = true;
    250 break;
    251 case "1.2.840.10008.1.2.20"://显示big
    252 isLitteEndian = false;
    253 isExplicitVR = true;
    254 break;
    255 case "1.2.840.10008.1.20"://隐式little
    256 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 else
    276 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开始偏移 而我的程序是根据窗宽中间值没有偏移
    没有偏移的情况稍微符合逻辑点吧。
    但是可以看到原理是一样的 结果是一样的。

    2019-07-17 19:06:38
    赞同 展开评论 打赏
问答排行榜
最热
最新

相关电子书

更多
神龙云服务器产品及技术深度解析 立即下载
弹性创造价值:基于ECS的最佳性价比实践解析 立即下载
又快又稳:阿里云下一代虚拟交换机解析 立即下载

相关镜像