Orthanc是博主发现的一个很完美的DICOM和HTTP服务端开源软件,前几篇分别介绍了Orthanc的基本使用。Orthanc从0.8.0版本之后给出了Plugin SDK,通过该SDK可以利用Orthanc内建的REST API实现WADO服务,下面就参照官网给出的说明介绍一下如何使用SDK实现WADO服务,并且对官网的实例进行更新,采用最新的方式直接实现WADO服务。
DICOM标准定义了文件格式以及医学影像网络传输协议。WADO,即Web Access to DICOM Persistent Objects,是DICOM3.0标准中制定的一种基于网络web服务访问医学图像的协议(具体在DICOM3.0第18部分)。通过WADO协议,专业的医生可以使用常见的浏览器(目前Orthanc貌似不支持IE)预览和下载医学图像。
本博文所附代码给出了一个实现简单WADO服务的示例。该示例可以返回原始的DICOM图像,或者JPEG格式图像;并可以作为Orthanc的插件来运行。借助于Orthanc的框架,可以通过简单的几行代码实现WADO服务。
Orthanc是一款开源的、轻型的、独立的,并支持脚本化的DICOM服务端。Orthanc主要用来精简临床就医流程和简化医学图像的管理。另外通过兼容常见的JSON、PNG格式和RESTful API,使得DICOM标准在计算机图像领域得到更广泛的应用。Orthanc隐藏了DICOM文件格式和DICOM协议的复杂性,因此医院普通的网络管理人员以及专业的医学图像自动分析软件开发人员都可以使用。Orthanc可以作为一个健壮的医学影像处理中心,为各个医院提供服务。
从0.8.0(2014 7月份发布)开始,Orthanc给外部开发人员 提供了插件开发SDK。利用SDK开发的动态库形式的插件可以被导入到Orthanc服务中,插件通过注册回调函数来响应浏览器的HTTP请求。回调函数反过来可以访问Orthanc数据库提取目标DICOM文件的信息。Orthanc Plugin SDK以C语言头文件形式给出,链接如下:https://code.google.com/p/orthanc/source/browse/Plugins/OrthancCPlugin/OrthancCPlugin.h?name=Orthanc-0.8.0,说明文档:http://www.codeproject.com/KB/webservices/797118/OrthancPluginDocumentation.zip
本小节只概括介绍WADO协议,详细介绍参见DICOM3.0标准的第18部分。
DICOM协议里规定了如下标准:一个患者(Patient)可以做多次检查(Studies)。每一个检查(Study)包含一系列医学图像,即序列(Series)。举个例子:标准的PET-CT检查(Study)会包含两组序列,CT 序列和PET 序列。序列中通常对应人体的二维/三维/四维影像。每种影像会被分割成多个文件存储,即Instance(也就是我们看到的包含单幅图像的单个后缀为DCM的文件)。
通常,一个DICOM实例(Instance)可以看做是二维图像与存储了患者元信息(人口统计学信息,如姓名、年龄、身高、体重等等)结构的组合。患者元信息通常是包含了DICOM标签对应值的数组。每个标签由两个十六进制数表示。非常重要的是,每一级的检查(Study)、序列(Series)和图像(Instance)要求全局唯一。
例如(0x0020,0x000d)代表的是Study Instance UID,定义检查(Study)的唯一性;
…………
一个WADO请求就是一个简单的HTTP GET请求,请求中包含了Study、Series和Instance标识符。例如:
http://localhost/wado?
studyUID=1.2.840.113845.11.1000000001951524609.20121203131451.1457891&
seriesUID=1.2.840.113619.2.278.3.262930758.589.1354512768.115&
objectUID=1.2.840.113619.2.278.3.262930758.589.1354512768.116.1&
requestType=WADO
该WADO请求的响应会是与studyUID/seriesUID/objectUID对应的DICOM图像的JPEG格式。如果希望直接获取DICOM格式文件,应在WADO请求中添加contentType=application%2Fdicom,如下所示:
http://localhost/wado?
studyUID=1.2.840.113845.11.1000000001951524609.20121203131451.1457891&
seriesUID=1.2.840.113619.2.278.3.262930758.589.1354512768.115&
objectUID=1.2.840.113619.2.278.3.262930758.589.1354512768.116.1&
contentType=application%2Fdicom&
requestType=WADO
该示例代码依赖于下面四部分:Orthanc Plugin SDK(0.8.0版本之后);CImg Library(用于将DICOM图像转换成PNG格式);JsonCpp库,用于解析Orthanc服务返回的Json格式的文件;CMake,用于编译源码。
下载官方说明中的源码(http://www.codeproject.com/KB/webservices/797118/WadoPluginSources.zip),解压后按照README.txt中的说明编译安装Orthanc WADO Plugin:
第一步:进入cmd命令行模式,创建编译目录,输入指令:mkdir Build
第二步:进入Build目录
第三步:启动Cmake,开始编译,输入cmake ..\WadoPluginSources(注:这里..意思是返回WadoPluginSources源码中CmakeList.txt所在的目录,README.txt中的写法是错误的)
第四步:打开Build目录下的WadoPlugin.sln工程,利用VS进行编译,会在Build\Debug目录下看到WadoPlugin.dll,说明WADO插件生成成功。
源码包中README.txt给出的安装说明有误,应该将WadoPlugin.dll全路径名添加到Configuration.json文件中Plugin对应的字段内,如下图所示:
注意:在Windows系统中输入的WadoPlugin.dll的路径应该使用上图中的【/】,或者输入"c:\\WadoPluginSources\\Build\\Debug\\WadoPlugin.dll”。否则会出现错误。
修改完Configuration.json文件后,准到Orthanc.exe所在目录,例如我本机为:C:\Orthanc-0.8.5\Debug>Orthanc.exe ../../Orthanc/Configuration.json。【注意:后面跟的是添加了WadoPlugin.dll的Configuration.json的路径,如果输入Orthanc.exe --config=Configuration.json,是生成默认Configuration.json的结果,而并不会启动WadoPlugin服务)。
按照前几篇博文方式,上传两幅测试图像,结果如下:
其中已知test1的各级UID为:
StudyInstanceUID=1.3.6.1.4.1.30071.6.176694098609799.4240639413125000;
SeriesInstanceUID=1.3.6.1.4.1.30071.6.176694098609799.4240639413125000.1;
SOPInstanceUID=2.16.840.114421.81623.9430067258.9493139258;
构造WADO请求,查询test1图像,请求连接为:http://localhost:8042/wado?studyUID=1.3.6.1.4.1.30071.6.176694098609799.4240639413125000&seriesUID=1.3.6.1.4.1.30071.6.176694098609799.4240639413125000.1&objectUID=2.16.840.114421.81623.9430067258.9493139258&requestType=WADO
浏览器结果如下所示,与test1.dcm原文件相同。
官方说明中有这样一段:(http://www.codeproject.com/Articles/797118/Implementing-a-WADO-Server-using-Orthanc)
当从WADO HTTP请求中解析出study/series/instance标识符后,需要在Orthance数据库中进行查询。为了实现查询定位,官方博文中给出的代码实例直接借用了Orthanc内建的RESTful API服务(而不是直接响应WADO HTTP 请求)。
首先需要定位study级,代码如下:
static bool LocateStudy(Json::Value& study, const std::string& studyUID) { // Retrieve the list of the studies that are stored in Orthanc Json::Value listOfStudies; if (!OrthancContext::GetInstance().RestApiDoGet(listOfStudies, "/studies")) { return false; } // Retrieve information about each of these studies for (Json::Value::ArrayIndex i = 0; i < listOfStudies.size(); i++) { std::string studyUri = "/studies/" + listOfStudies[i].asString(); if (OrthancContext::GetInstance().RestApiDoGet(study, studyUri)) { // If the "StudyInstanceUID" of this study matches, we are done if (study["MainDicomTags"]["StudyInstanceUID"].asString() == studyUID) { return true; } } } return false; }
LocateStudy函数内部先构造出/studies形式的RESTful API的uri请求,查询出Orthanc中的所有study的UUID,然后再循环遍历每一个获得的studyUUID,构造出/studies/{id}形式的RESTful API请求,逐个对比返回JSON结果中的StudyInstanceUID标签,实现study定位;
其次定位sereis级,代码如下:
static bool LocateSeries(Json::Value& series, const Json::Value& parentStudy, const std::string& seriesUID) { // Loop over the child series of the located study const Json::Value& listOfSeries = parentStudy["Series"]; for (Json::Value::ArrayIndex j = 0; j < listOfSeries.size(); j++) { std::string seriesUri = "/series/" + listOfSeries[j].asString(); // If the "SeriesInstanceUID" of this series matches, we are done if (OrthancContext::GetInstance().RestApiDoGet(series, seriesUri) && series["MainDicomTags"]["SeriesInstanceUID"].asString() == seriesUID) { return true; } } return false; }
与study级类同;
最后是Instance级,代码如下:
static bool LocateInstance(Json::Value& instance, const Json::Value& parentSeries, const std::string& objectUID) { // Loop over the child instances of the located series const Json::Value& listOfInstances = parentSeries["Instances"]; for (Json::Value::ArrayIndex k = 0; k < listOfInstances.size(); k++) { std::string instanceUri = "/instances/" + listOfInstances[k].asString(); // If the "SOPInstanceUID" of this series matches "objectUID", we are done if (OrthancContext::GetInstance().RestApiDoGet(instance, instanceUri) && instance["MainDicomTags"]["SOPInstanceUID"].asString() == objectUID) { return true; } } return false; }
上述定位流程复杂,从0.8.0版本之后Orthanc提供了直接访问数据库中DICOM索引的函数,OrthancPluginLookupPatient(),OrthancPluginLookupStudy(), OrthancPluginLookupStudyWithAccessionNumber(), OrthancPluginLookupSeries()and OrthancPluginLookupInstance()。利用该类函数就需不要先定位study、再定位series、最后定位instance如此繁琐了,修改后的代码如下:
//2014-12-07:zssure //利用新的Orthanc插件的接口,直接定位Instance //http://www.codeproject.com/Articles/797118/Implementing-a-WADO-Server-using-Orthanc static bool LocateInstance(Json::Value& instance, const std::string& objectUID) { char* instanceId = OrthancPluginLookupInstance (OrthancContext::GetInstance().GetContext(), objectUID.c_str()); if (instanceId == NULL) { return false; } std::string instanceUri = "/instances/" + std::string(instanceId); OrthancPluginFreeString(OrthancContext::GetInstance().GetContext(), instanceId); return (OrthancContext::GetInstance().RestApiDoGet(instance, instanceUri) && instance["MainDicomTags"]["SOPInstanceUID"].asString() == objectUID); } //zssure:end
按照官方的说明Orthanc Plugin SDK是以C头文件格式给出,因此直接利用Orthanc-0.8.5中的OrthancCPlugin.h文件替换掉WadoPluginSources中的OrthancCPlugin.h后,发现WadoPlugin.cpp中我们新添加的LocateInstance函数中的GetContext()无法识别:
打开OrthancContext.h文件发现,文件中并不存在GetContext()函数,因此需要手动添加公有函数:
public: OrthancPluginContext* GetContext() { return context_; }
修改完成后可以识别GetContext()函数了,但是编译后出现如下错误:
将#include "../../Resources/ThirdParty/VisualStudio/stdint.h"代码修改为#include "stdint.h"后即可消除上述错误。重新生成后可获得新版WadoPlugin.dll插件。重新输入WADO Request,得到测试结果如下:
至此Orthanc WADO Plugin的开发就讲解完成了。
fo-dicom搭建简单的DICOM Server
时间:2014-12-10