DICOM:DICOM三大开源库对比分析之“数据加载”

背景:

上一篇博文DICOM:DICOM万能编辑工具之Sante DICOM Editor介绍了DICOM万能编辑工具,在日常使用过程中发现,“只要Sante DICOM Editor打不开的数据,基本可以判定此DICOM文件格式错误(准确率达99.9999%^_^)”。在感叹Sante DICOM Editor神器牛掰的同时,想了解一下其底层是如何实现的。通过日常使用以及阅读软件帮助手册推断其底层依赖库很可能是dcmtk,就如同本人使用dcmtk、fo-dicom、dcm4che3等诸多DICOM开源库遇到的兼容性问题类似,——dcmtk兼容性最强,fo-dicom次之,dcm4che3最差

问题:

本篇通过对比dcmtk3.6与dcm4che3.x解析同一特殊dicom文件包含非标准VR的元素)分析dcmtk、dcm4che以及fo-dicom数据加载的兼容性问题。

特殊的dicom文件内容如下:

28 00 20 01 20 20 02 00 30 F8,具体描述如下:

使用dcmtk与fo-dicom加载数据时都未出现错误,例如dcmtk加载数据时的提示如下:

由此可以看出dcmtk已经顺利识别出了非标准VR的元素(0028,0120),并成功加载。

虽然使用fo-dicom加载数据没有出现错误,但是对于上述非标准VR的元素(0028,0120)后的元素未顺利加载,如下图所示:

而dcm4che3加载过程中直接弹出了错误,如下所示:

问题分析:

出现该问题的原因是dcm4che3和fo-dicom在解析0028,0120元素时,对于20 20的非标准VR无法识别。下文中将通过分析dcm4che3与dcmtk的源码来定位问题的具体位置并给出解决方案(此处暂时只对比分析了dcm4che3.3.8最新版与dmctk3.6的源码,对于fo-dicom的源码分析待后续整理完成后再补充)。

1. dcmtk3.6源码:

使用dcmtk编写本次数据加载测试工程,简单的示例代码如下:

int main()
{
    OFLog::configure(OFLogger::TRACE_LOG_LEVEL);
    char* ifname = "c:\\1.dcm";
    E_FileReadMode readMode = /*ERM_fileOnly*/ERM_autoDetect;
    E_TransferSyntax xfer =  EXS_Unknown;
    Uint32 maxReadLength = DCM_MaxReadLength;
    bool loadIntoMemory = true;
    DcmFileFormat dfile;
    DcmObject *dset = &dfile;
    if (readMode == ERM_dataset) dset = dfile.getDataset();
    OFCondition cond = dfile.loadFile(ifname, xfer, EGL_noChange, maxReadLength, readMode);
    if (cond.bad())
    {
        return 1;
    }
    return 0;
}

单步调试,可以知道dcmtk加载dicom文件的流程如下:

  1. 创建DcmMetaInfo、DcmDataset元素
  2. 分别加载DcmMetaInfo、DcmDataset元素
  3. 使用DcmItem中的readGroupLength、readTagAndLength、readSubElement逐步加载DcmMetaInfo、DcmDataset的各个子元素。

在DcmItem类中对于非标准VR元素有相应的警告提示信息,

/* if the VR which was read is not a standard VR, print a warning */
        if (!vr.isStandard())
        {
            OFOStringStream oss;
            oss << "DcmItem: Non-standard VR ‘"
                << ((OFstatic_cast(unsigned char, vrstr[0]) < 32) ? ‘ ‘ : vrstr[0])
                << ((OFstatic_cast(unsigned char, vrstr[1]) < 32) ? ‘ ‘ : vrstr[1]) << "‘ ("
                << STD_NAMESPACE hex << STD_NAMESPACE setfill(‘0‘)
                << STD_NAMESPACE setw(2) << OFstatic_cast(unsigned int, vrstr[0] & 0xff) << "\\"
                << STD_NAMESPACE setw(2) << OFstatic_cast(unsigned int, vrstr[1] & 0xff)
                << ") encountered while parsing element " << newTag << OFStringStream_ends;
            OFSTRINGSTREAM_GETSTR(oss, tmpString)
            /* encoding of this data element might be wrong, try to correct it */
            if (dcmAcceptUnexpectedImplicitEncoding.get())
            {
                DCMDATA_WARN(tmpString << ", trying again with Implicit VR Little Endian");
                /* put back read bytes to input stream ... */
                inStream.putback();
                bytesRead = 0;
                /* ... and retry with Implicit VR Little Endian transfer syntax */
                return readTagAndLength(inStream, EXS_LittleEndianImplicit, tag, length, bytesRead);
            } else {
                DCMDATA_WARN(tmpString << ", assuming " << (vr.usesExtendedLengthEncoding() ? "4" : "2")
                    << " byte length field");
            }
            OFSTRINGSTREAM_FREESTR(tmpString)
        }

        /* set the VR which was read in the above created tag object. */
        newTag.setVR(vr);

        /* increase counter by 2 */
        bytesRead += 2;

在警告后,对于非标准VR元素的处理过程如下:

 /* read the value in the length field. In some cases, it is 4 bytes wide, in other */
    /* cases only 2 bytes (see DICOM standard part 5, section 7.1.1) */
    if (xferSyn.isImplicitVR() || nxtobj == EVR_na)   //note that delimitation items don‘t have a VR
    {
        inStream.read(&valueLength, 4);            //length field is 4 bytes wide
        swapIfNecessary(gLocalByteOrder, byteOrder, &valueLength, 4, 4);
        bytesRead += 4;
    } else {                                       //the transfer syntax is explicit VR
        DcmVR vr(newTag.getEVR());
        if (vr.usesExtendedLengthEncoding())
        {
            Uint16 reserved;
            inStream.read(&reserved, 2);           // 2 reserved bytes
            inStream.read(&valueLength, 4);        // length field is 4 bytes wide
            swapIfNecessary(gLocalByteOrder, byteOrder, &valueLength, 4, 4);
            bytesRead += 6;
        } else {
            Uint16 tmpValueLength;
            inStream.read(&tmpValueLength, 2);     // length field is 2 bytes wide
            swapIfNecessary(gLocalByteOrder, byteOrder, &tmpValueLength, 2, 2);
            bytesRead += 2;
            valueLength = tmpValueLength;
        }
    }

由上述代码可知,0028,0120VR=20,20,被dcmtk解析为 EVR_UNKNOWN2B类型,如同代码注释中所描述:

/// used internally for elements with unknown VR with 2-byte length field in explicit VR

EVR_UNKNOWN2B

DICOM标准PS5的7.1.2有对于非标准VR的相关描述,如下:

2. dcm4che3.3.8源码:

再对比dcm4che3.3.8的源码,单步调试发现,对于0028,0120VR=20,20,被dcmtk直接标记为UN类型,

public static VR valueOf(int code) {
        try {
            VR vr = VALUE_OF[indexOf(code)];
            if (vr != null)
                return vr;
        } catch (IndexOutOfBoundsException e) {}
        LOG.warn("Unrecogniced VR code: {0}H - treat as UN",
                Integer.toHexString(code));
        return UN;
        }

并且在dcm4che3中对于UN类型定义为

此处UN类型是参照上述截图中DICOM3.0标准对于VR=UN(unknown)类型的标签约束来定义的,即,其VR字段应该是四个字节。然而此处0028,0120VR=20,20后的Value Length只有两个字节02 00。因此导致dcm4che3在加载0028,0120元素时,将其长度错误地解析为4163895298,即十六进制的F8 30 00 02,如下图所示:

解决方案:

至此我们找到了dcm4che3错误解析0028,0120VR=20,20非标准VR元素的原因。对于这种非标准VR不能统一当做VR.UN类型进行处理,而应该根据其后续的Value Length的具体长度为2或者4来进行分类处理关于该问题后续博文会继续深入剖析,请注意),需要修改的地方有两处:

1. 正确解析Non-standard VR:

//VR.java,Line 110
public static VR valueOf(int code) {
        try {
            VR vr = VALUE_OF[indexOf(code)];
            if (vr != null)
                return vr;
        } catch (IndexOutOfBoundsException e) {}
        LOG.warn("Unrecogniced VR code: {0}H - treat as UN",
                Integer.toHexString(code));
        //return UN;
        LOG.warn("zssure:to solve non-standard VR,Unrecogniced VR code: {0}H - treat as UN",
                Integer.toHexString(code));
        return null;//zssure:to solve non-standard VR
    }

2. 正确读取Non-standard VR的VL:

//DicomInputStream.java Line 386
 public int readHeader() throws IOException {
        byte[] buf = buffer;
        tagPos = pos;
        readFully(buf, 0, 8);
        switch(tag = ByteUtils.bytesToTag(buf, 0, bigEndian)) {
        case Tag.Item:
        case Tag.ItemDelimitationItem:
        case Tag.SequenceDelimitationItem:
           vr = null;
           break;
        default:
            if (explicitVR) {
                vr = VR.valueOf(ByteUtils.bytesToVR(buf, 4));
                //zssure:to solve non-standard VR
                //referred:dcmtk/dcitem.cc/readTagAndLength,Line 970
                if(vr == null)
                {
                    length = ByteUtils.bytesToUShort(buf, 6, bigEndian);
                    return tag;
                }
                //zssure:end
                if (vr.headerLength() == 8) {
                    length = ByteUtils.bytesToUShort(buf, 6, bigEndian);
                    return tag;
                }
                readFully(buf, 4, 4);
            } else {
                vr = VR.UN;
            }
        }
        length = ByteUtils.bytesToInt(buf, 4, bigEndian);
        return tag;
    }

测试文件下载:

本文中使用的测试数据已经上传到了我Github的CSDN仓库中,可自行下载,为了保护患者隐私已经进行了匿名化处理。

Download Non-standard VR test dcm file

后续博文介绍:

1. 由dcm4che3.x库看Java流操作之”流的拷贝”

2. Eclipse自动编译dcm4che3.x源码

3. DICOM三大开源库对比分析之“数据加载”(续)

作者:[email protected]

时间:2015-09-05

版权声明:本文为zssure原创文章,转载请注明出处,未经允许不得转载。

时间: 2024-12-26 19:58:06

DICOM:DICOM三大开源库对比分析之“数据加载”的相关文章

(android开源库android-gif-drawable)第二篇 加载网络gif图片

大家好,  今天给大家带来如何使用 android开源库android-gif-drawable来 加载网络gif图片 同样的DEMO下载地址在 最后 请大家去下载 . 如果gif图片地址无效 了.      请大家自行到网上去寻找一个 gif图片地址 复制过去就可以了.谢谢大家 不会在 eclipse下使用  (android开源库android-gif-drawable)     请看我的这篇博客   (android开源库android-gif-drawable)第一篇 eclipse使用

DICOM:DICOM开源库多线程分析之“ThreadPoolQueue in fo-dicom”

背景: 上篇博文介绍了dcm4chee中使用的Leader/Follower线程池模型,主要目的是节省上下文切换,提高运行效率.本博文同属[DICOM开源库多线程分析]系列,着重介绍fo-dicom中使用的ThreadPoolQueue线程池. ThreadPoolQueue in fo-dicom: 先看一下ThreadPoolQueue代码中自定义的数据结构, public class ThreadPoolQueue<T> { private class WorkItem { public

移动三大平台和三大开发模式对比分析

一:移动三大平台及其对比分析: 1)移动三大平台 2)移动三大平台对比分析 二:三大开发模式及其对比分析: 1)三大开发模式 2)三大开发模式对比分析 移动三大平台和三大开发模式对比分析,布布扣,bubuko.com

三大WEB服务器对比分析(apache ,lighttpd,nginx)

一.软件介绍(apache  lighttpd  nginx) 1. lighttpd Lighttpd是一个具有非常低的内存开销,cpu占用率低,效能好,以及丰富的模块等特点.lighttpd是众多OpenSource轻量级的web server中较为优秀的一个.支持FastCGI, CGI, Auth, 输出压缩(output compress), URL重写, Alias等重要功能. Lighttpd使用fastcgi方式运行php,它会使用很少的PHP进程响应很大的并发量. Fastcg

Android4.0图库Gallery2代码分析(二) 数据管理和数据加载

Android4.0图库Gallery2代码分析(二) 数据管理和数据加载 2012-09-07 11:19 8152人阅读 评论(12) 收藏 举报 代码分析android相册优化工作 Android4.0图库Gallery2代码分析(二) 数据管理和数据加载 一 图库数据管理 Gallery2的数据管理 DataManager(职责:管理数据源)- MediaSource(职责:管理数据集) - MediaSet(职责:管理数据项).DataManager中初始化所有的数据源(LocalSo

Android4.4 Telephony流程分析——SIM卡开机时的数据加载

本文代码以MTK平台Android 4.4为分析对象,与Google原生AOSP有些许差异,请读者知悉. 本文主要介绍sim卡数据的读取过程,当射频状态处于准备状态时,此时UiccCardApplication应处于AppState.APPSTATE_READY状态,我们沿着这个信号跟踪下去.阅读本文时可先阅读Android4.4 Telephony流程分析--SIM卡开机时的初始化一文,了解Radio和sim卡状态更新过程. 先来看一下数据加载的序列图: step1~step3,走的是更新过程

【Java&amp;amp;Android开源库代码分析】のandroid-async-http の开盘

在<[Java&Android开源库代码剖析]のandroid-smart-image-view>一文中我们提到了android-async-http这个开源库,本文正式开篇来具体介绍这个库的实现,同一时候结合源代码探讨怎样设计一个优雅的Android网络请求框架.做过一段时间Android开发的同学应该对这个库不陌生,由于它对Apache的HttpClient API的封装使得开发人员能够简洁优雅的实现网络请求和响应,而且同一时候支持同步和异步请求. 网络请求框架一般至少须要具备例如

链接库DLL的概念,加载方式的区别

使用LR进行基于windows socket协议做接口测试,只提供了lr_load_dll方法来动态加载动态链接库.之前学习阶段,对TinyXML的学习,使用的静态链接库,当时在程序调用的时候方法也跟LR里的不一样,那问题来了:lib和dll的区别是什么,每种链接库有多少种加载方式,怎么加载呢. 链接库可以向应用程序提供一些函数,变量和类.动态链接库的动态调用(也叫显式调用,手工加载)我是可以运用了,但是静态调用(也叫隐式调用,自动加载).静态链接库:lib中的函数不仅被连接,全部实现都被直接包

【Spring源码分析】Bean加载流程概览

代码入口 之前写文章都会啰啰嗦嗦一大堆再开始,进入[Spring源码分析]这个板块就直接切入正题了. 很多朋友可能想看Spring源码,但是不知道应当如何入手去看,这个可以理解:Java开发者通常从事的都是Java Web的工作,对于程序员来说,一个Web项目用到Spring,只是配置一下配置文件而已,Spring的加载过程相对是不太透明的,不太好去找加载的代码入口. 下面有很简单的一段代码可以作为Spring代码加载的入口: 1 ApplicationContext ac = new Clas