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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
单步调试,可以知道dcmtk加载dicom文件的流程如下:
- 创建DcmMetaInfo、DcmDataset元素
- 分别加载DcmMetaInfo、DcmDataset元素
- 使用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;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
在警告后,对于非标准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;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
由上述代码可知,0028,0120的VR=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,0120的VR=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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
并且在dcm4che3中对于UN类型定义为
此处UN类型是参照上述截图中DICOM3.0标准对于VR=UN(unknown)类型的标签约束来定义的,即,其VR字段应该是四个字节。然而此处0028,0120的VR=20,20后的Value Length只有两个字节02 00。因此导致dcm4che3在加载0028,0120元素时,将其长度错误地解析为4163895298,即十六进制的F8 30 00 02,如下图所示:
解决方案:
至此我们找到了dcm4che3错误解析0028,0120的VR=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
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
测试文件下载:
本文中使用的测试数据已经上传到了我Github的CSDN仓库中,可自行下载,为了保护患者隐私已经进行了匿名化处理。
Download Non-standard VR test dcm file
后续博文介绍:
1. 由dcm4che3.x库看Java流操作之”流的拷贝”
2. Eclipse自动编译dcm4che3.x源码
3. DICOM三大开源库对比分析之“数据加载”(续)
DICOM:DICOM三大开源库对比分析之“数据加载”的更多相关文章
- Android 7.0 Gallery图库源码分析3 - 数据加载及显示流程
前面分析Gallery启动流程时,说了传给DataManager的data的key是AlbumSetPage.KEY_MEDIA_PATH,value值,是”/combo/{/local/all,/p ...
- 使用开源库 SVPullToRefresh 实现上拉加载下拉刷新
SVPullToRefresh开源库地址 https://github.com/samvermette/SVPullToRefresh 将整个文件夹SVPullToRefresh拖入工程中并引入头文件 ...
- 转: 三大WEB服务器对比分析(apache ,lighttpd,nginx) (2008年的旧文,仅供参考之用)
from: http://www.blogjava.net/daniel-tu/archive/2008/12/29/248883.html 三大WEB服务器对比分析(apache ,lighttp ...
- 分析ELF的加载过程
http://blog.chinaunix.net/uid-72446-id-2060538.html 对于可执行文件来说,段的加载位置是固定的,程序段表中如实反映了段的加载地址.对于共享库来?段的加 ...
- 第42天学习打卡(Class类 Class类的常用方法 内存分析 类的加载过程 类加载器 反射操作泛型 反射操作注解)
Class类 对象照镜子后得到的信息:某个类的属性.方法和构造器.某个类到底实现了哪些接口.对于每个类而言,JRE都为其保留一个不变的Class类型的对象.一个Class对象包含了特定某个结构(cla ...
- Android4.0图库Gallery2代码分析(二) 数据管理和数据加载
Android4.0图库Gallery2代码分析(二) 数据管理和数据加载 2012-09-07 11:19 8152人阅读 评论(12) 收藏 举报 代码分析android相册优化工作 Androi ...
- Linux gadget驱动分析1------驱动加载过程
为了解决一个问题,简单看了一遍linux gadget驱动的加载流程.做一下记录. 使用的内核为linux 2.6.35 硬件为芯唐NUC950. gadget是在UDC驱动上面的一层,如果要编写ga ...
- 从SpringBoot源码分析 配置文件的加载原理和优先级
本文从SpringBoot源码分析 配置文件的加载原理和配置文件的优先级 跟入源码之前,先提一个问题: SpringBoot 既可以加载指定目录下的配置文件获取配置项,也可以通过启动参数( ...
- 使用 Golang 代码生成图表的开源库对比
本文的目标读者 对用 Golang 代码生成折线图.扇形图等图表有兴趣的朋友. 本文摘要 主要介绍 Go 中用以绘图的开源库,分别是: GitHub - wcharczuk/go-chart: go ...
随机推荐
- 面试题13:在O(1)时间删除链表节点
注意分情况讨论: 1. 要删除的不是尾节点 2. 链表只有一个节点 3. 链表中有多个节点,删除尾节点 void DeleteNode(ListNode** pListHead, ListNode* ...
- Asp.net vNext 学习之路(二)
View component(视图组件)应该是MVC6 新加的一个东西,类似于分部视图.本文将演示在mvc 6中 怎么添加视图组件以及怎么在视图中注入一个服务. 本文包括以下内容: 1,创建一个新的a ...
- 【51nod】2026 Gcd and Lcm
题解 话说LOJ说我今天宜学数论= =看到小迪学了杜教筛去蹭了一波小迪做的题 标解的杜教筛的函数不懂啊,怎么推的毫无思路= = 所以写了个复杂度稍微高一点的?? 首先,我们发现f是个积性函数,那么我们 ...
- SQL Server中Rowcount与@@Rowcount
rowcount的用法 rowcount的作用就是用来限定后面的sql在返回指定的行数之后便停止处理,比如下面的示例, set rowcount 10 select * from 表A 这样的查询只会 ...
- 【转】Serializers 序列化组件
https://www.cnblogs.com/MayDayTime/p/9890582.html 为什么要用序列化组件 当我们做前后端分离的项目~~我们前后端交互一般都选择JSON数据格式,JSON ...
- thinkphp5.0 配置
ThinkPHP提供了灵活的全局配置功能,采用最有效率的PHP返回数组方式定义,支持惯例配置.公共配置.模块配置.场景配置和动态配置. 对于有些简单的应用,你无需配置任何配置文件,而对于复杂的要求,你 ...
- React篇章-React 组件
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title&g ...
- openstack vm实例pxe无法启动
问题如下: 创建vm没有任何报错,打开控制台提示: SeaBIOS (versio xxxxxxx) Machine UUID xxxxxxxxxx iPXE (http://ipxe.org) 00 ...
- bzoj 3573: [Hnoi2014]米特运输
3573: [Hnoi2014]米特运输 Description 米特是D星球上一种非常神秘的物质,蕴含着巨大的能量.在以米特为主要能源的D星上,这种米特能源的运输和储存一直是一个大问题. D星 ...
- bzoj1036 count 树链剖分或LCT
这道题很久以前用树链剖分写的,最近在学LCT ,就用LCT再写了一遍,也有一些收获. 因为这道题点权可以是负数,所以在update时就要注意一下,因为平时我的0节点表示空,它的点权为0,这样可以处理点 ...