DICOM:由fo-dicom库解析DICOM文件引申出来的……

题记:

近期收到不少博友的邮件或私信,有刚入门者,来信咨询DICOM协议基础知识的;也有同行咨询如何开发DICOM服务器,如何选用dcmtk、dcm4che、fo-dicom等开源库;还有一部分是医疗从业者,大多在医院放射科或相关科室工作,希望实现与设备的交互,查询并获取数据,最好能有UI界面的开源测试软件。
希望能够解答这些问题也正是我坚持写博客,并开设DICOM技术专栏的初衷。怎奈DICOM是一个标准自身比较复杂、且行业专业度较高的协议,即使写了诸多系列的博文,依然未整理出一篇理想的科普贴——理想状态是希望通过这一篇科普贴就能够让任何人都了解并清楚DICOM协议是什么?能做什么?怎么做?——截至目前,只能给出如下建议:对于想深入了解DICOM的各位,推荐首先阅读博客置顶的路线图博文DICOM:DICOM标准学习路线图(初稿),按照博文中的分类依次阅读并学习“介绍篇”、“数据篇”和“协议篇”,直至最后的“应用篇”,如此下来应该可以形成一个整体的认识,不过这可能需要花费一些时间,任何知识的汲取都是需要时间来消化和吸收的,静下心来阅读想必你能够读懂我的博文。

背景:

近期由于收到博友咨询fo-dicom的兼容性和扩展性和自身Github的fo-dicom仓库更新等原因,发现fo-dicom官方主版本已经更新到了3.X系列,貌似完善了不少东西,不过还没有时间仔细研究。以后再抽时间试用并介绍吧。本篇博文由之前自己的fo-dicom版本库在解析不规范DICOM文件时弹出的【错误】:Requested xxxx bytes past end of file…引申,先给出【解决方案】,随后对DICOM文件的解析畅想一下,希望对DICOM文件能够做到高智能化、高鲁棒性的解析。

fo-dicom解析不规范DICOM文件:

1. 不规范DICOM文件

此次遇到的不规范DICOM文件,依然是由于Transfer Syntax(0x0002,0x0010)元素导致的。之前对于Transfer Syntax介绍过多次,在DICOM医学图像处理:DICOM网络传输中区别过Abstract Syntax与Transfer Syntax,在DICOM:dcmqrscp.exe与storescu.exe中C-STORE服务的差别中介绍过在网络服务中Transfer Syntax的作用。以及在DICOM:dcm4che工具包如何压缩dcm文件探讨(续篇)中介绍对dcm文件进行压缩时提到的JPEG LossLess压缩语义以及Implicit VR Little Endian。
Transfer Sytanx在DICOM标准中占有重要的一席之地,既作为必要元素写入到DCM文件元信息(MetaInformation)中,又是DICOM网络服务中双方数据传输的前提。之前也遇到过GE私有的Transfer Syntax(详情见博文DICOM:Transfer Syntax传输语义之奇葩GE Private TS)
这里需要指出的是TransferSyntax元素,即(0x0002,0x0010),作为DICOM文件头信息(File Meta Information)的必要字段被归类到group=0002组中,其常见的数值如下图所示:
DICOM:由fo-dicom库解析DICOM文件引申出来的……_第1张图片

由上图所示Transfer Syntax的数值表达了两种含义,即编码字节序+编码组成格式
举个例子,当Transfer Syntax=1.2.840.10008.1.2.1时,含义为Explicit VR Little Endian,即①编码字节序采用小端序(大端序与小端序即数据在内存中存储与对应的顺序,网络上有很多资料,大家可自行搜索)②编码组成格式采用显式VR,如下图所示元素Element中包括Tag+VR+VL+Value,相反如果改为隐式VR(即Implicit VR),那么元素Element中包含Tag+VL+Value,省掉了VR(那么有人可能会问,省掉了VR那如何解析后面的数据呢?其实DICOM标准的Tag都有固定的数值类型,隐式VR,即ImplicitVR模式下,可以通过查询DicomTag的字典文件来判别VR类型。DICOM:由fo-dicom库解析DICOM文件引申出来的……_第2张图片

通过上面了解了Transfer Syntax的含义,还有一点需要注意——Transfer Syntax“作用域”,即其能够影响的范围:DICOM协议规定包含头信息(File Meta Information)的文件,头信息(即group=0002)的所有元素默认采用Explicit VR Little Endian存储,数据体Dataset(即group>0002的分组)元素如何存储则由头信息File Meta Information中的Transfer Syntax来决定。
今天遇到的不规范文件的情况是:File Meta Information中的Transfer Syntax=1.2.840.10008.1.2,即Implicit VR Little Endian,但是Dataset数据体存储的方式采用的是1.2.840.10008.1.2.1,即Explicit VR Little Endian,构成是Tag+VR+VL+Value。因此fo-dicom在解析时刻抛出了异常信息font color=red>【错误】:Requested xxxx bytes past end of file…。

2. 解决方案

网络上对于这个错误有多种解决方案,例如https://groups.google.com/forum/#!topic/fo-dicom/3AOdHVa1IUQ、https://groups.google.com/forum/#!topic/fo-dicom/kV8zGpjyeqE,想必上述方案的作者遇到的问题跟本文的略有不同,因此并不能解决本文遇到的不规范文件。
通过跟踪fo-dicom代码(这里的fo-dicom版本是基于1.0.38分支fork而来,目前由我自己维护,详细的仓库连接为zssurethu\fo-dicom,所以不知道最新的fo-dicom3.x系列是否能够兼容本文提到的不规范文档,有兴趣的朋友可以亲测一下),发现DicomFileReader中来获取TransferSyntax的数值,并记录编码字节序+编码组成格式,详细代码如下:

                    // test for explicit VR
                    var vrt = Encoding.UTF8.GetBytes(tag.DictionaryEntry.ValueRepresentations[0].Code);
                    var vrs = _source.GetBytes(2);

                    if (vrt[0] != vrs[0] || vrt[1] != vrs[1]) {
                        // implicit VR
                        if (_syntax.Endian == Endian.Little)
                            _syntax = DicomTransferSyntax.ImplicitVRLittleEndian;
                        else
                            _syntax = DicomTransferSyntax.ImplicitVRBigEndian;
                    }

                    _source.Rewind();
                } while (_fileFormat == DicomFileFormat.Unknown);

                if (_fileFormat == DicomFileFormat.Unknown)
                    throw new DicomReaderException("Attempted to read invalid DICOM file");

                var obs = new DicomReaderCallbackObserver();
                if (_fileFormat != DicomFileFormat.DICOM3) {
                    obs.Add(DicomTag.RecognitionCodeRETIRED, (object sender, DicomReaderEventArgs ea) => {
                        try {
                            string code = Encoding.UTF8.GetString(ea.Data.Data, 0, ea.Data.Data.Length);
                            if (code == "ACR-NEMA 1.0")
                                _fileFormat = DicomFileFormat.ACRNEMA1;
                            else if (code == "ACR-NEMA 2.0")
                                _fileFormat = DicomFileFormat.ACRNEMA2;
                        } catch {
                        }
                    });
                }
                obs.Add(DicomTag.TransferSyntaxUID, (object sender, DicomReaderEventArgs ea) => {
                    try {
                        string uid = Encoding.UTF8.GetString(ea.Data.Data, 0, ea.Data.Data.Length);
                        _syntax = DicomTransferSyntax.Parse(uid);
                    } catch {
                    }
                });

                _source.Endian = _syntax.Endian;
                _reader.IsExplicitVR = _syntax.IsExplicitVR;

此后DicomReader的ParseDataset函数会严格按照上述代码获得的_syntax来逐个解析Dataset中的各个元素。继续单步调试发现,之所以抛除上述异常时因为TransferSyntax表明的元素编码组成为ImplicitVR,即DicomReader的IsExplicitVR=false,而Dataset中是按照ExplicitVR存储的,由于IsExplicitVR被错误的设置为了false,导致将VR解析成了VL,这与弹出的异常信息相一致。
因此在ParseDataset函数内部,当_state == ParseState.Tag且标签group>0002的时刻,需要添加对Transfer Syntax的自动校验,以确保File Meta Information的Transfer Syntax与Dataset真实存储相一致。详细代码参见我的github主页中的fo-dicom仓库

引申思考:

通过上述错误,以及之前博文遇到的GE私有Transfer SyntaxDICOM:Transfer Syntax传输语义之奇葩GE Private TS,是否可以运用技术手段(简单的状态机,或者更高端的机器学习)给出一种自动且高鲁棒性的解析任意格式的DICOM文件方法。考虑到效率原因,只有在常见的dicom库(dcmtk、dcm4che以及fo-dicom等)解析出现异常时调用。抽空尝试写一下,以能够解析任意DICOM文件为终极目的,如果大家有啥想法欢迎邮件或私信交流。




作者:[email protected]
时间:2016-06-25

你可能感兴趣的:(DICOM,DICOM医学图像处理)