使用 Orthanc 实现 WADO 服务器






4.94/5 (15投票s)
本文介绍了如何创建一个基本的 WADO 服务器,以提供对 DICOM 图像的网络访问。WADO 服务器作为 Orthanc(一个轻量级 DICOM 存储)的插件实现。
引言
DICOM 标准规定了医学影像的文件格式和网络协议。DICOM 标准在其PS 3.18部分定义了一个基于 Web 的图像访问服务,称为 WADO(Web Access to DICOM Persistent Objects,DICOM 持久对象的 Web 访问)。WADO 尤其允许医疗专业人员使用标准 Web 浏览器预览和下载医学图像。
本文随附的代码展示了如何实现一个基本的 WADO 服务器。该示例服务器可以返回原始 DICOM 图像,也可以返回这些图像的 JPEG 预览。WADO 服务器是作为 Orthanc 的插件实现的,Orthanc 是一个用纯 C++ 编写的开源 DICOM 服务器。通过利用 Orthanc 框架,我们的示例 WADO 服务器可以用很少的代码行编写。
重要提示:与本文相关的代码现在是 Orthanc 官方 DICOMweb 插件的一部分。
背景
Orthanc
Orthanc 是一个开源、轻量级、独立、可脚本化的 DICOM 存储。它旨在简化临床流程,方便医学图像的数据管理,并通过支持常用文件格式(JSON、PNG)和网络协议(RESTful API)使 DICOM 标准更接近计算机视觉社区。Orthanc 抽象了 DICOM 格式和 DICOM 协议的复杂性:因此,它的目标受众包括医院的网络管理员以及开发医学图像自动化分析软件的计算机科学家。Orthanc 主要被设计为自动化每个医院特有的各种医学成像任务的中心、强大的构建块。
从 0.8.0 版本(2014 年 7 月发布)开始,Orthanc 凭借 Orthanc 插件 SDK 提供了外部开发者为 Orthanc 创建和分发插件的可能性。Orthanc 插件以 C 或 C++ 编写的共享库的形式存在,可以加载到 Orthanc 服务器中(即 Windows 下的 DLL 或.soLinux 下的文件)。此类插件可以注册回调函数以响应传入的 HTTP 调用。这些回调又可以访问 Orthanc 数据库以检索有关存储的 DICOM 文件的信息。
Orthanc 插件 SDK 在一个C 头文件中定义,并使用 Doxygen 进行文档化。本文将利用此 SDK 创建基本的 WADO 服务器。
DICOM 和 WADO
现在我们总结一下 WADO 的基本思想。WADO 的完整描述超出了本文的范围。我们诚挚地邀请感兴趣的读者参考官方 WADO 规范。
医学影像世界的 DICOM 模型如下:一个患者是一组研究的主体。这些影像研究中的每一个都由一组医学图像组成,这些图像称为序列。例如,一个典型的 PET-CT 研究的结果由 2 个序列组成:CT 序列和 PET 序列。这些序列通常对应于人体 2D/3D/4D 图像。反过来,每个医学图像都被拆分为一组文件,这些文件称为实例。例如,上述 3D CT 序列通常会拆分为一组实例,每个实例包含 3D 图像的一个 2D 切片。
基本上,DICOM 实例可以看作是 2D 图像与存储患者元数据的类似 XML 的树形结构的组合。本质上,患者元数据是一个关联数组,它递归地将 DICOM 标签映射到值。每个标签由 2 个十六进制数标识。非常重要的是,研究/序列/实例层次结构的每个级别都必须是全局唯一的
- 该(0x0020, 0x000d)标签(称为“研究实例 UID”)唯一标识研究。
- 该(0x0020, 0x000e)标签(称为“序列实例 UID”)唯一标识序列。
- 该(0x0008, 0x0018)标签(称为“SOP 实例 UID”,或 WADO 中的“对象 UID”)唯一标识实例。
WADO 请求只是一个包含研究、序列和实例标识符的HTTP GET 请求。例如,这是一个可以发送到 WADO 服务器的示例 WADO 请求
https:///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
此请求的答案将是一个 JPEG 图像,对应于 UID 由objectUID给出,属于由seriesUID标识的序列,并对应于由studyUID标识的研究的实例。要检索原始 DICOM 实例而不是 JPEG 图像,可以在请求中添加一个contentType参数
https:///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
到此,我们关于理解基本 WADO 服务器的理论背景就介绍完了。
Using the Code
说明
此项目的源代码几乎是自包含的:为方便起见,所有第三方依赖项都随包一起提供。作为先决条件,您只需安装 CMake 并下载 Orthanc ≥ 0.8.0。
源包包含一个名为README.txt的文件,其中提供了构建说明并解释了如何将生成的插件加载到 Orthanc 中。
为方便起见,我们还提供了预编译的插件 Windows 版本。此共享库是在 Linux 下使用 MinGW 交叉编译的。
一旦 Orthanc 运行并加载了 WADO 插件,并且一些 DICOM 文件存储在 Orthanc 中,任何 Web 浏览器都可以用于发出 WADO 请求。当然,请确保在此请求中正确调整studyUID, seriesUID和objectUID以适应您的 DICOM 文件!请注意,我们使用 HTTP 端口 8042 来发出 WADO 请求,因为这是 Orthanc 的默认端口。
依赖项
示例代码依赖于 4 个组件
- Orthanc ≥ 0.8.0 的Orthanc 插件 SDK。
- CImg 库用于将 DICOM 图像从 PNG(由 Orthanc 提供)转换为 JPEG(WADO 要求)。
- JsonCpp 库用于解析 Orthanc 核心提供的 JSON 文件。
- 该项目使用 CMake 构建。
此外,当针对 Windows 时,示例代码将静态链接到 libpng、libjpeg 和 zlib。CImg 需要这些组件才能从 PNG 转换为 JPEG。因此,此示例代码还说明了如何使用 CMake 编译所有这些第三方库。
关注点
插件的入口点
示例代码最重要的点之一是定义每个 Orthanc 插件所需的 4 个入口点。这在EntryPoints.cpp文件
extern "C"
{
ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
{
OrthancContext::GetInstance().Initialize(context);
OrthancContext::GetInstance().LogWarning("Initializing WADO sample");
OrthancContext::GetInstance().Register("/wado", Wado);
return 0;
}
ORTHANC_PLUGINS_API void OrthancPluginFinalize()
{
OrthancContext::GetInstance().LogWarning("Finalizing WADO sample");
OrthancContext::GetInstance().Finalize();
}
ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
{
return "wado-sample";
}
ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
{
return "1.0";
}
}
中完成。重要的是,这些入口点都必须使用 C 链接模型(因此是extern "C")发布。实际上,为了避免与 C++ 相关的应用程序二进制接口 (ABI) 问题,Orthanc 插件是使用 C 语言定义的。
此代码片段还说明了OrthancContext类的使用,它提供了一个包含 Orthanc 插件引擎上下文的单例对象。OrthancContext::Initialize函数仅用于在单例中设置指向 Orthanc 插件引擎上下文的指针。
此外,此类将 Orthanc 插件 SDK 的低级纯 C 接口与 C++ STL 的标准类(例如std::string或std::map)封装起来。请注意,可以使用OrthancContext::GetInstance().LogWarning().
来访问 Orthanc 的日志功能。
WADO 回调WADO 服务器的 HTTP 回调安装在:
OrthancContext::GetInstance().Register("/wado", Wado);
OrthancPluginInitialize()的以下行。主Wado()函数在文件
ORTHANC_PLUGINS_API int32_t Wado(OrthancPluginRestOutput* output,
const char* url,
const OrthancPluginHttpRequest* request)
{
OrthancContext::GetInstance().LogWarning("Processing a WADO request");
// Extract the GET arguments of the WADO request
OrthancContext::Arguments arguments;
OrthancContext::GetInstance().ExtractGetArguments(arguments, *request);
...
}
WadoPlugin.cpp参数中定义。在此函数中,是一个std::map<std::string, std::string>studyUID, seriesUID, objectUID和contentType,它将 GET 参数映射到其值。此关联数组将(除其他外)包含 WADO 请求的参数。
在 Orthanc 中定位实例
注意: 如果您使用 Orthanc ≥ 0.8.1,您将在本页末尾找到更简单、更快速的定位实例方法。
从 HTTP WADO 请求中提取感兴趣的研究/序列/实例标识符后,必须在 Orthanc 存储中定位这些标识符。为了完成这项任务,我们利用了 Orthanc 插件可以直接访问Orthanc 内置 REST API(无需通过网络连接)的事实。我们首先尝试定位研究
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;
}
此函数首先获取 REST API 中“/studies”URI 的内容,该内容返回 Orthanc 已知研究的列表。Orthanc Wiki 提供了有关此过程的更多解释。然后,我们遍历这些研究,直到找到与 WADO 请求的studyUID参数匹配的研究。如果成功,函数返回true,并且参数study将包含有关研究的信息,格式为 JSON。类似的技术用于查找序列,然后查找感兴趣的实例。
提供 DICOM 实例
一旦实例被定位,如果 WADO 请求要求返回原始 DICOM 文件(即,如果其“contentType”HTTP GET 参数设置为“application/dicom”),则使用以下自解释函数返回 DICOM 实例
static int32_t AnswerDicom(OrthancPluginRestOutput* output,
const std::string& instance)
{
// Download the DICOM instance from Orthanc into a memory buffer
std::string dicom;
if (!OrthancContext::GetInstance().GetDicomForInstance(dicom, instance))
{
return -1;
}
// Send the DICOM instance to the HTTP client
OrthancContext::GetInstance().AnswerBuffer(output, dicom, "application/dicom");
return 0;
}
提供 JPEG 图像
默认情况下,WADO 规定如果没有给出“contentType”,则返回 JPEG 图像。为此,我们利用了 Orthanc 可以使用其 REST API 以 PNG 格式即时生成 DICOM 图像预览的事实
static int32_t AnswerJpegPreview(OrthancPluginRestOutput* output,
const std::string& instance)
{
// Generate a preview of this instance using Orthanc
std::string preview;
if (!OrthancContext::GetInstance().RestApiDoGet
(preview, "/instances/" + instance + "/preview"))
{
return -1;
}
// Save the preview image (a PNG file) into a temporary file
TemporaryFile tmp;
tmp.Write(preview);
请注意,PNG 图像保存到临时文件。这是必要的,因为 CImg 不支持从/向内存缓冲区读/写。为了尽可能跨平台,TemporaryFile类使用一个由随机 UUID 组成的文件名。TemporaryFile使用RAII 设计模式在对象销毁时删除文件。
现在,只需让 CImg 将输入的 PNG 图像转换为输出的 JPEG 图像(可能在进行一些图像修改后),并使用 Orthanc 插件 SDK 返回 JPEG 图像即可
cimg_library::CImg img;
img.load_png(tmp.GetPath().c_str());
// Do some manipulations with the image (e.g. according to the
// other GET arguments of the WADO request)
unsigned char red[] = { 255, 0, 0 };
img.draw_text(10, 10, "Orthanc WADO sample plugin", red);
// Save the modified image using JPEG
img.save_jpeg(tmp.GetPath().c_str());
// Send the JPEG image to the HTTP client
std::string result;
tmp.Read(result);
OrthancContext::GetInstance().AnswerBuffer(output, result, "image/jpg");
更新 - 使用 Orthanc ≥ 0.8.1 更简单的实例搜索
如果您使用 Orthanc ≥ 0.8.1,您可以极大地简化和加快根据 DICOM 标识符定位实例、序列、研究或患者的过程。从 0.8.1 版本开始,Orthanc 确实通过新引入的函数直接访问其 DICOM 索引OrthancPluginLookupPatient(), OrthancPluginLookupStudy(), OrthancPluginLookupStudyWithAccessionNumber(), OrthancPluginLookupSeries()和OrthancPluginLookupInstance()。以下是如何通过其SOPInstanceUID标签
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);
}
定位实例的方法:因此,不再需要像前面部分详细介绍的那样,先浏览研究,然后是序列,最后是实例。
历史
- 2016 年 7 月 18 日:链接到 DICOMweb 插件,更新了 SDK 链接。
- 2014 年 8 月 6 日:使用 Orthanc ≥ 0.8.1 定位实例。
- 2014 年 7 月 16 日:初始版本。
附件
- 基本 WADO 服务器的源代码。
- 插件的预编译 Windows 二进制文件。