Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

Implementing a WADO Server using Orthanc

4.94/5 (15 votes)
18 Jul 2016GPL39 min read 78K   2K  
This article explains how to create a basic WADO server to provide Web access to DICOM images. The WADO server is implemented as a plugin for Orthanc, a lightweight DICOM store.

Image 1

Introduction

The DICOM standard specifies the file format and the network protocols for medical imaging. The DICOM standard defines, in its part PS 3.18, a Web-based service for accessing images that is known as WADO (Web Access to DICOM Persistent Objects). WADO notably allows healthcare professionals to preview and download medical images using standard Web browsers.

The code attached to this article shows how to implement a basic WADO server. This sample server can return either raw DICOM images, or a JPEG preview of these images. The WADO server is implemented as a plugin to Orthanc, an open-source DICOM server that is written in pure C++. By taking advantage of the Orthanc framework, our sample WADO server can be written in very few lines of code.

Important: The code associated with this article is now part of the official DICOMweb plugin for Orthanc.

Background

Orthanc

Orthanc is an open-source, lightweight, standalone, scriptable DICOM store. It is designed to smooth clinical processes, to ease the data management of medical images, and to bring the DICOM standard closer to the Computer Vision community thanks to its support of well-known file formats (JSON, PNG) and network protocols (RESTful API). Orthanc abstracts the complexity of the DICOM format and of the DICOM protocol: As a consequence, its target audience consists of the network administrators of an hospital, as well as of the computer scientists that develop software for the automated analysis of medical images. Orthanc is primarily conceived as a central, robust building block for the automation of the various medical imaging tasks that are specific to each hospital.

Starting with its version 0.8.0 (that was released on July, 2014), Orthanc features the possibility for external developers to create and distribute plugins for Orthanc, thanks to the Orthanc Plugin SDK. An Orthanc plugin takes the form of a shared library written in C or C++ that can be loaded into the Orthanc server (i.e. a DLL under Windows, or a .so file under Linux). Such plugins can register callback functions to react to incoming HTTP calls. These callbacks can in turn access the Orthanc database to retrieve information about the stored DICOM files.

The Orthanc Plugin SDK is defined in a C header that is documented with Doxygen. This article will exploit this SDK to create the basic WADO server.

DICOM and WADO

We now summarize the basic ideas behind WADO. A full description of WADO is out of the scope of this article. We kindly invite the interested reader to refer to the official WADO specification.

The DICOM model of the medical imaging world is the following: A Patient is the subject of a set of Studies. Each of these imaging Studies is composed of a set of medical images that are called the Series. For instance, the result of a typical PET-CT Study is made of 2 Series: the CT Series and the PET Series. These series often correspond to 2D/3D/4D images of the human body. In turn, each medical image is split as a set of files that are called the Instances. For instance, the aforementioned 3D CT Series would most often be split into a set of Instances, each of these Instances containing a single 2D slice of the 3D image.

Basically, a DICOM Instance can be thought of as the combination of a 2D image together with a XML-like tree structure that stores the patient metadata. In essence, the patient metadata is an associative array that recursively maps DICOM tags to values. Each tag is identified by 2 hexadecimal numbers. Very importantly, each level of the Study/Series/Instance hierarchy is required to be globally unique:

  • The (0x0020, 0x000d) tag (known as "Study Instance UID") uniquely identifies the study.
  • The (0x0020, 0x000e) tag (known as "Series Instance UID") uniquely identifies the series.
  • The (0x0008, 0x0018) tag (known as "SOP Instance UID", or "Object UID" in WADO) uniquely identifies the instance.

A WADO request is simply a HTTP GET request that contains the Study, Series, and Instance identifiers. For instance, here is a sample WADO request that could be issued to a WADO server:

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

The answer to this request would be a JPEG image corresponding to the Instance whose UID is given by objectUID, that belongs to the Series identified by seriesUID, and that corresponds to the Study identified by studyUID. To retrieve the original DICOM instance instead of a JPEG image, one would add a contentType to the request:

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

This concludes the theoretical background to understand our basic WADO server.

Using the Code

Instructions

The source code of the project is almost self-containing: For convenience, all the third-party dependencies are shipped into the package. As a prerequisite, you only have to install CMake and download Orthanc ≥ 0.8.0.

The source package contains a file named README.txt that gives the build instructions and that explains how to load the resulting plugin into Orthanc.

For convenience, we also provide a pre-compiled Windows version of the plugin. This shared library was cross-compiled under Linux using MinGW.

Once Orthanc is running with the WADO plugin loaded, and once some DICOM files are stored in Orthanc, any Web browser can be used to make the WADO request. Of course, make sure to properly adapt the studyUID, seriesUID and objectUID to your DICOM file in this request! Note that we use HTTP port 8042 to issue the WADO request, as this is the default port of Orthanc.

Dependencies

The sample code depends on 4 components:

  • The Orthanc Plugin SDK from Orthanc ≥ 0.8.0.
  • The CImg library is used to convert the DICOM images from PNG (as provided by Orthanc) to JPEG (as required by WADO).
  • The JsonCpp library is used to parse the JSON files that are provided by the Orthanc core.
  • The project is built with CMake.

Furthermore, when targeting Windows, the sample code will statically link against libpng, libjpeg and zlib. These components are required by CImg to convert from PNG to JPEG. As consequence, this sample code also illustrates how to compile all these third-party libraries using CMake.

Points of Interest

Entry Points of the Plugin

One of the most important points of the sample code consists in the definition of the 4 entry points that are required for every Orthanc plugin. This is done in the EntryPoints.cpp file:

C++
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";
  }
}

Importantly, these entry points must all be published using the C linkage model (hence the extern "C"). Indeed, to avoid the application binary interface (ABI) problems that are related to C++, Orthanc plugins are defined using the C language.

This snippet also illustrates the use of the OrthancContext class, that provides a singleton object containing the context of the Orthanc plugin engine. The OrthancContext::Initialize function is only used to set the pointer to the context of the Orthanc plugin engine in the singleton.

Furthermore, this class wraps the low-level, pure C interface of the Orthanc Plugin SDK with the standard classes of the C++ STL (such as std::string or std::map). Notice how it is possible to access the logging facilities of Orthanc using OrthancContext::GetInstance().LogWarning().

The WADO Callback

The installation of the HTTP callback for the WADO server is done at the following line of OrthancPluginInitialize():

C++
OrthancContext::GetInstance().Register("/wado", Wado);

The main Wado() function is defined in the WadoPlugin.cpp file:

C++
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);

  ...
}

In this function, arguments is a std::map<std::string, std::string> that maps the GET arguments to their values. This associative array will, among others, contain the studyUID, seriesUID, objectUID and contentType parameters of the WADO request.

Locating the Instance within Orthanc

Note: If you use Orthanc ≥ 0.8.1, you will find a simpler, faster way of locating instances at the end of this page.

Once the identifiers of the study/series/instance of interest are extracted from the HTTP WADO request, these identifiers must be located in the Orthanc store. To achieve this task, we use the fact that Orthanc plugins have direct access to the built-in REST API of Orthanc (without having to go through a network connection). We first try and locate the study:

C++"
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;
}

This function first gets the content of the "/studies" URI in the REST API, which returns the list of the studies that are known to Orthanc. The Orthanc Wiki gives more explanation about this process. Then, we loop over these studies until we find the one that matches the studyUID parameter of the WADO request. In case of success, the function returns true and the parameter study will contain information about the study, in the JSON format. A similar technique is used to look for the series, then for the instance of interest.

Serving a DICOM Instance

Once the instance is located, if the WADO request asks to return the raw DICOM file (i.e. if its "contentType" HTTP GET argument is set to "application/dicom"), the following self-explaining function is used to return the DICOM instance:

C++
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;
}

Serving a JPEG Image

By default, WADO specifies that a JPEG image is to be returned if no "contentType" is given. To achieve this, we use the fact that Orthanc can on-the-fly generate previews of DICOM images using its REST API, in the PNG format:

C++
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);

Note that the PNG image is saved to a temporary file. This is necessary as CImg does not support reading/writing from/to memory buffer. To be as cross-platform as possible, the TemporaryFile class uses a filename that consists of a random UUID. The TemporaryFile uses the RAII design pattern to remove the file on the destruction of the object.

It is now sufficient to ask CImg to convert the input PNG image to the output JPEG image (possibly after some image modifications), and to return the JPEG image with the Orthanc Plugin SDK:

C++
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");

Update - Simpler Search for Instances using Orthanc ≥ 0.8.1

If you use Orthanc ≥ 0.8.1, you can greatly simplify and speed up the process of location an instance, a series, a study or a patient given their DICOM identifiers. Starting with version 0.8.1, Orthanc indeed gives a direct access to its DICOM index through the newly introduced functions OrthancPluginLookupPatient(), OrthancPluginLookupStudy(), OrthancPluginLookupStudyWithAccessionNumber(), OrthancPluginLookupSeries() and OrthancPluginLookupInstance(). Here is how you would locate an instance with its SOPInstanceUID tag:

C++
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);
}

There is thus no need anymore to browse through the studies, then the series, and finally the instances, contrarily to the solution that was detailed in the previous sections.

History

  • July 18th, 2016: Link to the DICOMweb plugin, updated links to the SDK.
  • August 6th, 2014: Locating instances with Orthanc ≥ 0.8.1.
  • July 16th, 2014: Initial version.

Attached Files

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)