DICOM醫學影像處理:DIMSE訊息發送與接收“大同小異”之DCMTK fo-dicom mDCM

來源:互聯網
上載者:User

標籤:dcmtk   fo-dicom   mdcm   dicom   dimse   

背景:

        從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 ServicesDIMSE-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的賦值。基本流程如下:

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



[email protected]

時間:2014-12-06

DICOM醫學影像處理:DIMSE訊息發送與接收“大同小異”之DCMTK fo-dicom mDCM

相關文章

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.