背景:
专栏中曾分别写过dcmqrscp.exe与storescu.exe工具包的介绍,但是并未深究两者之间的差别。dcmqrscp.exe工具是一个mini版PACS,可分别响应C-FIND、C-MOVE、C-GET、C-STORE等各种DIMSE服务,而storescu.exe工具是C-STORE服务使用者,可以看出这两个工具包都提供C-STORE SCU服务。前几天博友的一个问题使得重新研究了一下两者在实现C-STORE SCU服务上的差别。通过源码分析发现问题根源出在Presentation
Context构建方式不同,下面通过具体示例引出问题,最后给出一个蹩脚的解决方案。
博友提出的问题:
起初看到提问后简单的猜想是“参数设置有误”,但是周末回来在自己电脑上尝试了多种参数组合方式也没能解决问题,因此决定从源码入手查找问题的根源。下面简单记录一下错误排查的整个过程,按照由浅入深的思路,分别从工具包官方文档配置说明、多工具包实际测试和源码分析三个方面来阐述。
DCMTK工具包配置官方文档:
dcmqrscp.exe工具:
之前在博文DICOM医学图像处理:DCMTK的wiki资料学习之PACS调试中曾详细介绍过关于dcmqrscp.exe工具的具体配置,由于本地测试数据都采用DICOM标准中默认支持的非压缩格式,即1.2.840.10008.1.2.1,Little Endian Explicit,因此当时并未涉及到DCM文件传输语义(Transfer
Syntax)的设置。而此次博友y317215133y遇到的问题多半与Transfer Syntax相关,因此需要重新学习官网中关于传输语义的说明。
dcmqrscp.exe工具包官网中关于Transfer Syntax传输语义的主要配置参数如下图(详情参见http://support.dcmtk.org/docs/dcmqrscp.html。):
注意观察上图中矩形框标记的两部分,其内部的含义大多是重复的,那么为什么会分为两部分呢?并且分别以prefer和propose为前缀?其中的incoming association和outgoing association又分别代表什么呢?因为dcmqrscp.exe充当的是miniPACS,其中的DIMSE服务既有SCU端,也有SCP端,例如在C-MOVE服务中,dcmqrscp.exe工具同时扮演了C-MOVE SCP和C-STORE SCU两种角色。由此可以猜测上图中的传输语义分成两部分的原因应该是分别对应SCP和SCU端。为了验证这一想法,让我们继续看一下其他工具的参数配置。
storescu.exe工具:
关于storescu.exe工具的使用可阅读之前的两篇博文DICOM医学图像处理:storescp.exe与storescu.exe源码剖析,学习C-STORE请求和DICOM医学图形处理:storescp.exe与storescu.exe源码剖析,学习C-STORE请求(续),这里我们只关注storescu.exe工具包的参数配置,浏览官网文档(http://support.dcmtk.org/docs/storescu.html)得到如下结果:
通过上图基本验证了上一小节的猜想,propose与prefer分别用于控制SCU和SCP两端的传输语义,因此storescu.exe工具配置项中只有以propose为前缀的参数。为了确定我们的猜想,继续看一下storescp.exe工具的配置。
storescp.exe工具:
不罗嗦,直接贴出storescp.exe官方的配置说明(http://support.dcmtk.org/docs/storescp.html),如下图所示:
因为storescp.exe在DIMSE服务中充当SCP角色,因此其配置项中只存在以prefer为前缀的参数,至此验证了我们的猜测。
问题本地测试:
那么现在可以回忆一下博文开始时博友y317215133y提出的问题,它开启了dcmqrscp.exe充当PACS服务器,然后系统利用movescu.exe工具从dcmqrscp.exe服务端同时查询并下载非压缩Little Endian Explicit和JPEG无损压缩两种数据。
首先按照上文的分析,我们知道dcmqrscp.exe和movescu.exe工具包的配置参数中都包含prefer和propose两类。以C-MOVE服务为例,分析两个工具在具体实施过程中充当的角色,如下表:
工具包 | C-MOVE | C-STORE |
dcmqrscp.exe | SCP | SCU |
movescu.exe | SCU | SCP |
因此从参数配置来看想要同时查询并下载到压缩和非压缩两种数据,我们应该分别配置C-MOVE子服务C-STORE的两端。如是的话,大致配置过程如下表:
C-STORE SCP | movescu.exe +xa,此处的意思是movescu.exe工具开启的C-STORE SCP服务允许接收存储各种类型的数据 |
C-STORE SCU | dcmqrscp.exe –xs,通过添加-xs指出dcmqrscp.exe工具包中开启的C-STORE SCU服务允许读取并发送JPEG无损压缩的数据
dcmqrscp.exe -x=,通过添加-x=指出dcmqrscp.exe工具包中开启的C-STORE SCU服务允许读取并发送默认非压缩格式的数据 【注】:工具包中对于传输语义(Transfer Syntax)的参数配置是单选方式,不能通过设置多个参数来完成传输语义的任意组合,例如上述如果同时开启-xs,-x=,那么按照先后顺序-xs会覆盖-x=的设置,导致-xs设置无效。 |
默认非压缩数据的本地测试:
进入命令行模式,输入下述指令开启服务端,
dcmqrscp.exe –d –c d:\DcmScuScp\dcmqrscp.cfg
客户端使用STUDY级别查询来下载数据,指令如下:
movescu.exe -d -S -aec ACME_STORE -aet ACME1 -aem ACME1 --port 12345 -od d:\ localhost 11110 -k QueryRetrieveLevel=STUDY -k StudyInstanceUID=1.3.6.1.4.1.30071.6.116521528759.4534135570203453
movescu.exe指令中的参数具体数值,例如ACME_SOTRE、ACME1,都是参照之前博文中的dcmqrscp.cfg配置文件来的,具体的可阅读博文DICOM医学图像处理:DCMTK的wiki资料学习之PACS调试。实际运行结果图如下:
从dcmqrscp.exe和movescu.exe的输出结果可以看出,利用StudyInstanceUID=1.3.6.1.4.1.30071.6.116521528759.4534135570203453查询获取到了两组数据,其中传统的非压缩数据,即传输语义为Little Endian Explicit,已经成功保存到D:\(如下图所示),而另一组JPEG无损压缩数据传输失败,具体提示错误如上图白色框所示。
JPEG无损压缩数据的本地测试:
按照之前的分析,要想下载JPEG LossLess无损压缩数据,需要C-STORE的SCU和SCP两端开启对应的传输语义,即JPEG lossless TS。上一节已经说明了dcmqrscp.exe在C-STORE服务中充当的是SCU角色,movescu.exe充当的是SCP角色。重新设置SCU和SCP如下:
输入下述指令,开启dcmqrscp.exe服务端,
dcmqrscp.exe –d –c D:\dcmscuscp\dcmqrscp.cfg
–xs
输入下述指令,重新开启movescu.exe客户端,
movescu.exe -d -S -aec ACME_STORE -aet ACME1 -aem ACME1 --port 12345 -od d:\ localhost 11110 -k QueryRetrieveLevel=STUDY -k StudyInstanceUID=1.3.6.1.4.1.30071.6.116521528759.4534135570203453
+xs
实际测试结果与非压缩数据基本类似,如下图:
修改完传输语义后,这次在D盘根目录下只看到了JPEG LL无损压缩的数据(如下图),
对比两次dcmqrscp.exe服务端的输出错误提示,可以发现在C-STORE SCU和SCP双方都为对JPEG Lossless传输语义进行单独设置时,movescu只能下载非压缩数据,对于JPEG Lossless压缩数据服务端提示不能由JPEG Lossless, Non-hierarchical, 1st Order Prediction语义转换成Little Endian Explicit语义;而单独设置完JPEG
Lossless语义参数后,对于非压缩数据dcmqrscp.exe服务端提示不能由Little Endian Explicit语义转换成JPEG Lossless, Non-hierarchical, 1st Order Prediction语义。——这也就出现了前文中博友遇到的实际问题。
问题分析:
追根溯源:
让我们从根源来重新屡一下,movescu查询分别有Patient、Study、Series三个级别,是什么情况下才会同时查询到目标数据且分别包含压缩和非压缩两种类型呢?Patient级别下可以包含多个Study检查,而每个检查通常对应不同的影像设备,因此以Patient级别的查询有很大概率出现这种情况,如博文中给出的一个实例如下图:
上面我们一直使用的是Study级别的查询,那么会不会是查询级别出现问题呢?通过再次上传两组同一Patient的两套压缩和非压缩数据测试后,我可以明确地说不是由于查询级别导致的问题,那么问题究竟出在那里呢?是服务端dcmqrscp.exe?还是客户端movescu.exe?
从测试数据构造入手:
突然想到,起初为了复现博文中的问题,是通过手动修改同一Study下的同一Sereis中的两张图片,然后通过两次storescu.exe来存储到dcmqrscp.exe服务端来构造测试数据的。当时两次storescu.exe工具在提交不同格式数据时也用到了传输语义参数设置,让我们再回顾一下:
构造非压缩数据:
服务端:dcmqrscp.exe –d –c d:\dcmscuscp\dcmqrscp.cfg
客户端:storescu.exe -d localhost 11110 -aec ACME_STORE -aet ACME1 c:\test.dcm
本地测试结果如下,顺利完成测试数据上传
构造JPEG Lossless无损压缩数据:
服务端:dcmqrscp.exe –d –c d:\dcmscuscp\dcmqrscp.cfg
客户端:storescu.exe -d localhost 11110 -aec ACME_STORE -aet ACME1 c:\testLS.dcm
–xs
如是输入,发现出现了我们很熟悉的错误,无法从JPEG Lossless, Non-hierarchical, 1st Order Prediction语义转换成Little Endian Explicit语义,如下图:
按照之前的分析来看,应该是服务端配置有误,dcmqrscp.exe中的C-STORE SCP需要开启+xs
重新开启服务端:dcmqrscp.exe –d –c d:\dcmscuscp\dcmqrscp.cfg
+xs
客户端:storescu.exe -d localhost 11110 -aec ACME_STORE -aet ACME1 c:\testLS.dcm
–xs
利用storescp.exe排查:
在构造完成测试数据时,当下认为无法解答博友的问题,只能分别开启不同的服务来实现同时下载压缩和非压缩数据到本地。因为通过构造测试数据的整个过程已经很清楚的了解和掌握了工具包中关于传输语义的设置,却并未找到问题。利用源码调试也没有梳理出任何头绪,难道问题就这样终止了?
静下来想了想,dcmqrscp.exe是一个miniPACS服务端,功能比较复杂,在调试源码时不方便,那么能不能利用之前介绍C-STORE的博文,使用storescp.exe和storescu.exe两个工具包来调试查看一下具体的传输语义设置呢?
输入指令单独开启C-STORE SCP服务端:
storescp.exe –d 11110 –od c:\ +xa,此处为了省事直接使用+xa设置允许客户端的任何传输语义
输入指令开启C-STORE SCU客户端:
storescu.exe –d localhost 11110 c:\test.dcm
stroescu.exe –d localhost 11110 c:\testLS.dcm –xs
storescu.exe –d localhost 11110 c:\test.dcm –xs
在实际测试过程中发现服务单storescu.exe在开启-xs开关后,竟然可以同时实现非压缩和JPEG Lossless无损压缩两组数据
storescu.exe与dcmqrscp.exe源码对比分析:
既然已经验证了storescu.exe工具可以同时上传非压缩和压缩数据到服务端,那么问题应该出现在dcmqrscp.exe工具内部。在实现C-MOVE功能时,dcmqrscp.exe工具需要提供C-STORE SCU服务,因此猜测可能两者开启C-STORE SCU服务的方式不同,导致出现了博文中不能同时传输两种格式数据的问题。
分别找到storescu.exe工具和dcmqrscp.exe工具中关于开启C-STORE SCU服务的代码,主要查看关于Presentation Context设置部分(关于PresentationContext的介绍可参照博文DICOM医学图像处理:DICOM网络传输)
通过上图可以看出在storescu.exe工具包中对于PresentationContext的设置的鲁棒性比较强,例如在添加了-xs参数后,storescu.exe的addStoragePresentationContexts函数会添加两种PresentationContext,一种是包含JPEGLossLess压缩传输语义的(即下图中的preferredTransferSyntax),一种是不包含JPEGLossLess压缩传输语义的(fallbackSyntaxes),当然还有一种是两中语义都包含的,即combinedSyntaxes。
而dcmqrscp.exe通过moveCallback开启的sub operation(C-STORE SCU),在添加PresentationContext时虽然根据-xs选项也构造了TransferSyntax数组,包含下面四种传输语义(如下图),但是在添加PresentationContext时并未针对UID_JPEGProcess14SV1TransferSyntax和UID_LittleEndianExplicitTransferSyntax分别添加,因此导致当开启了-xs开关后,CTImageStorage对应的PresentationContext只能接受UID_JPEGProcess14SV1TransferSyntax无损压缩传输语义了。
为了验证上述猜测,我们利用命令行重定向工具将dcmqrscp.exe和storescu.exe配合构造测试数据时的调试信息写入到文件中,对比dcmqrscp.exe和movescu.exe工具查询下载数据的调试信息,看一下两者中C-STORE SCU服务对应的PresentationContext是否不同,详情如下图所示:
讲到这里博文中的问题的根源就算找到了,那么其对应的依据又在哪里呢?之前博文也提到过DICOM3.0标准第7部分附录D中有关于DICOM Association建立的具体细节,其中有这么一段话:
从中我们可以看出,对于同一种服务也就是PresentationContext中提到的AbstractSyntax,即本博文中的CTImageStorage,要想实现多种传输语义,就需要针对每种语义分别构造PresentationContext,如是才能实现同时兼容多种传输语义的目的。
修改尝试:
既然找到问题根源了,最后该是给出具体解决方案的时刻了,最恰当的修改方案应该是修改dcmqrscp.exe工具包中对应的addAllStoragePresentationContexts函数(具体代码在dcmqrcbm.cc文件的516行),在添加PresentationContext时根据传输语义状况分别添加PresentationContext,具体添加代码可仿照storescu.exe工具包中的addStoragePresentationContexts函数(具体代码在storescu.cc文件的1167行)内的1263-1280行。
这里由于时间关系我就不具体介绍修改过程了,为了演示本博文的分析,这里有一个简单的方法。当然该方法由于影响面较广,修改后对整个DCMTK库都有影响,因此不推荐使用。简单的方法就是直接修改dimse.cc中的DIMSE_sendMessage函数,如下图所示:
既然由于期望的传输语义(propose)与具体文件的实际传输语义(TransferSyntax UID)不匹配导致无法发送文件,那么在DIMSE_sendMessage函数内部当检测到两者不匹配时默认采用DCM文件原始的传输语义发送,需要注意的是此时在movescu.exe端需要开启+xa服务,允许接收所有传输语义。修改完成上述代码后,重新编译dcmnet工程,生成dcmnet.lib文件后即可实现同时下载两种传输语义的数据了,下图是我本机的测试结果:
最后记得修改完后,在还原回来。最靠谱的方法是修改dcmqrscp.exe工程中的代码,不要修改DCMTK库的基础文件(dimse.cc)代码。
(本文完)
作者:[email protected]时间:2015-01-17