【转】DICOM医学图像处理:DIMSE消息发送与接收“大同小异”之DCMTK fo-dicom mDCM
轉自: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中的概念類似),消息的具體傳輸是基于ASSOCIATE(DICOM3.0第8部分)中的P-DATA Service(博文http://blog.csdn.net/zssureqh/article/details/41016091中有介紹)。
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:
? ? ? ? 博文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; 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 style="white-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 style="white-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 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的賦值,另外需要注意的是此時在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
總結
以上是生活随笔為你收集整理的【转】DICOM医学图像处理:DIMSE消息发送与接收“大同小异”之DCMTK fo-dicom mDCM的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 第一次办理信用卡额度是多少
- 下一篇: 2019年各银行ETC最新优惠活动,超全