changeset 1964:2034ae383cfd deep-learning

integration default->deep-learning
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 28 Oct 2022 07:47:55 +0200
parents 0661115af939 (current diff) 79fdc3b1f031 (diff)
children 963f28eb40cb
files Applications/StoneWebViewer/WebApplication/app.js Applications/StoneWebViewer/WebApplication/index.html Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp
diffstat 20 files changed, 567 insertions(+), 78 deletions(-) [+]
line wrap: on
line diff
--- a/Applications/Samples/RtViewerPlugin/CMakeLists.txt	Tue Aug 16 15:05:51 2022 +0200
+++ b/Applications/Samples/RtViewerPlugin/CMakeLists.txt	Fri Oct 28 07:47:55 2022 +0200
@@ -28,7 +28,7 @@
   set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "mainline")
   set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
 else()
-  set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "1.10.1")
+  set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "1.11.2")
   set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web")
 endif()
 
--- a/Applications/Samples/WebAssembly/CMakeLists.txt	Tue Aug 16 15:05:51 2022 +0200
+++ b/Applications/Samples/WebAssembly/CMakeLists.txt	Fri Oct 28 07:47:55 2022 +0200
@@ -29,7 +29,7 @@
   set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "mainline")
   set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
 else()
-  set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "1.10.1")
+  set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "1.11.2")
   set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web")
 endif()
 
--- a/Applications/StoneWebViewer/NEWS	Tue Aug 16 15:05:51 2022 +0200
+++ b/Applications/StoneWebViewer/NEWS	Fri Oct 28 07:47:55 2022 +0200
@@ -1,8 +1,15 @@
 Pending changes in the mainline
 ===============================
 
+* Display of orientation markers
+* New configuration options:
+  - "ShowInfoPanelAtStartup" to control the info panel at startup
+  - "ShowUserPreferencesButton" to show the button for setting preferences
+  - "ShowNotForDiagnosticUsageDisclaimer" to show disclaimer about diagnostic usage
+  - "DicomWebHttpHeaders" to set HTTP headers in DICOMweb requests
 * More tolerance wrt. bad values of the Pixel Spacing (0028,0030) tag
 * Support of DICOM images without the Study Date (0008,0020) tag
+* Fix handling of "token": The authorization header was not set in QIDO-RS requests
 
 
 Version 2.3 (2022-03-24)
--- a/Applications/StoneWebViewer/NOTES.txt	Tue Aug 16 15:05:51 2022 +0200
+++ b/Applications/StoneWebViewer/NOTES.txt	Fri Oct 28 07:47:55 2022 +0200
@@ -92,6 +92,22 @@
   displayed at the startup.
 
 
+Minor changes
+-------------
+
+- Option "ShowInfoPanelButtonEnabled" in the Osimis Web viewer is
+  named "ShowUserPreferencesButton" in the Stone Web viewer.
+
+- Option "AlwaysShowNotForDiagnosticUsageDisclaimer" in the Osimis Web
+  viewer is named "ShowNotForDiagnosticUsageDisclaimer" in the Stone
+  Web viewer.
+
+- The allowed values for option "ShowInfoPanelAtStartup" are "Always",
+  "Never" of "User" (note the first character in upper case). In the
+  Osimis Web viewer, these options were in lower case.
+
+
+
 Authorization to the DICOMweb server (new in 2.0)
 ====================================
 
--- a/Applications/StoneWebViewer/Plugin/Plugin.cpp	Tue Aug 16 15:05:51 2022 +0200
+++ b/Applications/StoneWebViewer/Plugin/Plugin.cpp	Fri Oct 28 07:47:55 2022 +0200
@@ -65,7 +65,8 @@
       {
         std::vector<std::string> tokens;
         Orthanc::Toolbox::TokenizeString(tokens, version, '.');
-        if (tokens.size() != 2)
+        if (tokens.size() != 2 &&
+            tokens.size() != 3)
         {
           throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
                                           "Bad version of the DICOMweb plugin: " + version);
--- a/Applications/StoneWebViewer/Version.cmake	Tue Aug 16 15:05:51 2022 +0200
+++ b/Applications/StoneWebViewer/Version.cmake	Fri Oct 28 07:47:55 2022 +0200
@@ -24,6 +24,6 @@
   set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "mainline")
   set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
 else()
-  set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "1.10.1")
+  set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "1.11.2")
   set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web")
 endif()
--- a/Applications/StoneWebViewer/WebApplication/app.js	Tue Aug 16 15:05:51 2022 +0200
+++ b/Applications/StoneWebViewer/WebApplication/app.js	Fri Oct 28 07:47:55 2022 +0200
@@ -1123,8 +1123,6 @@
     if (localStorage.settingSoftwareRendering) {
       this.settingSoftwareRendering = (localStorage.settingSoftwareRendering == '1');
     }
-    
-    this.modalNotDiagnostic = this.settingNotDiagnostic;
 
     var that = this;
     
@@ -1157,6 +1155,11 @@
 
 
 window.addEventListener('StoneInitialized', function() {
+  /**
+   * Do NOT modify the order of the calls to "stone.XXX()" in this
+   * section, otherwise the HTTP headers might not be properly set.
+   **/
+
   stone.Setup(Module);
   stone.SetDicomWebRoot(app.globalConfiguration.DicomWebRoot,
                         true /* assume "/rendered" is available in DICOMweb (could be a configuration option) */);
@@ -1166,17 +1169,37 @@
     stone.SetDicomCacheSize(app.globalConfiguration.DicomCacheSize);
   }
 
-  if ('SkipSeriesFromModalities' in app.globalConfiguration) {
-    stone.SetSkipSeriesFromModalities(JSON.stringify(app.globalConfiguration.SkipSeriesFromModalities));
+  // Calls to "stone.AddHttpHeader()" must be after "stone.SetDicomWebRoot()",
+  // and before "stone.SetSkipSeriesFromModalities()"
+  for (var header in app.globalConfiguration.DicomWebHttpHeaders) {
+    stone.AddHttpHeader(header, app.globalConfiguration.DicomWebHttpHeaders[header]);
   }
   
   // Bearer token is new in Stone Web viewer 2.0
   var token = getParameterFromUrl('token');
-  if (token !== undefined)
-  {
+  if (token !== undefined) {
     stone.AddHttpHeader('Authorization', 'Bearer ' + token);
   }
+
+
+  /**
+   * Calls to "stone.XXX()" can be reordered after this point.
+   **/
   
+  if ('SkipSeriesFromModalities' in app.globalConfiguration) {
+    stone.SetSkipSeriesFromModalities(JSON.stringify(app.globalConfiguration.SkipSeriesFromModalities));
+  }
+  
+  if (app.globalConfiguration.ShowInfoPanelAtStartup == 'Always') {
+    app.modalNotDiagnostic = true;
+  } else if (app.globalConfiguration.ShowInfoPanelAtStartup == 'Never') {
+    app.modalNotDiagnostic = false;
+  } else if (app.globalConfiguration.ShowInfoPanelAtStartup == 'User') {
+    app.modalNotDiagnostic = app.settingNotDiagnostic;
+  } else {
+    alert('Bad value for option "ShowInfoPanelAtStartup": ' + app.globalConfiguration.ShowInfoPanelAtStartup);
+  }
+
   console.warn('Stone properly initialized');
 
   app.stoneWebViewerVersion = stone.GetStoneWebViewerVersion();
--- a/Applications/StoneWebViewer/WebApplication/configuration.json	Tue Aug 16 15:05:51 2022 +0200
+++ b/Applications/StoneWebViewer/WebApplication/configuration.json	Fri Oct 28 07:47:55 2022 +0200
@@ -119,6 +119,35 @@
     /**
      * Define a list of modality type that the viewer will ignore.
      **/
-    "SkipSeriesFromModalities": ["SR", "SEG", "PR"]
+    "SkipSeriesFromModalities": ["SR", "SEG", "PR"],
+
+    /**
+     * Whether to display the info panel at startup. Allowed values:
+     * "Always", "Never", "User". With "User", the user can decide to
+     * show or not the info panel in the user preferences panel (this
+     * is implemented using a cookie). (New in Stone Web viewer 2.4)
+     **/
+    "ShowInfoPanelAtStartup": "User",
+
+    /**
+     * Whether to give access to the user preferences window. (New in
+     * Stone Web viewer 2.4)
+     **/
+    "ShowUserPreferencesButton" : true,
+
+    /**
+     * Display a "not for diagnostic usage" disclaimer above the list
+     * of studies/series. (New in Stone Web viewer 2.4)
+     **/
+    "ShowNotForDiagnosticUsageDisclaimer": true,
+
+    /**
+     * HTTP headers to be set in each request to the DICOMweb server.
+     * Note that the value of the headers can be taken from the
+     * environment variables.
+     **/
+    "DicomWebHttpHeaders" : {
+      /* "Authorization" : "Bearer ${USER}" */
+    }
   }
 }
--- a/Applications/StoneWebViewer/WebApplication/index.html	Tue Aug 16 15:05:51 2022 +0200
+++ b/Applications/StoneWebViewer/WebApplication/index.html	Fri Oct 28 07:47:55 2022 +0200
@@ -40,11 +40,14 @@
             </p>
           </div>
           <div class="wvInfoPopupForm">
+            <div v-if="globalConfiguration.ShowInfoPanelAtStartup == 'User'">
+              <br>
+              <label>Show this information at startup
+                <input type="checkbox" style="margin-left: 20px" v-model="settingNotDiagnostic">
+              </label>
+              <br>
+            </div>
             <br>
-            <label>Show this information at startup
-              <input type="checkbox" style="margin-left: 20px" v-model="settingNotDiagnostic">
-            </label>
-            <br><br>
             <div style="text-align: center;">
               <button class="wvInfoPopupCloseButton" @click="modalNotDiagnostic = false">
                 Close
@@ -76,10 +79,12 @@
             <h3>User preferences</h3>
           </div>
           <div class="wvInfoPopupForm">
-            <label>Warn about the intended use at startup
-              <input type="checkbox" style="margin-left: 20px" v-model="settingNotDiagnostic">
-            </label>
-            <br>
+            <div v-if="globalConfiguration.ShowInfoPanelAtStartup == 'User'">
+              <label>Warn about the intended use at startup
+                <input type="checkbox" style="margin-left: 20px" v-model="settingNotDiagnostic">
+              </label>
+              <br>
+            </div>
             <label>Use software rendering (will reload the viewer)
               <input type="checkbox" style="margin-left: 20px" v-model="settingSoftwareRendering">
             </label>
@@ -172,7 +177,9 @@
                 <i class="fa fa-th"></i>
               </div>
 
-              <!--p class="clear disclaimer mbn">For patients, teachers and researchers.</p-->
+              <p v-if="globalConfiguration.ShowNotForDiagnosticUsageDisclaimer" class="clear disclaimer mbn">
+                For patients, researchers and quality assurance. Not for diagnostic usage.
+              </p>
             </div>        
             <div class="wvLayoutLeft__contentMiddle">
 
@@ -584,7 +591,7 @@
               </button>
             </div>
 
-            <div class="ng-scope inline-object">
+            <div class="ng-scope inline-object" v-if="globalConfiguration.ShowUserPreferencesButton">
               <button class="wvButton--underline text-center"
                       data-toggle="tooltip" data-title="User preferences"
                       v-on:click="modalPreferences = true">
--- a/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp	Tue Aug 16 15:05:51 2022 +0200
+++ b/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp	Fri Oct 28 07:47:55 2022 +0200
@@ -1585,11 +1585,12 @@
 
 private:
   static const int LAYER_TEXTURE = 0;
-  static const int LAYER_DEEP_LEARNING = 1;
-  static const int LAYER_OVERLAY = 2;
+  static const int LAYER_OVERLAY = 1;
+  static const int LAYER_ORIENTATION_MARKERS = 2;
   static const int LAYER_REFERENCE_LINES = 3;
   static const int LAYER_ANNOTATIONS_OSIRIX = 4;
   static const int LAYER_ANNOTATIONS_STONE = 5;
+  static const int LAYER_DEEP_LEARNING = 6;
 
   
   class ICommand : public Orthanc::IDynamicObject
@@ -2257,6 +2258,46 @@
 
     StoneAnnotationsRegistry::GetInstance().Load(*stoneAnnotations_, instance.GetSopInstanceUid(), frameIndex);
 
+    // Orientation markers, new in Stone Web viewer 2.4
+    std::unique_ptr<OrthancStone::MacroSceneLayer>  orientationMarkers;
+
+    if (instance.GetGeometry().IsValid())
+    {
+      orientationMarkers.reset(new OrthancStone::MacroSceneLayer);
+
+      std::string top, bottom, left, right;
+      instance.GetGeometry().GetOrientationMarkers(top, bottom, left, right);
+
+      std::unique_ptr<OrthancStone::TextSceneLayer> text;
+
+      text.reset(new OrthancStone::TextSceneLayer);
+      text->SetText(top);
+      text->SetPosition(pixelSpacingX * static_cast<double>(frame.GetWidth()) / 2.0, 0);
+      text->SetAnchor(OrthancStone::BitmapAnchor_TopCenter);
+      orientationMarkers->AddLayer(text.release());
+
+      text.reset(new OrthancStone::TextSceneLayer);
+      text->SetText(bottom);
+      text->SetPosition(pixelSpacingX * static_cast<double>(frame.GetWidth()) / 2.0,
+                        pixelSpacingY * static_cast<double>(frame.GetHeight()));
+      text->SetAnchor(OrthancStone::BitmapAnchor_BottomCenter);
+      orientationMarkers->AddLayer(text.release());
+
+      text.reset(new OrthancStone::TextSceneLayer);
+      text->SetText(left);
+      text->SetPosition(0, pixelSpacingY * static_cast<double>(frame.GetHeight()) / 2.0);
+      text->SetAnchor(OrthancStone::BitmapAnchor_CenterLeft);
+      orientationMarkers->AddLayer(text.release());
+
+      text.reset(new OrthancStone::TextSceneLayer);
+      text->SetText(right);
+      text->SetPosition(pixelSpacingX * static_cast<double>(frame.GetWidth()),
+                        pixelSpacingY * static_cast<double>(frame.GetHeight()) / 2.0);
+      text->SetAnchor(OrthancStone::BitmapAnchor_CenterRight);
+      orientationMarkers->AddLayer(text.release());
+    }
+
+
     {
       std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
 
@@ -2282,6 +2323,15 @@
         scene.DeleteLayer(LAYER_ANNOTATIONS_OSIRIX);
       }
 
+      if (orientationMarkers.get() != NULL)
+      {
+        scene.SetLayer(LAYER_ORIENTATION_MARKERS, orientationMarkers.release());
+      }
+      else
+      {
+        scene.DeleteLayer(LAYER_ORIENTATION_MARKERS);
+      }
+
       if (deepLearningLayer.get() != NULL)
       {
         scene.SetLayer(LAYER_DEEP_LEARNING, deepLearningLayer.release());
--- a/OrthancStone/Resources/Orthanc/CMake/DownloadOrthancFramework.cmake	Tue Aug 16 15:05:51 2022 +0200
+++ b/OrthancStone/Resources/Orthanc/CMake/DownloadOrthancFramework.cmake	Fri Oct 28 07:47:55 2022 +0200
@@ -146,6 +146,12 @@
         set(ORTHANC_FRAMEWORK_MD5 "8610c82d9153f22e929f2110f8f60279")
       elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.10.1")
         set(ORTHANC_FRAMEWORK_MD5 "caf667fc5ea452b3d0c2f70bfd02599c")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.11.0")
+        set(ORTHANC_FRAMEWORK_MD5 "962c4a4a706a2ef28b390d8515dd7091")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.11.1")
+        set(ORTHANC_FRAMEWORK_MD5 "a39661c406adf22cf574fde290cf4bbf")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.11.2")
+        set(ORTHANC_FRAMEWORK_MD5 "ede3de356493a8868545f8cb4b8bc8b5")
 
       # Below this point are development snapshots that were used to
       # release some plugin, before an official release of the Orthanc
--- a/OrthancStone/Resources/Orthanc/Toolchains/LinuxStandardBaseToolchain.cmake	Tue Aug 16 15:05:51 2022 +0200
+++ b/OrthancStone/Resources/Orthanc/Toolchains/LinuxStandardBaseToolchain.cmake	Fri Oct 28 07:47:55 2022 +0200
@@ -22,11 +22,11 @@
 #
 # Full build, as used on the BuildBot CIS:
 #
-#   $ LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake ../OrthancServer/ -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=../OrthancFramework/Resources/Toolchains/LinuxStandardBaseToolchain.cmake -DUSE_LEGACY_JSONCPP=ON -DUSE_LEGACY_LIBICU=ON -DBOOST_LOCALE_BACKEND=icu -DENABLE_PKCS11=ON -G Ninja
+#   $ LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake ../OrthancServer/ -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=../OrthancFramework/Resources/Toolchains/LinuxStandardBaseToolchain.cmake -DUSE_LEGACY_JSONCPP=ON -DUSE_LEGACY_LIBICU=ON -DUSE_LEGACY_BOOST=ON -DBOOST_LOCALE_BACKEND=icu -DENABLE_PKCS11=ON -G Ninja
 #
 # Or, more lightweight version (without libp11 and ICU):
 #
-#   $ LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake ../OrthancServer/ -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=../OrthancFramework/Resources/Toolchains/LinuxStandardBaseToolchain.cmake -DUSE_LEGACY_JSONCPP=ON -G Ninja
+#   $ LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake ../OrthancServer/ -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=../OrthancFramework/Resources/Toolchains/LinuxStandardBaseToolchain.cmake -DUSE_LEGACY_JSONCPP=ON -DUSE_LEGACY_BOOST=ON -G Ninja
 #
 
 INCLUDE(CMakeForceCompiler)
--- a/OrthancStone/Sources/Toolbox/BucketAccumulator2D.cpp	Tue Aug 16 15:05:51 2022 +0200
+++ b/OrthancStone/Sources/Toolbox/BucketAccumulator2D.cpp	Fri Oct 28 07:47:55 2022 +0200
@@ -195,7 +195,7 @@
 
       for (size_t x = 0; x < mapperX_.GetSize(); x++)
       {
-        fprintf(fp, "%7ld ", GetBucketContentSize(x, y));
+        fprintf(fp, "%7ld ", static_cast<long>(GetBucketContentSize(x, y)));
       }
 
       fprintf(fp, "\n");
--- a/OrthancStone/Sources/Toolbox/CoordinateSystem3D.cpp	Tue Aug 16 15:05:51 2022 +0200
+++ b/OrthancStone/Sources/Toolbox/CoordinateSystem3D.cpp	Fri Oct 28 07:47:55 2022 +0200
@@ -381,4 +381,109 @@
 
     return CoordinateSystem3D(a, axisX, axisY);
   }
+
+
+  static std::string GetOrientationString(const Vector& v)
+  {
+    /**
+     * This function directly comes from David Clunie:
+     * https://sites.google.com/site/dicomnotes/
+     *
+     * It is also a C++ reimplementation of function
+     * "getOrientationString()" from Cornerstone:
+     * https://bitbucket.org/osimis/osimis-webviewer-plugin/src/master/frontend/local_dependencies/cornerstoneTools/cornerstoneTools.js
+     **/
+
+    const char orientationX = v[0] < 0 ? 'R' : 'L';
+    const char orientationY = v[1] < 0 ? 'A' : 'P';
+    const char orientationZ = v[2] < 0 ? 'F' : 'H';
+
+    double absX = abs(v[0]);
+    double absY = abs(v[1]);
+    double absZ = abs(v[2]);
+
+    std::string result;
+
+    static const double THRESHOLD = 0.0001;
+
+    for (unsigned int i = 0; i < 3; i++)
+    {
+      if (absX > THRESHOLD && absX > absY && absX > absZ)
+      {
+        result.push_back(orientationX);
+        absX = 0;
+      }
+      else if (absY > THRESHOLD && absY > absX && absY > absZ)
+      {
+        result.push_back(orientationY);
+        absY = 0;
+      }
+      else if (absZ > THRESHOLD && absZ > absX && absZ > absY)
+      {
+        result.push_back(orientationZ);
+        absZ = 0;
+      }
+      else
+      {
+        break;
+      }
+    }
+
+    return result;
+  }
+
+
+  static std::string InvertOrientationString(const std::string& source)
+  {
+    std::string target;
+    target.resize(source.length());
+
+    for (unsigned int i = 0; i < source.length(); i++)
+    {
+      switch (source[i])
+      {
+        case 'H': target[i] = 'F'; break;
+        case 'F': target[i] = 'H'; break;
+        case 'R': target[i] = 'L'; break;
+        case 'L': target[i] = 'R'; break;
+        case 'A': target[i] = 'P'; break;
+        case 'P': target[i] = 'A'; break;
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+    }
+
+    return target;
+  }
+
+
+  void CoordinateSystem3D::GetOrientationMarkers(std::string& top /* out */,
+                                                 std::string& bottom /* out */,
+                                                 std::string& left /* out */,
+                                                 std::string& right /* out */) const
+  {
+    if (IsValid())
+    {
+      /**
+       * This is a C++ reimplementation of function
+       * "getOrientationMarkers()" from Cornerstone:
+       * https://bitbucket.org/osimis/osimis-webviewer-plugin/src/master/frontend/local_dependencies/cornerstoneTools/cornerstoneTools.js
+       *
+       * ImageOrientationPatient is row cosines then column cosines
+       * (i.e. horizontal then vertical).
+       * https://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.7.6.2
+       **/
+      const Vector& rowCosines = axisX_;  // horizontal
+      const Vector& columnCosines = axisY_;  // vertical
+
+      bottom = GetOrientationString(columnCosines);
+      right = GetOrientationString(rowCosines);
+      top = InvertOrientationString(bottom);
+      left = InvertOrientationString(right);
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
 }
--- a/OrthancStone/Sources/Toolbox/CoordinateSystem3D.h	Tue Aug 16 15:05:51 2022 +0200
+++ b/OrthancStone/Sources/Toolbox/CoordinateSystem3D.h	Fri Oct 28 07:47:55 2022 +0200
@@ -160,5 +160,10 @@
     static CoordinateSystem3D CreateFromThreePoints(const Vector& a,
                                                     const Vector& b,
                                                     const Vector& c);
+
+    void GetOrientationMarkers(std::string& top /* out */,
+                               std::string& bottom /* out */,
+                               std::string& left /* out */,
+                               std::string& right /* out */) const;
   };
 }
--- a/OrthancStone/UnitTestsSources/GeometryToolboxTests.cpp	Tue Aug 16 15:05:51 2022 +0200
+++ b/OrthancStone/UnitTestsSources/GeometryToolboxTests.cpp	Fri Oct 28 07:47:55 2022 +0200
@@ -1091,3 +1091,86 @@
     ASSERT_EQ(376u - 323u, slice);
   }
 }
+
+
+TEST(GeometryToolbox, OrientationMarkers)
+{
+  std::string top, bottom, left, right;
+
+  OrthancStone::CoordinateSystem3D s;
+  ASSERT_FALSE(s.IsValid());
+  ASSERT_THROW(s.GetOrientationMarkers(top, bottom, left, right), Orthanc::OrthancException);
+
+  {
+    // BRAINIX series: 3ca69615-fcd4a4fb-e5f2cc9d-9c7a49a5-add98bbf
+    s = OrthancStone::CoordinateSystem3D(
+      "0\\0\\0",
+      "0.99971222877502\\7.8810308973E-12\\0.02398800104856\\-0.0017278126906\\0.99740260839462\\0.07200747728347");
+    ASSERT_TRUE(s.IsValid());
+    s.GetOrientationMarkers(top, bottom, left, right);
+    ASSERT_EQ("AFL", top);
+    ASSERT_EQ("PHR", bottom);
+    ASSERT_EQ("RF", left);
+    ASSERT_EQ("LH", right);
+  }
+
+  {
+    // BRAINIX series: dc0216d2-a406a5ad-31ef7a78-113ae9d9-29939f9e
+    s = OrthancStone::CoordinateSystem3D(
+      "0\\0\\0",
+      "0.99971222877502\\7.8810308973E-12\\0.02398800104856\\0.02392569556832\\0.07202820479869\\-0.9971156120300");
+    s.GetOrientationMarkers(top, bottom, left, right);
+    ASSERT_EQ("HAR", top);
+    ASSERT_EQ("FPL", bottom);
+    ASSERT_EQ("RF", left);
+    ASSERT_EQ("LH", right);
+  }
+
+  {
+    // ASSURANCETOURIX series: 1de00990-03680ef4-0be6bd5b-73a7d350-fb46abfa
+    s = OrthancStone::CoordinateSystem3D(
+      "0\\0\\0",
+      "1\\0\\0\\0\\1\\0");
+    s.GetOrientationMarkers(top, bottom, left, right);
+    ASSERT_EQ("A", top);
+    ASSERT_EQ("P", bottom);
+    ASSERT_EQ("R", left);
+    ASSERT_EQ("L", right);
+  }
+
+  {
+    // KNEE series: 4d04593b-953ced51-87e93f11-ae4cf03c-25defdcd
+    s = OrthancStone::CoordinateSystem3D(
+      "0\\0\\0",
+      "-0\\1\\0\\-0\\-0\\-1");
+    s.GetOrientationMarkers(top, bottom, left, right);
+    ASSERT_EQ("H", top);
+    ASSERT_EQ("F", bottom);
+    ASSERT_EQ("A", left);
+    ASSERT_EQ("P", right);
+  }
+
+  {
+    // KNEE series: 20b9d0c2-97d85e07-f4dbf4d2-f09e7e6a-0c19062e
+    s = OrthancStone::CoordinateSystem3D(
+      "0\\0\\0",
+      "0.999993\\-0.0036927\\0\\-0\\-0\\-1");
+    s.GetOrientationMarkers(top, bottom, left, right);
+    ASSERT_EQ("H", top);
+    ASSERT_EQ("F", bottom);
+    ASSERT_EQ("RP", left);
+    ASSERT_EQ("LA", right);
+  }
+
+  {
+    // KNEE series: f2635388-f01d497a-15f7c06b-ad7dba06-c4c599fe
+    s = OrthancStone::CoordinateSystem3D(
+      "0\\0\\0",
+      "0.999841\\0.000366209\\0.0178227\\-0.000427244\\0.999995\\0.00326546");
+    s.GetOrientationMarkers(top, bottom, left, right);
+    ASSERT_EQ("AFL", top);
+    ASSERT_EQ("PHR", bottom);
+    ASSERT_EQ("RFA", left);
+    ASSERT_EQ("LHP", right);
+  }
+}
--- a/RenderingPlugin/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp	Tue Aug 16 15:05:51 2022 +0200
+++ b/RenderingPlugin/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp	Fri Oct 28 07:47:55 2022 +0200
@@ -250,26 +250,52 @@
     }
   }
 
+  // helper class to convert std::map of headers to the plugin SDK C structure
+  class PluginHttpHeaders
+  {
+    std::vector<const char*> headersKeys_;
+    std::vector<const char*> headersValues_;
+  public:
+
+    PluginHttpHeaders(const std::map<std::string, std::string>& httpHeaders)
+    {
+      for (std::map<std::string, std::string>::const_iterator
+           it = httpHeaders.begin(); it != httpHeaders.end(); it++)
+      {
+        headersKeys_.push_back(it->first.c_str());
+        headersValues_.push_back(it->second.c_str());
+      }      
+    }
+
+    const char* const* GetKeys()
+    {
+      return (headersKeys_.empty() ? NULL : &headersKeys_[0]);
+    }
+
+    const char* const* GetValues()
+    {
+      return (headersValues_.empty() ? NULL : &headersValues_[0]);
+    }
+
+    uint32_t GetSize()
+    {
+      return static_cast<uint32_t>(headersKeys_.size());
+    }
+  };
+
   bool MemoryBuffer::RestApiGet(const std::string& uri,
                                 const std::map<std::string, std::string>& httpHeaders,
                                 bool applyPlugins)
   {
     Clear();
 
-    std::vector<const char*> headersKeys;
-    std::vector<const char*> headersValues;
-    
-    for (std::map<std::string, std::string>::const_iterator
-           it = httpHeaders.begin(); it != httpHeaders.end(); it++)
-    {
-      headersKeys.push_back(it->first.c_str());
-      headersValues.push_back(it->second.c_str());
-    }
+    PluginHttpHeaders headers(httpHeaders);
 
     return CheckHttp(OrthancPluginRestApiGet2(
-                       GetGlobalContext(), &buffer_, uri.c_str(), httpHeaders.size(),
-                       (headersKeys.empty() ? NULL : &headersKeys[0]),
-                       (headersValues.empty() ? NULL : &headersValues[0]), applyPlugins));
+                       GetGlobalContext(), &buffer_, uri.c_str(), 
+                       headers.GetSize(),
+                       headers.GetKeys(),
+                       headers.GetValues(), applyPlugins));
   }
 
   bool MemoryBuffer::RestApiPost(const std::string& uri,
@@ -292,6 +318,41 @@
     }
   }
 
+#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+
+  bool MemoryBuffer::RestApiPost(const std::string& uri,
+                                 const void* body,
+                                 size_t bodySize,
+                                 const std::map<std::string, std::string>& httpHeaders,
+                                 bool applyPlugins)
+  {
+    MemoryBuffer answerHeaders;
+    uint16_t httpStatus;
+
+    PluginHttpHeaders headers(httpHeaders);
+
+    return CheckHttp(OrthancPluginCallRestApi(GetGlobalContext(), 
+                                              &buffer_,
+                                              *answerHeaders,
+                                              &httpStatus,
+                                              OrthancPluginHttpMethod_Post,
+                                              uri.c_str(),
+                                              headers.GetSize(), headers.GetKeys(), headers.GetValues(),
+                                              body, bodySize,
+                                              applyPlugins));
+  }
+
+
+  bool MemoryBuffer::RestApiPost(const std::string& uri,
+                                 const Json::Value& body,
+                                 const std::map<std::string, std::string>& httpHeaders,
+                                 bool applyPlugins)
+  {
+    std::string s;
+    WriteFastJson(s, body);
+    return RestApiPost(uri, s.c_str(), s.size(), httpHeaders, applyPlugins);
+  }
+#endif
 
   bool MemoryBuffer::RestApiPut(const std::string& uri,
                                 const void* body,
@@ -1457,6 +1518,30 @@
     }
   }
 
+#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+  bool RestApiPost(Json::Value& result,
+                   const std::string& uri,
+                   const Json::Value& body,
+                   const std::map<std::string, std::string>& httpHeaders,
+                   bool applyPlugins)
+  {
+    MemoryBuffer answer;
+
+    if (!answer.RestApiPost(uri, body, httpHeaders, applyPlugins))
+    {
+      return false;
+    }
+    else
+    {
+      if (!answer.IsEmpty())
+      {
+        answer.ToJson(result);
+      }
+      return true;
+    }
+  }
+#endif
+
 
   bool RestApiPost(Json::Value& result,
                    const std::string& uri,
@@ -1800,7 +1885,8 @@
 
   bool OrthancPeers::DoGet(MemoryBuffer& target,
                            size_t index,
-                           const std::string& uri) const
+                           const std::string& uri,
+                           const std::map<std::string, std::string>& headers) const
   {
     if (index >= index_.size())
     {
@@ -1809,10 +1895,12 @@
 
     OrthancPlugins::MemoryBuffer answer;
     uint16_t status;
+    PluginHttpHeaders pluginHeaders(headers);
+
     OrthancPluginErrorCode code = OrthancPluginCallPeerApi
       (GetGlobalContext(), *answer, NULL, &status, peers_,
        static_cast<uint32_t>(index), OrthancPluginHttpMethod_Get, uri.c_str(),
-       0, NULL, NULL, NULL, 0, timeout_);
+       pluginHeaders.GetSize(), pluginHeaders.GetKeys(), pluginHeaders.GetValues(), NULL, 0, timeout_);
 
     if (code == OrthancPluginErrorCode_Success)
     {
@@ -1828,21 +1916,23 @@
 
   bool OrthancPeers::DoGet(MemoryBuffer& target,
                            const std::string& name,
-                           const std::string& uri) const
+                           const std::string& uri,
+                           const std::map<std::string, std::string>& headers) const
   {
     size_t index;
     return (LookupName(index, name) &&
-            DoGet(target, index, uri));
+            DoGet(target, index, uri, headers));
   }
 
 
   bool OrthancPeers::DoGet(Json::Value& target,
                            size_t index,
-                           const std::string& uri) const
+                           const std::string& uri,
+                           const std::map<std::string, std::string>& headers) const
   {
     MemoryBuffer buffer;
 
-    if (DoGet(buffer, index, uri))
+    if (DoGet(buffer, index, uri, headers))
     {
       buffer.ToJson(target);
       return true;
@@ -1856,11 +1946,12 @@
 
   bool OrthancPeers::DoGet(Json::Value& target,
                            const std::string& name,
-                           const std::string& uri) const
+                           const std::string& uri,
+                           const std::map<std::string, std::string>& headers) const
   {
     MemoryBuffer buffer;
 
-    if (DoGet(buffer, name, uri))
+    if (DoGet(buffer, name, uri, headers))
     {
       buffer.ToJson(target);
       return true;
@@ -1875,22 +1966,24 @@
   bool OrthancPeers::DoPost(MemoryBuffer& target,
                             const std::string& name,
                             const std::string& uri,
-                            const std::string& body) const
+                            const std::string& body,
+                            const std::map<std::string, std::string>& headers) const
   {
     size_t index;
     return (LookupName(index, name) &&
-            DoPost(target, index, uri, body));
+            DoPost(target, index, uri, body, headers));
   }
 
 
   bool OrthancPeers::DoPost(Json::Value& target,
                             size_t index,
                             const std::string& uri,
-                            const std::string& body) const
+                            const std::string& body,
+                            const std::map<std::string, std::string>& headers) const
   {
     MemoryBuffer buffer;
 
-    if (DoPost(buffer, index, uri, body))
+    if (DoPost(buffer, index, uri, body, headers))
     {
       buffer.ToJson(target);
       return true;
@@ -1905,11 +1998,12 @@
   bool OrthancPeers::DoPost(Json::Value& target,
                             const std::string& name,
                             const std::string& uri,
-                            const std::string& body) const
+                            const std::string& body,
+                            const std::map<std::string, std::string>& headers) const
   {
     MemoryBuffer buffer;
 
-    if (DoPost(buffer, name, uri, body))
+    if (DoPost(buffer, name, uri, body, headers))
     {
       buffer.ToJson(target);
       return true;
@@ -1924,7 +2018,8 @@
   bool OrthancPeers::DoPost(MemoryBuffer& target,
                             size_t index,
                             const std::string& uri,
-                            const std::string& body) const
+                            const std::string& body,
+                            const std::map<std::string, std::string>& headers) const
   {
     if (index >= index_.size())
     {
@@ -1939,10 +2034,12 @@
 
     OrthancPlugins::MemoryBuffer answer;
     uint16_t status;
+    PluginHttpHeaders pluginHeaders(headers);
+
     OrthancPluginErrorCode code = OrthancPluginCallPeerApi
       (GetGlobalContext(), *answer, NULL, &status, peers_,
        static_cast<uint32_t>(index), OrthancPluginHttpMethod_Post, uri.c_str(),
-       0, NULL, NULL, body.empty() ? NULL : body.c_str(), body.size(), timeout_);
+       pluginHeaders.GetSize(), pluginHeaders.GetKeys(), pluginHeaders.GetValues(), body.empty() ? NULL : body.c_str(), body.size(), timeout_);
 
     if (code == OrthancPluginErrorCode_Success)
     {
@@ -1958,7 +2055,8 @@
 
   bool OrthancPeers::DoPut(size_t index,
                            const std::string& uri,
-                           const std::string& body) const
+                           const std::string& body,
+                           const std::map<std::string, std::string>& headers) const
   {
     if (index >= index_.size())
     {
@@ -1973,10 +2071,12 @@
 
     OrthancPlugins::MemoryBuffer answer;
     uint16_t status;
+    PluginHttpHeaders pluginHeaders(headers);
+
     OrthancPluginErrorCode code = OrthancPluginCallPeerApi
       (GetGlobalContext(), *answer, NULL, &status, peers_,
        static_cast<uint32_t>(index), OrthancPluginHttpMethod_Put, uri.c_str(),
-       0, NULL, NULL, body.empty() ? NULL : body.c_str(), body.size(), timeout_);
+       pluginHeaders.GetSize(), pluginHeaders.GetKeys(), pluginHeaders.GetValues(), body.empty() ? NULL : body.c_str(), body.size(), timeout_);
 
     if (code == OrthancPluginErrorCode_Success)
     {
@@ -1991,16 +2091,18 @@
 
   bool OrthancPeers::DoPut(const std::string& name,
                            const std::string& uri,
-                           const std::string& body) const
+                           const std::string& body,
+                           const std::map<std::string, std::string>& headers) const
   {
     size_t index;
     return (LookupName(index, name) &&
-            DoPut(index, uri, body));
+            DoPut(index, uri, body, headers));
   }
 
 
   bool OrthancPeers::DoDelete(size_t index,
-                              const std::string& uri) const
+                              const std::string& uri,
+                              const std::map<std::string, std::string>& headers) const
   {
     if (index >= index_.size())
     {
@@ -2009,10 +2111,12 @@
 
     OrthancPlugins::MemoryBuffer answer;
     uint16_t status;
+    PluginHttpHeaders pluginHeaders(headers);
+
     OrthancPluginErrorCode code = OrthancPluginCallPeerApi
       (GetGlobalContext(), *answer, NULL, &status, peers_,
        static_cast<uint32_t>(index), OrthancPluginHttpMethod_Delete, uri.c_str(),
-       0, NULL, NULL, NULL, 0, timeout_);
+       pluginHeaders.GetSize(), pluginHeaders.GetKeys(), pluginHeaders.GetValues(), NULL, 0, timeout_);
 
     if (code == OrthancPluginErrorCode_Success)
     {
@@ -2026,11 +2130,12 @@
 
 
   bool OrthancPeers::DoDelete(const std::string& name,
-                              const std::string& uri) const
+                              const std::string& uri,
+                              const std::map<std::string, std::string>& headers) const
   {
     size_t index;
     return (LookupName(index, name) &&
-            DoDelete(index, uri));
+            DoDelete(index, uri, headers));
   }
 #endif
 
@@ -3796,4 +3901,14 @@
     }
   }
 #endif
+
+  void GetHttpHeaders(std::map<std::string, std::string>& result, const OrthancPluginHttpRequest* request)
+  {
+    result.clear();
+
+    for (uint32_t i = 0; i < request->headersCount; ++i)
+    {
+      result[request->headersKeys[i]] = request->headersValues[i];
+    }    
+  }
 }
--- a/RenderingPlugin/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h	Tue Aug 16 15:05:51 2022 +0200
+++ b/RenderingPlugin/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h	Fri Oct 28 07:47:55 2022 +0200
@@ -115,6 +115,12 @@
 #  define HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP  0
 #endif
 
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 9, 2)
+#  define HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API  1
+#else
+#  define HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API  0
+#endif
+
 #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 10, 1)
 #  define HAS_ORTHANC_PLUGIN_WEBDAV  1
 #else
@@ -224,6 +230,19 @@
                      const Json::Value& body,
                      bool applyPlugins);
 
+#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+    bool RestApiPost(const std::string& uri,
+                     const Json::Value& body,
+                     const std::map<std::string, std::string>& httpHeaders,
+                     bool applyPlugins);
+
+    bool RestApiPost(const std::string& uri,
+                     const void* body,
+                     size_t bodySize,
+                     const std::map<std::string, std::string>& httpHeaders,
+                     bool applyPlugins);
+#endif
+
     bool RestApiPut(const std::string& uri,
                     const Json::Value& body,
                     bool applyPlugins);
@@ -535,6 +554,14 @@
                    size_t bodySize,
                    bool applyPlugins);
 
+#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+  bool RestApiPost(Json::Value& result,
+                   const std::string& uri,
+                   const Json::Value& body,
+                   const std::map<std::string, std::string>& httpHeaders,
+                   bool applyPlugins);
+#endif
+
   bool RestApiPost(Json::Value& result,
                    const std::string& uri,
                    const Json::Value& body,
@@ -728,53 +755,65 @@
 
     bool DoGet(MemoryBuffer& target,
                size_t index,
-               const std::string& uri) const;
+               const std::string& uri,
+               const std::map<std::string, std::string>& headers) const;
 
     bool DoGet(MemoryBuffer& target,
                const std::string& name,
-               const std::string& uri) const;
+               const std::string& uri,
+               const std::map<std::string, std::string>& headers) const;
 
     bool DoGet(Json::Value& target,
                size_t index,
-               const std::string& uri) const;
+               const std::string& uri,
+               const std::map<std::string, std::string>& headers) const;
 
     bool DoGet(Json::Value& target,
                const std::string& name,
-               const std::string& uri) const;
+               const std::string& uri,
+               const std::map<std::string, std::string>& headers) const;
 
     bool DoPost(MemoryBuffer& target,
                 size_t index,
                 const std::string& uri,
-                const std::string& body) const;
+                const std::string& body,
+                const std::map<std::string, std::string>& headers) const;
 
     bool DoPost(MemoryBuffer& target,
                 const std::string& name,
                 const std::string& uri,
-                const std::string& body) const;
+                const std::string& body,
+                const std::map<std::string, std::string>& headers) const;
 
     bool DoPost(Json::Value& target,
                 size_t index,
                 const std::string& uri,
-                const std::string& body) const;
+                const std::string& body,
+                const std::map<std::string, std::string>& headers) const;
 
     bool DoPost(Json::Value& target,
                 const std::string& name,
                 const std::string& uri,
-                const std::string& body) const;
+                const std::string& body,
+                const std::map<std::string, std::string>& headers) const;
 
     bool DoPut(size_t index,
                const std::string& uri,
-               const std::string& body) const;
+               const std::string& body,
+               const std::map<std::string, std::string>& headers) const;
 
     bool DoPut(const std::string& name,
                const std::string& uri,
-               const std::string& body) const;
+               const std::string& body,
+               const std::map<std::string, std::string>& headers) const;
 
     bool DoDelete(size_t index,
-                  const std::string& uri) const;
+                  const std::string& uri,
+                  const std::map<std::string, std::string>& headers) const;
 
     bool DoDelete(const std::string& name,
-                  const std::string& uri) const;
+                  const std::string& uri,
+                  const std::map<std::string, std::string>& headers) const;
   };
 #endif
 
@@ -1264,7 +1303,8 @@
 #endif
   };
 
-
+// helper method to convert Http headers from the plugin SDK to a std::map
+void GetHttpHeaders(std::map<std::string, std::string>& result, const OrthancPluginHttpRequest* request);
 
 #if HAS_ORTHANC_PLUGIN_WEBDAV == 1
   class IWebDavCollection : public boost::noncopyable
--- a/RenderingPlugin/Sources/Plugin.cpp	Tue Aug 16 15:05:51 2022 +0200
+++ b/RenderingPlugin/Sources/Plugin.cpp	Fri Oct 28 07:47:55 2022 +0200
@@ -53,7 +53,7 @@
     std::unique_ptr<OrthancStone::DicomStructureSet> rtstruct_;
 
   public:
-    Item(OrthancStone::DicomStructureSet* rtstruct) :
+    explicit Item(OrthancStone::DicomStructureSet* rtstruct) :
       rtstruct_(rtstruct)
     {
       if (rtstruct == NULL)
@@ -62,7 +62,7 @@
       }
     }
     
-    virtual size_t GetMemoryUsage() const
+    virtual size_t GetMemoryUsage() const ORTHANC_OVERRIDE
     {
       return 1;
     }
--- a/TODO	Tue Aug 16 15:05:51 2022 +0200
+++ b/TODO	Fri Oct 28 07:47:55 2022 +0200
@@ -38,7 +38,7 @@
   
 * add a button (and/or a keyboard shortcut) to select the next series in the main viewport
   https://groups.google.com/g/orthanc-users/c/u_lH9aqKsdw/m/KQ7U9CkiAAAJ.  In Osimis viewer,
-  this was implemented by up/down arrow keys (prev/next series).
+  this was implemented by up/down arrow keys (prev/next series)
 
 ------------
 Known issues
@@ -55,6 +55,8 @@
 * the authorization header ('token' query arg) is not included in the HTTP headers.
   Tested with https://bitbucket.org/osimis/orthanc-setup-samples/src/master/docker/authorization-plugin-viewer-query-args/.
 
+* the plugin does not work with DicomWeb plugin version 1.10.1 -> only 2 numbers are allowed:
+  https://groups.google.com/g/orthanc-users/c/xMcicKAldpM/m/b5Gz3wnyAQAJ
 
 -----------------
 Code refactorings