网站建设步骤图,现在市场网站建设怎么样,腾讯云个人网站备案,济宁恒德建设有限公司网站转自#xff1a;https://my.oschina.net/zssure/blog/354816
背景#xff1a; 从DICOM网络传输一文开始#xff0c;相继介绍了C-ECHO、C-FIND、C-STORE、C-MOVE等DIMSE-C服务的简单实现#xff0c;博文中的代码给出的实例都是基于fo-dicom库来实现的#xff0c;原因只有一…转自https://my.oschina.net/zssure/blog/354816
背景 从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构成如下截图所示 可以简单的理解为Command Set就是本博文即将要介绍的各种服务的请求和应答消息而Data Set可以认为类似于DCM后缀的文件是我们希望在对等DICOM实体间进行传输的信息。但是从本质上来说Command Set和Data Set两者都遵循DICOM3.0协议中IOD的定义都是“以(Group Number,Element Number)对”来进行标记的Data Element元素的集合更形象一点的说明可参考早期的博文http://blog.csdn.net/zssureqh/article/details/9275271。 DIMSE Protocol指出Message可能会分片fragmented这与传统的TCP/IP中的概念类似消息的具体传输是基于ASSOCIATEDICOM3.0第8部分中的P-DATA Service博文http://blog.csdn.net/zssureqh/article/details/41016091中有介绍。
DIMSE Services DIMSE服务因操作SOP类型的不同分为DIMSE-C Services和DIMSE-N ServicesDIMSE-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 博文http://blog.csdn.net/zssureqh/article/details/41016091之前简单介绍了一下DCMTK对于网络传输方面的封装更多的是偏重于协议的各层Layer例如对最底层的基于TCP/IP的Dicom Upper Layer的封装以DUL_为前缀对实体连接层的封装以ASC_为前缀最顶层的是DIMSE层以DIMSE_为前缀。本博文会从DIMSE Services中的DIMSE-C各种消息入手介绍DCMTK对于消息的封装和操作
DCMTK之C-ECHO DCMTK开源库相较于其他两者来说最大的优势是有完整的说明文档、稳定的维护团队同时也有成功的商业产品。在源码中也给出了各种服务工具包前面的好多博文都已经介绍过DCMTK的工具包例如针对于C-ECHO的echoscu.exe博文后续的工程实例是用dcmqrscp.exe作为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;
presIdASC_findAcceptedPresentationContextID(assoc,abstractSyntax);
//构造C-FIND-RQ消息
bzero(OFreinterpret_cast(char*, req), sizeof(req));
strcpy(req.AffectedSOPClassUID,abstractSyntax);
req.DataSetTypeDIMSE_DATASET_PRESENT;
req.PriorityDIMSE_PRIORITY_LOW;
req.MessageIDassoc-nextMsgID;
//构造数据体即我们具体希望在C-FIND SCP端获得的信息
DcmDataset* dcmdatasetnew 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 condDIMSE_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 cechoRQnew DicomCEchoRequest(); 一行代码就顺利的构建了一个C-ECHO-RQ请求指令。分析源码可知DicomCEchoRequest继承自DicomRequestDicomReqeust继承自DicomMessage。逐级查看各类的构造函数可以发现。虽然我们调用的是DicomCEchoRequest的默认构造函数但是在相继调用了基类DicomRequest(DicomCommandField.CEchoRequest, DicomUID.Verification, priority)和DicomMessage()后顺利的完成了对C-ECHO-RQ指令中各个参数构造其中DicomMessage中构造了空的Command Set和DataSetDicomRequest中对MessageID、Priority、SOPClassUID以及CommandFieldType进行了赋值这简直是太容易啦不过也正因为此刚入手的时候可能不知道如何来定制化自己的请求以为fo-dicom库留给我们的可操作性太少其实不然继续往下看。
fo-dicom之C-FIND DicomCFindRequest cfindDicomCFindRequest.CreateStudyQuery(patientId:”12345”);
cfind.OnResponseReceived(rq,rsp)
{
//接收到C-FIND-RSP响应消息后本机C-FIND SCU进行的操作
//例如可以输出到屏幕或其他窗口
Console.WriteLine(PatientAge:{0} PatientName:{1}, rsp.Dataset.Getstring(DicomTag.PatientAge), rsp.Dataset.Getstring(DicomTag.PatientName));
} 通过对比fo-dicom与DCMTK中C-FIND的构造是不是觉得很容易。但是越容易学习和上手的东西倘若不掌握其本质越容易忘。查看DicomCFindRequest.cs源码可以发现CreateStudyQuery函数已经帮助我们添加了Study查询级别所需的所有字段也就是上文中提到的Identifier参数部分。代码如下 那么如果我们想像DCMTK那样自由添加字段怎么办例如在已知服务端是自己定制实现的基础上来查询我们的私有字段。很简单直接覆盖一下CreateStudyQuery函数即可。另外fo-dicom还有一个比价便利的地方是将每种消息的回调函数直接绑定到消息中程序写起来比较方便逻辑上更清晰。
fo-dicom之C-STORE DicomCStoreRequest cstorenew DicomCStoreRequest(”c:\\test4.dcm”); 在DicomCStoreRequest一级只需要数据要发送的dcm文件名全路径名同样通过逐级来完成CommandSet和Dataset的赋值。基本流程如下 /// summary/// Initializes DICOM C-Store request to be sent to SCP./// /summary/// param namefileDICOM file to be sent/param/// param namepriorityPriority of request/parampublic DicomCStoreRequest(DicomFile file, DicomPriority priority DicomPriority.Medium) : base(DicomCommandField.CStoreRequest, file.Dataset.GetDicomUID(DicomTag.SOPClassUID), priority) {File file;Dataset file.Dataset;SOPInstanceUID File.Dataset.GetDicomUID(DicomTag.SOPInstanceUID);}
span stylewhite-space:pre /span//DicomRequest.cs文件protected DicomRequest(DicomCommandField type, DicomUID affectedClassUid, DicomPriority priority) : base() {Type type;SOPClassUID affectedClassUid;MessageID GetNextMessageID();Priority priority;Dataset null;}
span stylewhite-space:pre /span//DicomMessage.cs文件public DicomMessage() {Command new DicomDataset();Dataset null;} mDCM mDCM库与fo-dicom库其实是相同的只不过fo-dicom利用了最新的C#技术来重构mDCM。如博文http://blog.csdn.net/zssureqh/article/details/39621533中给出的mDCM库的继承图所示在顶层基类DcmNetworkBase中实现了DIMSE消息流的基本框架然后按照Client和Server进行了两路派生。mDCM的封装有点处于DCMTK和fo-dicom之间的状况既未做到像DCMTK那样完全提供各个层面底层操作函数也没有像fo-dicom那样更抽象的封装。 下面来看一下mDCM对各种消息的构造
mDCM之C-ECHO //DcmAssociation assoction;//已经顺利建立的DICOM对等实体间的连接
byte pcidassociate.FindAbstractSyntax(DicomUID.VerificationSOPClass;
SenCEchoRequest(pcid,NextMessageID(),Priority); mDCM比较特殊对于DIMSE-C服务请求的参数赋值流程与fo-dicom类似大多参数赋值都在基类中完成例如DcmClientBase中完成了MaxPDU、PriorityDicomClient完成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的赋值另外需要注意的是此时在ToDataset函数内部调用了一个虚函数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工程实例
百度网盘http://pan.baidu.com/s/1jGvaSr8
后续博文介绍
fo-dicom搭建简单的DICOM Server