DICOM医学图像处理:DIMSE消息发送与接收“⼤同⼩异”之DCMTKfo-
dicommDCM
背景:
从DICOM⽹络传输⼀⽂开始,相继介绍了C-ECHO、C-FIND、C-STORE、C-MOVE等DIMSE-C服务的简单实现,博⽂中的代码给出的实例都是基于fo-dicom库来实现的,原因只有⼀个:基于C#的fo-dicom库具有⾼封装性。对于初学者来说实现⼤多数的DIMSE-C、DIMSE-N服务⼏乎都是“傻⽠式”操作——构造C-XXX-RQ、N-XXX-RQ然后绑定相应的OnResponseReceived处理函数即可。本博⽂希望在前⼏篇预热的基础上,对⽐DCMTK、fo-dicom、mDCM三种库构建DIMSE消息的具体操作,来分析⼀下三者对于DIMSE消息的发送和接收的实现,为后续搭建简易版的Dicom Server服务器做准备。
DIMSE:
DIMSE,是DICOM Message Service Element的简称。DICOM3.0第7部分指出:DIMSE为对等DICOM应⽤实体进⾏医学影像及相关信息交换提供了⼀种应⽤服务元素定义(Application Service Element),包括服务和协议(DIMSE Service 和DIMSE Protocol)。
DIMSE Protocol:
DIMSE基于DIMSE协议来提供服务,DIMSE协议规定了构造消息必需的编码规则。⼀条DICOM MESSAGE由固定的指令集合(Command Set),外加可选择的数据集合(Data Set)构成,如下截图所⽰:
DIMSE Services:
DIMSE服务因操作SOP类型的不同分为DIMSE-C Services和DIMSE-N Services,DIMSE-C服务⽀持在对等DICOM实体间进⾏Composite SOP Instance操作,主要包括C-ECHO、C-FIND、C-STORE、C-MOVE、C-GET等;⽽DIMSE-N服务⽀持Normalized SOP Instance操作,主要包括N-EVENT-REPORT、N-GET、N-SET、N-CREATE、N-ACTION、N-DELETE。
从上图可以看出DIMSE-C服务只提供操作服务,即对等DICOM实体⼀⽅请求另⼀⽅对Composite SOP Instance进⾏操作(operation);⽽DIMSE-N服务除了提供操作以外,还提供通知(notification)服务。DIMSE中的所有操作和通知都是确认服务(confirmed services),即⼀⽅发出的请求都需要得到对⽅的应答(原
⽂:All DIMSE operations and notifications are confirmed services. The performing DIMSE-service-user shall report the response of each operation or notificationover the same Association on which the operation or notification was invoked.)。每种服务具体⽅式不同,例如某些操作可能会触发后续
的⼦操作、某些操作可能需要多个响应等等,如下图:
DCMTK、fo-dicom、mDCM构建DIMSE-C消息:
DIMSE-C服务在医学领域应⽤最⼴泛,常见的PACS、HIS、RIS、LIS等系统都会⽤到,⽽DIMSE-N服务主要应⽤在MPPS和DICOM打印中,⽇常学习中可能没有实际应⽤和测试的机会,因此这⾥就暂时不介绍,主要以DIMSE-C消息的构造为主,来分别介绍三种库的具体操作。
DCMTK:
DCMTK之C-ECHO:
DCMTK开源库相较于其他两者来说最⼤的优势是有完整的说明⽂档、稳定的维护团队,同时也有成功的商业产品。在源码中也给出了各种服务⼯具包,前⾯的好多博⽂都已经介绍过DCMTK的⼯具包,例如针对于C-ECHO的(博⽂后续的⼯程实例是⽤作为mini DICOM服务端进⾏测试的)。DCMTK对DIMSE-C中的各种消息的定义在dimse.h头⽂件中,其中C-ECHO-RQ消息定义如下:
/* C-ECHO */
struct T_DIMSE_C_EchoRQ {
DIC_US MessageID; /* M */
DIC_UI AffectedSOPClassUID; /* M */
T_DIMSE_DataSetType DataSetType; /* M */
} ;
struct T_DIMSE_C_EchoRSP {
DIC_US MessageIDBeingRespondedTo; /* M */
DIC_UI AffectedSOPClassUID; /* U(=) */
T_DIMSE_DataSetType DataSetType; /* M */
DIC_US DimseStatus; /* M */
unsigned int opts; /* which optional items are set */
#define O_ECHO_AFFECTEDSOPCLASSUID 0x0001
} ;
dimse.h中对于每⼀种DIMSE-C服务的请求消息(request)和响应消息(response)都给出了定义,并以union⽅式来统⼀了DICOM Message结构,如下所⽰:
/*
* Composite DIMSE Message
*/
struct T_DIMSE_Message {
T_DIMSE_Command CommandField; /* M */
union {
/* requests */
T_DIMSE_C_StoreRQ CStoreRQ;
T_DIMSE_C_EchoRQ CEchoRQ;
T_DIMSE_C_FindRQ CFindRQ;
T_DIMSE_C_GetRQ CGetRQ;
T_DIMSE_C_MoveRQ CMoveRQ;
T_DIMSE_C_CancelRQ CCancelRQ;
T_DIMSE_N_EventReportRQ NEventReportRQ;
T_DIMSE_N_GetRQ NGetRQ;
T_DIMSE_N_SetRQ NSetRQ;
T_DIMSE_N_ActionRQ NActionRQ;
T_DIMSE_N_CreateRQ NCreateRQ;
T_DIMSE_N_DeleteRQ NDeleteRQ;
/* responses */
T_DIMSE_C_StoreRSP CStoreRSP;
T_DIMSE_C_EchoRSP CEchoRSP;
T_DIMSE_C_FindRSP CFindRSP;
T_DIMSE_C_GetRSP CGetRSP;
T_DIMSE_C_MoveRSP CMoveRSP;
T_DIMSE_N_EventReportRSP NEventReportRSP;
T_DIMSE_N_GetRSP NGetRSP;
T_DIMSE_N_SetRSP NSetRSP;
T_DIMSE_N_ActionRSP NActionRSP;
T_DIMSE_N_CreateRSP NCreateRSP;
T_DIMSE_N_DeleteRSP NDeleteRSP;
} msg;
};
DICOM3.0第7部分中有关于C-ECHO消息的参数说明以及具体指令编码,正如前⽂所述Command同样也是以(Group Number,Element Number)标记的Data Element元素的集合,因此按照DICOM3.0标准中的要求只要向C-ECHO-RQ或者C-ECHO-RSP指令中插⼊规定的Data Element元素即可。
如上图所⽰,构造T_DIMSE_CEchoRQ需要填充MessageID/Affected SOP Class UID等,具体构造代码如下:(代码封装在DIMSE_echoUser函数中)
T_DIMSE_Message req, rsp;
T_ASC_PresentationContextID presID;
const char *sopClass = UID_VerificationSOPClass;
bzero((char*)&req, sizeof(req));
bzero((char*)&rsp, sizeof(rsp));
req.CommandField = DIMSE_C_ECHO_RQ;
req.msg.CEchoRQ.MessageID = msgId;
strcpy(req.msg.CEchoRQ.AffectedSOPClassUID,
sopClass);
req.msg.CEchoRQ.DataSetType = DIMSE_DATASET_NULL;
上⾯代码中的rsp与我们⾃⼰构建的req类似,唯⼀不同的是req是在C-ECHO SCU端构造,⽽rsp是在C-ECHO SCP端构造并通过⽹络传送过来的。
(具体的测试代码可参见博⽂后⽂给出的连接)
DCMTK之C-FIND:
下⾯我们看⼀下⽐较复杂的消息C-FIND,相较于C-ECHO消息,C-FIND中需要给出我们希望查询的⽬标属性列表(记住:同样也是⼀个DcmDataset类型,即Dicom Element集合)。
C-FIND-RQ消息的构造代码如下:
//定义临时变量
T_ASC_PresentationContextID presId;
T_DIMSE_C_FindRQ req;
T_DIMSE_C_FindRSP rsp;
DcmFileFormat dcmff;
OFString temp_str;
presId=ASC_findAcceptedPresentationContextID(assoc,abstractSyntax);
//构造C-FIND-RQ消息
bzero(OFreinterpret_cast(char*, &req), sizeof(req));
strcpy(req.AffectedSOPClassUID,abstractSyntax);
req.DataSetType=DIMSE_DATASET_PRESENT;
req.Priority=DIMSE_PRIORITY_LOW;
req.MessageID=assoc->nextMsgID++;
//构造数据体,即我们具体希望在C-FIND SCP端获得的信息
DcmDataset* dcmdataset=new DcmDataset();
dcmdataset->putAndInsertString(DCM_StudyInstanceUID,"");
dcmdataset->putAndInsertString(DCM_StudyDate,"");
dcmdataset->putAndInsertString(DCM_QueryRetrieveLevel,"STUDY");
DcmDataset *statusDetail = NULL;
//在DIMSE_findUser内部会将dcmdataset数据合并到req中,统⼀构成T_DIMSE_Message
OFCondition cond=DIMSE_findUser(assoc,presId,&req,dcmdataset,NULL,NULL,blockMode,dimse_timeout,&rsp,&statusDetail);
上述代码⽐较复杂的是需要构造参数列表中的Identifier元素,该元素包含了我们希望从C-FIND SCP服务端提供查询获得的属性,上⾯选择了STUDY级别的查询,因此需要添加DCM_QueryRetrieveLevel元素、StudyInstanceUID等(DCM_QueryRetrieveLevel元素必须添加,有时候会误认为添加了AffectedSOPClassUID 后就不需要了,这是错误的。否则服务端会返回如下错误,如下图)。
注:关于Patient、Study、Series等不同级别的查询的详细介绍可参考DICOM3.0标准第4部分的附录C。
DCMTK之C-STORE:
C-STORE与C-FIND类似,同样需要添加额外的数据,不同于C-FIND添加查询属性列表的是,C-STORE添加的是准备发送的DCM⽂件的数据体,即下图中的Data Set。
OFCondition cond = EC_Normal;
T_DIMSE_Message req, rsp;
DcmDataset
bzero((char*)&req, sizeof(req));
bzero((char*)&rsp, sizeof(rsp));
/* set corresponding values in the request message variable */
req.CommandField = DIMSE_C_STORE_RQ;
request->DataSetType = DIMSE_DATASET_PRESENT;
request->req.msg.CStoreRQ = *request;
暂时我们就只介绍C-ECHO、C-FIND和C-STORE三种服务的请求消息构造⽅法,其他的类似。
fo-dicom:
fo-dicom是基于C#开发的,封装性更强,封装思路更倾向于按DICOM消息流来进⾏,即fo-dicom库开发者在实现了整个DIMSE消息流框架的基础上,通过给⽤户预留各阶段的接⼝来⽅便⽤户定制⾃⼰的实现。对于DIMSE消息流框架的封装在DicomService.cs⽂件中(同时也有类似于DCMTK中的ASC_⽅⾯的封装,主要指的是A-ASSOCIATE服务及协议,在DICOM3.0第8部分有详细介绍),对于⽹络底层的封装放在DicomServer.cs⽂件中(等同于DCMTK中的DUL_层)。
DICOM Message消息的基类在DicomMessage.cs⽂件中,然后根据请求和应答派⽣了两个基类DicomRequest和DicomResponse。从fo-dicom库的封装以及fo-dicom对于Dataset的设计可以看出Command和Dataset都是数据集合,不同的是两者存储的元素类型不同。
在fo-dicom库中构造各类消息很⽅便,可谓是“傻⽠式”操作,详情如下:
fo-dicom之C-ECHO:
DicomCEchoRequest cechoRQ=new DicomCEchoRequest();
⼀⾏代码就顺利的构建了⼀个C-ECHO-RQ请求指令。分析源码可知DicomCEchoRequest继承⾃DicomRequest,DicomReqeust继承⾃DicomMessage。逐级查看各类的构造函数可以发现。虽然我们调⽤的是DicomCEchoRequest的默认构造函数,但是在相继调⽤了基类
DicomRequest(DicomCommandField.CEchoRequest, DicomUID.Verification, priority)和DicomMessage()后,顺利的完成了对C-ECHO-RQ指令中各个参数构造,其中DicomMessage中构造了空的Command Set和DataSet,DicomRequest中对MessageID、Priority、SOPClassUID以及CommandFieldType进⾏了赋值,这简直是太容易啦,不过也正因为此,刚⼊⼿的时候可能不知道如何来定制化⾃⼰的请求,以为fo-dicom库留给我们的可操作性太少,其实不然,继续往下看。
fo-dicom之C-FIND:
DicomCFindRequest cfind=DicomCFindRequest.CreateStudyQuery(patientId:”12345”);
cfind.OnResponseReceived=(rq,rsp)=>
{
//接收到C-FIND-RSP响应消息后,本机C-FIND SCU进⾏的操作
//例如可以输出到屏幕或其他窗⼝
Console.WriteLine("PatientAge:{0} PatientName:{1}", rsp.Dataset.Get<string>(DicomTag.PatientAge), rsp.Dataset.Get<string>(DicomTag.PatientName));
}
通过对⽐fo-dicom与DCMTK中C-FIND的构造,是不是觉得很容易。但是越容易学习和上⼿的东西,倘若不掌握其本质越容易忘。查看DicomCFindRequest.cs源码,可以发现CreateStudyQuery函数已经帮助我们添加了Study查询级别所需的所有字段,也就是上⽂中提到的Identifier参数部分。代码如下:
那么如果我们想像DCMTK那样⾃由添加字段怎么办?例如在已知服务端是⾃⼰定制实现的基础上来查询我们的私有字段。很简单直接覆盖⼀下CreateStudyQuery函数即可。另外fo-dicom还有⼀个⽐价便利的地⽅是将每种消息的回调函数直接绑定到消息中,程序写起来⽐较⽅便,逻辑上更清晰。
fo-dicom之C-STORE:
DicomCStoreRequest cstore=new DicomCStoreRequest(@”c:\\test4.dcm”);
在DicomCStoreRequest⼀级只需要数据要发送的dcm⽂件名(全路径名),同样通过逐级来完成CommandSet和Dataset的赋值。基本流程如下:
/// <summary>
/// Initializes DICOM C-Store request to be sent to SCP.
/// </summary>
/// <param name="file">DICOM file to be sent</param>
/// <param name="priority">Priority of request</param>
public DicomCStoreRequest(DicomFile file, DicomPriority priority = DicomPriority.Medium) : base(DicomCommandField.CStoreRequest, file.Dataset.Get<DicomUID>(DicomTag.SOPClassUID), priority) { File = file;
Dataset = file.Dataset;
SOPInstanceUID = File.Dataset.Get<DicomUID>(DicomTag.SOPInstanceUID);
}
<span > </span>//DicomRequest.cs⽂件
protected DicomRequest(DicomCommandField type, DicomUID affectedClassUid, DicomPriority priority) : base() {
Type = type;
SOPClassUID = affectedClassUid;
MessageID = GetNextMessageID();
Priority = priority;
Dataset = null;
}
<span > </span>//DicomMessage.cs⽂件
writeline输出数值变量public DicomMessage() {
Command = new DicomDataset();
Dataset = null;
}
mDCM:
下⾯来看⼀下mDCM对各种消息的构造:
mDCM之C-ECHO:
/
/DcmAssociation assoction;//已经顺利建⽴的DICOM对等实体间的连接
byte pcid=associate.FindAbstractSyntax(DicomUID.VerificationSOPClass;
SenCEchoRequest(pcid,NextMessageID(),Priority);
mDCM⽐较特殊,对于DIMSE-C服务请求的参数赋值流程与fo-dicom类似,⼤多参数赋值都在基类中完成,例如DcmClientBase中完成了MaxPDU、
Priority,DicomClient完成CallingAE和CalledAE等;⽽对于整体请求消息的拼接却⼜类似DCMTK,在SendCEchoRequest函数内部调⽤CreateRequest来完成。mDCM之C-FIND:
byte pcid = Associate.FindAbstractSyntax(FindSopClassUID);
if (Associate.GetPresentationContextResult(pcid) == DcmPresContextResult.Accept) {
DcmDataset dataset = query.ToDataset(Associate.GetAcceptedTransferSyntax(pcid));
SendCFindRequest(pcid, NextMessageID(), Priority, dataset);
在query.ToDataset函数内部完成了查询级别QueryRetrieveLevel的赋值,另外需要注意的是此时在To
Dataset函数内部调⽤了⼀个虚函数AdditonalMembers⽤于
⽅便派⽣添加⾃已要查询的Identifier元素。最终还是在SendCFindRequest函数内部利⽤CreateRequest创建C-FIND-RQ消息(在mDCM中的类型是DcmCommand)。
mDCM之C-STORE:
internal void SendCStoreRequest(byte pcid, DicomUID instUid, Stream stream) {
SendCStoreRequest(pcid, NextMessageID(), instUid, Priority, stream);
}
internal void SendCStoreRequest(byte pcid, DicomUID instUid, DcmDataset dataset) {
SendCStoreRequest(pcid, NextMessageID(), instUid, Priority, dataset);
}
在CStoreClient类内部通过Load来载⼊dcm⽂件,提取DcmDataset数据体,然后调⽤SendCStoreRequest来发送C-STORE-RQ请求(DicomCStoreClient中有
两种类型的SendCStoreRequest,⼀种是发送DcmDataset类型数据,⼀种是发送Stream类型数据)。
总结:
通过对⽐分析三种开源库对DIMSE-C服务消息的构造⽅式,可以更清晰的了解DCMTK、fo-dicom、mDCM三者各⾃的优势。如果想了解DICOM协议的细节及底
部代码的具体实现,⾃然DCMTK是⾸选,其按照Dicom Upper Layer、A-ASSOCIATE、DIMSE三层来划分的结构更⽅便我们研究DICOM⽹络传输的机制。并且DCMTK最新的3.6.1版本也逐渐开始按服务来对DUL_、ASC_、DIMSE_三类函数进⾏封装,已经实现了C-ECHO、C-STORE服务,即DcmSCU/DcmSCP和DcmStorageSCU/DcmStorageSCP。如果想快速⼊⼿,实现DICOM的相关服务,fo-dicom⾃然是⾸选,想必这对于C#程序员来说轻⽽易举(mDCM可以看做是DCMTK与fo-dicom的中间地带)。
DCMTK⼯程实例:
后续博⽂介绍:
fo-dicom搭建简单的DICOM Server
作者:
时间:2014-12-06
发布于 1年前,阅读(43) | 评论(0) | 投票(0) | 收藏(0)
原
分类:
112014-12
背景:
前段时间着重从dcmtk和fo-dicom(mDCM)源码⾓度进⾏剖析,期望加深对DICOM协议的理解。知其然,知其所以然。如果“所以然”很不好懂,那我们还是先多
多“知其然”吧。搞清楚原理的⽬的不也是为了更好的运⽤于实践么?所以理论和实践应该彼此交错进⾏,理论搞不动了就搞搞应⽤,应⽤久了就钻研钻研理论。
以前上DCMTK官⽹仅仅是浏览关于开源库中各个类的设计模式、依赖关系。最近在打开DCMTK官⽹
的wiki时,才发现OFFIS对DCMTK的介绍是如此的详细。正值国庆假⽇,就不深挖DCMTK源码了,那就按照DCMTK wiki中给出的介绍来实际体验分析⼀下DCMTK,从实践⾓度来学习⼀下。
PACSDebugging with DCMTK
前⼏篇博⽂分别介绍了worklist查询服务(DCMTK⼯具包学习和分析worklist、fo-dicom发送C-Find查询Worklist)、C-STORE服务(与源码剖析,学习C-STORE请求、与源码剖析,学习C-STORE请求(续))和C-MOVE服务(AETitle在C-FIND和C-MOVE请求中的设置问题)。此次参考wiki中的说明利⽤DCMTK中的⼯具来讲解⼀下如何调试PACS系统。
下⽂中会⽤到的⼯具有以下两类
服务端dcmqrscp
客户端echoscu、storescu、findscu、movescu
PACS是什么?在DICOM标准中并没有明确的定义,DICOM协议⼤多是通过定义SOP来描述相关⽹络服务。但是⼏乎每⼀个PACS系统会包含以下⼏种SOP类,
Verification SOP Class⼜称为DICOM ECHO服务,⽤于查明⽹络对端系统(即PACS)是否符合DICOM标准(即talks DICOM),以便双⽅按照DICOM标准进⾏对话。
Storage SOP Classes将⼀个或多个DICOM对象存储到PACS服务器。⼀个PACS系统往往需要⽀持多种Storage SOP Classes,⽤以存储不同设备的图像数据(如CT、US、MR等)。
Query SOP Classes根据指定的关键字查询PACS数据库。但是并不下载图像,仅仅是查询图像有关的信息。
Retrieve SOP Classes根据Query SOP Classes的结果到⽬标图像后,利⽤Retrieve SOP Classes服务从PACS服务器下载图像到本地。
Storage Commitment SOP
Classes客户通过该服务确认PACS服务端已经成功完成了图像的归档。
因此可以简单的理解为PACS就是提供了上述多种服务的服务端。在DCMTK⼯具包中给我们提供了⼀个PACS模拟⼯具——dcmqrscp,该⼯具提供了上表中的所有服务(Storage Commitment SOP Classes除外,该部分并未包含在DCMTK开源包中,⽽需要购买商⽤版本)。
下⾯就利⽤dcmqrscp与其他的dcmtk⼯具来模拟调试⼀下客户端与PACS服务端的交互过程,从实际应⽤的⾓度熟悉DICOM3.0标准。
1)安装PACS服务器:
利⽤DCMTK给出的dcmqrscp⼯具包结合⾃⼰定制的配置⽂件来搭建我们的PACS服务器(为了更好的学习DCMTK⼯具包,不建议直接使⽤wiki中给出的公⽤版PACS,即)
dcmqrscp跟其他dcmtk⼯具包⼀样,可以通过添加-h或--help命令⾏参数来查看⼯具包的使⽤说明。唯⼀不同的是要想启动PACS服务器还需要指定⼀个配置⽂件。DCMTK提供的默认的配置⽂件为dmqrscp.cfg。打开dcmtk⼯具包中的dcmqrscp.cfg⽂件,其中的注释已经很清楚。简单概括为三部分:
第⼀部分,⽹络配置,即传统⽹络编程中⽤到参数。如NetworkTCPPort——监听端⼝,⽤于监听来⾃客户端的各种连接请求(需要注意的是要配置⾃⼰的防⽕墙,开放指定的端⼝);MaxAssociations——允许的最⼤连接数;MaxPDUSize——定义PDU传输时刻的最⼤长度等等。
第⼆部分,关于连接到dcmqrscp服务器的客户机定义。该部分包含在dcmqrscp.cfg配置⽂件HostTable BEGIN和Host Table END内。默认的定义如下:
简⽽⾔之,该部分就是定义可能连接到PACS服务器的客户机信息,通常包含AETitle、HostName、PortNamer三部分。需要指出的是⽬前HostName(主机名称)还不⽀持直接IP地址的⽅式,因此在本地配置的时候要格外注意。
本地机的配置如下:
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论