changeset 788:e76c4eef1054

Merge from default
author Benjamin Golinvaux <bgo@osimis.io>
date Mon, 27 May 2019 16:01:47 +0200
parents 1a28fce57ff3 (current diff) cd13a062c9bd (diff)
children c83a45f864b2
files Samples/Sdl/Loader.cpp
diffstat 5 files changed, 941 insertions(+), 662 deletions(-) [+]
line wrap: on
line diff
--- a/Framework/Scene2D/FloatTextureSceneLayer.cpp	Fri May 24 16:13:50 2019 +0200
+++ b/Framework/Scene2D/FloatTextureSceneLayer.cpp	Mon May 27 16:01:47 2019 +0200
@@ -27,7 +27,8 @@
 
 namespace OrthancStone
 {
-  FloatTextureSceneLayer::FloatTextureSceneLayer(const Orthanc::ImageAccessor& texture)
+  FloatTextureSceneLayer::FloatTextureSceneLayer(const Orthanc::ImageAccessor& texture) :
+    inverted_(false)
   {
     {
       std::auto_ptr<Orthanc::ImageAccessor> t(
--- a/Framework/Toolbox/DicomInstanceParameters.h	Fri May 24 16:13:50 2019 +0200
+++ b/Framework/Toolbox/DicomInstanceParameters.h	Mon May 27 16:01:47 2019 +0200
@@ -89,6 +89,11 @@
     {
     }
 
+    DicomInstanceParameters* Clone() const
+    {
+      return new DicomInstanceParameters(*this);
+    }
+
     void SetOrthancInstanceIdentifier(const std::string& id)
     {
       data_.orthancInstanceId_ = id;
--- a/Framework/Volumes/VolumeReslicer.cpp	Fri May 24 16:13:50 2019 +0200
+++ b/Framework/Volumes/VolumeReslicer.cpp	Mon May 27 16:01:47 2019 +0200
@@ -779,6 +779,7 @@
                              double voxelSize)
   {
     Reset();
+    pixelSpacing_ = voxelSize;
 
     // Firstly, compute the intersection of the source volumetric
     // image with the reslicing plane. This leads to a polygon with 3
@@ -817,4 +818,17 @@
 
     success_ = true;
   }
+
+
+  double VolumeReslicer::GetPixelSpacing() const
+  {
+    if (success_)
+    {
+      return pixelSpacing_;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
 }
--- a/Framework/Volumes/VolumeReslicer.h	Fri May 24 16:13:50 2019 +0200
+++ b/Framework/Volumes/VolumeReslicer.h	Mon May 27 16:01:47 2019 +0200
@@ -43,6 +43,7 @@
     bool                           success_;
     Extent2D                       extent_;
     std::auto_ptr<Orthanc::Image>  slice_;
+    double                         pixelSpacing_;
 
     void CheckIterators(const ImageBuffer3D& source,
                         const CoordinateSystem3D& plane,
@@ -118,5 +119,7 @@
                const VolumeImageGeometry& geometry,
                const CoordinateSystem3D& plane,
                double voxelSize);
+
+    double GetPixelSpacing() const;
   };
 }
--- a/Samples/Sdl/Loader.cpp	Fri May 24 16:13:50 2019 +0200
+++ b/Samples/Sdl/Loader.cpp	Mon May 27 16:01:47 2019 +0200
@@ -39,6 +39,7 @@
 #include "../../Framework/Toolbox/SlicesSorter.h"
 #include "../../Framework/Volumes/ImageBuffer3D.h"
 #include "../../Framework/Volumes/VolumeImageGeometry.h"
+#include "../../Framework/Volumes/VolumeReslicer.h"
 
 // From Orthanc framework
 #include <Core/DicomFormat/DicomArray.h>
@@ -57,13 +58,148 @@
 
 namespace OrthancStone
 {
+  // Application-configurable, can be shared between 3D/2D
+  class ILayerStyleConfigurator
+  {
+  public:
+    virtual ~ILayerStyleConfigurator()
+    {
+    }
+    
+    virtual uint64_t GetRevision() const = 0;
+    
+    virtual TextureBaseSceneLayer* CreateTextureFromImage(const Orthanc::ImageAccessor& image) const = 0;
+
+    virtual TextureBaseSceneLayer* CreateTextureFromDicom(const Orthanc::ImageAccessor& frame,
+                                                          const DicomInstanceParameters& parameters) const = 0;
+
+    virtual void ApplyStyle(ISceneLayer& layer) const = 0;
+  };
+
+
+
+  class LookupTableStyleConfigurator : public ILayerStyleConfigurator
+  {
+  private:
+    uint64_t     revision_;
+    bool         hasLut_;
+    std::string  lut_;
+    bool         hasRange_;
+    float        minValue_;
+    float        maxValue_;
+    
+  public:
+    LookupTableStyleConfigurator() :
+      revision_(0),
+      hasLut_(false),
+      hasRange_(false)
+    {
+    }
+
+    void SetLookupTable(Orthanc::EmbeddedResources::FileResourceId resource)
+    {
+      hasLut_ = true;
+      Orthanc::EmbeddedResources::GetFileResource(lut_, resource);
+    }
+
+    void SetLookupTable(const std::string& lut)
+    {
+      hasLut_ = true;
+      lut_ = lut;
+    }
+
+    void SetRange(float minValue,
+                  float maxValue)
+    {
+      if (minValue > maxValue)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+      else
+      {
+        hasRange_ = true;
+        minValue_ = minValue;
+        maxValue_ = maxValue;
+      }
+    }
+
+    virtual uint64_t GetRevision() const
+    {
+      return revision_;
+    }
+    
+    virtual TextureBaseSceneLayer* CreateTextureFromImage(const Orthanc::ImageAccessor& image) const
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+
+    virtual TextureBaseSceneLayer* CreateTextureFromDicom(const Orthanc::ImageAccessor& frame,
+                                                          const DicomInstanceParameters& parameters) const
+    {
+      return parameters.CreateLookupTableTexture(frame);
+    }
+
+    virtual void ApplyStyle(ISceneLayer& layer) const
+    {
+      LookupTableTextureSceneLayer& l = dynamic_cast<LookupTableTextureSceneLayer&>(layer);
+      
+      if (hasLut_)
+      {
+        l.SetLookupTable(lut_);
+      }
+
+      if (hasRange_)
+      {
+        l.SetRange(minValue_, maxValue_);
+      }
+      else
+      {
+        l.FitRange();
+      }
+    }
+  };
+
+
+  class GrayscaleStyleConfigurator : public ILayerStyleConfigurator
+  {
+  private:
+    uint64_t revision_;
+    
+  public:
+    GrayscaleStyleConfigurator() :
+      revision_(0)
+    {
+    }
+
+    virtual uint64_t GetRevision() const
+    {
+      return revision_;
+    }
+    
+    virtual TextureBaseSceneLayer* CreateTextureFromImage(const Orthanc::ImageAccessor& image) const
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+
+    virtual TextureBaseSceneLayer* CreateTextureFromDicom(const Orthanc::ImageAccessor& frame,
+                                                          const DicomInstanceParameters& parameters) const
+    {
+      return parameters.CreateTexture(frame);
+    }
+
+    virtual void ApplyStyle(ISceneLayer& layer) const
+    {
+    }
+  };
+
+
   class IVolumeSlicer : public boost::noncopyable
   {
   public:
-    class ExtractedSlice : public boost::noncopyable
+    class IExtractedSlice : public boost::noncopyable
     {
     public:
-      virtual ~ExtractedSlice()
+      virtual ~IExtractedSlice()
       {
       }
 
@@ -73,54 +209,146 @@
       virtual uint64_t GetRevision() = 0;
 
       // This call can take some time
-      virtual ISceneLayer* CreateSceneLayer(const CoordinateSystem3D& cuttingPlane) = 0;
+      virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator,  // possibly absent
+                                            const CoordinateSystem3D& cuttingPlane) = 0;
     };
 
+    
+    class InvalidSlice : public IExtractedSlice
+    {
+    public:
+      virtual bool IsValid()
+      {
+        return false;
+      }
+
+      virtual uint64_t GetRevision()
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+
+      virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator,
+                                            const CoordinateSystem3D& cuttingPlane)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    };
+
+
     virtual ~IVolumeSlicer()
     {
     }
 
-    virtual ExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) const = 0;
-  };
-
-
-  class IVolumeImageSlicer : public IVolumeSlicer
-  {
-  public:
-    virtual bool HasGeometry() const = 0;
-
-    virtual const VolumeImageGeometry& GetGeometry() const = 0;
+    virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) = 0;
   };
 
 
-  class InvalidExtractedSlice : public IVolumeSlicer::ExtractedSlice
+
+  // This class combines a 3D image buffer, a 3D volume geometry and
+  // information about the DICOM parameters of the series.
+  class DicomVolumeImage : public boost::noncopyable
   {
   public:
-    virtual bool IsValid()
+    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, GeometryReadyMessage, DicomVolumeImage);
+    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, ContentUpdatedMessage, DicomVolumeImage);
+
+  private:
+    uint64_t                                revision_;
+    std::auto_ptr<VolumeImageGeometry>      geometry_;
+    std::auto_ptr<ImageBuffer3D>            image_;
+    std::auto_ptr<DicomInstanceParameters>  parameters_;
+
+    void CheckHasGeometry() const
     {
-      return false;
+      if (!HasGeometry())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    }
+    
+  public:
+    DicomVolumeImage() :
+      revision_(0)
+    {
+    }
+
+    void IncrementRevision()
+    {
+      revision_ ++;
+    }
+
+    void Initialize(const VolumeImageGeometry& geometry,
+                    Orthanc::PixelFormat format)
+    {
+      geometry_.reset(new VolumeImageGeometry(geometry));
+      image_.reset(new ImageBuffer3D(format, geometry_->GetWidth(), geometry_->GetHeight(),
+                                     geometry_->GetDepth(), false /* don't compute range */));
+
+      revision_ ++;
     }
 
-    virtual uint64_t GetRevision()
+    void SetDicomParameters(const DicomInstanceParameters& parameters)
+    {
+      parameters_.reset(parameters.Clone());
+      revision_ ++;
+    }
+    
+    uint64_t GetRevision() const
     {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      return revision_;
+    }
+
+    bool HasGeometry() const
+    {
+      return (geometry_.get() != NULL &&
+              image_.get() != NULL);
+    }
+
+    ImageBuffer3D& GetPixelData()
+    {
+      CheckHasGeometry();
+      return *image_;
     }
 
-    virtual ISceneLayer* CreateSceneLayer(const CoordinateSystem3D& cuttingPlane)
+    const ImageBuffer3D& GetPixelData() const
+    {
+      CheckHasGeometry();
+      return *image_;
+    }
+
+    const VolumeImageGeometry& GetGeometry() const
+    {
+      CheckHasGeometry();
+      return *geometry_;
+    }
+
+    bool HasDicomParameters() const
     {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      return parameters_.get() != NULL;
+    }      
+
+    const DicomInstanceParameters& GetDicomParameters() const
+    {
+      if (HasDicomParameters())
+      {
+        return *parameters_;
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }      
     }
   };
 
 
-  class DicomVolumeImageOrthogonalSlice : public IVolumeSlicer::ExtractedSlice
+
+  class DicomVolumeImageOrthogonalSlice : public IVolumeSlicer::IExtractedSlice
   {
   private:
-    const ImageBuffer3D&        image_;
-    const VolumeImageGeometry&  geometry_;
-    bool                        valid_;
-    VolumeProjection            projection_;
-    unsigned int                sliceIndex_;
+    const DicomVolumeImage&  volume_;
+    bool                 valid_;
+    VolumeProjection     projection_;
+    unsigned int         sliceIndex_;
 
     void CheckValid() const
     {
@@ -131,20 +359,20 @@
     }
 
   protected:
+    // Can be overloaded in subclasses
     virtual uint64_t GetRevisionInternal(VolumeProjection projection,
-                                         unsigned int sliceIndex) const = 0;
-
-    virtual const DicomInstanceParameters& GetDicomParameters(VolumeProjection projection,
-                                                              unsigned int sliceIndex) const = 0;
+                                         unsigned int sliceIndex) const
+    {
+      return volume_.GetRevision();
+    }
 
   public:
-    DicomVolumeImageOrthogonalSlice(const ImageBuffer3D& image,
-                                    const VolumeImageGeometry& geometry,
+    DicomVolumeImageOrthogonalSlice(const DicomVolumeImage& volume,
                                     const CoordinateSystem3D& cuttingPlane) :
-      image_(image),
-      geometry_(geometry)
+      volume_(volume)
     {
-      valid_ = geometry_.DetectSlice(projection_, sliceIndex_, cuttingPlane);
+      valid_ = (volume_.HasDicomParameters() &&
+                volume_.GetGeometry().DetectSlice(projection_, sliceIndex_, cuttingPlane));
     }
 
     VolumeProjection GetProjection() const
@@ -170,37 +398,27 @@
       return GetRevisionInternal(projection_, sliceIndex_);
     }
 
-    virtual ISceneLayer* CreateSceneLayer(const CoordinateSystem3D& cuttingPlane)
+    virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator,
+                                          const CoordinateSystem3D& cuttingPlane)
     {
       CheckValid();
 
+      if (configurator == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer,
+                                        "A style configurator is mandatory for textures");
+      }
+
       std::auto_ptr<TextureBaseSceneLayer> texture;
         
       {
-        const DicomInstanceParameters& parameters = GetDicomParameters(projection_, sliceIndex_);
-        ImageBuffer3D::SliceReader reader(image_, projection_, sliceIndex_);
-
-        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 DicomInstanceParameters& parameters = volume_.GetDicomParameters();
+        ImageBuffer3D::SliceReader reader(volume_.GetPixelData(), projection_, sliceIndex_);
+        texture.reset(dynamic_cast<TextureBaseSceneLayer*>
+                      (configurator->CreateTextureFromDicom(reader.GetAccessor(), parameters)));
       }
 
-      const CoordinateSystem3D& system = geometry_.GetProjectionGeometry(projection_);
+      const CoordinateSystem3D& system = volume_.GetGeometry().GetProjectionGeometry(projection_);
       
       double x0, y0, x1, y1;
       cuttingPlane.ProjectPoint(x0, y0, system.GetOrigin());
@@ -215,7 +433,7 @@
         texture->SetAngle(atan2(dy, dx));
       }
         
-      Vector tmp = geometry_.GetVoxelDimensions(projection_);
+      Vector tmp = volume_.GetGeometry().GetVoxelDimensions(projection_);
       texture->SetPixelSpacing(tmp[0], tmp[1]);
 
       return texture.release();
@@ -244,16 +462,211 @@
     }
   };
 
+  
 
-  // This class combines a 3D image buffer, a 3D volume geometry and
-  // information about the DICOM parameters of each slice.
-  class DicomSeriesVolumeImage : public boost::noncopyable
+
+
+  class OrthancSeriesVolumeProgressiveLoader : 
+    public IObserver,
+    public IObservable,
+    public IVolumeSlicer
   {
-  public:
-    class ExtractedOrthogonalSlice : public DicomVolumeImageOrthogonalSlice
+  private:
+    static const unsigned int LOW_QUALITY = 0;
+    static const unsigned int MIDDLE_QUALITY = 1;
+    static const unsigned int BEST_QUALITY = 2;
+    
+    
+    // Helper class internal to OrthancSeriesVolumeProgressiveLoader
+    class SeriesGeometry : public boost::noncopyable
     {
     private:
-      const DicomSeriesVolumeImage&  that_;
+      void CheckSlice(size_t index,
+                      const DicomInstanceParameters& reference) const
+      {
+        const DicomInstanceParameters& slice = *slices_[index];
+      
+        if (!GeometryToolbox::IsParallel(
+              reference.GetGeometry().GetNormal(),
+              slice.GetGeometry().GetNormal()))
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry,
+                                          "A slice in the volume image is not parallel to the others");
+        }
+
+        if (reference.GetExpectedPixelFormat() != slice.GetExpectedPixelFormat())
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat,
+                                          "The pixel format changes across the slices of the volume image");
+        }
+
+        if (reference.GetImageInformation().GetWidth() != slice.GetImageInformation().GetWidth() ||
+            reference.GetImageInformation().GetHeight() != slice.GetImageInformation().GetHeight())
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize,
+                                          "The width/height of slices are not constant in the volume image");
+        }
+
+        if (!LinearAlgebra::IsNear(reference.GetPixelSpacingX(), slice.GetPixelSpacingX()) ||
+            !LinearAlgebra::IsNear(reference.GetPixelSpacingY(), slice.GetPixelSpacingY()))
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry,
+                                          "The pixel spacing of the slices change across the volume image");
+        }
+      }
+
+    
+      void CheckVolume() const
+      {
+        for (size_t i = 0; i < slices_.size(); i++)
+        {
+          assert(slices_[i] != NULL);
+          if (slices_[i]->GetImageInformation().GetNumberOfFrames() != 1)
+          {
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry,
+                                            "This class does not support multi-frame images");
+          }
+        }
+
+        if (slices_.size() != 0)
+        {
+          const DicomInstanceParameters& reference = *slices_[0];
+
+          for (size_t i = 1; i < slices_.size(); i++)
+          {
+            CheckSlice(i, reference);
+          }
+        }
+      }
+
+
+      void Clear()
+      {
+        for (size_t i = 0; i < slices_.size(); i++)
+        {
+          assert(slices_[i] != NULL);
+          delete slices_[i];
+        }
+
+        slices_.clear();
+        slicesRevision_.clear();
+      }
+
+
+      void CheckSliceIndex(size_t index) const
+      {
+        if (!HasGeometry())
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+        }
+        else if (index >= slices_.size())
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        }
+        else
+        {
+          assert(slices_.size() == GetImageGeometry().GetDepth() &&
+                 slices_.size() == slicesRevision_.size());
+        }
+      }
+
+    
+      std::auto_ptr<VolumeImageGeometry>     geometry_;
+      std::vector<DicomInstanceParameters*>  slices_;
+      std::vector<uint64_t>                  slicesRevision_;
+
+    public:
+      ~SeriesGeometry()
+      {
+        Clear();
+      }
+
+      // WARNING: The payload of "slices" must be of class "DicomInstanceParameters"
+      void ComputeGeometry(SlicesSorter& slices)
+      {
+        Clear();
+      
+        if (!slices.Sort())
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
+                                          "Cannot sort the 3D slices of a DICOM series");          
+        }
+
+        if (slices.GetSlicesCount() == 0)
+        {
+          geometry_.reset(new VolumeImageGeometry);
+        }
+        else
+        {
+          slices_.reserve(slices.GetSlicesCount());
+          slicesRevision_.resize(slices.GetSlicesCount(), 0);
+
+          for (size_t i = 0; i < slices.GetSlicesCount(); i++)
+          {
+            const DicomInstanceParameters& slice =
+              dynamic_cast<const DicomInstanceParameters&>(slices.GetSlicePayload(i));
+            slices_.push_back(new DicomInstanceParameters(slice));
+          }
+
+          CheckVolume();
+
+          const double spacingZ = slices.ComputeSpacingBetweenSlices();
+          LOG(INFO) << "Computed spacing between slices: " << spacingZ << "mm";
+      
+          const DicomInstanceParameters& parameters = *slices_[0];
+
+          geometry_.reset(new VolumeImageGeometry);
+          geometry_->SetSize(parameters.GetImageInformation().GetWidth(),
+                             parameters.GetImageInformation().GetHeight(),
+                             static_cast<unsigned int>(slices.GetSlicesCount()));
+          geometry_->SetAxialGeometry(slices.GetSliceGeometry(0));
+          geometry_->SetVoxelDimensions(parameters.GetPixelSpacingX(),
+                                        parameters.GetPixelSpacingY(), spacingZ);
+        }
+      }
+
+      bool HasGeometry() const
+      {
+        return geometry_.get() != NULL;
+      }
+
+      const VolumeImageGeometry& GetImageGeometry() const
+      {
+        if (!HasGeometry())
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+        }
+        else
+        {
+          assert(slices_.size() == geometry_->GetDepth());
+          return *geometry_;
+        }
+      }
+
+      const DicomInstanceParameters& GetSliceParameters(size_t index) const
+      {
+        CheckSliceIndex(index);
+        return *slices_[index];
+      }
+
+      uint64_t GetSliceRevision(size_t index) const
+      {
+        CheckSliceIndex(index);
+        return slicesRevision_[index];
+      }
+
+      void IncrementSliceRevision(size_t index)
+      {
+        CheckSliceIndex(index);
+        slicesRevision_[index] ++;
+      }
+    };
+
+
+    class Slice : public DicomVolumeImageOrthogonalSlice
+    {
+    private:
+      const OrthancSeriesVolumeProgressiveLoader&  that_;
 
     protected:
       virtual uint64_t GetRevisionInternal(VolumeProjection projection,
@@ -261,288 +674,25 @@
       {
         if (projection == VolumeProjection_Axial)
         {
-          return that_.GetSliceRevision(sliceIndex);
+          return that_.seriesGeometry_.GetSliceRevision(sliceIndex);
         }
         else
         {
           // For coronal and sagittal projections, we take the global
           // revision of the volume
-          return that_.GetRevision();
+          return that_.volume_->GetRevision();
         }
       }
 
-      virtual const DicomInstanceParameters& GetDicomParameters(VolumeProjection projection,
-                                                                unsigned int sliceIndex) const
-      {
-        return that_.GetSliceParameters(projection == VolumeProjection_Axial ? sliceIndex : 0);
-      }
-
     public:
-      ExtractedOrthogonalSlice(const DicomSeriesVolumeImage& that,
-                               const CoordinateSystem3D& plane) :
-        DicomVolumeImageOrthogonalSlice(that.GetImage(), that.GetGeometry(), plane),
+      Slice(const OrthancSeriesVolumeProgressiveLoader& that,
+            const CoordinateSystem3D& plane) :
+        DicomVolumeImageOrthogonalSlice(*that.volume_, plane),
         that_(that)
       {
       }
     };
 
-
-  private:
-    std::auto_ptr<ImageBuffer3D>           image_;
-    std::auto_ptr<VolumeImageGeometry>     geometry_;
-    std::vector<DicomInstanceParameters*>  slices_;
-    uint64_t                               revision_;
-    std::vector<uint64_t>                  slicesRevision_;
-    std::vector<unsigned int>              slicesQuality_;
-
-    void CheckSlice(size_t index,
-                    const DicomInstanceParameters& reference) const
-    {
-      const DicomInstanceParameters& slice = *slices_[index];
-      
-      if (!GeometryToolbox::IsParallel(
-            reference.GetGeometry().GetNormal(),
-            slice.GetGeometry().GetNormal()))
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry,
-                                        "A slice in the volume image is not parallel to the others");
-      }
-
-      if (reference.GetExpectedPixelFormat() != slice.GetExpectedPixelFormat())
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat,
-                                        "The pixel format changes across the slices of the volume image");
-      }
-
-      if (reference.GetImageInformation().GetWidth() != slice.GetImageInformation().GetWidth() ||
-          reference.GetImageInformation().GetHeight() != slice.GetImageInformation().GetHeight())
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize,
-                                        "The width/height of slices are not constant in the volume image");
-      }
-
-      if (!LinearAlgebra::IsNear(reference.GetPixelSpacingX(), slice.GetPixelSpacingX()) ||
-          !LinearAlgebra::IsNear(reference.GetPixelSpacingY(), slice.GetPixelSpacingY()))
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry,
-                                        "The pixel spacing of the slices change across the volume image");
-      }
-    }
-
-    
-    void CheckVolume() const
-    {
-      for (size_t i = 0; i < slices_.size(); i++)
-      {
-        assert(slices_[i] != NULL);
-        if (slices_[i]->GetImageInformation().GetNumberOfFrames() != 1)
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry,
-                                          "This class does not support multi-frame images");
-        }
-      }
-
-      if (slices_.size() != 0)
-      {
-        const DicomInstanceParameters& reference = *slices_[0];
-
-        for (size_t i = 1; i < slices_.size(); i++)
-        {
-          CheckSlice(i, reference);
-        }
-      }
-    }
-
-
-    void Clear()
-    {
-      image_.reset();
-      geometry_.reset();
-      
-      for (size_t i = 0; i < slices_.size(); i++)
-      {
-        assert(slices_[i] != NULL);
-        delete slices_[i];
-      }
-
-      slices_.clear();
-      slicesRevision_.clear();
-      slicesQuality_.clear();
-    }
-
-
-    void CheckSliceIndex(size_t index) const
-    {
-      assert(slices_.size() == image_->GetDepth() &&
-             slices_.size() == slicesRevision_.size());
-
-      if (!HasGeometry())
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-      }
-      else if (index >= slices_.size())
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
-      }
-    }
-
-    
-  public:
-    DicomSeriesVolumeImage() :
-      revision_(0)
-    {
-    }
-
-    ~DicomSeriesVolumeImage()
-    {
-      Clear();
-    }
-
-    // WARNING: The payload of "slices" must be of class "DicomInstanceParameters"
-    void SetGeometry(SlicesSorter& slices)
-    {
-      Clear();
-      
-      if (!slices.Sort())
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
-                                        "Cannot sort the 3D slices of a DICOM series");          
-      }
-
-      geometry_.reset(new VolumeImageGeometry);
-
-      if (slices.GetSlicesCount() == 0)
-      {
-        // Empty volume
-        image_.reset(new ImageBuffer3D(Orthanc::PixelFormat_Grayscale8, 0, 0, 0,
-                                       false /* don't compute range */));
-      }
-      else
-      {
-        slices_.reserve(slices.GetSlicesCount());
-        slicesRevision_.resize(slices.GetSlicesCount(), 0);
-        slicesQuality_.resize(slices.GetSlicesCount(), 0);
-
-        for (size_t i = 0; i < slices.GetSlicesCount(); i++)
-        {
-          const DicomInstanceParameters& slice =
-            dynamic_cast<const DicomInstanceParameters&>(slices.GetSlicePayload(i));
-          slices_.push_back(new DicomInstanceParameters(slice));
-        }
-
-        CheckVolume();
-
-        const double spacingZ = slices.ComputeSpacingBetweenSlices();
-        LOG(INFO) << "Computed spacing between slices: " << spacingZ << "mm";
-      
-        const DicomInstanceParameters& parameters = *slices_[0];
-
-        image_.reset(new ImageBuffer3D(parameters.GetExpectedPixelFormat(),
-                          parameters.GetImageInformation().GetWidth(),
-                          parameters.GetImageInformation().GetHeight(),
-                          static_cast<unsigned int>(slices.GetSlicesCount()),
-                          false /* don't compute range */));      
-
-        geometry_->SetSize(image_->GetWidth(), image_->GetHeight(), image_->GetDepth());
-        geometry_->SetAxialGeometry(slices.GetSliceGeometry(0));
-        geometry_->SetVoxelDimensions(parameters.GetPixelSpacingX(),
-                                      parameters.GetPixelSpacingY(), spacingZ);
-      }
-      
-      image_->Clear();
-
-      revision_++;
-    }
-
-    uint64_t GetRevision() const
-    {
-      return revision_;
-    }
-
-    bool HasGeometry() const
-    {
-      return (image_.get() != NULL &&
-              geometry_.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_;
-      }
-    }
-
-    size_t GetSlicesCount() const
-    {
-      if (!HasGeometry())
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-      }
-      else
-      {
-        return slices_.size();
-      }
-    }
-
-    const DicomInstanceParameters& GetSliceParameters(size_t index) const
-    {
-      CheckSliceIndex(index);
-      return *slices_[index];
-    }
-
-    uint64_t GetSliceRevision(size_t index) const
-    {
-      CheckSliceIndex(index);
-      return slicesRevision_[index];
-    }
-
-    void SetSliceContent(size_t index,
-                         const Orthanc::ImageAccessor& image,
-                         unsigned int quality)
-    {
-      CheckSliceIndex(index);
-
-      // If a better image quality is already available, don't update the content
-      if (quality >= slicesQuality_[index])
-      {
-        {
-          ImageBuffer3D::SliceWriter writer
-            (*image_, VolumeProjection_Axial, static_cast<unsigned int>(index));
-          Orthanc::ImageProcessing::Copy(writer.GetAccessor(), image);
-        }
-        
-        revision_ ++;
-        slicesRevision_[index] += 1;
-      }
-    }
-  };
-
-
-
-  class OrthancSeriesVolumeProgressiveLoader : 
-    public IObserver
-  {
-  private:
-    static const unsigned int LOW_QUALITY = 0;
-    static const unsigned int MIDDLE_QUALITY = 1;
-    static const unsigned int BEST_QUALITY = 2;
     
     
     static unsigned int GetSliceIndexPayload(const OracleCommandWithPayload& command)
@@ -561,7 +711,7 @@
       {
         assert(quality <= BEST_QUALITY);
 
-        const DicomInstanceParameters& slice = volume_.GetSliceParameters(sliceIndex);
+        const DicomInstanceParameters& slice = seriesGeometry_.GetSliceParameters(sliceIndex);
           
         const std::string& instance = slice.GetOrthancInstanceIdentifier();
         if (instance.empty())
@@ -623,29 +773,66 @@
           slices.AddSlice(geometry, instance.release());
         }
 
-        volume_.SetGeometry(slices);
+        seriesGeometry_.ComputeGeometry(slices);
       }
 
-      if (volume_.GetSlicesCount() != 0)
+      size_t slicesCount = seriesGeometry_.GetImageGeometry().GetDepth();
+
+      if (slicesCount == 0)
+      {
+        volume_->Initialize(seriesGeometry_.GetImageGeometry(), Orthanc::PixelFormat_Grayscale8);
+      }
+      else
       {
-        strategy_.reset(new BasicFetchingStrategy(sorter_->CreateSorter(
-          static_cast<unsigned int>(volume_.GetSlicesCount())), BEST_QUALITY));
+        const DicomInstanceParameters& parameters = seriesGeometry_.GetSliceParameters(0);
+        
+        volume_->Initialize(seriesGeometry_.GetImageGeometry(), parameters.GetExpectedPixelFormat());
+        volume_->SetDicomParameters(parameters);
+        volume_->GetPixelData().Clear();
 
+        strategy_.reset(new BasicFetchingStrategy(sorter_->CreateSorter(slicesCount), BEST_QUALITY));
+        
         assert(simultaneousDownloads_ != 0);
         for (unsigned int i = 0; i < simultaneousDownloads_; i++)
         {
           ScheduleNextSliceDownload();
         }
       }
+
+      slicesQuality_.resize(slicesCount, 0);
+
+      BroadcastMessage(DicomVolumeImage::GeometryReadyMessage(*volume_));
+    }
+
+
+    void SetSliceContent(unsigned int sliceIndex,
+                         const Orthanc::ImageAccessor& image,
+                         unsigned int quality)
+    {
+      assert(sliceIndex < slicesQuality_.size() &&
+             slicesQuality_.size() == volume_->GetPixelData().GetDepth());
+      
+      if (quality >= slicesQuality_[sliceIndex])
+      {
+        {
+          ImageBuffer3D::SliceWriter writer(volume_->GetPixelData(), VolumeProjection_Axial, sliceIndex);
+          Orthanc::ImageProcessing::Copy(writer.GetAccessor(), image);
+        }
+
+        volume_->IncrementRevision();
+        seriesGeometry_.IncrementSliceRevision(sliceIndex);
+        slicesQuality_[sliceIndex] = quality;
+
+        BroadcastMessage(DicomVolumeImage::ContentUpdatedMessage(*volume_));
+      }
+
+      ScheduleNextSliceDownload();
     }
 
 
     void LoadBestQualitySliceContent(const GetOrthancImageCommand::SuccessMessage& message)
-    {      
-      volume_.SetSliceContent(GetSliceIndexPayload(message.GetOrigin()),
-                              message.GetImage(), BEST_QUALITY);
-
-      ScheduleNextSliceDownload();
+    {
+      SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), message.GetImage(), BEST_QUALITY);
     }
 
 
@@ -667,81 +854,31 @@
           throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
       }
       
-      volume_.SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), message.GetImage(), quality);
-
-      ScheduleNextSliceDownload();
+      SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), message.GetImage(), quality);
     }
 
 
-    IOracle&                oracle_;
-    bool                    active_;
-    DicomSeriesVolumeImage  volume_;
-    unsigned int            simultaneousDownloads_;
-
+    IOracle&        oracle_;
+    bool            active_;
+    unsigned int    simultaneousDownloads_;
+    SeriesGeometry  seriesGeometry_;
+    
+    boost::shared_ptr<DicomVolumeImage>      volume_;
     std::auto_ptr<IFetchingItemsSorter::IFactory>  sorter_;
     std::auto_ptr<IFetchingStrategy>               strategy_;
+    std::vector<unsigned int>   slicesQuality_;
 
 
-    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,
+    OrthancSeriesVolumeProgressiveLoader(const boost::shared_ptr<DicomVolumeImage>& volume,
+                                         IOracle& oracle,
                                          IObservable& oracleObservable) :
       IObserver(oracleObservable.GetBroker()),
+      IObservable(oracleObservable.GetBroker()),
       oracle_(oracle),
       active_(false),
       simultaneousDownloads_(4),
+      volume_(volume),
       sorter_(new BasicFetchingItemsSorter::Factory)
     {
       oracleObservable.RegisterObserverCallback(
@@ -789,17 +926,36 @@
         oracle_.Schedule(*this, command.release());
       }
     }
-    
+
+
+    virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane)
+    {
+      if (volume_->HasGeometry())
+      {
+        std::auto_ptr<Slice> slice(new Slice(*this, cuttingPlane));
 
-    const DicomSeriesVolumeImage& GetVolume() const
-    {
-      return volume_;
+        if (strategy_.get() != NULL &&
+            slice->IsValid() &&
+            slice->GetProjection() == VolumeProjection_Axial)
+        {
+          strategy_->SetCurrent(slice->GetSliceIndex());
+        }
+
+        return slice.release();
+      }
+      else
+      {
+        return new IVolumeSlicer::InvalidSlice;
+      }
     }
   };
 
 
 
-  class OrthancMultiframeVolumeLoader : public IObserver
+  class OrthancMultiframeVolumeLoader :
+    public IObserver,
+    public IObservable,
+    public IVolumeSlicer
   {
   private:
     class State : public Orthanc::IDynamicObject
@@ -949,15 +1105,11 @@
    
     
 
+    boost::shared_ptr<DicomVolumeImage>   volume_;
     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
@@ -976,7 +1128,7 @@
     void ScheduleFrameDownloads()
     {
       if (transferSyntaxUid_.empty() ||
-          !HasGeometry())
+          !volume_->HasGeometry())
       {
         return;
       }
@@ -1010,19 +1162,20 @@
 
     void SetGeometry(const Orthanc::DicomMap& dicom)
     {
-      dicom_.reset(new DicomInstanceParameters(dicom));
-
+      DicomInstanceParameters parameters(dicom);
+      volume_->SetDicomParameters(parameters);
+      
       Orthanc::PixelFormat format;
-      if (!dicom_->GetImageInformation().ExtractPixelFormat(format, true))
+      if (!parameters.GetImageInformation().ExtractPixelFormat(format, true))
       {
         throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
       }
 
       double spacingZ;
-      switch (dicom_->GetSopClassUid())
+      switch (parameters.GetSopClassUid())
       {
         case SopClassUid_RTDose:
-          spacingZ = dicom_->GetThickness();
+          spacingZ = parameters.GetThickness();
           break;
 
         default:
@@ -1031,22 +1184,24 @@
             "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();
+      const unsigned int width = parameters.GetImageInformation().GetWidth();
+      const unsigned int height = parameters.GetImageInformation().GetHeight();
+      const unsigned int depth = parameters.GetImageInformation().GetNumberOfFrames();
 
-      geometry_.reset(new VolumeImageGeometry);
-      geometry_->SetSize(width, height, depth);
-      geometry_->SetAxialGeometry(dicom_->GetGeometry());
-      geometry_->SetVoxelDimensions(dicom_->GetPixelSpacingX(),
-                                    dicom_->GetPixelSpacingY(),
-                                    spacingZ);
+      {
+        VolumeImageGeometry geometry;
+        geometry.SetSize(width, height, depth);
+        geometry.SetAxialGeometry(parameters.GetGeometry());
+        geometry.SetVoxelDimensions(parameters.GetPixelSpacingX(),
+                                    parameters.GetPixelSpacingY(), spacingZ);
+        volume_->Initialize(geometry, format);
+      }
 
-      image_.reset(new ImageBuffer3D(format, width, height, depth,
-                                     false /* don't compute range */));
-      image_->Clear();
+      volume_->GetPixelData().Clear();
 
       ScheduleFrameDownloads();
+
+      BroadcastMessage(DicomVolumeImage::GeometryReadyMessage(*volume_));
     }
 
 
@@ -1062,11 +1217,13 @@
     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();
+      ImageBuffer3D& target = volume_->GetPixelData();
+      
+      const Orthanc::PixelFormat format = target.GetFormat();
+      const unsigned int bpp = target.GetBytesPerPixel();
+      const unsigned int width = target.GetWidth();
+      const unsigned int height = target.GetHeight();
+      const unsigned int depth = target.GetDepth();
 
       if (pixelData.size() != bpp * width * height * depth)
       {
@@ -1083,7 +1240,7 @@
 
       for (unsigned int z = 0; z < depth; z++)
       {
-        ImageBuffer3D::SliceWriter writer(*image_, VolumeProjection_Axial, z);
+        ImageBuffer3D::SliceWriter writer(target, VolumeProjection_Axial, z);
 
         assert (writer.GetAccessor().GetWidth() == width &&
                 writer.GetAccessor().GetHeight() == height);
@@ -1108,7 +1265,7 @@
 
     void SetUncompressedPixelData(const std::string& pixelData)
     {
-      switch (image_->GetFormat())
+      switch (volume_->GetPixelData().GetFormat())
       {
         case Orthanc::PixelFormat_Grayscale32:
           CopyPixelData<uint32_t>(pixelData);
@@ -1118,135 +1275,33 @@
           throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
       }
 
-      revision_ ++;
+      volume_->IncrementRevision();
+
+      BroadcastMessage(DicomVolumeImage::ContentUpdatedMessage(*volume_));
     }
 
 
-  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,
+    OrthancMultiframeVolumeLoader(const boost::shared_ptr<DicomVolumeImage>& volume,
+                                  IOracle& oracle,
                                   IObservable& oracleObservable) :
       IObserver(oracleObservable.GetBroker()),
+      IObservable(oracleObservable.GetBroker()),
+      volume_(volume),
       oracle_(oracle),
-      active_(false),
-      revision_(0)
+      active_(false)
     {
+      if (volume.get() == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      }
+      
       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_)
@@ -1274,6 +1329,136 @@
         }
       }
     }
+
+
+    virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane)
+    {
+      if (volume_->HasGeometry())
+      {
+        return new DicomVolumeImageOrthogonalSlice(*volume_, cuttingPlane);
+      }
+      else
+      {
+        return new IVolumeSlicer::InvalidSlice;
+      }
+    }
+  };
+
+
+
+  class VolumeImageReslicer : public IVolumeSlicer
+  {
+  private:
+    class Slice : public IExtractedSlice
+    {
+    private:
+      VolumeImageReslicer&  that_;
+      CoordinateSystem3D    cuttingPlane_;
+      
+    public:
+      Slice(VolumeImageReslicer& that,
+            const CoordinateSystem3D& cuttingPlane) :
+        that_(that),
+        cuttingPlane_(cuttingPlane)
+      {
+      }
+      
+      virtual bool IsValid()
+      {
+        return true;
+      }
+
+      virtual uint64_t GetRevision()
+      {
+        return that_.volume_->GetRevision();
+      }
+
+      virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator,  // possibly absent
+                                            const CoordinateSystem3D& cuttingPlane)
+      {
+        VolumeReslicer& reslicer = that_.reslicer_;
+        
+        if (configurator == NULL)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
+                                          "Must provide a layer style configurator");
+        }
+        
+        reslicer.SetOutputFormat(that_.volume_->GetPixelData().GetFormat());
+        reslicer.Apply(that_.volume_->GetPixelData(),
+                       that_.volume_->GetGeometry(),
+                       cuttingPlane);
+
+        if (reslicer.IsSuccess())
+        {
+          std::auto_ptr<TextureBaseSceneLayer> layer
+            (configurator->CreateTextureFromDicom(reslicer.GetOutputSlice(),
+                                                  that_.volume_->GetDicomParameters()));
+          if (layer.get() == NULL)
+          {
+            return NULL;
+          }
+
+          double s = reslicer.GetPixelSpacing();
+          layer->SetPixelSpacing(s, s);
+          layer->SetOrigin(reslicer.GetOutputExtent().GetX1() + 0.5 * s,
+                           reslicer.GetOutputExtent().GetY1() + 0.5 * s);
+
+          // TODO - Angle!!
+                           
+          return layer.release();
+        }
+        else
+        {
+          return NULL;
+        }          
+      }
+    };
+    
+    boost::shared_ptr<DicomVolumeImage>  volume_;
+    VolumeReslicer                       reslicer_;
+
+  public:
+    VolumeImageReslicer(const boost::shared_ptr<DicomVolumeImage>& volume) :
+      volume_(volume)
+    {
+      if (volume.get() == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      }
+    }
+
+    ImageInterpolation GetInterpolation() const
+    {
+      return reslicer_.GetInterpolation();
+    }
+
+    void SetInterpolation(ImageInterpolation interpolation)
+    {
+      reslicer_.SetInterpolation(interpolation);
+    }
+
+    bool IsFastMode() const
+    {
+      return reslicer_.IsFastMode();
+    }
+
+    void SetFastMode(bool fast)
+    {
+      reslicer_.EnableFastMode(fast);
+    }
+    
+    virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane)
+    {
+      if (volume_->HasGeometry())
+      {
+        return new Slice(*this, cuttingPlane);
+      }
+      else
+      {
+        return new IVolumeSlicer::InvalidSlice;
+      }
+    }
   };
 
 
@@ -1281,26 +1466,36 @@
   class VolumeSceneLayerSource : public boost::noncopyable
   {
   private:
-    int                                layerDepth_;
-    boost::shared_ptr<IVolumeSlicer>   slicer_;
-    bool                               linearInterpolation_;
-    std::auto_ptr<CoordinateSystem3D>  lastPlane_;
-    uint64_t                           lastRevision_;
+    Scene2D&                                scene_;
+    int                                     layerDepth_;
+    boost::shared_ptr<IVolumeSlicer>        slicer_;
+    std::auto_ptr<ILayerStyleConfigurator>  configurator_;
+    std::auto_ptr<CoordinateSystem3D>       lastPlane_;
+    uint64_t                                lastRevision_;
+    uint64_t                                lastConfiguratorRevision_;
 
     static bool IsSameCuttingPlane(const CoordinateSystem3D& a,
                                    const CoordinateSystem3D& b)
     {
+      // TODO - What if the normal is reversed?
       double distance;
       return (CoordinateSystem3D::ComputeDistance(distance, a, b) &&
               LinearAlgebra::IsCloseToZero(distance));
     }
 
+    void ClearLayer()
+    {
+      scene_.DeleteLayer(layerDepth_);
+      lastPlane_.reset(NULL);
+    }
+
   public:
-    VolumeSceneLayerSource(int layerDepth,
-                           IVolumeSlicer* slicer) :   // Takes ownership
+    VolumeSceneLayerSource(Scene2D& scene,
+                           int layerDepth,
+                           const boost::shared_ptr<IVolumeSlicer>& slicer) :
+      scene_(scene),
       layerDepth_(layerDepth),
-      slicer_(slicer),
-      linearInterpolation_(false)
+      slicer_(slicer)
     {
       if (slicer == NULL)
       {
@@ -1313,21 +1508,44 @@
       return *slicer_;
     }
 
-    void SetLinearInterpolation(bool enabled)
+    void RemoveConfigurator()
+    {
+      configurator_.reset();
+      lastPlane_.reset();
+    }
+
+    void SetConfigurator(ILayerStyleConfigurator* configurator)  // Takes ownership
     {
-      linearInterpolation_ = enabled;
+      if (configurator == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      }
+
+      configurator_.reset(configurator);
+
+      // Invalidate the layer
+      lastPlane_.reset(NULL);
     }
 
-    bool IsLinearInterpolation() const
+    bool HasConfigurator() const
     {
-      return linearInterpolation_;
+      return configurator_.get() != NULL;
     }
 
-    void Update(Scene2D& scene,
-                const CoordinateSystem3D& plane)
+    ILayerStyleConfigurator& GetConfigurator() const
+    {
+      if (configurator_.get() == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      
+      return *configurator_;
+    }
+
+    void Update(const CoordinateSystem3D& plane)
     {
       assert(slicer_.get() != NULL);
-      std::auto_ptr<IVolumeSlicer::ExtractedSlice> slice(slicer_->ExtractSlice(plane));
+      std::auto_ptr<IVolumeSlicer::IExtractedSlice> slice(slicer_->ExtractSlice(plane));
 
       if (slice.get() == NULL)
       {
@@ -1337,14 +1555,21 @@
       if (!slice->IsValid())
       {
         // The slicer cannot handle this cutting plane: Clear the layer
-        scene.DeleteLayer(layerDepth_);
-        lastPlane_.reset(NULL);
+        ClearLayer();
       }
       else if (lastPlane_.get() != NULL &&
                IsSameCuttingPlane(*lastPlane_, plane) &&
                lastRevision_ == slice->GetRevision())
       {
-        // The content of the slice has not changed: Do nothing
+        // The content of the slice has not changed: Don't update the
+        // layer content, but possibly update its style
+
+        if (configurator_.get() != NULL &&
+            configurator_->GetRevision() != lastConfiguratorRevision_ &&
+            scene_.HasLayer(layerDepth_))
+        {
+          configurator_->ApplyStyle(scene_.GetLayer(layerDepth_));
+        }
       }
       else
       {
@@ -1352,19 +1577,21 @@
         lastPlane_.reset(new CoordinateSystem3D(plane));
         lastRevision_ = slice->GetRevision();
 
-        std::auto_ptr<ISceneLayer> layer(slice->CreateSceneLayer(plane));
+        std::auto_ptr<ISceneLayer> layer(slice->CreateSceneLayer(configurator_.get(), plane));
         if (layer.get() == NULL)
         {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);        
+          ClearLayer();
         }
+        else
+        {
+          if (configurator_.get() != NULL)
+          {
+            lastConfiguratorRevision_ = configurator_->GetRevision();
+            configurator_->ApplyStyle(*layer);
+          }
 
-        if (layer->GetType() == ISceneLayer::Type_ColorTexture ||
-            layer->GetType() == ISceneLayer::Type_FloatTexture)
-        {
-          dynamic_cast<TextureBaseSceneLayer&>(*layer).SetLinearInterpolation(linearInterpolation_);
+          scene_.SetLayer(layerDepth_, layer.release());
         }
-        
-        scene.SetLayer(layerDepth_, layer.release());
       }
     }
   };
@@ -1410,8 +1637,8 @@
 
     public:
       ReaderLock(NativeApplicationContext& that) : 
-      that_(that),
-      lock_(that.mutex_)
+        that_(that),
+        lock_(that.mutex_)
       {
       }
     };
@@ -1425,8 +1652,8 @@
 
     public:
       WriterLock(NativeApplicationContext& that) : 
-      that_(that),
-      lock_(that.mutex_)
+        that_(that),
+        lock_(that.mutex_)
       {
       }
 
@@ -1448,31 +1675,59 @@
 class Toto : public OrthancStone::IObserver
 {
 private:
-  OrthancStone::IOracle& oracle_;
+  OrthancStone::CoordinateSystem3D  plane_;
+  OrthancStone::IOracle&            oracle_;
   OrthancStone::Scene2D             scene_;
   std::auto_ptr<OrthancStone::VolumeSceneLayerSource>  source1_, source2_;
 
-  
-  OrthancStone::CoordinateSystem3D GetSamplePlane
-  (const OrthancStone::VolumeSceneLayerSource& source) const
+
+  void Refresh()
   {
-    const OrthancStone::IVolumeImageSlicer& slicer =
-      dynamic_cast<const OrthancStone::IVolumeImageSlicer&>(source.GetSlicer());
-        
-    OrthancStone::CoordinateSystem3D plane;
-
-    if (slicer.HasGeometry())
+    if (source1_.get() != NULL)
     {
-      //plane = slicer.GetGeometry().GetSagittalGeometry();
-      //plane = slicer.GetGeometry().GetAxialGeometry();
-      plane = slicer.GetGeometry().GetCoronalGeometry();
-      plane.SetOrigin(slicer.GetGeometry().GetCoordinates(0.5f, 0.5f, 0.5f));
+      source1_->Update(plane_);
+    }
+      
+    if (source2_.get() != NULL)
+    {
+      source2_->Update(plane_);
     }
 
-    return plane;
+    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);
+        
+      static unsigned int count = 0;
+      char buf[64];
+      sprintf(buf, "scene-%06d.png", count++);
+        
+      Orthanc::PngWriter writer;
+      writer.WriteToFile(buf, tmp);
+    }
+  }
+
+  
+  void Handle(const OrthancStone::DicomVolumeImage::GeometryReadyMessage& message)
+  {
+    printf("Geometry ready\n");
+    
+    //plane_ = message.GetOrigin().GetGeometry().GetSagittalGeometry();
+    //plane_ = message.GetOrigin().GetGeometry().GetAxialGeometry();
+    plane_ = message.GetOrigin().GetGeometry().GetCoronalGeometry();
+    plane_.SetOrigin(message.GetOrigin().GetGeometry().GetCoordinates(0.5f, 0.5f, 0.5f));
+
+    Refresh();
   }
   
-
+  
   void Handle(const OrthancStone::SleepOracleCommand::TimeoutMessage& message)
   {
     if (message.GetOrigin().HasPayload())
@@ -1483,46 +1738,7 @@
     {
       printf("TIMEOUT\n");
 
-      OrthancStone::CoordinateSystem3D plane;
-
-      if (source1_.get() != NULL)
-      {
-        plane = GetSamplePlane(*source1_);
-      }
-      else if (source2_.get() != NULL)
-      {
-        plane = GetSamplePlane(*source2_);
-      }
-
-      if (source1_.get() != NULL)
-      {
-        source1_->Update(scene_, plane);
-      }
-      
-      if (source2_.get() != NULL)
-      {
-        source2_->Update(scene_, plane);
-      }
-
-      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);
-        
-        static unsigned int count = 0;
-        char buf[64];
-        sprintf(buf, "scene-%06d.png", count++);
-        
-        Orthanc::PngWriter writer;
-        writer.WriteToFile(buf, tmp);
-      }
+      Refresh();
 
       /**
        * The sleep() leads to a crash if the oracle is still running,
@@ -1596,16 +1812,35 @@
        <Toto, OrthancStone::OracleCommandExceptionMessage>(*this, &Toto::Handle));
   }
 
+  void SetReferenceLoader(OrthancStone::IObservable& loader)
+  {
+    loader.RegisterObserverCallback
+      (new OrthancStone::Callable
+       <Toto, OrthancStone::DicomVolumeImage::GeometryReadyMessage>(*this, &Toto::Handle));
+  }
+
   void SetVolume1(int depth,
-                  OrthancStone::IVolumeSlicer* volume)
+                  const boost::shared_ptr<OrthancStone::IVolumeSlicer>& volume,
+                  OrthancStone::ILayerStyleConfigurator* style)
   {
-    source1_.reset(new OrthancStone::VolumeSceneLayerSource(depth, volume));
+    source1_.reset(new OrthancStone::VolumeSceneLayerSource(scene_, depth, volume));
+
+    if (style != NULL)
+    {
+      source1_->SetConfigurator(style);
+    }
   }
 
   void SetVolume2(int depth,
-                  OrthancStone::IVolumeSlicer* volume)
+                  const boost::shared_ptr<OrthancStone::IVolumeSlicer>& volume,
+                  OrthancStone::ILayerStyleConfigurator* style)
   {
-    source2_.reset(new OrthancStone::VolumeSceneLayerSource(depth, volume));
+    source2_.reset(new OrthancStone::VolumeSceneLayerSource(scene_, depth, volume));
+
+    if (style != NULL)
+    {
+      source2_->SetConfigurator(style);
+    }
   }
 };
 
@@ -1613,16 +1848,39 @@
 void Run(OrthancStone::NativeApplicationContext& context,
          OrthancStone::ThreadedOracle& oracle)
 {
+  boost::shared_ptr<OrthancStone::DicomVolumeImage>  ct(new OrthancStone::DicomVolumeImage);
+  boost::shared_ptr<OrthancStone::DicomVolumeImage>  dose(new OrthancStone::DicomVolumeImage);
+
+  
   boost::shared_ptr<Toto> toto;
-  boost::shared_ptr<OrthancStone::OrthancSeriesVolumeProgressiveLoader> loader1, loader2;
-  boost::shared_ptr<OrthancStone::OrthancMultiframeVolumeLoader> loader3;
+  boost::shared_ptr<OrthancStone::OrthancSeriesVolumeProgressiveLoader> ctLoader;
+  boost::shared_ptr<OrthancStone::OrthancMultiframeVolumeLoader> doseLoader;
 
   {
     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()));
+    ctLoader.reset(new OrthancStone::OrthancSeriesVolumeProgressiveLoader(ct, oracle, lock.GetOracleObservable()));
+    doseLoader.reset(new OrthancStone::OrthancMultiframeVolumeLoader(dose, oracle, lock.GetOracleObservable()));
+  }
+
+
+  toto->SetReferenceLoader(*ctLoader);
+
+
+#if 1
+  toto->SetVolume1(0, ctLoader, new OrthancStone::GrayscaleStyleConfigurator);
+#else
+  {
+    boost::shared_ptr<OrthancStone::IVolumeSlicer> reslicer(new OrthancStone::VolumeImageReslicer(ct));
+    toto->SetVolume1(0, reslicer, new OrthancStone::GrayscaleStyleConfigurator);
+  }
+#endif  
+  
+
+  {
+    std::auto_ptr<OrthancStone::LookupTableStyleConfigurator> config(new OrthancStone::LookupTableStyleConfigurator);
+    config->SetLookupTable(Orthanc::EmbeddedResources::COLORMAP_HOT);
+    toto->SetVolume2(1, doseLoader, config.release());
   }
 
   oracle.Schedule(*toto, new OrthancStone::SleepOracleCommand(100));
@@ -1703,24 +1961,22 @@
   }
 
   // 2017-11-17-Anonymized
-  loader1->LoadSeries("cb3ea4d1-d08f3856-ad7b6314-74d88d77-60b05618");  // CT
-  loader3->LoadInstance("41029085-71718346-811efac4-420e2c15-d39f99b6");  // RT-DOSE
+  ctLoader->LoadSeries("cb3ea4d1-d08f3856-ad7b6314-74d88d77-60b05618");  // CT
+  doseLoader->LoadInstance("41029085-71718346-811efac4-420e2c15-d39f99b6");  // RT-DOSE
 
   // 2015-01-28-Multiframe
-  //loader3->LoadInstance("88f71e2a-5fad1c61-96ed14d6-5b3d3cf7-a5825279");  // Multiframe CT
+  //doseLoader->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
+  //ctLoader->LoadSeries("5990e39c-51e5f201-fe87a54c-31a55943-e59ef80e");  // CT
+  //ctLoader->LoadSeries("67f1b334-02c16752-45026e40-a5b60b6b-030ecab5");  // Lung 1/10mm
 
 
-  toto->SetVolume2(1, new OrthancStone::OrthancMultiframeVolumeLoader::MPRSlicer(loader3));
-  toto->SetVolume1(0, new OrthancStone::OrthancSeriesVolumeProgressiveLoader::MPRSlicer(loader1));
+  {
+    LOG(WARNING) << "...Waiting for Ctrl-C...";
 
-  {
     oracle.Start();
 
-    LOG(WARNING) << "...Waiting for Ctrl-C...";
     Orthanc::SystemToolbox::ServerBarrier();
 
     /**
@@ -1746,7 +2002,7 @@
 int main(int argc, char* argv[])
 {
   OrthancStone::StoneInitialize();
-  Orthanc::Logging::EnableInfoLevel(true);
+  //Orthanc::Logging::EnableInfoLevel(true);
 
   try
   {