dicom格式文件 界定标识符的处理
转自:http://www.cnblogs.com/assassinx/archive/2013/05/18/3084854.html
说到底无非几个事情 :
1传输语法确定
2数据元素读取
3 7fe0,0010元素 也就是图像数据处理。
关于这整个过程已经不想多说了 在我的上上一篇博客里已经基本实现了。 当然还很有问题比如图像调窗就有bug 这个以后再说吧。众所周知dicom格式文件是由一个接一个连续的“数据元素”组成的。
这次我们只讲怎样去处理文件里一种特殊的数据元素:那就是VR为SQ类型的元素 还有delimited 也就是界定标识付 我们暂且把它归为一类。 为什么特殊呢?因为其他元素都很简单 ,根据传输语法:-> tag_有无显式VR_len_VF 这样的结构。 len是数据长度, VF是固定字节数的字节流数据。 而遇到SQ类型的数据元素就麻烦了 首先他的len是FFFFFFFF 就是无长度。然后是VF 他是一种“文件夹”结构 ,就是里面嵌套其他数据元素,可能嵌套一层 可能嵌套几层 处理是很棘手的问题。
说了这么多我们先来直观的看一下 找到我们上一篇文章《Dicom格式文件解析器》打开测试数据IM-0001-0002.dcm 看到tag数据里有很多类似这样的:
0040,0275SQ
0040,0275(SQ):00
--fffe,e000(**):
--0040,0007(LO):CT2 t阾e, face, sinus
----0040,0008(SQ):00
------fffe,e000(**):
------0008,0100(SH):CTTETE
------0008,0102(SH):XPLORE
------0008,0104(LO):CT2 T蔜E, FACE, SINUS
------fffe,e00d(**):
----fffe,e0dd(**):
----0040,0009(SH):A10011234815
----0040,1001(SH):A10011234814
--fffe,e00d(**):
fffe,e0dd(**):
事先已经知道VR是显式方式的explicit VR 字节序是little edition,这里只是测试 那么我们直接就在处理代码class的变量里写死了:
class Reader
{ WarpedStream.byteOrder bytOrder = WarpedStream.byteOrder.littleEdition;
bool ExplicitVR = true; }
然后我们把组号=0002开头的元素都剔除了 然后把7fe0,0010 也就是图像数据去掉了。这里我们已经有根据上述方式从IM-0001-0002.dcm分离出来的数据元素内容IM-0001-0002.bin 我们来看下IM-0001-0002.bin的二进制流数据组织方式:
对应元素0040,0275看
通过观察有如下规律,tag的VR类型如果等于SQ len=ffffffff 那么它必定以fffe,e0dd len=00000000结尾。 如果tag=fffe,e000 len=ffffffff 必定以fffe,e00d len=00000000 结尾 但是tag=fffe,e000 并不能称之为为节点下的元素。通过dicom标识我们知道 元素同一节点下只能出现一次,而tag=fffe,e000 可以出现多次。这称之为界定标识符 即Delimiter,并且他们是成对的, 有DelimiterStart 就有DelimiterEnd 就是通过这种一个包一个的嵌套方式实现了一个树状目录结构 。 简而言之我们要做的就是解析他。 但是在《Dicom格式文件解析器》里不是已经实现了么,他的关键代码 是这样做的:
if ((VR == "SQ" && Len == UInt32.MaxValue) || (tag == "fffe,e000" && Len == UInt32.MaxValue))// 遇到文件夹开始标签了
{
if (enDir == false)
{
enDir = true;
folderData.Remove(, folderData.Length);
folderTag = tag;
}
else
{
leve++;//VF不赋值
}
}
else if ((tag == "fffe,e00d" && Len == UInt32.MinValue) || (tag == "fffe,e0dd" && Len == UInt32.MinValue))//文件夹结束标签
{
if (enDir == true)
{
enDir = false;
}
else
{
leve--;
}
}
else
VF = dicomFile.ReadBytes((int)Len); string VFStr; VFStr = getVF(VR, VF); for (int i = ; i <= leve; i++)
tag = "--" + tag;
//------------------------------------数据搜集代码
if ((VR == "SQ" && Len == UInt32.MaxValue) || (tag == "fffe,e000" && Len == UInt32.MaxValue) || leve > )//文件夹标签代码
{
folderData.AppendLine(tag + "(" + VR + "):" + VFStr);
}
else if (((tag == "fffe,e00d" && Len == UInt32.MinValue) || (tag == "fffe,e0dd" && Len == UInt32.MinValue)) && leve == )//文件夹结束标签
{
folderData.AppendLine(tag + "(" + VR + "):" + VFStr);
tags.Add(folderTag + "SQ", folderData.ToString());
}
else
tags.Add(tag, "(" + VR + "):" + VFStr);
看得出基本逻辑就是 遇见DelimiterStart 则level++,遇见DelimiterEnd 则level-- 直至根节点VR=SQ的元素结束,把所有同一节点下的数据全都append到一个stringBuilder下。看得出来当时只是为了实现功能 代码比较简单 并没有用到递归 ,并且dataelement的“数据模型”也没有实现。
现在我们就用递归算法来重新实现这个解析过程:
首先应当把数据元素封装成一个结构体,为了他能够实现层级目录结构 ,通过观察windows文件系统的结构 那么它应该是这样的:一个目录下有很多项 有的是文件夹 有的是文件,如果是文件夹那么它下面可能又包括有文件,就是说如果是文件夹则递归 否则结束。
dataelement的struct代码:
struct DataElement
{
public uint _tag;
public WarpedStream.byteOrder bytOrder;
public bool explicitVR;//是显式VR的 否则隐式VR
public uint tag
{
get { return _tag; }
set
{
_tag = value;
VR = VRs.GetVR(value);
uint _len = VRs.getLen(VR);
if (_len != )
len = _len;
}
}
public ushort VR;
//虽然长度为uint 但要看情况隐式时都是4字节 显式时除ow那几个外都是2字节
//如果为ow 显示不但长度为4 在之前还要跳过2字节,除ow那几个之外不用跳过
public uint len;
public byte[] value;
public IList<DataElement> items;
public bool haveItems; public string showValue()
{
if (haveItems)
return null; if (value != null)
return Tags.VFdecoding(VR, value, bytOrder);
else
return null;
}
public void setValue(string valStr)
{
if (haveItems)
return; if (len != )
value = Tags.VFencoding(VR, valStr, bytOrder, len);
else
{
value = Tags.VFencoding(VR, valStr, bytOrder);
if (VRs.IsStringValue(VR))
len = (uint)value.Length;
}
} }
haveItems 代表是否是Delimiter ,items则代表它里面的项。母项 跟子项之间用haveItems 和Delimiter 来产生联系 。为了实现解析我们先得做些前期处理 以供方便调用。就是itemHeader的读取 ,itemHeader指的是一个数据元素除VF部分的所有 详细请看《Dicom格式文件解析器》。上面第一段那句话 普通dataelement跟 “文件夹”dataelement的区别 ,前面部分一样的 主要区别于VF部分 所以我们才写了这个itemHeader读取的函数(潜意识的是说通过header去确定VF部分是否包含子元素)。
贴出代码:
public DataElement readItemHeader(WarpedStream pdv_stream)
{
//bool ExplicitVR = true;
//WarpedStream.byteOrder bytOrder = WarpedStream.byteOrder.littleEdition; DataElement item_tmp = new DataElement();
item_tmp.bytOrder = bytOrder; #region tag 和len 处理部分
item_tmp.tag = pdv_stream.readTag(); if (item_tmp.tag == 0xfffee000)//针对界定标识符的处理 delimited
{
item_tmp.len = 0xffffffff;
pdv_stream.skip();
}
else if (item_tmp.tag == 0xfffee00d || item_tmp.tag == 0xfffee0dd)
{
item_tmp.len = 0x00000000;
pdv_stream.skip();
}
else if (ExplicitVR)//显示VR
{
byte[] vrData = pdv_stream.readBytes();
Array.Reverse(vrData);
item_tmp.VR = BitConverter.ToUInt16(vrData, );
//ow情况 length=4字节 5个不是 OB OW OF UT SQ UN 外加NONE
if (item_tmp.VR == VRs.OB || item_tmp.VR == VRs.OW || item_tmp.VR == VRs.OF ||
item_tmp.VR == VRs.UT || item_tmp.VR == VRs.SQ || item_tmp.VR == VRs.UN)
{
pdv_stream.skip();
item_tmp.len = pdv_stream.readUint();
}
else
{ //非ow情况 length=2字节
item_tmp.len = pdv_stream.readUshort();
}
}
else//隐式VR 自己通过tag找
{
item_tmp.VR = VRs.GetVR(item_tmp.tag);//调用根据tag找寻vr的函数
if (item_tmp.tag == 0xfffee000)
item_tmp.len = 0xffffffff;
else if (item_tmp.tag == 0xfffee00d || item_tmp.tag == 0xfffee0dd)
item_tmp.len = 0x00000000;
else
item_tmp.len = pdv_stream.readUint();
}
#endregion
return item_tmp;
}
没啥特殊的 就是读取数据 只要遵循dicom标准就行了 。看代码的时候留意下。如果不熟悉请看《Dicom格式文件解析器》一章。解析的递归算法代码实现, 说着挺简单的实际上还是比较复杂的 但是中心思想还是跟上面一样遇见DelimiterStart 则level++,遇见DelimiterEnd 则level--。
看上面那句话“主要区别与VF部分 ”,为什么呢 因为VF部分涉及到递归调用 把header跟VF部分区分开 ,如果VF类型是文件夹 则递归否则结束。遇见DelimiterStart 则陷入递归调用,遇见DelimiterEnd 则从递归调用中退出一级。
贴出代码:
public void readItem(ref DataElement item, WarpedStream pdv_stream)
{
//bool ExplicitVR = true;
//WarpedStream.byteOrder bytOrder = WarpedStream.byteOrder.littleEdition; #region value 处理部分
//文件夹标签情况
if ((item.VR == VRs.SQ && item.len == UInt32.MaxValue) || (item.tag == 0xfffee000 && item.len == UInt32.MaxValue))
{
item.haveItems = true;
item.items = new List<DataElement>(); while (true) //读取所有item 直到根据文件夹结尾标识 不断的退出所有的递归循环;
{ DataElement item_tmp = readItemHeader(pdv_stream);//读取tag的头部 即 tag VR Len
if (item_tmp.tag == 0xfffee00d || item_tmp.tag == 0xfffee0dd)
{
//检查是否文件夹结尾标识的代码 如果遇到文件夹结尾标识 立即break 别忘了把读到的tag 字节偏移退回去;
//即从已经陷入的递归循环里退一级
pdv_stream.seek(-item_tmp.getHeaderLen(), SeekOrigin.Current);
break;
}
else if ((item_tmp.VR == VRs.SQ && item_tmp.len == UInt32.MaxValue) || (item_tmp.tag == 0xfffee000 && item_tmp.len == UInt32.MaxValue))
{
//pdv_stream.seek(-item_tmp.getHeaderLen(), SeekOrigin.Current);//字节偏移退回去;貌似不用偏移
//文件夹标签起始标识 递归
//即往递归循环里陷入一级
readItem(ref item_tmp, pdv_stream);
item.items.Add(item_tmp);//items.add代码(文件夹元素)
}
else
{
//普通tag及数据读取代码
item_tmp.value = pdv_stream.readBytes((int)item_tmp.len);
item.items.Add(item_tmp);//items.add代码(普通元素)
}
} //针对文件夹结束标签的处理 //读取文件夹结尾标签 以跟开始标签相呼应
if (item.VR == VRs.SQ && item.len == UInt32.MaxValue)
{
//0xfffee0dd len=00000000//(item_tmp.tag == 0xfffee0dd && item_tmp.len == UInt32.MinValue)
pdv_stream.skip( + );
}
else if (item.tag == 0xfffee000 && item.len == UInt32.MaxValue)
{
//0xfffee00d len=00000000
pdv_stream.skip( + );
}
}//普通元素情况
else
{
item.value = pdv_stream.readBytes((int)item.len);
}
#endregion }
代码没什么好解释的 看就是了 有注释。 最后说下源文件里 VRs.cs 跟Tags.cs 是根据dicom标准编写的 。里面实现的是几千个tag跟VR的对应关系。 这当然不是我写的。 用的别人的成果,几千个啊你想想不把我整疯么。
大功告成 我们来调用试下结果:
public IDictionary<uint, DataElement> pdvDecoding()
{ //pdvBuffer.Seek(0, SeekOrigin.Begin);//把读取偏移点设置到开始处 FileStream fs = new FileStream("IM-0001-0002.bin", FileMode.Open);
WarpedStream ws = new WarpedStream(fs, bytOrder); IDictionary<uint, DataElement> ds = new Dictionary<uint, DataElement>();
//int indx = 0;
while (ws.getPostion() < fs.Length)
{
DataElement item = readItemHeader(ws);
//Console.WriteLine(Tags.ToHexString(item.tag));
readItem(ref item, ws);
ds.Add(item.tag, item);
//indx++;
//if (indx >= 22)
// break; showItem(item);
} ws.close();
return ds;
} int level = ;
public void showItem(DataElement element)
{
for (int i = ; i < level; i++)
{
Console.Write("-");
}
if (element.haveItems)
{
level++; Console.WriteLine(Tags.ToHexString(element.tag));
foreach (DataElement item in element.items)
{
showItem(item);
}
level--;
}
else
{
Console.WriteLine(Tags.ToHexString(element.tag));
}
}
这种解析跟数据组织方式 更方便了dicom数据对象的处理 。我这讲讲当然很简单 看上去很容易的样子 ,因为我已经亲手一行代码一行代码的实现了。 代码很多请同学们 仔细阅读每一个细节 他们之间的调用关系及逻辑。很多地方我没讲到 为了限制篇幅其实有很多与重点部分无关的代码贴出来的时候我已经删除了,但是源码文件里有。
dicom格式文件 界定标识符的处理的更多相关文章
- Dicom格式文件解析器
转自:http://www.cnblogs.com/assassinx/archive/2013/01/09/dicomViewer.html Dicom全称是医学数字图像与通讯,这里讲的暂不涉及通讯 ...
- Dicom格式文件解析器[转]
Dicom格式文件解析器 Dicom全称是医学数字图像与通讯,这里讲的暂不涉及通讯那方面的问题 只讲*.dcm 也就是diocm格式文件的读取,读取本身是没啥难度的 无非就是字节码数据流处理.只不 ...
- 【NLP】Tika 文本预处理:抽取各种格式文件内容
Tika常见格式文件抽取内容并做预处理 作者 白宁超 2016年3月30日18:57:08 摘要:本文主要针对自然语言处理(NLP)过程中,重要基础部分抽取文本内容的预处理.首先我们要意识到预处理的重 ...
- MMIO----Wav格式文件解析
DirectSound只支持Wav格式的音频文件,在创建次缓冲区之前需要先确定播放的Wav音频数据的格式.如果是从本地Wav文件播放,则需要先读出它的数据格式. 1. Wav音频格式布局 Wav是WA ...
- DICOM医学文件的解析
最近导师一直让做智慧医疗的一个项目,这里面涉及到DICOM格式的文件处理,在这里分享一下自己学到的关于DCM文件的一些内容. DICOM DICOM(DigitalImaging andCommuni ...
- Mac新建文件夹、txt文件、无格式文件
新建文件夹: mkdir test 新建txt touch test.txt 新建无后缀格式文件 touch test 如果要删除文件夹 rm -r -f test
- 针对格式文件,Python读取一定大小的文件内容
由数据库导出的数据是格式化数据,如下所示,每两个<REC>之间的数据是一个记录的所有字段数据,如<TITLE>.<ABSTRACT>.<SUBJECT_COD ...
- 报表开发导出各种格式文件的API
文件输出的多样性,准确性和稳定性对于我们常用的报表软件来说很重要.报表的输入是指从报表的模板文件(XML格式的)创建WorkBook对象,输出则指将报表保存为各种格式文件,比如Pdf.Excel.Wo ...
- 2013xlsm格式文件处理
2013xlsm格式文件处理 2013格式的xlsm文件在低版本打开为空白的处理 1.关闭2013的宏2.打开文件,另存(去打开密码)3.2007打开另存(格式已变为2007)4.仅破解VBA密码5. ...
随机推荐
- workflow 工作流
https://documentation.devexpress.com/#Xaf/CustomDocument3356
- mahout 运行Twenty Newsgroups Classification实例
按照mahout官网https://cwiki.apache.org/confluence/display/MAHOUT/Twenty+Newsgroups的说法,我只用运行一条命令就可以完成这个算法 ...
- Java RESTful Web Service相关概念
原文地址:http://1.liangtao.sinaapp.com/?p=647 接上一篇文章REST|RESTful初步认识:p=639">http://1.liangtao.si ...
- JavaWeb学习总结
http://www.cnblogs.com/xdp-gacl/tag/JavaWeb%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93/ http://www.cnblogs. ...
- VC编译连接选项详解(转)
大家可能一直在用VC开发软件,但是对于这个编译器却未必很了解.原因是多方面的.大多数情况下,我们只停留在“使用”它,而不会想去“了解”它.因为它只是一个工具,我们宁可把更多的精力放在C++语言和软件设 ...
- 关于 ArtifactTransferException: Failure to transfer
eclipse 在导入maven project后,pom.xml有可能出现这种错误. 这里update maven project解决了:右键点击Maven项目->Maven->Upda ...
- C#使用Monitor类、Lock和Mutex类进行多线程同步
在多线程中,为了使数据保持一致性必须要对数据或是访问数据的函数加锁,在数据库中这是很常见的,但是在程序中由于大部分都是单线程的程序,所以没有加锁的必要,但是在多线程中,为了保持数据的同步,一定要加锁, ...
- 多线程和并发管理 .NET多线程服务
线程相关静态变量 默认静态变量应用程序域所有线程可见.如果静态变量需要在线程间共享,同步访问也就必然了. 线程相关静态变量保证线程安全,同一时间只有一个线程可访问,且每个线程都有该静态变量的拷贝. p ...
- 如何利用log4Net自定义属性配置功能记录完整的日志信息
log4Net作为专业的log记录控件,对于它的强大功能大家一定不陌生.下面我将详细介绍如何利用其自定义属性,让日志信息更完整. 一,创建测试工程,log4Net组件可以自己从网上下载,也可通过Nug ...
- Amazon Launches FBA Export to Expand Beyond Media Categories
Amazon launched a new program called FBA Export for third-party sellers to help them export products ...