该系列博文同属于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的实现示例。
DICOM3.0协议第7部分的第8章对两种协议进行了详细介绍:
DIMSE制定了构建消息的流程和编码规则,用于在两个DICOM服务使用者(例如,两个DICOM实体)之间传输请求和响应指令。流程(Procedures)规定了请求和响应指令消息的传输规则,用于解释指令消息中的众多字段(fields)。但是并没有规定请求发起方和执行方如何来对消息进行处理。DIMSE协议指出消息(Messages)可能会被分段(fragmented)利用P-DATA服务在两个DICOM服务使用者之间传输。
连接(Association)的建立包含两个DICOM服务使用者。一个被称为连接请求方(requester),一个被叫做连接接收方(acceptor);双方使用A-ASSOCIATE服务来建立连接。在A-ASSOCIATE服务中,双方所需的参数被称为“应用上下文(Application Context)”,其中给出了两端DICOM应用实体连接建立的相关规则。(在第7部分的附录A和附录D中有详细的介绍)
大致了解了网络传输所需的协议后,我们开始介绍C-FIND和C-STORE服务的具体实现。
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):
下面直接给出C-FIND SCU和C-FIND SCP的代码,其中包含相关的注释,所以就不详细介绍了。
C-FIND SCU在fo-dicom官方的README.md中给出了C-FIND 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(); } } }
【注】:代码中只列出了主要的函数,完整代码参见后面给出的工程连接。
//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-FIND public 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与C-FIND请求相类似,比较复杂的是C-MOVE请求会启动上一篇博文中介绍过的C-STORE子操作,详情如下:
这里与C-FIND等其他操作不同的是多出了四项关于子操作(Sub-operations)的参数,用于表明剩余子操作的数量(剩余 and 完成)以及相关完成状况(失败or警告)。
具体的参数编码格式如下:
fo-dicom官方实例中的C-MOVE SCU并未给出C-STORE SCP的实现代码,因此默认的是由第三方来实现C-STORE SCP服务,如下图所示:
在本篇博文里,为了减少测试终端的数量,我直接在C-MOVE SCU端实现了C-STORE SCP服务,用于接收从C-MOVE SCP发送回来的图像。
//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是12 DicomCMoveRequest 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绑定的端口是22345 client.Send("127.0.0.1", 12345,false, "DEST-AE", "SCP-AE"); Console.ReadLine(); } }
【注】:代码中只列出了主要的函数,完整代码参见后面给出的工程连接。
//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-MOVE public 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-RQ IList<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.dcm var 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
原本的C-MOVE SCP代码中存在着一个缺陷,即在IEnumerable<DicomCMoveResponse> OnCMoveRequest(DicomCMoveRequest request)函数中,是在发送完所有的C-SOTRE 请求后,在接收到的每个C-STORE-RSP函数中将对应的C-MOVE-RSP添加到返回队列中,然后通过DicomService循环再一次发送C-MOVE-RSP,这个在逻辑上是有错误的,应该是每接收到一个C-STORE-RSP,在其响应函数内部判别如果成功就立即发送一个C-MOVE-RSP给客户端,此时可以使用DicomService的SendResponse函数。修改后的代码如下:
#region C-MOVE public 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-RQ IList<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.dcm var 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; //zssure:2014-12-24 //修复发送C-MOVE-RSP的逻辑错误 SendResponse(rsponse); //rsp.Add(rsponse); //zssure:end } }; 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
时间:2014-12-01