changeset 775:cf1102295ae5

Merge from default
author Benjamin Golinvaux <bgo@osimis.io>
date Fri, 24 May 2019 16:00:24 +0200
parents 66ac7a2d1e3a (current diff) b8dfd966b5f4 (diff)
children 7b404c853e66 1a28fce57ff3
files Resources/CMake/OrthancStoneConfiguration.cmake
diffstat 21 files changed, 1291 insertions(+), 198 deletions(-) [+]
line wrap: on
line diff
--- a/Framework/Oracle/IOracle.h	Fri May 24 15:59:51 2019 +0200
+++ b/Framework/Oracle/IOracle.h	Fri May 24 16:00:24 2019 +0200
@@ -32,6 +32,10 @@
     {
     }
 
+    virtual void Start() = 0;
+
+    virtual void Stop() = 0;
+
     virtual void Schedule(const IObserver& receiver,
                           IOracleCommand* command) = 0;  // Takes ownership
   };
--- a/Framework/Oracle/ThreadedOracle.cpp	Fri May 24 15:59:51 2019 +0200
+++ b/Framework/Oracle/ThreadedOracle.cpp	Fri May 24 16:00:24 2019 +0200
@@ -204,6 +204,9 @@
           
       Orthanc::GzipCompressor compressor;
       compressor.Uncompress(answer, compressed.c_str(), compressed.size());
+
+      LOG(INFO) << "Uncompressing gzip Encoding: from " << compressed.size()
+                << " to " << answer.size() << " bytes";
     }
   }
 
@@ -424,6 +427,29 @@
   }
 
 
+  ThreadedOracle::~ThreadedOracle()
+  {
+    if (state_ == State_Running)
+    {
+      LOG(ERROR) << "The threaded oracle is still running, explicit call to "
+                 << "Stop() is mandatory to avoid crashes";
+    }
+
+    try
+    {
+      StopInternal();
+    }
+    catch (Orthanc::OrthancException& e)
+    {
+      LOG(ERROR) << "Exception while stopping the threaded oracle: " << e.What();
+    }
+    catch (...)
+    {
+      LOG(ERROR) << "Native exception while stopping the threaded oracle";
+    }           
+  }
+
+  
   void ThreadedOracle::SetOrthancParameters(const Orthanc::WebServiceParameters& orthanc)
   {
     boost::mutex::scoped_lock lock(mutex_);
--- a/Framework/Oracle/ThreadedOracle.h	Fri May 24 15:59:51 2019 +0200
+++ b/Framework/Oracle/ThreadedOracle.h	Fri May 24 16:00:24 2019 +0200
@@ -72,10 +72,7 @@
   public:
     ThreadedOracle(IMessageEmitter& emitter);
 
-    virtual ~ThreadedOracle()
-    {
-      StopInternal();
-    }
+    virtual ~ThreadedOracle();
 
     void SetOrthancParameters(const Orthanc::WebServiceParameters& orthanc);
 
@@ -83,9 +80,9 @@
 
     void SetSleepingTimeResolution(unsigned int milliseconds);
 
-    void Start();
+    virtual void Start();
 
-    void Stop()
+    virtual void Stop()
     {
       StopInternal();
     }
--- a/Framework/Scene2D/CairoCompositor.cpp	Fri May 24 15:59:51 2019 +0200
+++ b/Framework/Scene2D/CairoCompositor.cpp	Fri May 24 16:00:24 2019 +0200
@@ -24,6 +24,7 @@
 #include "Internals/CairoColorTextureRenderer.h"
 #include "Internals/CairoFloatTextureRenderer.h"
 #include "Internals/CairoInfoPanelRenderer.h"
+#include "Internals/CairoLookupTableTextureRenderer.h"
 #include "Internals/CairoPolylineRenderer.h"
 #include "Internals/CairoTextRenderer.h"
 
@@ -60,6 +61,9 @@
       case ISceneLayer::Type_FloatTexture:
         return new Internals::CairoFloatTextureRenderer(*this, layer);
 
+      case ISceneLayer::Type_LookupTableTexture:
+        return new Internals::CairoLookupTableTextureRenderer(*this, layer);
+
       case ISceneLayer::Type_Text:
       {
         const TextSceneLayer& l = dynamic_cast<const TextSceneLayer&>(layer);
--- a/Framework/Scene2D/ColorTextureSceneLayer.h	Fri May 24 15:59:51 2019 +0200
+++ b/Framework/Scene2D/ColorTextureSceneLayer.h	Fri May 24 16:00:24 2019 +0200
@@ -28,6 +28,7 @@
   class ColorTextureSceneLayer : public TextureBaseSceneLayer
   {
   public:
+    // If using RGBA32, premultiplied alpha is assumed
     ColorTextureSceneLayer(const Orthanc::ImageAccessor& texture);
 
     virtual ISceneLayer* Clone() const;
--- a/Framework/Scene2D/FloatTextureSceneLayer.cpp	Fri May 24 15:59:51 2019 +0200
+++ b/Framework/Scene2D/FloatTextureSceneLayer.cpp	Fri May 24 16:00:24 2019 +0200
@@ -86,6 +86,13 @@
   }
 
 
+  void FloatTextureSceneLayer::SetInverted(bool inverted)
+  {
+    inverted_ = inverted;
+    IncrementRevision();
+  }
+
+  
   void FloatTextureSceneLayer::FitRange()
   {
     float minValue, maxValue;
@@ -116,6 +123,7 @@
     cloned->windowing_ = windowing_;
     cloned->customCenter_ = customCenter_;
     cloned->customWidth_ = customWidth_;
+    cloned->inverted_ = inverted_;
 
     return cloned.release();
   }
--- a/Framework/Scene2D/FloatTextureSceneLayer.h	Fri May 24 15:59:51 2019 +0200
+++ b/Framework/Scene2D/FloatTextureSceneLayer.h	Fri May 24 16:00:24 2019 +0200
@@ -31,9 +31,10 @@
     ImageWindowing   windowing_;
     float            customCenter_;
     float            customWidth_;
+    bool             inverted_;
 
   public:
-    // The pixel format must be "Float32"
+    // The pixel format must be convertible to "Float32"
     FloatTextureSceneLayer(const Orthanc::ImageAccessor& texture);
 
     void SetWindowing(ImageWindowing windowing);
@@ -49,6 +50,14 @@
       return windowing_;
     }
 
+    // To achieve MONOCHROME1 photometric interpretation
+    void SetInverted(bool inverted);
+
+    bool IsInverted() const
+    {
+      return inverted_;
+    }
+
     void FitRange();
 
     virtual ISceneLayer* Clone() const;
--- a/Framework/Scene2D/ISceneLayer.h	Fri May 24 15:59:51 2019 +0200
+++ b/Framework/Scene2D/ISceneLayer.h	Fri May 24 16:00:24 2019 +0200
@@ -37,7 +37,8 @@
       Type_ColorTexture,
       Type_Polyline,
       Type_Text,
-      Type_FloatTexture
+      Type_FloatTexture,
+      Type_LookupTableTexture
     };
 
     virtual ~ISceneLayer()
--- a/Framework/Scene2D/Internals/CairoColorTextureRenderer.cpp	Fri May 24 15:59:51 2019 +0200
+++ b/Framework/Scene2D/Internals/CairoColorTextureRenderer.cpp	Fri May 24 16:00:24 2019 +0200
@@ -44,13 +44,17 @@
       isLinearInterpolation_ = l.IsLinearInterpolation();
     }
 
-    
-    void CairoColorTextureRenderer::Render(const AffineTransform2D& transform)
+
+    void CairoColorTextureRenderer::RenderColorTexture(ICairoContextProvider& target,
+                                                       const AffineTransform2D& transform,
+                                                       CairoSurface& texture,
+                                                       const AffineTransform2D& textureTransform,
+                                                       bool isLinearInterpolation)
     {
-      cairo_t* cr = target_.GetCairoContext();
+      cairo_t* cr = target.GetCairoContext();
 
       AffineTransform2D t =
-        AffineTransform2D::Combine(transform, textureTransform_);
+        AffineTransform2D::Combine(transform, textureTransform);
       Matrix h = t.GetHomogeneousMatrix();
       
       cairo_save(cr);
@@ -60,9 +64,9 @@
       cairo_transform(cr, &m);
 
       cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
-      cairo_set_source_surface(cr, texture_.GetObject(), 0, 0);
+      cairo_set_source_surface(cr, texture.GetObject(), 0, 0);
 
-      if (isLinearInterpolation_)
+      if (isLinearInterpolation)
       {
         cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_BILINEAR);
       }
--- a/Framework/Scene2D/Internals/CairoColorTextureRenderer.h	Fri May 24 15:59:51 2019 +0200
+++ b/Framework/Scene2D/Internals/CairoColorTextureRenderer.h	Fri May 24 16:00:24 2019 +0200
@@ -43,7 +43,17 @@
 
       virtual void Update(const ISceneLayer& layer);
     
-      virtual void Render(const AffineTransform2D& transform);
+      virtual void Render(const AffineTransform2D& transform)
+      {
+        RenderColorTexture(target_, transform, texture_,
+                           textureTransform_, isLinearInterpolation_);
+      }
+
+      static void RenderColorTexture(ICairoContextProvider& target,
+                                     const AffineTransform2D& transform,
+                                     CairoSurface& texture,
+                                     const AffineTransform2D& textureTransform,
+                                     bool isLinearInterpolation);
     };
   }
 }
--- a/Framework/Scene2D/Internals/CairoFloatTextureRenderer.cpp	Fri May 24 15:59:51 2019 +0200
+++ b/Framework/Scene2D/Internals/CairoFloatTextureRenderer.cpp	Fri May 24 16:00:24 2019 +0200
@@ -21,6 +21,7 @@
 
 #include "CairoFloatTextureRenderer.h"
 
+#include "CairoColorTextureRenderer.h"
 #include "../FloatTextureSceneLayer.h"
 
 namespace OrthancStone
@@ -37,8 +38,8 @@
       float windowCenter, windowWidth;
       l.GetWindowing(windowCenter, windowWidth);
 
-      const float a = windowCenter - windowWidth;
-      const float slope = 256.0f / (2.0f * windowWidth);
+      const float a = windowCenter - windowWidth / 2.0f;
+      const float slope = 256.0f / windowWidth;
 
       const Orthanc::ImageAccessor& source = l.GetTexture();
       const unsigned int width = source.GetWidth();
@@ -71,6 +72,11 @@
 
           uint8_t vv = static_cast<uint8_t>(v);
 
+          if (l.IsInverted())
+          {
+            vv = 255 - vv;
+          }
+
           q[0] = vv;
           q[1] = vv;
           q[2] = vv;
@@ -84,33 +90,8 @@
       
     void CairoFloatTextureRenderer::Render(const AffineTransform2D& transform)
     {
-      cairo_t* cr = target_.GetCairoContext();
-
-      AffineTransform2D t =
-        AffineTransform2D::Combine(transform, textureTransform_);
-      Matrix h = t.GetHomogeneousMatrix();
-      
-      cairo_save(cr);
-
-      cairo_matrix_t m;
-      cairo_matrix_init(&m, h(0, 0), h(1, 0), h(0, 1), h(1, 1), h(0, 2), h(1, 2));
-      cairo_transform(cr, &m);
-
-      cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
-      cairo_set_source_surface(cr, texture_.GetObject(), 0, 0);
-
-      if (isLinearInterpolation_)
-      {
-        cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_BILINEAR);
-      }
-      else
-      {
-        cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST);
-      }
-
-      cairo_paint(cr);
-
-      cairo_restore(cr);
+      CairoColorTextureRenderer::RenderColorTexture(target_, transform, texture_,
+                                                    textureTransform_, isLinearInterpolation_);
     }
   }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/CairoLookupTableTextureRenderer.cpp	Fri May 24 16:00:24 2019 +0200
@@ -0,0 +1,108 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., 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 "CairoLookupTableTextureRenderer.h"
+
+#include "CairoColorTextureRenderer.h"
+#include "../LookupTableTextureSceneLayer.h"
+
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    void CairoLookupTableTextureRenderer::Update(const ISceneLayer& layer)
+    {
+      const LookupTableTextureSceneLayer& l = dynamic_cast<const LookupTableTextureSceneLayer&>(layer);
+
+      textureTransform_ = l.GetTransform();
+      isLinearInterpolation_ = l.IsLinearInterpolation();
+
+      const float a = l.GetMinValue();
+      float slope;
+
+      if (l.GetMinValue() >= l.GetMaxValue())
+      {
+        slope = 0;
+      }
+      else
+      {
+        slope = 256.0f / (l.GetMaxValue() - l.GetMinValue());
+      }
+
+      const Orthanc::ImageAccessor& source = l.GetTexture();
+      const unsigned int width = source.GetWidth();
+      const unsigned int height = source.GetHeight();
+      texture_.SetSize(width, height, true /* alpha channel is enabled */);
+
+      Orthanc::ImageAccessor target;
+      texture_.GetWriteableAccessor(target);
+
+      const std::vector<uint8_t>& lut = l.GetLookupTable();
+      if (lut.size() != 4 * 256)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+
+      assert(source.GetFormat() == Orthanc::PixelFormat_Float32 &&
+             target.GetFormat() == Orthanc::PixelFormat_BGRA32 &&
+             sizeof(float) == 4);
+
+      for (unsigned int y = 0; y < height; y++)
+      {
+        const float* p = reinterpret_cast<const float*>(source.GetConstRow(y));
+        uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
+
+        for (unsigned int x = 0; x < width; x++)
+        {
+          float v = (*p - a) * slope;
+          if (v <= 0)
+          {
+            v = 0;
+          }
+          else if (v >= 255)
+          {
+            v = 255;
+          }
+
+          uint8_t vv = static_cast<uint8_t>(v);
+
+          q[0] = lut[4 * vv + 2];  // B
+          q[1] = lut[4 * vv + 1];  // G
+          q[2] = lut[4 * vv + 0];  // R
+          q[3] = lut[4 * vv + 3];  // A
+
+          p++;
+          q += 4;
+        }
+      }
+
+      cairo_surface_mark_dirty(texture_.GetObject());
+    }
+
+    void CairoLookupTableTextureRenderer::Render(const AffineTransform2D& transform)
+    {
+      CairoColorTextureRenderer::RenderColorTexture(target_, transform, texture_,
+                                                    textureTransform_, isLinearInterpolation_);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/CairoLookupTableTextureRenderer.h	Fri May 24 16:00:24 2019 +0200
@@ -0,0 +1,53 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., 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/>.
+ **/
+
+
+#pragma once
+
+#include "../../Viewport/CairoSurface.h"
+#include "CompositorHelper.h"
+#include "ICairoContextProvider.h"
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    class CairoLookupTableTextureRenderer : public CompositorHelper::ILayerRenderer
+    {
+    private:
+      ICairoContextProvider&  target_;
+      CairoSurface            texture_;
+      AffineTransform2D       textureTransform_;
+      bool                    isLinearInterpolation_;
+    
+    public:
+      CairoLookupTableTextureRenderer(ICairoContextProvider& target,
+                                      const ISceneLayer& layer) :
+        target_(target)
+      {
+        Update(layer);
+      }
+
+      virtual void Update(const ISceneLayer& layer);
+    
+      virtual void Render(const AffineTransform2D& transform);
+    };
+  }
+}
--- a/Framework/Scene2D/Internals/OpenGLFloatTextureProgram.cpp	Fri May 24 15:59:51 2019 +0200
+++ b/Framework/Scene2D/Internals/OpenGLFloatTextureProgram.cpp	Fri May 24 16:00:24 2019 +0200
@@ -29,28 +29,28 @@
 
 static const char* FRAGMENT_SHADER = 
   ORTHANC_STONE_OPENGL_SHADER_VERSION_DIRECTIVE
-  "uniform float u_offset;                       \n"
-  "uniform float u_slope;                        \n"
-  "uniform float u_windowCenter;                 \n"
-  "uniform float u_windowWidth;                  \n"
-  "uniform sampler2D u_texture;                  \n"
-  "varying vec2 v_texcoord;                      \n"
-  "void main()                                   \n"
-  "{                                             \n"
-  "  vec4 t = texture2D(u_texture, v_texcoord);  \n"
-  "  float v = (t.r * 256.0 + t.g) * 256.0;      \n"
-  "  v = v * u_slope + u_offset;                 \n"  // (*)
-  "  float a = u_windowCenter - u_windowWidth;   \n"
-  "  float dy = 1.0 / (2.0 * u_windowWidth);     \n"
-  "  if (v <= a)                                 \n"
-  "    v = 0.0;                                  \n"
-  "  else                                        \n"
-  "  {                                           \n"
-  "    v = (v - a) * dy;                         \n"
-  "    if (v >= 1.0)                             \n"
-  "      v = 1.0;                                \n"
-  "  }                                           \n"
-  "  gl_FragColor = vec4(v, v, v, 1);            \n"
+  "uniform float u_offset;                           \n"
+  "uniform float u_slope;                            \n"
+  "uniform float u_windowCenter;                     \n"
+  "uniform float u_windowWidth;                      \n"
+  "uniform sampler2D u_texture;                      \n"
+  "varying vec2 v_texcoord;                          \n"
+  "void main()                                       \n"
+  "{                                                 \n"
+  "  vec4 t = texture2D(u_texture, v_texcoord);      \n"
+  "  float v = (t.r * 256.0 + t.g) * 256.0;          \n"
+  "  v = v * u_slope + u_offset;                     \n"  // (*)
+  "  float a = u_windowCenter - u_windowWidth / 2.0; \n"
+  "  float dy = 1.0 / u_windowWidth;                 \n"
+  "  if (v <= a)                                     \n"
+  "    v = 0.0;                                      \n"
+  "  else                                            \n"
+  "  {                                               \n"
+  "    v = (v - a) * dy;                             \n"
+  "    if (v >= 1.0)                                 \n"
+  "      v = 1.0;                                    \n"
+  "  }                                               \n"
+  "  gl_FragColor = vec4(v, v, v, 1);                \n"
   "}";
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/LookupTableTextureSceneLayer.cpp	Fri May 24 16:00:24 2019 +0200
@@ -0,0 +1,178 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., 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 "LookupTableTextureSceneLayer.h"
+
+#include <Core/Images/Image.h>
+#include <Core/Images/ImageProcessing.h>
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  static void StringToVector(std::vector<uint8_t>& target,
+                             const std::string& source)
+  {
+    target.resize(source.size());
+
+    for (size_t i = 0; i < source.size(); i++)
+    {
+      target[i] = source[i];
+    }
+  }
+
+  
+  LookupTableTextureSceneLayer::LookupTableTextureSceneLayer(const Orthanc::ImageAccessor& texture)
+  {
+    {
+      std::auto_ptr<Orthanc::ImageAccessor> t(
+        new Orthanc::Image(Orthanc::PixelFormat_Float32, 
+                           texture.GetWidth(), 
+                           texture.GetHeight(), 
+                           false));
+
+      Orthanc::ImageProcessing::Convert(*t, texture);
+      SetTexture(t.release());
+    }
+
+    SetLookupTableGrayscale();
+    SetRange(0, 1);
+  }
+
+
+  void LookupTableTextureSceneLayer::SetLookupTableGrayscale()
+  {
+    std::vector<uint8_t> rgb(3 * 256);
+
+    for (size_t i = 0; i < 256; i++)
+    {
+      rgb[3 * i] = i;
+      rgb[3 * i + 1] = i;
+      rgb[3 * i + 2] = i;
+    }
+
+    SetLookupTableRgb(rgb);
+  }  
+
+
+  void LookupTableTextureSceneLayer::SetLookupTableRgb(const std::vector<uint8_t>& lut)
+  {
+    if (lut.size() != 3 * 256)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    lut_.resize(4 * 256);
+
+    for (size_t i = 0; i < 256; i++)
+    {
+      // Premultiplied alpha
+      
+      if (i == 0)
+      {
+        // Make zero transparent
+        lut_[4 * i] = 0;        // R
+        lut_[4 * i + 1] = 0;    // G
+        lut_[4 * i + 2] = 0;    // B
+        lut_[4 * i + 3] = 0;    // A
+      }
+      else
+      {
+        float a = static_cast<float>(i) / 255.0f;
+        
+        float r = static_cast<float>(lut[3 * i]) * a;
+        float g = static_cast<float>(lut[3 * i + 1]) * a;
+        float b = static_cast<float>(lut[3 * i + 2]) * a;
+        
+        lut_[4 * i] = static_cast<uint8_t>(std::floor(r));
+        lut_[4 * i + 1] = static_cast<uint8_t>(std::floor(g));
+        lut_[4 * i + 2] = static_cast<uint8_t>(std::floor(b));
+        lut_[4 * i + 3] = static_cast<uint8_t>(std::floor(a * 255.0f));
+      }
+    }
+
+    IncrementRevision();
+  }
+
+
+  void LookupTableTextureSceneLayer::SetLookupTable(const std::vector<uint8_t>& lut)
+  {
+    if (lut.size() == 4 * 256)
+    {
+      lut_ = lut;
+      IncrementRevision();
+    }
+    else if (lut.size() == 3 * 256)
+    {
+      SetLookupTableRgb(lut);
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  void LookupTableTextureSceneLayer::SetLookupTable(const std::string& lut)
+  {
+    std::vector<uint8_t> tmp;
+    StringToVector(tmp, lut);
+    SetLookupTable(tmp);
+  }
+
+  
+  void LookupTableTextureSceneLayer::SetRange(float minValue,
+                                              float maxValue)
+  {
+    if (minValue > maxValue)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      minValue_ = minValue;
+      maxValue_ = maxValue;
+      IncrementRevision();
+    }
+  }
+    
+
+  void LookupTableTextureSceneLayer::FitRange()
+  {
+    Orthanc::ImageProcessing::GetMinMaxFloatValue(minValue_, maxValue_, GetTexture());
+    assert(minValue_ <= maxValue_);
+    
+    IncrementRevision();
+  }
+
+    
+  ISceneLayer* LookupTableTextureSceneLayer::Clone() const
+  {
+    std::auto_ptr<LookupTableTextureSceneLayer> cloned
+      (new LookupTableTextureSceneLayer(GetTexture()));
+
+    cloned->CopyParameters(*this);
+    cloned->minValue_ = minValue_;
+    cloned->maxValue_ = maxValue_;
+    cloned->lut_ = lut_;
+
+    return cloned.release();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/LookupTableTextureSceneLayer.h	Fri May 24 16:00:24 2019 +0200
@@ -0,0 +1,78 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., 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/>.
+ **/
+
+
+#pragma once
+
+#include "TextureBaseSceneLayer.h"
+
+namespace OrthancStone
+{
+  class LookupTableTextureSceneLayer : public TextureBaseSceneLayer
+  {
+  private:
+    ImageWindowing        windowing_;
+    float                 minValue_;
+    float                 maxValue_;
+    std::vector<uint8_t>  lut_;
+
+    void SetLookupTableRgb(const std::vector<uint8_t>& lut);
+
+  public:
+    // The pixel format must be convertible to Float32
+    LookupTableTextureSceneLayer(const Orthanc::ImageAccessor& texture);
+
+    void SetLookupTableGrayscale();
+
+    // The vector must contain either 3 * 256 values (RGB), or 4 * 256
+    // (RGBA). In the RGB case, an alpha channel will be automatically added.
+    void SetLookupTable(const std::vector<uint8_t>& lut);
+
+    void SetLookupTable(const std::string& lut);
+
+    void SetRange(float minValue,
+                  float maxValue);
+    
+    void FitRange();
+
+    float GetMinValue() const
+    {
+      return minValue_;
+    }
+
+    float GetMaxValue() const
+    {
+      return maxValue_;
+    }
+
+    // This returns a vector of 4 * 256 values between 0 and 255, in RGBA.
+    const std::vector<uint8_t>& GetLookupTable() const
+    {
+      return lut_;
+    }
+
+    virtual ISceneLayer* Clone() const;
+
+    virtual Type GetType() const
+    {
+      return Type_LookupTableTexture;
+    }
+  };
+}
--- a/Framework/Toolbox/DicomInstanceParameters.cpp	Fri May 24 15:59:51 2019 +0200
+++ b/Framework/Toolbox/DicomInstanceParameters.cpp	Fri May 24 16:00:24 2019 +0200
@@ -62,12 +62,7 @@
     {
       if (frameOffsets_.size() >= 2)
       {
-        thickness_ = frameOffsets_[1] - frameOffsets_[0];
-
-        if (thickness_ < 0)
-        {
-          thickness_ = -thickness_;
-        }
+        thickness_ = std::abs(frameOffsets_[1] - frameOffsets_[0]);
       }
     }
   }
@@ -325,7 +320,24 @@
   }
 
 
-  TextureBaseSceneLayer* DicomInstanceParameters::CreateTexture(const Orthanc::ImageAccessor& pixelData) const
+  Orthanc::ImageAccessor* DicomInstanceParameters::ConvertToFloat(const Orthanc::ImageAccessor& pixelData) const
+  {
+    std::auto_ptr<Orthanc::Image> converted(new Orthanc::Image(Orthanc::PixelFormat_Float32, 
+                                                               pixelData.GetWidth(), 
+                                                               pixelData.GetHeight(),
+                                                               false));
+    Orthanc::ImageProcessing::Convert(*converted, pixelData);
+
+    // Correct rescale slope/intercept if need be
+    data_.ApplyRescale(*converted, (pixelData.GetFormat() == Orthanc::PixelFormat_Grayscale32));
+
+    return converted.release();
+  }
+    
+
+
+  TextureBaseSceneLayer* DicomInstanceParameters::CreateTexture
+  (const Orthanc::ImageAccessor& pixelData) const
   {
     assert(sizeof(float) == 4);
 
@@ -343,26 +355,16 @@
     }
     else
     {
-      if (sourceFormat != Orthanc::PixelFormat_Grayscale16 &&
-          sourceFormat != Orthanc::PixelFormat_Grayscale32 &&
-          sourceFormat != Orthanc::PixelFormat_SignedGrayscale16)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-      }
+      // This is the case of a grayscale frame. Convert it to Float32.
+      std::auto_ptr<FloatTextureSceneLayer> texture;
 
-      std::auto_ptr<FloatTextureSceneLayer> texture;
-        
+      if (pixelData.GetFormat() == Orthanc::PixelFormat_Float32)
       {
-        // This is the case of a grayscale frame. Convert it to Float32.
-        std::auto_ptr<Orthanc::Image> converted(new Orthanc::Image(Orthanc::PixelFormat_Float32, 
-                                                                   pixelData.GetWidth(), 
-                                                                   pixelData.GetHeight(),
-                                                                   false));
-        Orthanc::ImageProcessing::Convert(*converted, pixelData);
-
-        // Correct rescale slope/intercept if need be
-        data_.ApplyRescale(*converted, (sourceFormat == Orthanc::PixelFormat_Grayscale32));
-
+        texture.reset(new FloatTextureSceneLayer(pixelData));
+      }
+      else
+      {
+        std::auto_ptr<Orthanc::ImageAccessor> converted(ConvertToFloat(pixelData));
         texture.reset(new FloatTextureSceneLayer(*converted));
       }
 
@@ -375,4 +377,21 @@
       return texture.release();
     }
   }
+
+
+  LookupTableTextureSceneLayer* DicomInstanceParameters::CreateLookupTableTexture
+  (const Orthanc::ImageAccessor& pixelData) const
+  {
+    std::auto_ptr<FloatTextureSceneLayer> texture;
+
+    if (pixelData.GetFormat() == Orthanc::PixelFormat_Float32)
+    {
+      return new LookupTableTextureSceneLayer(pixelData);
+    }
+    else
+    {
+      std::auto_ptr<Orthanc::ImageAccessor> converted(ConvertToFloat(pixelData));
+      return new LookupTableTextureSceneLayer(*converted);
+    }
+  }
 }
--- a/Framework/Toolbox/DicomInstanceParameters.h	Fri May 24 15:59:51 2019 +0200
+++ b/Framework/Toolbox/DicomInstanceParameters.h	Fri May 24 16:00:24 2019 +0200
@@ -22,7 +22,7 @@
 #pragma once
 
 #include "../StoneEnumerations.h"
-#include "../Scene2D/TextureBaseSceneLayer.h"
+#include "../Scene2D/LookupTableTextureSceneLayer.h"
 #include "../Toolbox/CoordinateSystem3D.h"
 
 #include <Core/IDynamicObject.h>
@@ -72,6 +72,9 @@
     };
 
     
+    Orthanc::ImageAccessor* ConvertToFloat(const Orthanc::ImageAccessor& pixelData) const;
+    
+
     Data  data_;
 
 
@@ -181,5 +184,7 @@
     }
 
     TextureBaseSceneLayer* CreateTexture(const Orthanc::ImageAccessor& pixelData) const;
+
+    LookupTableTextureSceneLayer* CreateLookupTableTexture(const Orthanc::ImageAccessor& pixelData) const;
   };
 }
--- a/Framework/Volumes/ImageBuffer3D.h	Fri May 24 15:59:51 2019 +0200
+++ b/Framework/Volumes/ImageBuffer3D.h	Fri May 24 16:00:24 2019 +0200
@@ -99,6 +99,11 @@
       return format_;
     }
 
+    unsigned int GetBytesPerPixel() const
+    {
+      return Orthanc::GetBytesPerPixel(format_);
+    }
+
     uint64_t GetEstimatedMemorySize() const;
 
     bool GetRange(float& minValue,
--- a/Resources/CMake/OrthancStoneConfiguration.cmake	Fri May 24 15:59:51 2019 +0200
+++ b/Resources/CMake/OrthancStoneConfiguration.cmake	Fri May 24 16:00:24 2019 +0200
@@ -380,10 +380,12 @@
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoColorTextureRenderer.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoFloatTextureRenderer.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoInfoPanelRenderer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoLookupTableTextureRenderer.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoPolylineRenderer.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoTextRenderer.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CompositorHelper.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/FixedPointAligner.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/LookupTableTextureSceneLayer.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/PanSceneTracker.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/PointerEvent.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/PolylineSceneLayer.cpp
--- a/Samples/Sdl/Loader.cpp	Fri May 24 15:59:51 2019 +0200
+++ b/Samples/Sdl/Loader.cpp	Fri May 24 16:00:24 2019 +0200
@@ -32,6 +32,8 @@
 #include "../../Framework/Loaders/BasicFetchingStrategy.h"
 #include "../../Framework/Scene2D/CairoCompositor.h"
 #include "../../Framework/Scene2D/Scene2D.h"
+#include "../../Framework/Scene2D/PolylineSceneLayer.h"
+#include "../../Framework/Scene2D/LookupTableTextureSceneLayer.h"
 #include "../../Framework/StoneInitialization.h"
 #include "../../Framework/Toolbox/GeometryToolbox.h"
 #include "../../Framework/Toolbox/SlicesSorter.h"
@@ -39,12 +41,17 @@
 #include "../../Framework/Volumes/VolumeImageGeometry.h"
 
 // From Orthanc framework
+#include <Core/DicomFormat/DicomArray.h>
 #include <Core/Images/Image.h>
 #include <Core/Images/ImageProcessing.h>
 #include <Core/Images/PngWriter.h>
 #include <Core/Logging.h>
 #include <Core/OrthancException.h>
 #include <Core/SystemToolbox.h>
+#include <Core/Toolbox.h>
+
+
+#include <EmbeddedResources.h>
 
 
 namespace OrthancStone
@@ -65,7 +72,7 @@
       virtual uint64_t GetRevision() = 0;
 
       // This call can take some time
-      virtual ISceneLayer* CreateSceneLayer() = 0;
+      virtual ISceneLayer* CreateSceneLayer(const CoordinateSystem3D& cuttingPlane) = 0;
     };
 
     virtual ~IVolumeSlicer()
@@ -76,6 +83,15 @@
   };
 
 
+  class IVolumeImageSlicer : public IVolumeSlicer
+  {
+  public:
+    virtual bool HasGeometry() const = 0;
+
+    virtual const VolumeImageGeometry& GetGeometry() const = 0;
+  };
+
+
   class InvalidExtractedSlice : public IVolumeSlicer::ExtractedSlice
   {
   public:
@@ -89,7 +105,7 @@
       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
     }
 
-    virtual ISceneLayer* CreateSceneLayer()
+    virtual ISceneLayer* CreateSceneLayer(const CoordinateSystem3D& cuttingPlane)
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
     }
@@ -153,7 +169,7 @@
       return GetRevisionInternal(projection_, sliceIndex_);
     }
 
-    virtual ISceneLayer* CreateSceneLayer()
+    virtual ISceneLayer* CreateSceneLayer(const CoordinateSystem3D& cuttingPlane)
     {
       CheckValid();
 
@@ -162,14 +178,32 @@
       {
         const DicomInstanceParameters& parameters = GetDicomParameters(projection_, sliceIndex_);
         ImageBuffer3D::SliceReader reader(image_, projection_, sliceIndex_);
-        texture.reset(parameters.CreateTexture(reader.GetAccessor()));
+
+        static unsigned int i = 1;
+        
+        if (i % 2)
+        {
+          texture.reset(parameters.CreateTexture(reader.GetAccessor()));
+        }
+        else
+        {
+          std::string lut;
+          Orthanc::EmbeddedResources::GetFileResource(lut, Orthanc::EmbeddedResources::COLORMAP_HOT);
+          
+          std::auto_ptr<LookupTableTextureSceneLayer> tmp(parameters.CreateLookupTableTexture(reader.GetAccessor()));
+          tmp->FitRange();
+          tmp->SetLookupTable(lut);
+          texture.reset(tmp.release());
+        }
+
+        i++;
       }
 
       const CoordinateSystem3D& system = geometry_.GetProjectionGeometry(projection_);
-
+      
       double x0, y0, x1, y1;
-      system.ProjectPoint(x0, y0, system.GetOrigin());
-      system.ProjectPoint(x1, y1, system.GetOrigin() + system.GetAxisX());
+      cuttingPlane.ProjectPoint(x0, y0, system.GetOrigin());
+      cuttingPlane.ProjectPoint(x1, y1, system.GetOrigin() + system.GetAxisX());
       texture->SetOrigin(x0, y0);
 
       double dx = x1 - x0;
@@ -184,6 +218,28 @@
       texture->SetPixelSpacing(tmp[0], tmp[1]);
 
       return texture.release();
+
+#if 0
+      double w = texture->GetTexture().GetWidth() * tmp[0];
+      double h = texture->GetTexture().GetHeight() * tmp[1];
+      printf("%.1f %.1f %.1f => %.1f %.1f => %.1f %.1f\n",
+             system.GetOrigin() [0],
+             system.GetOrigin() [1],
+             system.GetOrigin() [2],
+             x0, y0, x0 + w, y0 + h);
+
+      std::auto_ptr<PolylineSceneLayer> toto(new PolylineSceneLayer);
+
+      PolylineSceneLayer::Chain c;
+      c.push_back(ScenePoint2D(x0, y0));
+      c.push_back(ScenePoint2D(x0 + w, y0));
+      c.push_back(ScenePoint2D(x0 + w, y0 + h));
+      c.push_back(ScenePoint2D(x0, y0 + h));
+      
+      toto->AddChain(c, true);
+
+      return toto.release();
+#endif
     }
   };
 
@@ -193,7 +249,7 @@
   class DicomSeriesVolumeImage : public boost::noncopyable
   {
   public:
-    class ExtractedSlice : public DicomVolumeImageOrthogonalSlice
+    class ExtractedOrthogonalSlice : public DicomVolumeImageOrthogonalSlice
     {
     private:
       const DicomSeriesVolumeImage&  that_;
@@ -221,8 +277,8 @@
       }
 
     public:
-      ExtractedSlice(const DicomSeriesVolumeImage& that,
-                     const CoordinateSystem3D& plane) :
+      ExtractedOrthogonalSlice(const DicomSeriesVolumeImage& that,
+                               const CoordinateSystem3D& plane) :
         DicomVolumeImageOrthogonalSlice(that.GetImage(), that.GetGeometry(), plane),
         that_(that)
       {
@@ -331,7 +387,8 @@
 
     
   public:
-    DicomSeriesVolumeImage()
+    DicomSeriesVolumeImage() :
+      revision_(0)
     {
     }
 
@@ -479,8 +536,7 @@
 
 
   class OrthancSeriesVolumeProgressiveLoader : 
-    public IObserver, 
-    public IVolumeSlicer
+    public IObserver
   {
   private:
     static const unsigned int LOW_QUALITY = 0;
@@ -624,7 +680,61 @@
     std::auto_ptr<IFetchingItemsSorter::IFactory>  sorter_;
     std::auto_ptr<IFetchingStrategy>               strategy_;
 
+
+    IVolumeSlicer::ExtractedSlice* ExtractOrthogonalSlice(const CoordinateSystem3D& cuttingPlane) const
+    {
+      if (volume_.HasGeometry() &&
+          volume_.GetSlicesCount() != 0)
+      {
+        std::auto_ptr<DicomVolumeImageOrthogonalSlice> slice
+          (new DicomSeriesVolumeImage::ExtractedOrthogonalSlice(volume_, cuttingPlane));
+
+        assert(slice.get() != NULL &&
+               strategy_.get() != NULL);            
+
+        if (slice->IsValid() &&
+            slice->GetProjection() == VolumeProjection_Axial)
+        {
+          strategy_->SetCurrent(slice->GetSliceIndex());
+        }
+
+        return slice.release();
+      }
+      else
+      {
+        return new InvalidExtractedSlice;
+      }
+    }
+
+    
   public:
+    class MPRSlicer : public IVolumeImageSlicer
+    {
+    private:
+      boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader>  that_;
+
+    public:
+      MPRSlicer(const boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader>& that) :
+        that_(that)
+      {
+      }
+
+      virtual ExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) const
+      {
+        return that_->ExtractOrthogonalSlice(cuttingPlane);
+      }
+
+      virtual bool HasGeometry() const
+      {
+        return that_->GetVolume().HasGeometry();
+      }
+
+      virtual const VolumeImageGeometry& GetGeometry() const
+      {
+        return that_->GetVolume().GetGeometry();
+      }
+    };
+    
     OrthancSeriesVolumeProgressiveLoader(IOracle& oracle,
                                          IObservable& oracleObservable) :
       IObserver(oracleObservable.GetBroker()),
@@ -684,64 +794,494 @@
     {
       return volume_;
     }
+  };
+
+
+
+  class OrthancMultiframeVolumeLoader : public IObserver
+  {
+  private:
+    class State : public Orthanc::IDynamicObject
+    {
+    private:
+      OrthancMultiframeVolumeLoader&  that_;
+
+    protected:
+      void Schedule(OrthancRestApiCommand* command) const
+      {
+        that_.oracle_.Schedule(that_, command);
+      }
+
+      OrthancMultiframeVolumeLoader& GetTarget() const
+      {
+        return that_;
+      }
+
+    public:
+      State(OrthancMultiframeVolumeLoader& that) :
+        that_(that)
+      {
+      }
+      
+      virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) const = 0;
+    };
+    
+    void Handle(const OrthancRestApiCommand::SuccessMessage& message)
+    {
+      dynamic_cast<const State&>(message.GetOrigin().GetPayload()).Handle(message);
+    }
+
+
+    class LoadRTDoseGeometry : public State
+    {
+    private:
+      std::auto_ptr<Orthanc::DicomMap>  dicom_;
+
+    public:
+      LoadRTDoseGeometry(OrthancMultiframeVolumeLoader& that,
+                         Orthanc::DicomMap* dicom) :
+        State(that),
+        dicom_(dicom)
+      {
+        if (dicom == NULL)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+        }
+      }
+
+      virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) const
+      {
+        // Complete the DICOM tags with just-received "Grid Frame Offset Vector"
+        std::string s = Orthanc::Toolbox::StripSpaces(message.GetAnswer());
+        dicom_->SetValue(Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR, s, false);
+
+        GetTarget().SetGeometry(*dicom_);
+      }      
+    };
 
 
-    virtual IVolumeSlicer::ExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) const
+    static std::string GetSopClassUid(const Orthanc::DicomMap& dicom)
     {
-      if (volume_.HasGeometry() &&
-          volume_.GetSlicesCount() != 0)
+      std::string s;
+      if (!dicom.CopyToString(s, Orthanc::DICOM_TAG_SOP_CLASS_UID, false))
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
+                                        "DICOM file without SOP class UID");
+      }
+      else
       {
-        std::auto_ptr<DicomVolumeImageOrthogonalSlice> slice
-          (new DicomSeriesVolumeImage::ExtractedSlice(volume_, cuttingPlane));
+        return s;
+      }
+    }
+    
 
-        assert(slice.get() != NULL &&
-               strategy_.get() != NULL);            
-
-        if (slice->GetProjection() == VolumeProjection_Axial)
+    class LoadGeometry : public State
+    {
+    public:
+      LoadGeometry(OrthancMultiframeVolumeLoader& that) :
+        State(that)
+      {
+      }
+      
+      virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) const
+      {
+        Json::Value body;
+        message.ParseJsonBody(body);
+        
+        if (body.type() != Json::objectValue)
         {
-          strategy_->SetCurrent(slice->GetSliceIndex());          
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
         }
 
-        return slice.release();
+        std::auto_ptr<Orthanc::DicomMap> dicom(new Orthanc::DicomMap);
+        dicom->FromDicomAsJson(body);
+
+        if (StringToSopClassUid(GetSopClassUid(*dicom)) == SopClassUid_RTDose)
+        {
+          // Download the "Grid Frame Offset Vector" DICOM tag, that is
+          // mandatory for RT-DOSE, but is too long to be returned by default
+          
+          std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+          command->SetUri("/instances/" + GetTarget().GetInstanceId() + "/content/" +
+                          Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR.Format());
+          command->SetPayload(new LoadRTDoseGeometry(GetTarget(), dicom.release()));
+
+          Schedule(command.release());
+        }
+        else
+        {
+          GetTarget().SetGeometry(*dicom);
+        }
+      }
+    };
+
+
+
+    class LoadTransferSyntax : public State
+    {
+    public:
+      LoadTransferSyntax(OrthancMultiframeVolumeLoader& that) :
+        State(that)
+      {
+      }
+      
+      virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) const
+      {
+        GetTarget().SetTransferSyntax(message.GetAnswer());
+      }
+    };
+   
+    
+    class LoadUncompressedPixelData : public State
+    {
+    public:
+      LoadUncompressedPixelData(OrthancMultiframeVolumeLoader& that) :
+        State(that)
+      {
+      }
+      
+      virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) const
+      {
+        GetTarget().SetUncompressedPixelData(message.GetAnswer());
+      }
+    };
+   
+    
+
+    IOracle&     oracle_;
+    bool         active_;
+    std::string  instanceId_;
+    std::string  transferSyntaxUid_;
+    uint64_t     revision_;
+
+    std::auto_ptr<DicomInstanceParameters>  dicom_;
+    std::auto_ptr<VolumeImageGeometry>      geometry_;
+    std::auto_ptr<ImageBuffer3D>            image_;
+
+
+    const std::string& GetInstanceId() const
+    {
+      if (active_)
+      {
+        return instanceId_;
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    }
+
+
+    void ScheduleFrameDownloads()
+    {
+      if (transferSyntaxUid_.empty() ||
+          !HasGeometry())
+      {
+        return;
+      }
+      
+      if (transferSyntaxUid_ == "1.2.840.10008.1.2" ||
+          transferSyntaxUid_ == "1.2.840.10008.1.2.1" ||
+          transferSyntaxUid_ == "1.2.840.10008.1.2.2")
+      {
+        std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+        command->SetHttpHeader("Accept-Encoding", "gzip");
+        command->SetUri("/instances/" + instanceId_ + "/content/" +
+                        Orthanc::DICOM_TAG_PIXEL_DATA.Format() + "/0");
+        command->SetPayload(new LoadUncompressedPixelData(*this));
+        oracle_.Schedule(*this, command.release());
       }
       else
       {
-        return new InvalidExtractedSlice;
+        throw Orthanc::OrthancException(
+          Orthanc::ErrorCode_NotImplemented,
+          "No support for multiframe instances with transfer syntax: " + transferSyntaxUid_);
+      }
+    }
+      
+
+    void SetTransferSyntax(const std::string& transferSyntax)
+    {
+      transferSyntaxUid_ = Orthanc::Toolbox::StripSpaces(transferSyntax);
+      ScheduleFrameDownloads();
+    }
+    
+
+    void SetGeometry(const Orthanc::DicomMap& dicom)
+    {
+      dicom_.reset(new DicomInstanceParameters(dicom));
+
+      Orthanc::PixelFormat format;
+      if (!dicom_->GetImageInformation().ExtractPixelFormat(format, true))
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
+
+      double spacingZ;
+      switch (dicom_->GetSopClassUid())
+      {
+        case SopClassUid_RTDose:
+          spacingZ = dicom_->GetThickness();
+          break;
+
+        default:
+          throw Orthanc::OrthancException(
+            Orthanc::ErrorCode_NotImplemented,
+            "No support for multiframe instances with SOP class UID: " + GetSopClassUid(dicom));
+      }
+
+      const unsigned int width = dicom_->GetImageInformation().GetWidth();
+      const unsigned int height = dicom_->GetImageInformation().GetHeight();
+      const unsigned int depth = dicom_->GetImageInformation().GetNumberOfFrames();
+
+      geometry_.reset(new VolumeImageGeometry);
+      geometry_->SetSize(width, height, depth);
+      geometry_->SetAxialGeometry(dicom_->GetGeometry());
+      geometry_->SetVoxelDimensions(dicom_->GetPixelSpacingX(),
+                                    dicom_->GetPixelSpacingY(),
+                                    spacingZ);
+
+      image_.reset(new ImageBuffer3D(format, width, height, depth,
+                                     false /* don't compute range */));
+      image_->Clear();
+
+      ScheduleFrameDownloads();
+    }
+
+
+    ORTHANC_FORCE_INLINE
+    static void CopyPixel(uint32_t& target,
+                          const void* source)
+    {
+      // TODO - check alignement?
+      target = le32toh(*reinterpret_cast<const uint32_t*>(source));
+    }
+      
+
+    template <typename T>
+    void CopyPixelData(const std::string& pixelData)
+    {
+      const Orthanc::PixelFormat format = image_->GetFormat();
+      const unsigned int bpp = image_->GetBytesPerPixel();
+      const unsigned int width = image_->GetWidth();
+      const unsigned int height = image_->GetHeight();
+      const unsigned int depth = image_->GetDepth();
+
+      if (pixelData.size() != bpp * width * height * depth)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
+                                        "The pixel data has not the proper size");
+      }
+
+      if (pixelData.empty())
+      {
+        return;
+      }
+
+      const uint8_t* source = reinterpret_cast<const uint8_t*>(pixelData.c_str());
+
+      for (unsigned int z = 0; z < depth; z++)
+      {
+        ImageBuffer3D::SliceWriter writer(*image_, VolumeProjection_Axial, z);
+
+        assert (writer.GetAccessor().GetWidth() == width &&
+                writer.GetAccessor().GetHeight() == height);
+
+        for (unsigned int y = 0; y < height; y++)
+        {
+          assert(sizeof(T) == Orthanc::GetBytesPerPixel(format));
+
+          T* target = reinterpret_cast<T*>(writer.GetAccessor().GetRow(y));
+
+          for (unsigned int x = 0; x < width; x++)
+          {
+            CopyPixel(*target, source);
+            
+            target ++;
+            source += bpp;
+          }
+        }
+      }
+    }
+    
+
+    void SetUncompressedPixelData(const std::string& pixelData)
+    {
+      switch (image_->GetFormat())
+      {
+        case Orthanc::PixelFormat_Grayscale32:
+          CopyPixelData<uint32_t>(pixelData);
+          break;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
+
+      revision_ ++;
+    }
+
+
+  private:
+    class ExtractedOrthogonalSlice : public DicomVolumeImageOrthogonalSlice
+    {
+    private:
+      const OrthancMultiframeVolumeLoader&  that_;
+
+    protected:
+      virtual uint64_t GetRevisionInternal(VolumeProjection projection,
+                                           unsigned int sliceIndex) const
+      {
+        return that_.revision_;
+      }
+
+      virtual const DicomInstanceParameters& GetDicomParameters(VolumeProjection projection,
+                                                                unsigned int sliceIndex) const
+      {
+        return that_.GetDicomParameters();
+      }
+
+    public:
+      ExtractedOrthogonalSlice(const OrthancMultiframeVolumeLoader& that,
+                               const CoordinateSystem3D& plane) :
+        DicomVolumeImageOrthogonalSlice(that.GetImage(), that.GetGeometry(), plane),
+        that_(that)
+      {
+      }
+    };
+    
+    
+  public:
+    class MPRSlicer : public IVolumeImageSlicer
+    {
+    private:
+      boost::shared_ptr<OrthancMultiframeVolumeLoader>  that_;
+
+    public:
+      MPRSlicer(const boost::shared_ptr<OrthancMultiframeVolumeLoader>& that) :
+        that_(that)
+      {
+      }
+
+      virtual ExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) const
+      {
+        if (that_->HasGeometry())
+        {
+          return new ExtractedOrthogonalSlice(*that_, cuttingPlane);
+        }
+        else
+        {
+          return new InvalidExtractedSlice;
+        }
+      }
+
+      virtual bool HasGeometry() const
+      {
+        return that_->HasGeometry();
+      }
+
+      virtual const VolumeImageGeometry& GetGeometry() const
+      {
+        return that_->GetGeometry();
+      }
+    };
+
+    
+    OrthancMultiframeVolumeLoader(IOracle& oracle,
+                                  IObservable& oracleObservable) :
+      IObserver(oracleObservable.GetBroker()),
+      oracle_(oracle),
+      active_(false),
+      revision_(0)
+    {
+      oracleObservable.RegisterObserverCallback(
+        new Callable<OrthancMultiframeVolumeLoader, OrthancRestApiCommand::SuccessMessage>
+        (*this, &OrthancMultiframeVolumeLoader::Handle));
+    }
+
+
+    bool HasGeometry() const
+    {
+      return (dicom_.get() != NULL &&
+              geometry_.get() != NULL &&
+              image_.get() != NULL);
+    }
+
+
+    const ImageBuffer3D& GetImage() const
+    {
+      if (!HasGeometry())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        return *image_;
+      }
+    }
+
+    
+    const VolumeImageGeometry& GetGeometry() const
+    {
+      if (!HasGeometry())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        return *geometry_;
+      }
+    }
+
+
+    const DicomInstanceParameters& GetDicomParameters() const
+    {
+      if (!HasGeometry())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        return *dicom_;
+      }
+    }
+    
+
+    void LoadInstance(const std::string& instanceId)
+    {
+      if (active_)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        active_ = true;
+        instanceId_ = instanceId;
+
+        {
+          std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+          command->SetHttpHeader("Accept-Encoding", "gzip");
+          command->SetUri("/instances/" + instanceId + "/tags");
+          command->SetPayload(new LoadGeometry(*this));
+          oracle_.Schedule(*this, command.release());
+        }
+
+        {
+          std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+          command->SetUri("/instances/" + instanceId + "/metadata/TransferSyntax");
+          command->SetPayload(new LoadTransferSyntax(*this));
+          oracle_.Schedule(*this, command.release());
+        }
       }
     }
   };
 
 
 
-#if 0
-  void LoadInstance(const std::string& instanceId)
-  {
-    if (active_)
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-    }
-
-    active_ = true;
-
-    // Tag "3004-000c" is "Grid Frame Offset Vector", which is
-    // mandatory to read RT DOSE, but is too long to be returned by default
-
-    // TODO => Should be part of a second call if needed
-
-    std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
-    command->SetUri("/instances/" + instanceId + "/tags?ignore-length=3004-000c");
-    command->SetPayload(new LoadInstanceGeometryHandler(*this));
-
-    oracle_.Schedule(*this, command.release());
-  }
-#endif
-
-
-  class SceneVolumeSlicer : public boost::noncopyable
+  class VolumeSceneLayerSource : public boost::noncopyable
   {
   private:
     int                                layerDepth_;
-    std::auto_ptr<IVolumeSlicer>       volume_;
+    boost::shared_ptr<IVolumeSlicer>   slicer_;
     bool                               linearInterpolation_;
     std::auto_ptr<CoordinateSystem3D>  lastPlane_;
     uint64_t                           lastRevision_;
@@ -755,13 +1295,13 @@
     }
 
   public:
-    SceneVolumeSlicer(int layerDepth,
-                      IVolumeSlicer* volume) :   // Takes ownership
+    VolumeSceneLayerSource(int layerDepth,
+                           IVolumeSlicer* slicer) :   // Takes ownership
       layerDepth_(layerDepth),
-      volume_(volume),
+      slicer_(slicer),
       linearInterpolation_(false)
     {
-      if (volume == NULL)
+      if (slicer == NULL)
       {
         throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
       }
@@ -769,7 +1309,7 @@
 
     const IVolumeSlicer& GetSlicer() const
     {
-      return *volume_;
+      return *slicer_;
     }
 
     void SetLinearInterpolation(bool enabled)
@@ -785,8 +1325,8 @@
     void Update(Scene2D& scene,
                 const CoordinateSystem3D& plane)
     {
-      assert(volume_.get() != NULL);
-      std::auto_ptr<IVolumeSlicer::ExtractedSlice> slice(volume_->ExtractSlice(plane));
+      assert(slicer_.get() != NULL);
+      std::auto_ptr<IVolumeSlicer::ExtractedSlice> slice(slicer_->ExtractSlice(plane));
 
       if (slice.get() == NULL)
       {
@@ -811,7 +1351,7 @@
         lastPlane_.reset(new CoordinateSystem3D(plane));
         lastRevision_ = slice->GetRevision();
 
-        std::auto_ptr<ISceneLayer> layer(slice->CreateSceneLayer());
+        std::auto_ptr<ISceneLayer> layer(slice->CreateSceneLayer(plane));
         if (layer.get() == NULL)
         {
           throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);        
@@ -909,7 +1449,28 @@
 private:
   OrthancStone::IOracle& oracle_;
   OrthancStone::Scene2D             scene_;
-  std::auto_ptr<OrthancStone::SceneVolumeSlicer>  slicer_;
+  std::auto_ptr<OrthancStone::VolumeSceneLayerSource>  source1_, source2_;
+
+  
+  OrthancStone::CoordinateSystem3D GetSamplePlane
+  (const OrthancStone::VolumeSceneLayerSource& source) const
+  {
+    const OrthancStone::IVolumeImageSlicer& slicer =
+      dynamic_cast<const OrthancStone::IVolumeImageSlicer&>(source.GetSlicer());
+        
+    OrthancStone::CoordinateSystem3D plane;
+
+    if (slicer.HasGeometry())
+    {
+      //plane = slicer.GetGeometry().GetSagittalGeometry();
+      //plane = slicer.GetGeometry().GetAxialGeometry();
+      plane = slicer.GetGeometry().GetCoronalGeometry();
+      plane.SetOrigin(slicer.GetGeometry().GetCoordinates(0.5f, 0.5f, 0.5f));
+    }
+
+    return plane;
+  }
+  
 
   void Handle(const OrthancStone::SleepOracleCommand::TimeoutMessage& message)
   {
@@ -921,40 +1482,53 @@
     {
       printf("TIMEOUT\n");
 
-      if (slicer_.get() != NULL)
-      {
-        OrthancStone::CoordinateSystem3D plane;
+      OrthancStone::CoordinateSystem3D plane;
 
-        const OrthancStone::OrthancSeriesVolumeProgressiveLoader& loader =
-          dynamic_cast<const OrthancStone::OrthancSeriesVolumeProgressiveLoader&>(slicer_->GetSlicer());
-        
-        if (loader.GetVolume().HasGeometry())
-        {
-          plane = loader.GetVolume().GetGeometry().GetSagittalGeometry();
-          plane.SetOrigin(loader.GetVolume().GetGeometry().GetCoordinates(0.5f, 0.5f, 0.5f));
-        }
+      if (source1_.get() != NULL)
+      {
+        plane = GetSamplePlane(*source1_);
+      }
+      else if (source2_.get() != NULL)
+      {
+        plane = GetSamplePlane(*source2_);
+      }
 
-        slicer_->Update(scene_, plane);
-        scene_.FitContent(1024, 768);
+      if (source1_.get() != NULL)
+      {
+        source1_->Update(scene_, plane);
+      }
+      
+      if (source2_.get() != NULL)
+      {
+        source2_->Update(scene_, plane);
+      }
 
-        {
-          OrthancStone::CairoCompositor compositor(scene_, 1024, 768);
-          compositor.Refresh();
-
-          Orthanc::ImageAccessor accessor;
-          compositor.GetCanvas().GetReadOnlyAccessor(accessor);
+      scene_.FitContent(1024, 768);
+      
+      {
+        OrthancStone::CairoCompositor compositor(scene_, 1024, 768);
+        compositor.Refresh();
+        
+        Orthanc::ImageAccessor accessor;
+        compositor.GetCanvas().GetReadOnlyAccessor(accessor);
 
-          Orthanc::Image tmp(Orthanc::PixelFormat_RGB24, accessor.GetWidth(), accessor.GetHeight(), false);
-          Orthanc::ImageProcessing::Convert(tmp, accessor);
+        Orthanc::Image tmp(Orthanc::PixelFormat_RGB24, accessor.GetWidth(), accessor.GetHeight(), false);
+        Orthanc::ImageProcessing::Convert(tmp, accessor);
+        
+        static unsigned int count = 0;
+        char buf[64];
+        sprintf(buf, "scene-%06d.png", count++);
+        
+        Orthanc::PngWriter writer;
+        writer.WriteToFile(buf, tmp);
+      }
 
-          static unsigned int count = 0;
-          char buf[64];
-          sprintf(buf, "scene-%06d.png", count++);
-
-          Orthanc::PngWriter writer;
-          writer.WriteToFile(buf, tmp);
-        }
-      }
+      /**
+       * The sleep() leads to a crash if the oracle is still running,
+       * while this object is destroyed. Always stop the oracle before
+       * destroying active objects.  (*)
+       **/
+      // boost::this_thread::sleep(boost::posix_time::seconds(2));
 
       oracle_.Schedule(*this, new OrthancStone::SleepOracleCommand(message.GetOrigin().GetDelay()));
     }
@@ -1021,25 +1595,33 @@
        <Toto, OrthancStone::OracleCommandExceptionMessage>(*this, &Toto::Handle));
   }
 
-  void SetVolume(int depth,
-                 OrthancStone::IVolumeSlicer* volume)
+  void SetVolume1(int depth,
+                  OrthancStone::IVolumeSlicer* volume)
   {
-    slicer_.reset(new OrthancStone::SceneVolumeSlicer(0, volume));
+    source1_.reset(new OrthancStone::VolumeSceneLayerSource(depth, volume));
+  }
+
+  void SetVolume2(int depth,
+                  OrthancStone::IVolumeSlicer* volume)
+  {
+    source2_.reset(new OrthancStone::VolumeSceneLayerSource(depth, volume));
   }
 };
 
 
 void Run(OrthancStone::NativeApplicationContext& context,
-         OrthancStone::IOracle& oracle)
+         OrthancStone::ThreadedOracle& oracle)
 {
-  std::auto_ptr<Toto> toto;
-  std::auto_ptr<OrthancStone::OrthancSeriesVolumeProgressiveLoader> loader1, loader2;
+  boost::shared_ptr<Toto> toto;
+  boost::shared_ptr<OrthancStone::OrthancSeriesVolumeProgressiveLoader> loader1, loader2;
+  boost::shared_ptr<OrthancStone::OrthancMultiframeVolumeLoader> loader3;
 
   {
     OrthancStone::NativeApplicationContext::WriterLock lock(context);
     toto.reset(new Toto(oracle, lock.GetOracleObservable()));
     loader1.reset(new OrthancStone::OrthancSeriesVolumeProgressiveLoader(oracle, lock.GetOracleObservable()));
     loader2.reset(new OrthancStone::OrthancSeriesVolumeProgressiveLoader(oracle, lock.GetOracleObservable()));
+    loader3.reset(new OrthancStone::OrthancMultiframeVolumeLoader(oracle, lock.GetOracleObservable()));
   }
 
   oracle.Schedule(*toto, new OrthancStone::SleepOracleCommand(100));
@@ -1120,19 +1702,37 @@
   }
 
   // 2017-11-17-Anonymized
-  //loader1->LoadSeries("cb3ea4d1-d08f3856-ad7b6314-74d88d77-60b05618");  // CT
-  //loader2->LoadInstance("41029085-71718346-811efac4-420e2c15-d39f99b6");  // RT-DOSE
+  loader1->LoadSeries("cb3ea4d1-d08f3856-ad7b6314-74d88d77-60b05618");  // CT
+  loader3->LoadInstance("41029085-71718346-811efac4-420e2c15-d39f99b6");  // RT-DOSE
 
+  // 2015-01-28-Multiframe
+  //loader3->LoadInstance("88f71e2a-5fad1c61-96ed14d6-5b3d3cf7-a5825279");  // Multiframe CT
+  
   // Delphine
   //loader1->LoadSeries("5990e39c-51e5f201-fe87a54c-31a55943-e59ef80e");  // CT
-  loader1->LoadSeries("67f1b334-02c16752-45026e40-a5b60b6b-030ecab5");  // Lung 1/10mm
+  //loader1->LoadSeries("67f1b334-02c16752-45026e40-a5b60b6b-030ecab5");  // Lung 1/10mm
 
 
-  toto->SetVolume(0, loader1.release());
+  toto->SetVolume2(1, new OrthancStone::OrthancMultiframeVolumeLoader::MPRSlicer(loader3));
+  toto->SetVolume1(0, new OrthancStone::OrthancSeriesVolumeProgressiveLoader::MPRSlicer(loader1));
+
+  {
+    oracle.Start();
+
+    LOG(WARNING) << "...Waiting for Ctrl-C...";
+    Orthanc::SystemToolbox::ServerBarrier();
 
-  LOG(WARNING) << "...Waiting for Ctrl-C...";
-  Orthanc::SystemToolbox::ServerBarrier();
-  //boost::this_thread::sleep(boost::posix_time::seconds(1));
+    /**
+     * WARNING => The oracle must be stopped BEFORE the objects using
+     * it are destroyed!!! This forces to wait for the completion of
+     * the running callback methods. Otherwise, the callbacks methods
+     * might still be running while their parent object is destroyed,
+     * resulting in crashes. This is very visible if adding a sleep(),
+     * as in (*).
+     **/
+
+    oracle.Stop();
+  }
 }
 
 
@@ -1152,7 +1752,7 @@
     OrthancStone::NativeApplicationContext context;
 
     OrthancStone::ThreadedOracle oracle(context);
-    oracle.SetThreadsCount(1);
+    //oracle.SetThreadsCount(1);
 
     {
       Orthanc::WebServiceParameters p;
@@ -1161,11 +1761,11 @@
       oracle.SetOrthancParameters(p);
     }
 
-    oracle.Start();
+    //oracle.Start();
 
     Run(context, oracle);
-
-    oracle.Stop();
+    
+    //oracle.Stop();
   }
   catch (Orthanc::OrthancException& e)
   {