Skip to content

MiniPlugin

Marco BARNIG edited this page Jan 17, 2020 · 3 revisions

Here comes lesson 11 with a recipe for the development of the third Orthanc plugin. Instead of down-scaling a lonely DICOM file, as we have done in lesson 10, we will scale a whole series.

Scale Series button

To do so, we use a new function. In the Orthanc webpage showing the list of DICOM files included in a series, we will add a button to initiate the down-scaling process for these files.

button-skizze

Adding a button in the Orthanc Explorer code can be done with a few lines of Javacsript. This script is embedded into the plugin and the existing explorer.js code is extended with the Orthanc SDK function OrthancPluginExtendOrthancExplorer.

Here comes the script to add a button which initiates a REST call to the CallbackScaleSeries() function via the URL /series/<seriesID>/scale.

$('#series').live('pagebeforecreate', function() {
  var b = $('<a>')
    .attr('data-role', 'button')
    .attr('href', '#')
    .attr('data-icon', 'action')
    .attr('data-theme', 'e')
    .text('Scale images from this series to 50%');
  b.insertBefore($('#series-delete').parent().parent());
  b.click(function() {
    if ($.mobile.pageData) {
      var myURL = myFunction();
      console.log("URL : " + myURL);
      fetch(myURL).then(function(response) {
        return response.text().then(function(text) {
          console.log("Response: " + text);
          if(text == "OK") {
            // close popup-window wait with spinning wheel
            $.mobile.sdCurrentDialog.close();
          }
        });
      });
    }
  });
});

function myFunction() {
  var myLocation = window.location;
  console.log("Location : " + myLocation);
  var myProtocol = myLocation.protocol;
  var myHost = myLocation.host;  // hostname + port number
  console.log("Host : " + myHost);
  var myHash = myLocation.hash;
  console.log("Hash : " + myHash);
  var myURL = myProtocol + "//" + myHost + "/series/" + myHash.split('=')[1]  + "/scale";
  // display popup window with waiting message and spinning wheel
  $(document).simpledialog2({
    mode: 'blank',
    themeHeader: 'b',
    themeDialog: 'a',
    headerText: 'Scaling',
    headerClose: false,
    blankContent :
    '<div style="text-align:center">' +
    '<p>Please wait</p>' +
    '<img src="' + myProtocol + '//' + myHost + '/radiologic/images/ajax-loader.gif" alt="loading" width="200" height="200" />' +
    '</div>'
  });
  return myURL;
}

The first lines create the new button with jQuery Mobile. When the button is clicked, the function myFunction, which defines the Backcall REST API URL based on window.location, is called. The seriesId is added to the URL and a pop-up window, showing a spinning wheel with the message "Please Wait", is displayed. The spinning-wheel-image is hosted in the serve-folder. The URL is returned by myFunction and the HTTP REST request is launched with the native asynchronous Javascript command fetch. When the OK HTTP answer, send by the CallbackScaleSeries() function, is received by the Javascript, the pop-up window is closed.

In this lesson, and in the following ones, we add plenty of comments and log messages, due to the increasing complexity of the code. This helps us to better understand and to debug the program.

To embed the Javascript inside the plugin we add the following code into the CMakeList.txt file.

EmbedResources(
  ORTHANC_EXPLORER  ${CMAKE_SOURCE_DIR}/Resources/OrthancExplorer.js
)

To extract and apply the script we add the commands below in the MiniPlugin.cpp file.

#include <EmbeddedResources.h>

// inside the OrthancPluginInitialize function :
// Extend the default Orthanc Explorer with custom JavaScript
std::string explorer;
Orthanc::EmbeddedResources::GetFileResource(explorer,
  Orthanc::EmbeddedResources::ORTHANC_EXPLORER);
OrthancPluginExtendOrthancExplorer(context, explorer.c_str());

Get the SeriesID

In the past lessons we have learned how the callback function is coded. For the present plugin we apply some additional features in the CallbackScaleSeries() function. First we extract the seriesID from the incoming HTTP call.

LOG(INFO) << "CallbackScaleSeries request received";
// extract the GET arguments of the request
if (request->method == OrthancPluginHttpMethod_Get) {
  const char * mySeriesId = request->groups[0];
  LOG(INFO) << " *** mySeriesID " << mySeriesId;
}

Next we retrieve the list of DICOM files included in the series by calling the REST API /series/<seriesID> in a browser. Let's first look how the data returned by this call is structured.

series-json

The list of the instanceId's is provided in an array with the JSON key Instances. Here is the C++ code to extract this array:

// get list of instances related to series
OrthancPluginMemoryBuffer temp;
std::string seriesUri = "/series/" + std::string(mySeriesId);
OrthancPluginErrorCode seriesError = OrthancPluginRestApiGetAfterPlugins
  (context, &temp, seriesUri.c_str());
  LOG(INFO) << "*** seriesError: " << seriesError;
  if (seriesError == 0) {
    // show content of buffer
    const char * seriesJson = static_cast< const char * >(temp.data);
    LOG(INFO) << "JSON Data: " << seriesJson;
    std::string jsonString = std::string(seriesJson);
    Json::Reader reader;
    Json::Value root;
    bool parseStatus = reader.parse(jsonString, root);
    if (parseStatus == true) {
      const Json::Value& instances = root["Instances"];
      LOG(INFO) << "*** InstanceId's: " << instances;
    }
    OrthancPluginFreeMemoryBuffer(context, &temp);
  }
  OrthancPluginAnswerBuffer
    (context, output, "OK", 2, "text/plain");
}

The Orthanc SDK function OrthancPluginRestApiGetAfterPlugins() saves the HTTP body content into the buffer temp. If the request is successful, the open source jsoncpp library is used to parse the content and to write it into the variable jsonString. If there is no parsing error, the list of InstanceId's is stored into the variable instances.

Finally the memory buffer is freed and a HTTP answer, with the plain text OK, is returned as a confirmation and received by the Javascript.

ScaleImage files

The real work to scale all the DICOM images of the instances list is done inside the function ScaleImage(). This function is derived from the ScaleDicomImage() function, included in the MicroPlugin code. We create a separate file for this function, named ScaleImage.cpp. To include the file into the MiniPlugin project we must take the following actions:

1. Create ScaleImage header file

The C++ header file has the extension .h. Here is the related ScaleImage.h file which contains only the function interface.

#pragma once
#include <string>
void ScaleImage(std::string instanceId, std::string seriesUid);

The instruction #pragma states to include the header file only once in the project. The function ScaleImage() takes two parameters:

  • instanceId
  • seriesUid

Both are strings. Therefore we need to include the <string> library into the header file.

2. Include header file in cpp files

In both the ScaleImage.cpp and the MiniPlugin.cpp we must include the ScaleImage header file:

#include "ScaleImage.h"

3. Add the ScaleImage.cpp to the shared library

In the CMakeLists.txt file we must include the additional .cppfile in the add_library code sequence.

add_library(MiniPlugin SHARED 
  ${CORE_SOURCES}
  ${CMAKE_SOURCE_DIR}/MiniPlugin.cpp
  ${CMAKE_SOURCE_DIR}/ScaleImage.cpp
  ${AUTOGENERATED_SOURCES}
)

New Series UID

The DICOM standard requires unique identifiers for all DICOM objects. A new instanceUID is generated automatically when a new DICOM instance is uploaded to the Orthanc server. To store the scaled instances in a new Series we must however modify the related SeriesUID manually. The Orthanc SDK function OrthancPluginGenerateUuid is the right tool to execute this task. Before calling the ScaleImage() function inside the CallbackScaleSeries` function, we apply the following code:

// create new SeriesUid
char* newSeriesUid = OrthancPluginGenerateUuid(context);
std::string newSeriesUidString = std::string(newSeriesUid);

The generated newSeriesUidString variable is passed as second parameter to the ScaleImage() function. The seriesUID (DICOM tag 0020,000e) and the SeriesDescription (DICOM tag 0008,103e) are modified inside the ScaleImage() function with a system call to the dcmodify command line utility which is part of the Offis DCM Toolkit.

// modify DICOM tags with new SeriesUID and SeriesDescription
std::string dcmodifyCommand
  = "/dcmtk-3.6.4-linux-x86_64-static/bin/dcmodify -m \"(0020,000e)="
  + seriesUid + "\" -m \"(0008,103e)=Series with scaled images\" -gin -nb /tmp/scaled-"
  + instanceId + ".dcm";
int dcmodifyStatus = system(dcmodifyCommand.c_str());
LOG(INFO) << "*** System command dcmodify status: " << dcmodifyStatus;

Instance Loop

The last code snippet to present concerns the loop handling the call to the ScaleImage() function for each instanceId.

for (unsigned int i = 0; i < instances.size(); i++) {
  std::string instanceId = instances[i].asString();
  ScaleImage(instanceId, newSeriesUid);
}

Plugin Review and Build

Before building the plugin it's wise to check a last time all files and to validate the code with a C++ validator.

The next step is the same procedure as usual to build the plugin.

wget https://www.web3.lu/downloads/MiniPlugin.zip
unzip MiniPlugin.zip
rm MinipluginPlugin.zip
cd Miniplugin
mkdir Build
cd Build
cmake -DSTATIC_BUILD=ON -DCMAKE_BUILD_TYPE=Release ../
make

Here are the Build results:

miniplugin-build-results

Great! Let's add the compiled plugin to the RadioLogicArchive and test it.

mini-plugin

The MiniPlugin is shown in the list of the installed plugins. I use a recent CT of my own head to check if the images are scaled down as expected.

scale-button

Let's click the scale button to start the process.

mini-plugin-wait

If we go back to the studies page in the Orthanc Explorer we see that a second series with the scaled images has been added.

mini-plugin-scaled-series

Awesome! 😃😀😄

Log Files

Before leaving the present lesson, we should have a look at the log files. Printing messages to the log system or to the console doesn't make sense if we ignore them. Let's first view the output in the Javascript console opened for the DICOM Series webpage where we started the Scale process:

miniplugin-javascript-console

The messages comply to our expectation. Otherwise, we could easely inspect the Explorer.js extension in a browser with the URL /plugin/explorer.js.

miniplugin-javascript-show

To analyse the C++ messages when Orthanc is started in --verbose mode, we can read the log files in the related Docker container in the Synology diskstation, or we can scroll through the output of the terminal, if the RadioLogicArchive was started in interactive mode. Below is an extract of the terminal window messages:

mini-plugin-log

To make a distinction between the plugin log messages and the Orthanc core messages, the first ones are marked with three stars (***) at the message beginning. It's easy to follow the process sequence:

  1. HTTP request for URI: /series/3c3d557b-fb8c27d1-39be765e-e8bdd59c-95a1ca82/scale
  2. CallbackSeriesScale request received
  3. SeriesId extracted
  4. REST GET call to URI /series/3c3d557b-fb8c27d1-39be765e-e8bdd59c-95a1ca82 without error
  5. JSON parsing in the request body
  6. Three system commands executed without error
  7. REST POST call to URI /instances
  8. Attachments created ann new instance stored
  9. Temporary files successfully removed

Next lesson

We are now fit to face new challenges in the next lesson. We will develop a simple plugin running a job. The job engine helps us to avoid the spinning wheel which blocks our system when doing tasks taking some time, for example scaling several hundred DICOM images in one batch.

spinning-wheel

Links

Clone this wiki locally