changeset 117:42c05a3baee3 wasm

loading multi-frame instances as 3D volumes
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 28 Sep 2017 16:55:51 +0200
parents 4c5f7cda8624
children a4d0b6c82b29
files Applications/BasicApplicationContext.cpp Applications/Samples/SingleFrameApplication.h Applications/Samples/SingleVolumeApplication.h Framework/Enumerations.cpp Framework/Enumerations.h Framework/Layers/OrthancFrameLayerSource.cpp Framework/Layers/OrthancFrameLayerSource.h Framework/Toolbox/OrthancSlicesLoader.cpp Framework/Toolbox/OrthancSlicesLoader.h Framework/Toolbox/Slice.cpp Framework/Toolbox/Slice.h Framework/dev.h Resources/CMake/OrthancStoneConfiguration.cmake
diffstat 13 files changed, 447 insertions(+), 79 deletions(-) [+]
line wrap: on
line diff
--- a/Applications/BasicApplicationContext.cpp	Mon Sep 25 13:43:47 2017 +0200
+++ b/Applications/BasicApplicationContext.cpp	Thu Sep 28 16:55:51 2017 +0200
@@ -41,6 +41,7 @@
 
   BasicApplicationContext::BasicApplicationContext(Orthanc::WebServiceParameters& orthanc) :
     oracle_(viewportMutex_, 4),  // Use 4 threads to download
+    //oracle_(viewportMutex_, 1),  // Disable threading to be reproducible
     webService_(oracle_, orthanc),
     stopped_(true),
     updateDelay_(100)   // By default, 100ms between each refresh of the content
--- a/Applications/Samples/SingleFrameApplication.h	Mon Sep 25 13:43:47 2017 +0200
+++ b/Applications/Samples/SingleFrameApplication.h	Thu Sep 28 16:55:51 2017 +0200
@@ -252,7 +252,7 @@
         std::auto_ptr<OrthancFrameLayerSource> layer
           (new OrthancFrameLayerSource(context.GetWebService()));
         //layer->SetImageQuality(SliceImageQuality_Jpeg50);
-        layer->LoadInstance(instance, frame);
+        layer->LoadFrame(instance, frame);
         //layer->LoadSeries("6f1b492a-e181e200-44e51840-ef8db55e-af529ab6");
         layer->Register(*this);
         source_ = layer.get();
--- a/Applications/Samples/SingleVolumeApplication.h	Mon Sep 25 13:43:47 2017 +0200
+++ b/Applications/Samples/SingleVolumeApplication.h	Thu Sep 28 16:55:51 2017 +0200
@@ -45,6 +45,8 @@
         generic.add_options()
           ("series", boost::program_options::value<std::string>(), 
            "Orthanc ID of the series")
+          ("instance", boost::program_options::value<std::string>(), 
+           "Orthanc ID of a multi-frame instance that describes a 3D volume")
           ("threads", boost::program_options::value<unsigned int>()->default_value(3), 
            "Number of download threads")
           ("projection", boost::program_options::value<std::string>()->default_value("axial"), 
@@ -62,13 +64,39 @@
       {
         using namespace OrthancStone;
 
-        if (parameters.count("series") != 1)
+        if (parameters.count("series") > 1 ||
+            parameters.count("instance") > 1)
         {
-          LOG(ERROR) << "The series ID is missing";
+          LOG(ERROR) << "Only one series or instance is allowed";
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        }
+
+        if (parameters.count("series") == 1 &&
+            parameters.count("instance") == 1)
+        {
+          LOG(ERROR) << "Cannot specify both a series and an instance";
           throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
         }
 
-        std::string series = parameters["series"].as<std::string>();
+        std::string series;
+        if (parameters.count("series") == 1)
+        {
+          series = parameters["series"].as<std::string>();
+        }
+        
+        std::string instance;
+        if (parameters.count("instance") == 1)
+        {
+          instance = parameters["instance"].as<std::string>();
+        }
+        
+        if (series.empty() &&
+            instance.empty())
+        {
+          LOG(ERROR) << "The series ID or instance ID is missing";
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        }
+
         unsigned int threads = parameters["threads"].as<unsigned int>();
         bool reverse = parameters["reverse"].as<bool>();
 
@@ -98,7 +126,14 @@
 
 #if 1
         std::auto_ptr<OrthancVolumeImage> volume(new OrthancVolumeImage(context.GetWebService()));
-        volume->ScheduleLoadSeries(series);
+        if (series.empty())
+        {
+          volume->ScheduleLoadInstance(instance);
+        }
+        else
+        {
+          volume->ScheduleLoadSeries(series);
+        }
 
         widget->AddLayer(new VolumeImageSource(*volume));
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Enumerations.cpp	Thu Sep 28 16:55:51 2017 +0200
@@ -0,0 +1,42 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "Enumerations.h"
+
+#include <Core/Logging.h>
+
+namespace OrthancStone
+{  
+  bool StringToSopClassUid(SopClassUid& result,
+                           const std::string& source)
+  {
+    if (source == "1.2.840.10008.5.1.4.1.1.481.2")
+    {
+      result = SopClassUid_RTDose;
+      return true;
+    }
+    else
+    {
+      //LOG(INFO) << "Unknown SOP class UID: " << source;
+      return false;
+    }
+  }  
+}
--- a/Framework/Enumerations.h	Mon Sep 25 13:43:47 2017 +0200
+++ b/Framework/Enumerations.h	Thu Sep 28 16:55:51 2017 +0200
@@ -21,6 +21,8 @@
 
 #pragma once
 
+#include <string>
+
 namespace OrthancStone
 {
   enum SliceOffsetMode
@@ -79,4 +81,12 @@
     SliceImageQuality_Jpeg90,
     SliceImageQuality_Jpeg95
   };
+
+  enum SopClassUid
+  {
+    SopClassUid_RTDose
+  };
+
+  bool StringToSopClassUid(SopClassUid& result,
+                           const std::string& source);
 }
--- a/Framework/Layers/OrthancFrameLayerSource.cpp	Mon Sep 25 13:43:47 2017 +0200
+++ b/Framework/Layers/OrthancFrameLayerSource.cpp	Thu Sep 28 16:55:51 2017 +0200
@@ -75,16 +75,22 @@
   }
 
   
-  void OrthancFrameLayerSource::LoadInstance(const std::string& instanceId,
-                                             unsigned int frame)
+  void OrthancFrameLayerSource::LoadSeries(const std::string& seriesId)
   {
-    loader_.ScheduleLoadInstance(instanceId, frame);
+    loader_.ScheduleLoadSeries(seriesId);
   }
 
 
-  void OrthancFrameLayerSource::LoadSeries(const std::string& seriesId)
+  void OrthancFrameLayerSource::LoadInstance(const std::string& instanceId)
   {
-    loader_.ScheduleLoadSeries(seriesId);
+    loader_.ScheduleLoadInstance(instanceId);
+  }
+
+
+  void OrthancFrameLayerSource::LoadFrame(const std::string& instanceId,
+                                          unsigned int frame)
+  {
+    loader_.ScheduleLoadFrame(instanceId, frame);
   }
 
 
--- a/Framework/Layers/OrthancFrameLayerSource.h	Mon Sep 25 13:43:47 2017 +0200
+++ b/Framework/Layers/OrthancFrameLayerSource.h	Thu Sep 28 16:55:51 2017 +0200
@@ -53,10 +53,12 @@
   public:
     OrthancFrameLayerSource(IWebService& orthanc);
 
-    void LoadInstance(const std::string& instanceId,
-                      unsigned int frame);
+    void LoadSeries(const std::string& seriesId);
 
-    void LoadSeries(const std::string& seriesId);
+    void LoadInstance(const std::string& instanceId);
+
+    void LoadFrame(const std::string& instanceId,
+                   unsigned int frame);
 
     void SetImageQuality(SliceImageQuality quality)
     {
--- a/Framework/Toolbox/OrthancSlicesLoader.cpp	Mon Sep 25 13:43:47 2017 +0200
+++ b/Framework/Toolbox/OrthancSlicesLoader.cpp	Thu Sep 28 16:55:51 2017 +0200
@@ -27,6 +27,7 @@
 #include <Core/Images/ImageProcessing.h>
 #include <Core/Images/JpegReader.h>
 #include <Core/Images/PngReader.h>
+#include <Core/Compression/GzipCompressor.h>
 #include <Core/Logging.h>
 #include <Core/OrthancException.h>
 #include <Core/Toolbox.h>
@@ -96,25 +97,29 @@
 
     unsigned int GetSliceIndex() const
     {
-      assert(mode_ == Mode_LoadImage);
+      assert(mode_ == Mode_LoadImage ||
+             mode_ == Mode_LoadRawImage);
       return sliceIndex_;
     }
 
     const Slice& GetSlice() const
     {
-      assert(mode_ == Mode_LoadImage && slice_ != NULL);
+      assert(mode_ == Mode_LoadImage ||
+             mode_ == Mode_LoadRawImage);
+      assert(slice_ != NULL);
       return *slice_;
     }
 
     unsigned int GetFrame() const
     {
-      assert(mode_ == Mode_InstanceGeometry);
+      assert(mode_ == Mode_FrameGeometry);
       return frame_;
     }
       
     const std::string& GetInstanceId() const
     {
-      assert(mode_ == Mode_InstanceGeometry);
+      assert(mode_ == Mode_FrameGeometry ||
+             mode_ == Mode_InstanceGeometry);
       return instanceId_;
     }
       
@@ -123,11 +128,18 @@
       return new Operation(Mode_SeriesGeometry);
     }
 
-    static Operation* DownloadInstanceGeometry(const std::string& instanceId,
-                                               unsigned int frame)
+    static Operation* DownloadInstanceGeometry(const std::string& instanceId)
     {
       std::auto_ptr<Operation> operation(new Operation(Mode_InstanceGeometry));
       operation->instanceId_ = instanceId;
+      return operation.release();
+    }
+
+    static Operation* DownloadFrameGeometry(const std::string& instanceId,
+                                            unsigned int frame)
+    {
+      std::auto_ptr<Operation> operation(new Operation(Mode_FrameGeometry));
+      operation->instanceId_ = instanceId;
       operation->frame_ = frame;
       return operation.release();
     }
@@ -142,6 +154,15 @@
       tmp->quality_ = quality;
       return tmp.release();
     }
+
+    static Operation* DownloadSliceRawImage(unsigned int  sliceIndex,
+                                            const Slice&  slice)
+    {
+      std::auto_ptr<Operation> tmp(new Operation(Mode_LoadRawImage));
+      tmp->sliceIndex_ = sliceIndex;
+      tmp->slice_ = &slice;
+      return tmp.release();
+    }
   };
     
 
@@ -170,8 +191,12 @@
           break;
 
         case Mode_InstanceGeometry:
-          that_.ParseInstanceGeometry(operation->GetInstanceId(),
-                                      operation->GetFrame(), answer, answerSize);
+          that_.ParseInstanceGeometry(operation->GetInstanceId(), answer, answerSize);
+          break;
+
+        case Mode_FrameGeometry:
+          that_.ParseFrameGeometry(operation->GetInstanceId(),
+                                   operation->GetFrame(), answer, answerSize);
           break;
 
         case Mode_LoadImage:
@@ -193,6 +218,10 @@
                 
           break;
 
+        case Mode_LoadRawImage:
+          that_.ParseSliceRawImage(*operation, answer, answerSize);
+          break;
+
         default:
           throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
       }
@@ -206,7 +235,7 @@
 
       switch (operation->GetMode())
       {
-        case Mode_InstanceGeometry:
+        case Mode_FrameGeometry:
         case Mode_SeriesGeometry:
           that_.userCallback_.NotifyGeometryError(that_);
           that_.state_ = State_Error;
@@ -266,15 +295,25 @@
     for (size_t i = 0; i < instances.size(); i++)
     {
       OrthancPlugins::FullOrthancDataset dataset(series[instances[i]]);
+      OrthancPlugins::DicomDatasetReader reader(dataset);
+      
+      unsigned int frames;
+      if (!reader.GetUnsignedIntegerValue(frames, OrthancPlugins::DICOM_TAG_NUMBER_OF_FRAMES))
+      {
+        frames = 1;
+      }
 
-      Slice slice;
-      if (slice.ParseOrthancFrame(dataset, instances[i], 0 /* todo */))
+      for (unsigned int frame = 0; frame < frames; frame++)
       {
-        slices_.AddSlice(slice);
-      }
-      else
-      {
-        LOG(WARNING) << "Skipping invalid instance " << instances[i];
+        Slice slice;
+        if (slice.ParseOrthancFrame(dataset, instances[i], frame))
+        {
+          slices_.AddSlice(slice);
+        }
+        else
+        {
+          LOG(WARNING) << "Skipping invalid frame " << frame << " within instance " << instances[i];
+        }
       }
     }
 
@@ -308,7 +347,6 @@
 
 
   void OrthancSlicesLoader::ParseInstanceGeometry(const std::string& instanceId,
-                                                  unsigned int frame,
                                                   const void* answer,
                                                   size_t size)
   {
@@ -321,6 +359,51 @@
     }
 
     OrthancPlugins::FullOrthancDataset dataset(tags);
+    OrthancPlugins::DicomDatasetReader reader(dataset);
+
+    unsigned int frames;
+    if (!reader.GetUnsignedIntegerValue(frames, OrthancPlugins::DICOM_TAG_NUMBER_OF_FRAMES))
+    {
+      frames = 1;
+    }
+    
+    LOG(INFO) << "Instance " << instanceId << " contains " << frames << " frame(s)";
+
+    state_ = State_GeometryReady;
+
+    for (unsigned int frame = 0; frame < frames; frame++)
+    {
+      Slice slice;
+      if (slice.ParseOrthancFrame(dataset, instanceId, frame))
+      {
+        slices_.AddSlice(slice);
+      }
+      else
+      {
+        LOG(WARNING) << "Skipping invalid multi-frame instance " << instanceId;
+        userCallback_.NotifyGeometryError(*this);
+        return;
+      }
+    }
+
+    userCallback_.NotifyGeometryReady(*this);
+  }
+
+
+  void OrthancSlicesLoader::ParseFrameGeometry(const std::string& instanceId,
+                                               unsigned int frame,
+                                               const void* answer,
+                                               size_t size)
+  {
+    Json::Value tags;
+    if (!MessagingToolbox::ParseJson(tags, answer, size) ||
+        tags.type() != Json::objectValue)
+    {
+      userCallback_.NotifyGeometryError(*this);
+      return;
+    }
+
+    OrthancPlugins::FullOrthancDataset dataset(tags);
 
     state_ = State_GeometryReady;
       
@@ -526,6 +609,19 @@
   }
     
     
+  void OrthancSlicesLoader::ParseSliceRawImage(const Operation& operation,
+                                               const void* answer,
+                                               size_t size)
+  {
+    Orthanc::GzipCompressor compressor;
+
+    std::string raw;
+    compressor.Uncompress(raw, answer, size);
+    
+    printf("[%d => %d]\n", size, raw.size());
+  }
+
+
   OrthancSlicesLoader::OrthancSlicesLoader(ICallback& callback,
                                            IWebService& orthanc) :
     webCallback_(new WebCallback(*this)),
@@ -551,8 +647,27 @@
   }
 
 
-  void OrthancSlicesLoader::ScheduleLoadInstance(const std::string& instanceId,
-                                                 unsigned int frame)
+  void OrthancSlicesLoader::ScheduleLoadInstance(const std::string& instanceId)
+  {
+    if (state_ != State_Initialization)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      state_ = State_LoadingGeometry;
+
+      // Tag "3004-000c" is "Grid Frame Offset Vector", which is
+      // mandatory to read RT DOSE, but is too long to be returned by default
+      std::string uri = "/instances/" + instanceId + "/tags?ignore-length=3004-000c";
+      orthanc_.ScheduleGetRequest
+        (*webCallback_, uri, Operation::DownloadInstanceGeometry(instanceId));
+    }
+  }
+  
+
+  void OrthancSlicesLoader::ScheduleLoadFrame(const std::string& instanceId,
+                                              unsigned int frame)
   {
     if (state_ != State_Initialization)
     {
@@ -563,7 +678,7 @@
       state_ = State_LoadingGeometry;
       std::string uri = "/instances/" + instanceId + "/tags";
       orthanc_.ScheduleGetRequest
-        (*webCallback_, uri, Operation::DownloadInstanceGeometry(instanceId, frame));
+        (*webCallback_, uri, Operation::DownloadFrameGeometry(instanceId, frame));
     }
   }
   
@@ -608,10 +723,9 @@
   }
   
 
-  void OrthancSlicesLoader::ScheduleSliceImagePng(size_t index)
+  void OrthancSlicesLoader::ScheduleSliceImagePng(const Slice& slice,
+                                                  size_t index)
   {
-    const Slice& slice = GetSlice(index);
-
     std::string uri = ("/instances/" + slice.GetOrthancInstanceId() + "/frames/" + 
                        boost::lexical_cast<std::string>(slice.GetFrame()));
 
@@ -638,7 +752,8 @@
   }
 
 
-  void OrthancSlicesLoader::ScheduleSliceImageJpeg(size_t index,
+  void OrthancSlicesLoader::ScheduleSliceImageJpeg(const Slice& slice,
+                                                   size_t index,
                                                    SliceImageQuality quality)
   {
     unsigned int value;
@@ -662,7 +777,6 @@
     }
     
     // This requires the official Web viewer plugin to be installed!
-    const Slice& slice = GetSlice(index);
     std::string uri = ("/web-viewer/instances/jpeg" + 
                        boost::lexical_cast<std::string>(value) + 
                        "-" + slice.GetOrthancInstanceId() + "_" + 
@@ -673,6 +787,7 @@
   }
 
 
+
   void OrthancSlicesLoader::ScheduleLoadSliceImage(size_t index,
                                                    SliceImageQuality quality)
   {
@@ -681,13 +796,25 @@
       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
     }
 
-    if (quality == SliceImageQuality_Full)
+    const Slice& slice = GetSlice(index);
+
+    if (slice.HasOrthancDecoding())
     {
-      ScheduleSliceImagePng(index);
+      if (quality == SliceImageQuality_Full)
+      {
+        ScheduleSliceImagePng(slice, index);
+      }
+      else
+      {
+        ScheduleSliceImageJpeg(slice, index, quality);
+      }
     }
     else
     {
-      ScheduleSliceImageJpeg(index, quality);
+      std::string uri = ("/instances/" + slice.GetOrthancInstanceId() + "/frames/" + 
+                         boost::lexical_cast<std::string>(slice.GetFrame()) + "/raw.gz");
+      orthanc_.ScheduleGetRequest(*webCallback_, uri,
+                                  Operation::DownloadSliceRawImage(index, slice));
     }
   }
 }
--- a/Framework/Toolbox/OrthancSlicesLoader.h	Mon Sep 25 13:43:47 2017 +0200
+++ b/Framework/Toolbox/OrthancSlicesLoader.h	Thu Sep 28 16:55:51 2017 +0200
@@ -47,7 +47,7 @@
                                          unsigned int sliceIndex,
                                          const Slice& slice,
                                          std::auto_ptr<Orthanc::ImageAccessor>& image,
-                                         SliceImageQuality quality) = 0;
+                                         SliceImageQuality effectiveQuality) = 0;
 
       virtual void NotifySliceImageError(const OrthancSlicesLoader& loader,
                                          unsigned int sliceIndex,
@@ -68,7 +68,9 @@
     {
       Mode_SeriesGeometry,
       Mode_InstanceGeometry,
-      Mode_LoadImage
+      Mode_FrameGeometry,
+      Mode_LoadImage,
+      Mode_LoadRawImage
     };
 
     class Operation;
@@ -90,10 +92,14 @@
                              size_t size);
 
     void ParseInstanceGeometry(const std::string& instanceId,
-                               unsigned int frame,
                                const void* answer,
                                size_t size);
 
+    void ParseFrameGeometry(const std::string& instanceId,
+                            unsigned int frame,
+                            const void* answer,
+                            size_t size);
+
     void ParseSliceImagePng(const Operation& operation,
                             const void* answer,
                             size_t size);
@@ -102,9 +108,15 @@
                              const void* answer,
                              size_t size);
 
-    void ScheduleSliceImagePng(size_t index);
+    void ParseSliceRawImage(const Operation& operation,
+                            const void* answer,
+                            size_t size);
+
+    void ScheduleSliceImagePng(const Slice& slice,
+                               size_t index);
     
-    void ScheduleSliceImageJpeg(size_t index,
+    void ScheduleSliceImageJpeg(const Slice& slice,
+                                size_t index,
                                 SliceImageQuality quality);
     
   public:
@@ -113,8 +125,10 @@
 
     void ScheduleLoadSeries(const std::string& seriesId);
 
-    void ScheduleLoadInstance(const std::string& instanceId,
-                              unsigned int frame);
+    void ScheduleLoadInstance(const std::string& instanceId);
+
+    void ScheduleLoadFrame(const std::string& instanceId,
+                           unsigned int frame);
 
     bool IsGeometryReady() const;
 
@@ -126,6 +140,6 @@
                      const CoordinateSystem3D& plane) const;
 
     void ScheduleLoadSliceImage(size_t index,
-                                SliceImageQuality quality);
+                                SliceImageQuality requestedQuality);
   };
 }
--- a/Framework/Toolbox/Slice.cpp	Mon Sep 25 13:43:47 2017 +0200
+++ b/Framework/Toolbox/Slice.cpp	Thu Sep 28 16:55:51 2017 +0200
@@ -21,32 +21,135 @@
 
 #include "Slice.h"
 
+#include "../Enumerations.h"
+
+#include <Core/Logging.h>
 #include <Core/OrthancException.h>
+#include <Core/Toolbox.h>
+
+#include <boost/lexical_cast.hpp>
 
 namespace OrthancStone
 {
+  static bool ParseDouble(double& target,
+                          const std::string& source)
+  {
+    try
+    {
+      target = boost::lexical_cast<double>(source);
+      return true;
+    }
+    catch (boost::bad_lexical_cast&)
+    {
+      return false;
+    }
+  }
+  
+  bool Slice::ComputeRTDoseGeometry(const OrthancPlugins::DicomDatasetReader& reader,
+                                    unsigned int frame)
+  {
+    // http://dicom.nema.org/medical/Dicom/2016a/output/chtml/part03/sect_C.8.8.3.2.html
+    static const OrthancPlugins::DicomTag DICOM_TAG_GRID_FRAME_OFFSET_VECTOR(0x3004, 0x000c);
+    static const OrthancPlugins::DicomTag DICOM_TAG_FRAME_INCREMENT_POINTER(0x0028, 0x0009);
+
+    std::string increment = reader.GetStringValue(DICOM_TAG_FRAME_INCREMENT_POINTER, "");
+    std::string offsetTag;
+
+    bool ok = reader.GetDataset().GetStringValue(offsetTag, DICOM_TAG_GRID_FRAME_OFFSET_VECTOR);
+    if (!ok)
+    {
+      LOG(ERROR) << "Cannot read the \"GridFrameOffsetVector\" tag, check you are using Orthanc >= 1.3.1";
+      return false;
+    }
+
+    Orthanc::Toolbox::ToUpperCase(increment);
+    if (increment != "3004,000C" ||
+        offsetTag.empty())
+    {
+      return false;
+    }
+
+    std::vector<std::string> offsets;
+    Orthanc::Toolbox::TokenizeString(offsets, offsetTag, '\\');
+
+    if (frameCount_ == 0 ||
+        offsets.size() != frameCount_ ||
+        frame >= frameCount_)
+    {
+      LOG(ERROR) << "No information about the 3D location of some slice(s) in a RT DOSE";
+      return false;
+    }
+
+    double offset0, z;
+
+    if (!ParseDouble(offset0, offsets[0]) ||
+        !ParseDouble(z, offsets[frame]))
+    {
+      LOG(ERROR) << "Invalid syntax";
+      return false;
+    }
+
+    if (!GeometryToolbox::IsCloseToZero(offset0))
+    {
+      LOG(ERROR) << "Invalid syntax";
+      return false;
+    }
+
+    geometry_ = CoordinateSystem3D(geometry_.GetOrigin() + z * geometry_.GetNormal(),
+                                   geometry_.GetAxisX(),
+                                   geometry_.GetAxisY());
+    
+    return true;
+  }
+
+  
   bool Slice::ParseOrthancFrame(const OrthancPlugins::IDicomDataset& dataset,
                                 const std::string& instanceId,
                                 unsigned int frame)
   {
+    orthancInstanceId_ = instanceId;
+    frame_ = frame;
+    type_ = Type_OrthancDecodableFrame;
+
     OrthancPlugins::DicomDatasetReader reader(dataset);
 
-    unsigned int frameCount;
-    if (!reader.GetUnsignedIntegerValue(frameCount, OrthancPlugins::DICOM_TAG_NUMBER_OF_FRAMES))
+    sopClassUid_ = reader.GetStringValue(OrthancPlugins::DICOM_TAG_SOP_CLASS_UID, "");
+    if (sopClassUid_.empty())
     {
-      frameCount = 1;   // Assume instance with one frame
+      LOG(ERROR) << "Instance without a SOP class UID";
+      return false; 
+    }
+  
+    if (!reader.GetUnsignedIntegerValue(frameCount_, OrthancPlugins::DICOM_TAG_NUMBER_OF_FRAMES))
+    {
+      frameCount_ = 1;   // Assume instance with one frame
     }
 
-    if (frame >= frameCount)
+    if (frame >= frameCount_)
     {
       return false;
     }
 
-    if (!reader.GetDoubleValue(thickness_, OrthancPlugins::DICOM_TAG_SLICE_THICKNESS))
+    if (!reader.GetUnsignedIntegerValue(width_, OrthancPlugins::DICOM_TAG_COLUMNS) ||
+        !reader.GetUnsignedIntegerValue(height_, OrthancPlugins::DICOM_TAG_ROWS))
     {
-      thickness_ = 100.0 * std::numeric_limits<double>::epsilon();
+      return false;
     }
 
+    thickness_ = 100.0 * std::numeric_limits<double>::epsilon();
+
+    std::string tmp;
+    if (dataset.GetStringValue(tmp, OrthancPlugins::DICOM_TAG_SLICE_THICKNESS))
+    {
+      if (!tmp.empty() &&
+          !ParseDouble(thickness_, tmp))
+      {
+        return false;  // Syntax error
+      }
+    }
+    
+    converter_.ReadParameters(dataset);
+
     GeometryToolbox::GetPixelSpacing(pixelSpacingX_, pixelSpacingY_, dataset);
 
     std::string position, orientation;
@@ -54,33 +157,47 @@
         dataset.GetStringValue(orientation, OrthancPlugins::DICOM_TAG_IMAGE_ORIENTATION_PATIENT))
     {
       geometry_ = CoordinateSystem3D(position, orientation);
+
+      bool ok = true;
+      SopClassUid tmp;
+
+      if (StringToSopClassUid(tmp, sopClassUid_))
+      {
+        switch (tmp)
+        {
+          case SopClassUid_RTDose:
+            type_ = Type_OrthancRawFrame;
+            ok = ComputeRTDoseGeometry(reader, frame);
+            break;
+            
+          default:
+            break;
+        }
+      }
+
+      if (!ok)
+      {
+        LOG(ERROR) << "Cannot deduce the 3D location of frame " << frame
+                   << " in instance " << instanceId << ", whose SOP class UID is: " << sopClassUid_;
+        return false;
+      }
     }
-      
-    if (reader.GetUnsignedIntegerValue(width_, OrthancPlugins::DICOM_TAG_COLUMNS) &&
-        reader.GetUnsignedIntegerValue(height_, OrthancPlugins::DICOM_TAG_ROWS))
-    {
-      orthancInstanceId_ = instanceId;
-      frame_ = frame;
-      converter_.ReadParameters(dataset);
 
-      type_ = Type_OrthancInstance;
-      return true;
-    }
-    else
-    {
-      return false;
-    }
+    return true;
   }
 
   
   const std::string Slice::GetOrthancInstanceId() const
   {
-    if (type_ != Type_OrthancInstance)
+    if (type_ == Type_OrthancDecodableFrame ||
+        type_ == Type_OrthancRawFrame)
+    {
+      return orthancInstanceId_;
+    }
+    else
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-    }
-        
-    return orthancInstanceId_;
+    }   
   }
 
   
--- a/Framework/Toolbox/Slice.h	Mon Sep 25 13:43:47 2017 +0200
+++ b/Framework/Toolbox/Slice.h	Thu Sep 28 16:55:51 2017 +0200
@@ -33,13 +33,19 @@
     {
       Type_Invalid,
       Type_Standalone,
-      Type_OrthancInstance
+      Type_OrthancDecodableFrame,
+      Type_OrthancRawFrame
       // TODO A slice could come from some DICOM file (URL)
     };
 
+    bool ComputeRTDoseGeometry(const OrthancPlugins::DicomDatasetReader& reader,
+                               unsigned int frame);
+
     Type                 type_;
     std::string          orthancInstanceId_;
+    std::string          sopClassUid_;
     unsigned int         frame_;
+    unsigned int         frameCount_;
     CoordinateSystem3D   geometry_;
     double               pixelSpacingX_;
     double               pixelSpacingY_;
@@ -59,6 +65,7 @@
           double thickness) :
       type_(Type_Standalone),
       frame_(0),
+      frameCount_(0),
       geometry_(plane),
       pixelSpacingX_(1),
       pixelSpacingY_(1),
@@ -76,6 +83,7 @@
           unsigned int height,
           const DicomFrameConverter& converter) :
       type_(Type_Standalone),
+      frameCount_(1),
       geometry_(plane),
       pixelSpacingX_(pixelSpacingX),
       pixelSpacingY_(pixelSpacingY),
@@ -95,9 +103,9 @@
                            const std::string& instanceId,
                            unsigned int frame);
 
-    bool IsOrthancInstance() const
+    bool HasOrthancDecoding() const
     {
-      return type_ == Type_OrthancInstance;
+      return type_ == Type_OrthancDecodableFrame;
     }
 
     const std::string GetOrthancInstanceId() const;
--- a/Framework/dev.h	Mon Sep 25 13:43:47 2017 +0200
+++ b/Framework/dev.h	Thu Sep 28 16:55:51 2017 +0200
@@ -211,10 +211,15 @@
       loader_.ScheduleLoadSeries(seriesId);
     }
 
-    void ScheduleLoadInstance(const std::string& instanceId,
-                              unsigned int frame)
+    void ScheduleLoadInstance(const std::string& instanceId)
     {
-      loader_.ScheduleLoadInstance(instanceId, frame);
+      loader_.ScheduleLoadInstance(instanceId);
+    }
+
+    void ScheduleLoadFrame(const std::string& instanceId,
+                           unsigned int frame)
+    {
+      loader_.ScheduleLoadFrame(instanceId, frame);
     }
 
     virtual size_t GetSliceCount() const
--- a/Resources/CMake/OrthancStoneConfiguration.cmake	Mon Sep 25 13:43:47 2017 +0200
+++ b/Resources/CMake/OrthancStoneConfiguration.cmake	Thu Sep 28 16:55:51 2017 +0200
@@ -164,6 +164,7 @@
   #${ORTHANC_STONE_DIR}/Framework/Layers/SeriesFrameRendererFactory.cpp
   #${ORTHANC_STONE_DIR}/Framework/Layers/SiblingSliceLocationFactory.cpp
   #${ORTHANC_STONE_DIR}/Framework/Layers/SingleFrameRendererFactory.cpp
+  ${ORTHANC_STONE_DIR}/Framework/Enumerations.cpp
   ${ORTHANC_STONE_DIR}/Framework/Layers/CircleMeasureTracker.cpp
   ${ORTHANC_STONE_DIR}/Framework/Layers/ColorFrameRenderer.cpp
   ${ORTHANC_STONE_DIR}/Framework/Layers/FrameRenderer.cpp