DICOM医学图像处理:fo-dicom网络传输之C-FIND and 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-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的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是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();
}
}

【注】:代码中只列出了主要的函数,完整代码参见后面给出的工程连接。

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-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

作者:[email protected]

时间:2014-12-01

时间: 2024-10-05 20:38:21

DICOM医学图像处理:fo-dicom网络传输之C-FIND and C-MOVE的相关文章

DICOM医学图像处理:fo-dicom网络传输之 C-Echo and C-Store

背景: 上一篇博文对DICOM中的网络传输进行了介绍.主要參照DCMTK Wiki中的英文原文.通过对照DCMTK与fo-dicom两个开源库对DICOM标准的详细实现,对理解DICOM标准有一个更直观的认识.此篇博文是对上一篇博文的补充.由于专栏前面的演示样例大多是利用DCMTK工具包来进行的,此次借着分析fo-dicom源代码结构的机会,參照fo-dicom的README.md,给出C-ECHO 和C-STORE服务的详细实现.在实现的同一时候给出DICOM3.0标准中的相关介绍,帮助我们理

DICOM医学图像处理:DICOM存储操作之 “多幅JPG图像数据存入DCM文件”

背景: 续上篇,继续介绍如何将多幅JPG图像数据存入DCM文件.即将有损压缩数据直接写入DCM文件,存储为Multi-frame形式. 多幅JPG图像数据存入DCM文件: 为了避免引起歧义,这里着重说明一下.本博文的描述的场景是:假设我们手中有多张JPG文件,想把JPG文件写入DCM文件,即单个DCM文件包含多幅图像信息的Multi-Frame形式.该问题之前与CSDN博友y317215133y也讨论过,当时我在OFFIS论坛中找到了一个帖子直接给了y317215133y答复.今天重新梳理了一下

DICOM医学图像处理:DICOM网络传输

背景: 专栏取名为DICOM医学图像处理原因是:博主是从医学图像处理算法研究时开始接触DICOM协议的.当初认识有局限性,认为DICOM只是一个简单的文件格式约定,简而言之,我当时认为DICOM协议就是扩展名为DCM文件的格式说明.其实不然,随着对医疗行业的深入,对DICOM协议也有了更全面的认识.而今才发现DCM文件只是DICOM协议一部分中的一小节,仅仅是整个协议中的一个数据结构,而DICOM协议更多的是关于医疗行业各种服务及相关流程的约定,因此其实DICOM协议中最主要的是信息流,是对医院

DICOM医学图像处理:DICOM存储操作之“多幅BMP图像数据存入DCM文件”

背景: 本专栏"DICOM医学图像处理"受众较窄,起初只想作为自己学习积累和工作经验的简单整理.前几天无聊浏览了一下,发现阅读量两极化严重,主要集中在"关于BMP(JPG)与DCM格式转换"和"DICOM 通讯协议",尤其是许久前的第一篇博文DCMTK开源库的学习笔记1:将DCM文件保存成BMP文件或数据流(即数组).因此在2014年底前打算写几篇关于DCM格式转换的文章,此次主要聚焦"如何将BMP.JPG等常规图像保存成DCM文件&q

DICOM医学图像处理:DCMTK的wiki资料学习之PACS调试

背景: 前段时间着重从dcmtk和fo-dicom(mDCM)源码角度进行剖析,期望加深对DICOM协议的理解.知其然,知其所以然.如果"所以然"很不好懂,那我们还是先多多"知其然"吧.搞清楚原理的目的不也是为了更好的运用于实践么?所以理论和实践应该彼此交错进行,理论搞不动了就搞搞应用,应用久了就钻研钻研理论. 以前上DCMTK官网仅仅是浏览关于开源库中各个类的设计模式.依赖关系.最近在打开DCMTK官网的wiki时,才发现OFFIS对DCMTK的介绍是如此的详细.

DICOM医学图像处理:storescp.exe与storescu.exe源码剖析,学习C-STORE请求

背景: 上一篇专栏博文中针对PACS终端(或设备终端,如CT设备)与RIS系统之间worklist查询进行了介绍,并着重对比分析了DICOM3.0中各部分对DICOM网络通讯服务的定义.此次通过结合早些时间的博文DICOM医学图像处理:基于DCMTK工具包学习和分析worklist,对DCMTK开源库中提供的storescp.exe和storescu.exe工具的源码进行剖析,从底层深入了解C-STORE服务的触发及响应. 分析思路: storescp.exe和storescu.exe分别充当着

DICOM医学图像处理:开源库mDCM与DCMTK的比较分析(一),JPEG无损压缩DCM图像(续)

背景: 上周通过单步调试,找出了开源库mDCM与DCMTK在对DICOM图像进行JPEG无损压缩时的细小区别,并顺利实现了在C++和C#环境下对DICOM图像的压缩.但是问题接踵而至啊,随着项目的深入,发现在单独的测试工程中可以实现的mDCM版本,在嵌入到项目整体中后,却意外地出现了错误,并未顺利实现DICOM图像的JPEG无损压缩.因此需要继续详细对比分析mDCM与DCMTK两者,期望寻找原因. 问题分析: 开启项目的日志功能后,得到的信息反馈为: No registered codec fo

DICOM医学图像处理:DCMTK在VS2012中的配置

背景: 最近由于项目需要,将原本的开发IDE环境由VS2008升级到了VS2012.本以为编译完成后的DCMTK开源库可以直接从VS2008移植到VS2012.但是通过项目属性添加完包含目录和依赖库后,编译会出现大量的链接错误(大多是跟dcmdata.lib.oflog.lib有关). 解决方法: 重新按照原本的博客前辈柳北风儿(大神目前已经博客转移到网易:http://blog.163.com/[email protected]/),利用CMake工具,选择VS2012本地编译器对DCMTK3

DICOM医学图像处理:Dcmtk与fo-dicom保存文件的不同设计模式之“同步VS异步”+“单线程VS多线程”

一.背景: 最近一直在做DCM相关的编程工作,以前项目使用C++居多,所以使用DCMTK开源库,而目前团队使用C#居多,所以需要转向使用fo-dicom库,由于前一篇专栏文章DICOM医学图像处理:利用fo-dicom发送C-Find查询Worklist在调试过程中需要对DIMSE信息进行手动保存,偶然间发现了dcmtk开源库与fo-dicom开源库在保存dcm文件时使用的方式差异很大,因此决定研究一下,期望通过对比分析来看一下孰优孰劣. 二.dcmtk与fo-dicom保存文件函数的源码剖析: