標籤:dicom dimse acse c-find c-move
背景:
該系列博文同屬於DICOM協議中的“網路傳輸”部分,前兩篇系列文章分別介紹了DCMTK和fo-dicom開源庫對DICOM標準的具體實現(http://blog.csdn.net/zssureqh/article/details/41016091),以及給出了fo-dicom庫對C-ECHO 和C-STORE的簡單實現(http://blog.csdn.net/zssureqh/article/details/41250973)。此篇博文是對前一篇的補充,同樣採用分析DICOM3.0標準的方式,給出fo-dicom庫對C-FIND和C-MOVE的實現樣本。
DIMSE協議與ASSOCIATE協議:
DICOM3.0協議第7部分的第8章對兩種協議進行了詳細介紹:
DIMSE協議:
DIMSE制定了構建訊息的流程和編碼規則,用於在兩個DICOM服務使用者(例如,兩個DICOM實體)之間傳輸請求和響應指令。流程(Procedures)規定了請求和響應指令訊息的傳輸規則,用於解釋指令訊息中的眾多欄位(fields)。但是並沒有規定請求發起方和執行方如何來對訊息進行處理。DIMSE協議指出訊息(Messages)可能會被分段(fragmented)利用P-DATA服務在兩個DICOM服務使用者之間傳輸。
ASSOCIATE協議:
串連(Association)的建立包含兩個DICOM服務使用者。一個被稱為串連請求方(requester),一個被叫做串連接收方(acceptor);雙方使用A-ASSOCIATE服務來建立串連。在A-ASSOCIATE服務中,雙方所需的參數被稱為“應用上下文(Application Context)”,其中給出了兩端DICOM應用實體串連建立的相關規則。(在第7部分的附錄A和附錄D中有詳細的介紹)
大致瞭解了網路傳輸所需的協議後,我們開始介紹C-FIND和C-STORE服務的具體實現。
C-FIND的fo-dicom實現:1)C-FIND參數說明:
C-FIND是一項確認服務(confirmed Service),用於匹配對方一系列複合SOP執行個體的各項屬性。該服務指令需要的參數如下:
其中用於匹配對方一系列複合SOP執行個體屬性的值用Identifier來給出。簡單來說,在請求方訊息(C-FIND-RQ)中Identifier包含了需要查詢的各個屬性,而在回應程式訊息(C-FIND-RSP)中,Identifier是返回的查詢結果。注意:在發送查詢返回結果時Status一直處於Pending狀態;當查詢結果發送完成後,最後一個C-FIND-RSP訊息中Status為Success,且該訊息並不包含任何查詢結果。
具體的C-FIND-RQ和C-FIND-RSP的編碼格式如下所示,除此以外關於C-FIND的相關請求還有其他,例如C-CANCEL-FIND-RQ等。(關於DICOM協議的閱讀方法可參照本系列之前的文章http://blog.csdn.net/zssureqh/article/details/41250973):
2)C-FIND程式碼範例:
下面直接給出C-FIND SCU和C-FIND SCP的代碼,其中包含相關的注釋,所以就不詳細介紹了。
C-FIND SCU在fo-dicom官方的README.md中給出了C-FIND SCU的代碼,如下,
CFIND SCU:
namespace CFINDScu{class Program{static void Main(string[] args){//構造要發送的C-FIND-RQ訊息,如果查看DicomCFindRequest類的話//可以看到其定義與DICOM3.0標準第7部分第9章中規定的編碼格式一致//在構造Study層級的查詢時,我們的參數patientID會被填充到訊息的Indentifier部分,用來在SCP方進行匹配查詢var cfind = DicomCFindRequest.CreateStudyQuery(patientId: "12345");//當接收到對方發揮的響應訊息時,進行相應的操作【注】:該操作在DICOM3.0協議//第7部分第8章中有說明,DIMSE協議並未對其做出規定,而應該有使用者自己設定cfind.OnResponseReceived = (rq, rsp) =>{//此處我們只是簡單的將查詢到的結果輸出到螢幕Console.WriteLine("PatientAge:{0} PatientName:{1}", rsp.Dataset.Get<string>(DicomTag.PatientAge), rsp.Dataset.Get<string>(DicomTag.PatientName));};//發起C-FIND-RQ://該部分就是利用A-ASSOCIATE服務來建立DICOM實體雙方之間的串連。var client = new DicomClient();client.AddRequest(cfind);client.Send(host:"127.0.0.1",port: 12345,useTls: false,callingAe: "SCU-AE",calledAe: "SCP-AE");Console.ReadLine();}}}
【注】:代碼中只列出了主要的函數,完整代碼參見後面給出的工程串連。
C-FIND SCP:
//DICOM3.0協議第7部分第8章中DIMSE協議並未規定請求方和實現方如何來進行具體操作//此處定義的DcmCFindCallback代理由使用者自己來實現接收到C-FIND-RQ後的操作public delegate IList<DicomDataset> DcmCFindCallback(DicomCFindRequest request);//要想提供C-FIND SCP服務,需要繼承DicomService類,該類中實現了DICOM協議的基礎架構,//另外還需要實現IDicomCFindProvider介面,用於實現具體的C-FIND SCP服務。class ZSCFindSCP : DicomService, IDicomServiceProvider, IDicomCFindProvider{public ZSCFindSCP(Stream stream,Logger log):base(stream,log){}#region C-FINDpublic static DcmCFindCallback OnZSCFindRequest;public virtual IEnumerable<DicomCFindResponse> OnCFindRequest(DicomCFindRequest request){DicomStatus status = DicomStatus.Success;IList<DicomDataset> queries;List<DicomCFindResponse> responses = new List<DicomCFindResponse>();if (OnZSCFindRequest != null){//此處通過代理來返回在服務端本機進行的操作結果,也就是DICOM協議中說的匹配查詢結果queries = OnZSCFindRequest(request);if (queries != null){Logger.Info("查詢到{0}個資料", queries.Count);foreach (var item in queries){//對於每一個查詢匹配的結果,都需要有一個單獨的C-FIND-RSP訊息來返回到請求端//【注】:每次發送的狀態都必須是Pending,表明後續還會繼續發送查詢結果DicomCFindResponse rsp = new DicomCFindResponse(request, DicomStatus.Pending);rsp.Dataset = item;responses.Add(rsp);}}else{status = DicomStatus.QueryRetrieveOutOfResources;}}//隨後需要發送查詢結束的狀態,即Success到C-FIND SCU端responses.Add(new DicomCFindResponse(request, DicomStatus.Success));//這裡貌似是一起將多個response發送出去的?需要後續在研究一下DicomService中的實現代碼//搞清楚具體的發送機制return responses;}#endregion}class Program{static void Main(string[] args){//類比一下接收到查詢請求後原生資料庫等相關查詢操作,即綁定DcmCFindCallback代理ZSCFindSCP.OnZSCFindRequest = (request) =>{//request中的Identifier欄位中包含了SCU希望在SCP端進行匹配查詢的資訊//我們需要類比相關操作,此處簡單的假設本機中存在滿足條件的結果,直接返回IList<DicomDataset> queries = new List<DicomDataset>();//我們此次查詢到了三條記錄for (int i = 0; i < 3; ++i){DicomDataset dataset = new DicomDataset();DicomDataset dt = new DicomDataset();dt.Add(DicomTag.PatientID, "20141130");dt.Add(DicomTag.PatientName, "zsure");dt.Add(DicomTag.PatientAge, i.ToString());queries.Add(dt);}return queries;};var cfindServer = new DicomServer<ZSCFindSCP>(12345);//控制台程式,用於確保主程式不退出才可一直提供DICOM C-FIND 服務Console.ReadLine();}}
【注】:代碼中只列出了主要的函數,完整代碼參見後面給出的工程串連。
實際輸出結果:
當然真正的C-FIND請求需要兩端DICOM應用實體進行相關的查詢和輸出操作配合,從而才能實現更多複雜的功能,此處僅僅是為了示範一下整個流程。
C-MOVE的fo-dicom實現:
C-MOVE與C-FIND請求相類似,比較複雜的是C-MOVE請求會啟動上一篇博文中介紹過的C-STORE子操作,詳情如下:
1)C-MOVE參數說明:
這裡與C-FIND等其他動作不同的是多出了四項關於子操作(Sub-operations)的參數,用於表明剩餘子操作的數量(剩餘 and 完成)以及相關完成狀況(失敗or警告)。
具體的參數編碼格式如下:
2)C-MOVE程式碼範例:
fo-dicom官方執行個體中的C-MOVE SCU並未給出C-STORE SCP的實現代碼,因此預設的是由第三方來實現C-STORE SCP服務,如所示:
在本篇博文裡,為了減少測試終端的數量,我直接在C-MOVE SCU端實現了C-STORE SCP服務,用於接收從C-MOVE SCP發送回來的映像。
C-MOVE SCU:
//C-MOVE SCU端需要實現C-STORE SCP服務//當然也不一定是C-MOVE SCU端來實現,也可能是第三方來實現C-STORE SCP服務,意思就是說://A向B發送C-MOVE RQ,B接收到C-MOVE-RQ並查詢到映像後向C發起C-STORE-RQ,然後C對C-STORE-RQ進行分析並儲存映像。/// <summary>/// 單獨實現了C-STORE SCP服務,為C-MOVE SCU做準備/// </summary>public delegate DicomCStoreResponse OnCStoreRequestCallback(DicomCStoreRequest request);class CStoreSCP : DicomService, IDicomServiceProvider, IDicomCStoreProvider, IDicomCEchoProvider{private static DicomTransferSyntax[] AcceptedTransferSyntaxes = new DicomTransferSyntax[] {DicomTransferSyntax.ExplicitVRLittleEndian,DicomTransferSyntax.ExplicitVRBigEndian,DicomTransferSyntax.ImplicitVRLittleEndian};public CStoreSCP(Stream stream, Logger log): base(stream, log){}public static OnCStoreRequestCallback OnCStoreRequestCallBack;public DicomCStoreResponse OnCStoreRequest(DicomCStoreRequest request){//to do yourself//實現自訂的儲存方案if (OnCStoreRequestCallBack != null){return OnCStoreRequestCallBack(request);}return new DicomCStoreResponse(request, DicomStatus.NoSuchActionType);}}class Program{static void Main(string[] args){//開啟C-STORE SCP服務,用於接收C-MOVE SCP返回的映像CStoreSCP.OnCStoreRequestCallBack = (request) =>{var studyUid = request.Dataset.Get<string>(DicomTag.StudyInstanceUID);var instUid = request.SOPInstanceUID.UID;var path = Path.GetFullPath(@"c:\cmove-scu");path = Path.Combine(path, studyUid);if (!Directory.Exists(path))Directory.CreateDirectory(path);path = Path.Combine(path, instUid) + ".dcm";request.File.Save(path);return new DicomCStoreResponse(request, DicomStatus.Success);};var cstoreServer = new DicomServer<CStoreSCP>(22345);//發起C-MOVE-RQ操作,發送請求的StudyID是12DicomCMoveRequest req=new DicomCMoveRequest("DEST-AE","12");var client=new DicomClient();client.NegotiateAsyncOps();client.AddRequest(req);//這裡的IP地址是C-MOVE SCP的地址,12345連接埠號碼是C-MOVE SCP提供C-MOVE服務的連接埠//在C-MOVE SCP端發出的C-STORE-RQ子操作請求的是C-MOVE SCU端我們實現的C-STORE SCP,C-STORE SCP綁定的連接埠是22345client.Send("127.0.0.1", 12345,false, "DEST-AE", "SCP-AE");Console.ReadLine();}}
【注】:代碼中只列出了主要的函數,完整代碼參見後面給出的工程串連。
C-MOVE SCP:
//DICOM3.0協議第7部分第8章中DIMSE協議並未規定請求方和實現方如何來進行具體操作//此處定義的DcmCMoveCallback代理由使用者自己來實現接收到C-MOVE-RQ後的操作public delegate IList<DicomDataset> DcmCMoveCallback(DicomCMoveRequest request);//要想提供C-FIND SCP服務,需要繼承DicomService類,該類中實現了DICOM協議的基礎架構,//另外還需要實現IDicomCMoveProvider介面,用於實現具體的C-MOVE SCP服務。class ZSCMoveSCP : DicomService, IDicomServiceProvider, IDicomCMoveProvider{public ZSCMoveSCP(Stream stream, Logger log): base(stream, log){}#region C-MOVEpublic static DcmCMoveCallback OnZSCMoveRequest;public virtual IEnumerable<DicomCMoveResponse> OnCMoveRequest(DicomCMoveRequest request){DicomStatus status = DicomStatus.Success;IList<DicomCMoveResponse> rsp = new List<DicomCMoveResponse>();/*----to do------*///添加查詢資料庫的代碼,即根據request的條件提取指定的映像//然後將映像資訊添加到rsp響應中//建立C-STORE-SCU,發起C-STORE-RQIList<DicomDataset> queries;DicomClient clt = new DicomClient();if (OnZSCMoveRequest != null){queries = OnZSCMoveRequest(request);if (queries != null){Logger.Info("需要發送{0}個資料", queries.Count);int len = queries.Count;int cnt = 0;foreach (var item in queries){//zssure://取巧的方法直接利用request來構造response中相同的部分//這部分與mDCM方式很不同var studyUid = item.Get<string>(DicomTag.StudyInstanceUID);var instUid = item.Get<string>(DicomTag.SOPInstanceUID);//需要在c:\cmovetest目錄下手動添加C-MOVE SCU請求的映像//本地構造的目錄結構為,// c:\cmovetest\12\0.dcm// c:\cmovetest\12\1.dcm// c:\cmovetest\12\2.dcmvar path = Path.GetFullPath(@"c:\cmovetest");try{path = Path.Combine(path, studyUid);if (!Directory.Exists(path))Directory.CreateDirectory(path);path = Path.Combine(path, instUid) + ".dcm";DicomCStoreRequest cstorerq = new DicomCStoreRequest(path);cstorerq.OnResponseReceived = (rq, rs) =>{if (rs.Status != DicomStatus.Pending){}if (rs.Status == DicomStatus.Success){DicomCMoveResponse rsponse = new DicomCMoveResponse(request, DicomStatus.Pending);rsponse.Remaining = --len;rsponse.Completed = ++cnt;rsponse.Warnings = 0;rsponse.Failures = 0;rsp.Add(rsponse);}};clt.AddRequest(cstorerq);//注意:這裡給出的IP地址與C-MOVE請求的IP地址相同,意思就是說C-MOVE SCP需要向C-MOVE SCU發送C-STORE-RQ請求//將查詢到的映像返回給C-MOVE SCU//所以四尺C-STORE-RQ中的IP地址與C-MOVE SCU相同,但是連接埠不同,因為同一個連接埠不能被綁定多次。clt.Send("127.0.0.1", 22345, false, this.Association.CalledAE, request.DestinationAE);}catch (System.Exception ex){DicomCMoveResponse rs = new DicomCMoveResponse(request, DicomStatus.StorageStorageOutOfResources);rsp.Add(rs);return rsp;}}//zssure://發送完成後統一返回C-MOVE RESPONSE //貌似響應流程有問題,有待進一步核實//注意,如果最後為發送DicomStatus.Success訊息,TCP串連不會釋放,浪費資源rsp.Add(new DicomCMoveResponse(request, DicomStatus.Success));return rsp;}else{rsp.Add(new DicomCMoveResponse(request, DicomStatus.NoSuchObjectInstance));return rsp;}}rsp.Add(new DicomCMoveResponse(request, DicomStatus.NoSuchObjectInstance));return rsp;}#endregion}class Program{static void Main(string[] args){ZSCMoveSCP.OnZSCMoveRequest = (request) =>{List<DicomDataset> dataset = new List<DicomDataset>();for (int i = 0; i < 3; ++i){DicomDataset dt = new DicomDataset();dt.Add(DicomTag.StudyInstanceUID, "12");dt.Add(DicomTag.SOPInstanceUID, i.ToString());dataset.Add(dt);}return dataset;};var cmoveScp = new DicomServer<ZSCMoveSCP>(12345);Console.ReadLine();}}
【注】:代碼中只列出了主要的函數,完整代碼參見後面給出的工程串連。
實際測試結果:
開啟我們手動構建的測試目錄,可以看到三個映像順利轉移到了C-MOVE SCU端設定的儲存目錄。
關於C-MOVE SCP需要同時實現C-STORE SCP的問題,特此說明一下。並非一定要求C-MOVE SCP來實現C-STORE SCP服務,C-MOVE服務本身並未要求是雙方互動,有可能是多方互動。比如A作為C-MOVE SCU向B發出C-MOVE-RQ請求,此時作為C-MOVE SCP的B在查詢到結果後可以向C發出C-STORE-RQ請求,只要C提供了C-STORE SCP服務,就可以接收到由B發送過來的映像。因此C-MOVE服務可能是三方之間的互動。僅限於雙方資料的雙向傳輸的是C-GET,關於C-GET與C-MOVE的區別可以參照前輩的博文http://qimo601.iteye.com/blog/1693764。
工程代碼:
C-FIND工程:百度網盤 http://pan.baidu.com/s/1c0fDUP6 Github https://github.com/zssure-thu/CSDN/tree/master/CFINDScuScp
C-MOVE工程:百度網盤 http://pan.baidu.com/s/1mgmjlTe Github https://github.com/zssure-thu/CSDN/tree/master/CMOVEScuScp
後續專欄博文介紹:
fo-dicom搭建簡單的Dicom Server
[email protected]
時間:2014-12-01
DICOM醫學影像處理:fo-dicom網路傳輸之C-FIND and C-MOVE