changeset 119:ba83e38cf3ff wasm

rendering of rt-dose
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 02 Oct 2017 22:01:41 +0200
parents a4d0b6c82b29
children 063f7f3d9f14
files Applications/Samples/SingleVolumeApplication.h Framework/Layers/SliceOutlineRenderer.h Framework/Toolbox/DicomFrameConverter.cpp Framework/Toolbox/DicomFrameConverter.h Framework/Toolbox/MessagingToolbox.cpp Framework/Toolbox/OrthancSlicesLoader.cpp Framework/Toolbox/Slice.cpp Framework/Toolbox/Slice.h Framework/Toolbox/SlicesSorter.cpp Framework/Toolbox/SlicesSorter.h Framework/Volumes/ImageBuffer3D.cpp Framework/Volumes/ImageBuffer3D.h Framework/Widgets/LayerWidget.cpp Framework/Widgets/LayerWidget.h Framework/dev.h
diffstat 15 files changed, 449 insertions(+), 111 deletions(-) [+]
line wrap: on
line diff
--- a/Applications/Samples/SingleVolumeApplication.h	Mon Oct 02 14:31:26 2017 +0200
+++ b/Applications/Samples/SingleVolumeApplication.h	Mon Oct 02 22:01:41 2017 +0200
@@ -23,8 +23,7 @@
 
 #include "SampleApplicationBase.h"
 #include "../../Framework/dev.h"
-//#include "SampleInteractor.h"
-#include "../../Framework/Widgets/LayerWidget.h"
+#include "../../Framework/Layers/ILayerSource.h"
 #include "../../Framework/Layers/LineMeasureTracker.h"
 #include "../../Framework/Layers/CircleMeasureTracker.h"
 
@@ -35,9 +34,44 @@
 {
   namespace Samples
   {
-    class SingleVolumeApplication :
-      public SampleApplicationBase
+    class SingleVolumeApplication : public SampleApplicationBase
     {
+    private:
+      class Interactor : public VolumeImageInteractor
+      {
+      private:
+        LayerWidget&  widget_;
+        size_t        layer_;
+        
+      protected:
+        virtual void NotifySliceChange(const ISlicedVolume& volume,
+                                       const size_t& sliceIndex,
+                                       const Slice& slice)
+        {
+          const OrthancVolumeImage& image = dynamic_cast<const OrthancVolumeImage&>(volume);
+
+          RenderStyle s = widget_.GetLayerStyle(layer_);
+
+          if (image.FitWindowingToRange(s, slice.GetConverter()))
+          {
+            //printf("ICI: %f => %f\n", s.customWindowCenter_, s.customWindowWidth_);
+            widget_.SetLayerStyle(layer_, s);
+          }
+        }
+      
+      public:
+        Interactor(OrthancVolumeImage& volume,
+                   LayerWidget& widget,
+                   VolumeProjection projection,
+                   size_t layer) :
+          VolumeImageInteractor(volume, widget, projection),
+          widget_(widget),
+          layer_(layer)
+        {
+          printf("OOO\n");
+        }
+      };
+
     public:
       virtual void DeclareCommandLineOptions(boost::program_options::options_description& options)
       {
@@ -124,8 +158,8 @@
 
         std::auto_ptr<LayerWidget> widget(new LayerWidget);
 
-#if 1
-        std::auto_ptr<OrthancVolumeImage> volume(new OrthancVolumeImage(context.GetWebService()));
+#if 0
+        std::auto_ptr<OrthancVolumeImage> volume(new OrthancVolumeImage(context.GetWebService(), true));
         if (series.empty())
         {
           volume->ScheduleLoadInstance(instance);
@@ -137,19 +171,33 @@
 
         widget->AddLayer(new VolumeImageSource(*volume));
 
-        context.AddInteractor(new VolumeImageInteractor(*volume, *widget, projection));
+        context.AddInteractor(new Interactor(*volume, *widget, projection, 0));
         context.AddVolume(volume.release());
+
+        {
+          RenderStyle s;
+          s.alpha_ = 1;
+          s.applyLut_ = true;
+          s.lut_ = Orthanc::EmbeddedResources::COLORMAP_JET;
+          s.interpolation_ = ImageInterpolation_Linear;
+          widget->SetLayerStyle(0, s);
+        }
 #else
-        std::auto_ptr<OrthancVolumeImage> ct(new OrthancVolumeImage(context.GetWebService()));
-        ct->ScheduleLoadSeries("dd069910-4f090474-7d2bba07-e5c10783-f9e4fb1d");
+        std::auto_ptr<OrthancVolumeImage> ct(new OrthancVolumeImage(context.GetWebService(), false));
+        //ct->ScheduleLoadSeries("dd069910-4f090474-7d2bba07-e5c10783-f9e4fb1d");
+        //ct->ScheduleLoadSeries("3025d8df-a82f3b00-83942fa3-ee6a6be3-a8bf32e8");
+        ct->ScheduleLoadSeries("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa");
 
-        std::auto_ptr<OrthancVolumeImage> pet(new OrthancVolumeImage(context.GetWebService()));
-        pet->ScheduleLoadSeries("aabad2e7-80702b5d-e599d26c-4f13398e-38d58a9e");
+        std::auto_ptr<OrthancVolumeImage> pet(new OrthancVolumeImage(context.GetWebService(), true));
+        //pet->ScheduleLoadSeries("aabad2e7-80702b5d-e599d26c-4f13398e-38d58a9e");
+        //pet->ScheduleLoadInstance("830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb");
+        //pet->ScheduleLoadInstance("337876a1-a68a9718-f15abccd-38faafa1-b99b496a");
+        pet->ScheduleLoadInstance("830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb");
 
         widget->AddLayer(new VolumeImageSource(*ct));
         widget->AddLayer(new VolumeImageSource(*pet));
         
-        context.AddInteractor(new VolumeImageInteractor(*pet, *widget, projection));
+        context.AddInteractor(new Interactor(*pet, *widget, projection, 1));
         context.AddVolume(ct.release());
         context.AddVolume(pet.release());
 
@@ -165,7 +213,7 @@
           RenderStyle s;
           //s.drawGrid_ = true;
           s.SetColor(255, 0, 0);  // Draw missing PET layer in red
-          s.alpha_ = 0.3;
+          s.alpha_ = 0.5;
           s.applyLut_ = true;
           s.lut_ = Orthanc::EmbeddedResources::COLORMAP_JET;
           s.interpolation_ = ImageInterpolation_Linear;
--- a/Framework/Layers/SliceOutlineRenderer.h	Mon Oct 02 14:31:26 2017 +0200
+++ b/Framework/Layers/SliceOutlineRenderer.h	Mon Oct 02 22:01:41 2017 +0200
@@ -29,12 +29,20 @@
   class SliceOutlineRenderer : public ILayerRenderer
   {
   private:
-    Slice        slice_;
-    RenderStyle  style_;
+    CoordinateSystem3D  geometry_;
+    double              pixelSpacingX_;
+    double              pixelSpacingY_;
+    unsigned int        width_;
+    unsigned int        height_;
+    RenderStyle         style_;
 
   public:
-    SliceOutlineRenderer(const Slice& slice) : 
-      slice_(slice)
+    SliceOutlineRenderer(const Slice& slice) :
+      geometry_(slice.GetGeometry()),
+      pixelSpacingX_(slice.GetPixelSpacingX()),
+      pixelSpacingY_(slice.GetPixelSpacingY()),
+      width_(slice.GetWidth()),
+      height_(slice.GetHeight())
     {
     }
 
@@ -48,7 +56,7 @@
 
     virtual const CoordinateSystem3D& GetLayerSlice()
     {
-      return slice_.GetGeometry();
+      return geometry_;
     }
 
     virtual bool IsFullQuality()
--- a/Framework/Toolbox/DicomFrameConverter.cpp	Mon Oct 02 14:31:26 2017 +0200
+++ b/Framework/Toolbox/DicomFrameConverter.cpp	Mon Oct 02 22:01:41 2017 +0200
@@ -39,26 +39,7 @@
     rescaleSlope_ = 1;
     defaultWindowCenter_ = 128;
     defaultWindowWidth_ = 256;
-  }
-
-
-  Orthanc::PixelFormat DicomFrameConverter::GetExpectedPixelFormat() const
-  {
-    // TODO Add more checks, e.g. on the number of bytes per value
-    // (cf. DicomImageInformation.h in Orthanc)
-
-    if (isColor_)
-    {
-      return Orthanc::PixelFormat_RGB24;
-    }
-    else if (isSigned_)
-    {
-      return Orthanc::PixelFormat_SignedGrayscale16;
-    }
-    else
-    {
-      return Orthanc::PixelFormat_Grayscale16;
-    }
+    expectedPixelFormat_ = Orthanc::PixelFormat_Grayscale16;
   }
 
 
@@ -85,11 +66,22 @@
 
     isSigned_ = (tmp == 1);
 
-    if (dicom.ParseFloat(rescaleIntercept_, Orthanc::DICOM_TAG_RESCALE_INTERCEPT) &&
-        dicom.ParseFloat(rescaleSlope_, Orthanc::DICOM_TAG_RESCALE_SLOPE))
+    double doseGridScaling;
+    bool isRTDose = false;
+    
+    if (dicom.ParseDouble(rescaleIntercept_, Orthanc::DICOM_TAG_RESCALE_INTERCEPT) &&
+        dicom.ParseDouble(rescaleSlope_, Orthanc::DICOM_TAG_RESCALE_SLOPE))
     {
       hasRescale_ = true;
     }
+    else if (dicom.ParseDouble(doseGridScaling, Orthanc::DICOM_TAG_DOSE_GRID_SCALING))
+    {
+      // This is for RT-DOSE
+      hasRescale_ = true;
+      isRTDose = true;
+      rescaleIntercept_ = 0;
+      rescaleSlope_ = doseGridScaling;
+    }
 
     std::string photometric;
     if (dicom.CopyToString(photometric, Orthanc::DICOM_TAG_PHOTOMETRIC_INTERPRETATION, false))
@@ -104,6 +96,26 @@
     
     isColor_ = (photometric != "MONOCHROME1" &&
                 photometric != "MONOCHROME2");
+
+    // TODO Add more checks, e.g. on the number of bytes per value
+    // (cf. DicomImageInformation.h in Orthanc)
+
+    if (isRTDose)
+    {
+      expectedPixelFormat_ = Orthanc::PixelFormat_Grayscale32;
+    }
+    else if (isColor_)
+    {
+      expectedPixelFormat_ = Orthanc::PixelFormat_RGB24;
+    }
+    else if (isSigned_)
+    {
+      expectedPixelFormat_ = Orthanc::PixelFormat_SignedGrayscale16;
+    }
+    else
+    {
+      expectedPixelFormat_ = Orthanc::PixelFormat_Grayscale16;
+    }
   }
 
 
@@ -130,6 +142,7 @@
     }
 
     assert(sourceFormat == Orthanc::PixelFormat_Grayscale16 ||
+           sourceFormat == Orthanc::PixelFormat_Grayscale32 ||
            sourceFormat == Orthanc::PixelFormat_SignedGrayscale16);
 
     // This is the case of a grayscale frame. Convert it to Float32.
@@ -142,25 +155,51 @@
     source.reset(NULL);  // We don't need the source frame anymore
 
     // Correct rescale slope/intercept if need be
+    ApplyRescale(*converted, sourceFormat != Orthanc::PixelFormat_Grayscale32);
+      
+    source = converted;
+  }
+
+
+  void DicomFrameConverter::ApplyRescale(Orthanc::ImageAccessor& image,
+                                         bool useDouble) const
+  {
+    if (image.GetFormat() != Orthanc::PixelFormat_Float32)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
+    }
+    
     if (hasRescale_)
     {
-      for (unsigned int y = 0; y < converted->GetHeight(); y++)
+      for (unsigned int y = 0; y < image.GetHeight(); y++)
       {
-        float* p = reinterpret_cast<float*>(converted->GetRow(y));
-        for (unsigned int x = 0; x < converted->GetWidth(); x++, p++)
-        {
-          float value = *p;
+        float* p = reinterpret_cast<float*>(image.GetRow(y));
 
-          if (hasRescale_)
+        if (useDouble)
+        {
+          // Slower, accurate implementation using double
+          for (unsigned int x = 0; x < image.GetWidth(); x++, p++)
           {
-            value = value * rescaleSlope_ + rescaleIntercept_;
+            double value = static_cast<double>(*p);
+            *p = static_cast<float>(value * rescaleSlope_ + rescaleIntercept_);
           }
-            
-          *p = value;
+        }
+        else
+        {
+          // Fast, approximate implementation using float
+          for (unsigned int x = 0; x < image.GetWidth(); x++, p++)
+          {
+            *p = (*p) * static_cast<float>(rescaleSlope_) + static_cast<float>(rescaleIntercept_);
+          }
         }
       }
     }
-      
-    source = converted;
   }
+
+  
+  double DicomFrameConverter::Apply(double x) const
+  {
+    return x * rescaleSlope_ + rescaleIntercept_;
+  }
+
 }
--- a/Framework/Toolbox/DicomFrameConverter.h	Mon Oct 02 14:31:26 2017 +0200
+++ b/Framework/Toolbox/DicomFrameConverter.h	Mon Oct 02 22:01:41 2017 +0200
@@ -41,10 +41,12 @@
     bool    isSigned_;
     bool    isColor_;
     bool    hasRescale_;
-    float   rescaleIntercept_;
-    float   rescaleSlope_;
-    float   defaultWindowCenter_;
-    float   defaultWindowWidth_;
+    double  rescaleIntercept_;
+    double  rescaleSlope_;
+    double  defaultWindowCenter_;
+    double  defaultWindowWidth_;
+
+    Orthanc::PixelFormat  expectedPixelFormat_;
 
     void SetDefaultParameters();
 
@@ -54,20 +56,28 @@
       SetDefaultParameters();
     }
 
-    Orthanc::PixelFormat GetExpectedPixelFormat() const;
+    Orthanc::PixelFormat GetExpectedPixelFormat() const
+    {
+      return expectedPixelFormat_;
+    }
 
     void ReadParameters(const Orthanc::DicomMap& dicom);
 
-    float GetDefaultWindowCenter() const
+    double GetDefaultWindowCenter() const
     {
       return defaultWindowCenter_;
     }
     
-    float GetDefaultWindowWidth() const
+    double GetDefaultWindowWidth() const
     {
       return defaultWindowWidth_;
     }
 
-    void ConvertFrame(std::auto_ptr<Orthanc::ImageAccessor>& source) const;  
+    void ConvertFrame(std::auto_ptr<Orthanc::ImageAccessor>& source) const;
+
+    void ApplyRescale(Orthanc::ImageAccessor& image,
+                      bool useDouble) const;
+
+    double Apply(double x) const;
   };
 }
--- a/Framework/Toolbox/MessagingToolbox.cpp	Mon Oct 02 14:31:26 2017 +0200
+++ b/Framework/Toolbox/MessagingToolbox.cpp	Mon Oct 02 22:01:41 2017 +0200
@@ -422,6 +422,7 @@
       AddTag(target, source, Orthanc::DICOM_TAG_BITS_ALLOCATED);
       AddTag(target, source, Orthanc::DICOM_TAG_BITS_STORED);
       AddTag(target, source, Orthanc::DICOM_TAG_COLUMNS);
+      AddTag(target, source, Orthanc::DICOM_TAG_DOSE_GRID_SCALING);
       AddTag(target, source, Orthanc::DICOM_TAG_FRAME_INCREMENT_POINTER);
       AddTag(target, source, Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR);
       AddTag(target, source, Orthanc::DICOM_TAG_HIGH_BIT);
--- a/Framework/Toolbox/OrthancSlicesLoader.cpp	Mon Oct 02 14:31:26 2017 +0200
+++ b/Framework/Toolbox/OrthancSlicesLoader.cpp	Mon Oct 02 22:01:41 2017 +0200
@@ -90,7 +90,8 @@
 
     SliceImageQuality GetQuality() const
     {
-      assert(mode_ == Mode_LoadImage);
+      assert(mode_ == Mode_LoadImage ||
+             mode_ == Mode_LoadRawImage);
       return quality_;
     }
 
@@ -160,6 +161,7 @@
       std::auto_ptr<Operation> tmp(new Operation(Mode_LoadRawImage));
       tmp->sliceIndex_ = sliceIndex;
       tmp->slice_ = &slice;
+      tmp->quality_ = SliceImageQuality_Full;
       return tmp.release();
     }
   };
@@ -306,10 +308,10 @@
 
       for (unsigned int frame = 0; frame < frames; frame++)
       {
-        Slice slice;
-        if (slice.ParseOrthancFrame(dicom, instances[i], frame))
+        std::auto_ptr<Slice> slice(new Slice);
+        if (slice->ParseOrthancFrame(dicom, instances[i], frame))
         {
-          slices_.AddSlice(slice);
+          slices_.AddSlice(slice.release());
         }
         else
         {
@@ -376,10 +378,10 @@
 
     for (unsigned int frame = 0; frame < frames; frame++)
     {
-      Slice slice;
-      if (slice.ParseOrthancFrame(dicom, instanceId, frame))
+      std::auto_ptr<Slice> slice(new Slice);
+      if (slice->ParseOrthancFrame(dicom, instanceId, frame))
       {
-        slices_.AddSlice(slice);
+        slices_.AddSlice(slice.release());
       }
       else
       {
@@ -413,11 +415,11 @@
     Orthanc::DicomMap dicom;
     MessagingToolbox::ConvertDataset(dicom, dataset);
 
-    Slice slice;
-    if (slice.ParseOrthancFrame(dicom, instanceId, frame))
+    std::auto_ptr<Slice> slice(new Slice);
+    if (slice->ParseOrthancFrame(dicom, instanceId, frame))
     {
       LOG(INFO) << "Loaded instance " << instanceId;
-      slices_.AddSlice(slice);
+      slices_.AddSlice(slice.release());
       userCallback_.NotifyGeometryReady(*this);
     }
     else
@@ -613,8 +615,36 @@
 
     NotifySliceImageSuccess(operation, image);
   }
+
+
+  class StringImage :
+    public Orthanc::ImageAccessor,
+    public boost::noncopyable
+  {
+  private:
+    std::string  buffer_;
     
-    
+  public:
+    StringImage(Orthanc::PixelFormat format,
+                unsigned int width,
+                unsigned int height,
+                std::string& buffer)
+    {
+      if (buffer.size() != Orthanc::GetBytesPerPixel(format) * width * height)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
+      }
+
+      buffer_.swap(buffer);  // The source buffer is now empty
+
+      void* data = (buffer_.empty() ? NULL : &buffer_[0]);
+
+      AssignWritable(format, width, height,
+                     Orthanc::GetBytesPerPixel(format) * width, data);
+    }
+  };
+
+  
   void OrthancSlicesLoader::ParseSliceRawImage(const Operation& operation,
                                                const void* answer,
                                                size_t size)
@@ -624,7 +654,39 @@
     std::string raw;
     compressor.Uncompress(raw, answer, size);
     
-    printf("[%d => %d]\n", size, raw.size());
+    const Orthanc::DicomImageInformation& info = operation.GetSlice().GetImageInformation();
+    unsigned int frame = operation.GetSlice().GetFrame();
+    
+    if (info.GetBitsAllocated() == 32 &&
+        info.GetBitsStored() == 32 &&
+        info.GetHighBit() == 31 &&
+        info.GetChannelCount() == 1 &&
+        !info.IsSigned() &&
+        info.GetPhotometricInterpretation() == Orthanc::PhotometricInterpretation_Monochrome2 &&
+        raw.size() == info.GetWidth() * info.GetHeight() * 4)
+    {
+      // This is the case of RT-DOSE (uint32_t values)
+      
+      std::auto_ptr<Orthanc::ImageAccessor> image
+        (new StringImage(Orthanc::PixelFormat_Grayscale32, info.GetWidth(),
+                         info.GetHeight(), raw));
+
+      for (unsigned int y = 0; y < image->GetHeight(); y++)
+      {
+        uint32_t *p = reinterpret_cast<uint32_t*>(image->GetRow(y));
+        for (unsigned int x = 0; x < image->GetWidth(); x++, p++)
+        {
+          *p = le32toh(*p);
+        }
+      }
+
+      NotifySliceImageSuccess(operation, image);
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+        
   }
 
 
--- a/Framework/Toolbox/Slice.cpp	Mon Oct 02 14:31:26 2017 +0200
+++ b/Framework/Toolbox/Slice.cpp	Mon Oct 02 22:01:41 2017 +0200
@@ -107,6 +107,7 @@
     orthancInstanceId_ = instanceId;
     frame_ = frame;
     type_ = Type_OrthancDecodableFrame;
+    imageInformation_.reset(new Orthanc::DicomImageInformation(dataset));
 
     if (!dataset.CopyToString(sopClassUid_, Orthanc::DICOM_TAG_SOP_CLASS_UID, false) ||
         sopClassUid_.empty())
@@ -314,4 +315,18 @@
     points.push_back(GetGeometry().MapSliceToWorldCoordinates(-0.5      * sx, (h - 0.5) * sy));
     points.push_back(GetGeometry().MapSliceToWorldCoordinates((w - 0.5) * sx, (h - 0.5) * sy));
   }
+
+
+  const Orthanc::DicomImageInformation& Slice::GetImageInformation() const
+  {
+    if (imageInformation_.get() == NULL)
+    {
+      // Only available if constructing the "Slice" object with a DICOM map
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return *imageInformation_;
+    }
+  }
 }
--- a/Framework/Toolbox/Slice.h	Mon Oct 02 14:31:26 2017 +0200
+++ b/Framework/Toolbox/Slice.h	Mon Oct 02 22:01:41 2017 +0200
@@ -24,11 +24,11 @@
 #include "CoordinateSystem3D.h"
 #include "DicomFrameConverter.h"
 
-#include <Core/DicomFormat/DicomMap.h>
+#include <Core/DicomFormat/DicomImageInformation.h>
 
 namespace OrthancStone
 {
-  class Slice
+  class Slice : public boost::noncopyable
   {
   private:
     enum Type
@@ -47,17 +47,20 @@
     std::string          orthancInstanceId_;
     std::string          sopClassUid_;
     unsigned int         frame_;
-    unsigned int         frameCount_;
+    unsigned int         frameCount_;   // TODO : Redundant with "imageInformation_"
     CoordinateSystem3D   geometry_;
     double               pixelSpacingX_;
     double               pixelSpacingY_;
     double               thickness_;
-    unsigned int         width_;
-    unsigned int         height_;
-    DicomFrameConverter  converter_;
-      
+    unsigned int         width_;   // TODO : Redundant with "imageInformation_"
+    unsigned int         height_;   // TODO : Redundant with "imageInformation_"
+    DicomFrameConverter  converter_;   // TODO : Partially redundant with "imageInformation_"
+
+    std::auto_ptr<Orthanc::DicomImageInformation>  imageInformation_;
+
   public:
-    Slice() : type_(Type_Invalid)
+    Slice() :
+      type_(Type_Invalid)
     {        
     }
 
@@ -131,5 +134,7 @@
     bool ContainsPlane(const CoordinateSystem3D& plane) const;
 
     void GetExtent(std::vector<Vector>& points) const;
+
+    const Orthanc::DicomImageInformation& GetImageInformation() const;
   };
 }
--- a/Framework/Toolbox/SlicesSorter.cpp	Mon Oct 02 14:31:26 2017 +0200
+++ b/Framework/Toolbox/SlicesSorter.cpp	Mon Oct 02 22:01:41 2017 +0200
@@ -28,20 +28,25 @@
   class SlicesSorter::SliceWithDepth : public boost::noncopyable
   {
   private:
-    Slice   slice_;
-    double  depth_;
+    std::auto_ptr<Slice>   slice_;
+    double                 depth_;
 
   public:
-    SliceWithDepth(const Slice& slice) :
+    SliceWithDepth(Slice* slice) :
       slice_(slice),
       depth_(0)
     {
+      if (slice == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      }
     }
 
     void SetNormal(const Vector& normal)
     {
+      assert(slice_.get() != NULL);
       depth_ = boost::numeric::ublas::inner_prod
-        (slice_.GetGeometry().GetOrigin(), normal);
+        (slice_->GetGeometry().GetOrigin(), normal);
     }
 
     double GetDepth() const
@@ -51,7 +56,8 @@
 
     const Slice& GetSlice() const
     {
-      return slice_;
+      assert(slice_.get() != NULL);
+      return *slice_;
     }
   };
 
@@ -76,7 +82,7 @@
   }
 
 
-  void SlicesSorter::AddSlice(const Slice& slice)
+  void SlicesSorter::AddSlice(Slice* slice)
   {
     slices_.push_back(new SliceWithDepth(slice));
   }
--- a/Framework/Toolbox/SlicesSorter.h	Mon Oct 02 14:31:26 2017 +0200
+++ b/Framework/Toolbox/SlicesSorter.h	Mon Oct 02 22:01:41 2017 +0200
@@ -48,7 +48,7 @@
       slices_.reserve(count);
     }
 
-    void AddSlice(const Slice& slice);
+    void AddSlice(Slice* slice);  // Takes ownership
 
     size_t GetSliceCount() const
     {
--- a/Framework/Volumes/ImageBuffer3D.cpp	Mon Oct 02 14:31:26 2017 +0200
+++ b/Framework/Volumes/ImageBuffer3D.cpp	Mon Oct 02 22:01:41 2017 +0200
@@ -112,12 +112,15 @@
   ImageBuffer3D::ImageBuffer3D(Orthanc::PixelFormat format,
                                unsigned int width,
                                unsigned int height,
-                               unsigned int depth) :
+                               unsigned int depth,
+                               bool computeRange) :
     image_(format, width, height * depth, false),
     format_(format),
     width_(width),
     height_(height),
-    depth_(depth)
+    depth_(depth),
+    computeRange_(computeRange),
+    hasRange_(false)
   {
     GeometryToolbox::AssignVector(voxelDimensions_, 1, 1, 1);
 
@@ -265,6 +268,91 @@
   }
 
 
+  void ImageBuffer3D::ExtendImageRange(const Orthanc::ImageAccessor& slice)
+  {
+    if (!computeRange_ ||
+        slice.GetWidth() == 0 ||
+        slice.GetHeight() == 0)
+    {
+      return;
+    }
+
+    float sliceMin, sliceMax;
+      
+    switch (slice.GetFormat())
+    {
+      case Orthanc::PixelFormat_Grayscale8:
+      case Orthanc::PixelFormat_Grayscale16:
+      case Orthanc::PixelFormat_Grayscale32:
+      case Orthanc::PixelFormat_SignedGrayscale16:
+      {
+        int64_t a, b;
+        Orthanc::ImageProcessing::GetMinMaxIntegerValue(a, b, slice);
+        sliceMin = static_cast<float>(a);
+        sliceMax = static_cast<float>(b);
+        break;
+      }
+
+      case Orthanc::PixelFormat_Float32:
+        Orthanc::ImageProcessing::GetMinMaxFloatValue(sliceMin, sliceMax, slice);
+        break;
+
+      default:
+        return;
+    }
+
+    if (hasRange_)
+    {
+      minValue_ = std::min(minValue_, sliceMin);
+      maxValue_ = std::max(maxValue_, sliceMax);
+    }
+    else
+    {
+      hasRange_ = true;
+      minValue_ = sliceMin;
+      maxValue_ = sliceMax;
+    }
+  }
+
+
+  bool ImageBuffer3D::GetRange(float& minValue,
+                               float& maxValue) const
+  {
+    if (hasRange_)
+    {
+      minValue = minValue_;
+      maxValue = maxValue_;
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  bool ImageBuffer3D::FitWindowingToRange(RenderStyle& style,
+                                          const DicomFrameConverter& converter) const
+  {
+    if (hasRange_)
+    {
+      style.windowing_ = ImageWindowing_Custom;
+      style.customWindowCenter_ = converter.Apply((minValue_ + maxValue_) / 2.0);
+      style.customWindowWidth_ = converter.Apply(maxValue_ - minValue_);
+      
+      if (style.customWindowWidth_ > 1)
+      {
+        return true;
+      }
+    }
+
+    style.windowing_ = ImageWindowing_Custom;
+    style.customWindowCenter_ = 128.0;
+    style.customWindowWidth_ = 256.0;
+    return false;
+  }
+
+
   ImageBuffer3D::SliceReader::SliceReader(ImageBuffer3D& that,
                                           VolumeProjection projection,
                                           unsigned int slice)
@@ -299,6 +387,10 @@
         // TODO
         throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);          
       }
+
+      // Update the dynamic range of the underlying image, if
+      // "computeRange_" is set to true
+      that_.ExtendImageRange(accessor_);
     }
   }
 
@@ -306,7 +398,8 @@
   ImageBuffer3D::SliceWriter::SliceWriter(ImageBuffer3D& that,
                                           VolumeProjection projection,
                                           unsigned int slice) :
-  modified_(false)
+    that_(that),
+    modified_(false)
   {
     switch (projection)
     {
--- a/Framework/Volumes/ImageBuffer3D.h	Mon Oct 02 14:31:26 2017 +0200
+++ b/Framework/Volumes/ImageBuffer3D.h	Mon Oct 02 22:01:41 2017 +0200
@@ -22,7 +22,9 @@
 #pragma once
 
 #include "../Enumerations.h"
+#include "../Layers/RenderStyle.h"
 #include "../Toolbox/CoordinateSystem3D.h"
+#include "../Toolbox/DicomFrameConverter.h"
 #include "../Toolbox/ParallelSlices.h"
 
 #include <Core/Images/Image.h>
@@ -39,6 +41,12 @@
     unsigned int           width_;
     unsigned int           height_;
     unsigned int           depth_;
+    bool                   computeRange_;
+    bool                   hasRange_;
+    float                  minValue_;
+    float                  maxValue_;
+
+    void ExtendImageRange(const Orthanc::ImageAccessor& slice);
 
     Orthanc::ImageAccessor GetAxialSliceAccessor(unsigned int slice,
                                                  bool readOnly);
@@ -52,7 +60,8 @@
     ImageBuffer3D(Orthanc::PixelFormat format,
                   unsigned int width,
                   unsigned int height,
-                  unsigned int depth);
+                  unsigned int depth,
+                  bool computeRange);
 
     void Clear();
 
@@ -94,6 +103,12 @@
     
     uint64_t GetEstimatedMemorySize() const;
 
+    bool GetRange(float& minValue,
+                  float& maxValue) const;
+
+    bool FitWindowingToRange(RenderStyle& style,
+                             const DicomFrameConverter& converter) const;
+
 
     class SliceReader : public boost::noncopyable
     {
@@ -116,6 +131,7 @@
     class SliceWriter : public boost::noncopyable
     {
     private:
+      ImageBuffer3D&                 that_;
       bool                           modified_;
       Orthanc::ImageAccessor         accessor_;
       std::auto_ptr<Orthanc::Image>  sagittal_;  // Unused for axial and coronal
--- a/Framework/Widgets/LayerWidget.cpp	Mon Oct 02 14:31:26 2017 +0200
+++ b/Framework/Widgets/LayerWidget.cpp	Mon Oct 02 22:01:41 2017 +0200
@@ -350,6 +350,18 @@
   }
 
   
+  const RenderStyle& LayerWidget::GetLayerStyle(size_t layer) const
+  {
+    if (layer >= layers_.size())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    assert(layers_.size() == styles_.size());
+    return styles_[layer];
+  }
+  
+
   void LayerWidget::SetLayerStyle(size_t layer,
                                   const RenderStyle& style)
   {
--- a/Framework/Widgets/LayerWidget.h	Mon Oct 02 14:31:26 2017 +0200
+++ b/Framework/Widgets/LayerWidget.h	Mon Oct 02 22:01:41 2017 +0200
@@ -98,6 +98,8 @@
       return layers_.size();
     }
 
+    const RenderStyle& GetLayerStyle(size_t layer) const;
+
     void SetLayerStyle(size_t layer,
                        const RenderStyle& style);
 
--- a/Framework/dev.h	Mon Oct 02 14:31:26 2017 +0200
+++ b/Framework/dev.h	Mon Oct 02 22:01:41 2017 +0200
@@ -47,7 +47,7 @@
     OrthancSlicesLoader           loader_;
     std::auto_ptr<ImageBuffer3D>  image_;
     std::auto_ptr<DownloadStack>  downloadStack_;
-
+    bool                          computeRange_;
     
     void ScheduleSliceDownload()
     {
@@ -151,7 +151,7 @@
       LOG(INFO) << "Creating a volume image of size " << width << "x" << height 
                 << "x" << loader.GetSliceCount() << " in " << Orthanc::EnumerationToString(format);
 
-      image_.reset(new ImageBuffer3D(format, width, height, loader.GetSliceCount()));
+      image_.reset(new ImageBuffer3D(format, width, height, loader.GetSliceCount(), computeRange_));
       image_->SetAxialGeometry(loader.GetSlice(0).GetGeometry());
       image_->SetVoxelDimensions(loader.GetSlice(0).GetPixelSpacingX(), 
                                  loader.GetSlice(0).GetPixelSpacingY(), spacingZ);
@@ -201,8 +201,10 @@
     }
 
   public:
-    OrthancVolumeImage(IWebService& orthanc) : 
-      loader_(*this, orthanc)
+    OrthancVolumeImage(IWebService& orthanc,
+                       bool computeRange) : 
+      loader_(*this, orthanc),
+      computeRange_(computeRange)
     {
     }
 
@@ -244,6 +246,19 @@
         return *image_;
       }
     }
+
+    bool FitWindowingToRange(RenderStyle& style,
+                             const DicomFrameConverter& converter) const
+    {
+      if (image_.get() == NULL)
+      {
+        return false;
+      }
+      else
+      {
+        return image_->FitWindowingToRange(style, converter);
+      }
+    }
   };
 
 
@@ -322,8 +337,8 @@
                  axialThickness * axial.GetGeometry().GetNormal());
       
       reference_ = CoordinateSystem3D(origin,
-                                 axial.GetGeometry().GetAxisX(), 
-                                 -axial.GetGeometry().GetNormal());
+                                      axial.GetGeometry().GetAxisX(), 
+                                      -axial.GetGeometry().GetNormal());
     }
 
     void SetupSagittal(const OrthancVolumeImage& volume)
@@ -344,8 +359,8 @@
                  axialThickness * axial.GetGeometry().GetNormal());
       
       reference_ = CoordinateSystem3D(origin,
-                                 axial.GetGeometry().GetAxisY(), 
-                                 axial.GetGeometry().GetNormal());
+                                      axial.GetGeometry().GetAxisY(), 
+                                      axial.GetGeometry().GetNormal());
     }
 
   public:
@@ -376,6 +391,8 @@
         default:
           throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
       }
+
+      //sliceThickness_ = 3.0f;  // TODO XXXXX
     }
 
     size_t GetSliceCount() const
@@ -416,7 +433,7 @@
       }
     }
 
-    Slice GetSlice(size_t slice) const
+    Slice* GetSlice(size_t slice) const
     {
       if (slice < 0 ||
           slice >= depth_)
@@ -426,12 +443,12 @@
       else
       {
         CoordinateSystem3D origin(reference_.GetOrigin() +
-                             static_cast<double>(slice) * sliceThickness_ * reference_.GetNormal(),
-                             reference_.GetAxisX(),
-                             reference_.GetAxisY());
+                                  static_cast<double>(slice) * sliceThickness_ * reference_.GetNormal(),
+                                  reference_.GetAxisX(),
+                                  reference_.GetAxisY());
 
-        return Slice(origin, pixelSpacingX_, pixelSpacingY_, sliceThickness_,
-                     width_, height_, converter_);
+        return new Slice(origin, pixelSpacingX_, pixelSpacingY_, sliceThickness_,
+                         width_, height_, converter_);
       }
     }
   };
@@ -564,7 +581,8 @@
       {       
         // As the slices of the volumic image are arranged in a box,
         // we only consider one single reference slice (the one with index 0).
-        GetProjectionGeometry(projection).GetSlice(0).GetExtent(points);
+        std::auto_ptr<Slice> slice(GetProjectionGeometry(projection).GetSlice(0));
+        slice->GetExtent(points);
         
         return true;
       }
@@ -595,11 +613,11 @@
             frame.reset(Orthanc::Image::Clone(reader.GetAccessor()));
           }
 
-          Slice slice = geometry.GetSlice(closest);
+          std::auto_ptr<Slice> slice(geometry.GetSlice(closest));
           LayerSourceBase::NotifyLayerReady(
-            FrameRenderer::CreateRenderer(frame.release(), slice, isFullQuality),
+            FrameRenderer::CreateRenderer(frame.release(), *slice, isFullQuality),
             //new SliceOutlineRenderer(slice),
-            slice, false);
+            *slice, false);
           return;
         }
       }
@@ -613,14 +631,15 @@
 
   class VolumeImageInteractor :
     public IWorldSceneInteractor,
-    private ISlicedVolume::IObserver
+    protected ISlicedVolume::IObserver
   {
   private:
     LayerWidget&                        widget_;
     VolumeProjection                    projection_;
     std::auto_ptr<VolumeImageGeometry>  slices_;
     size_t                              slice_;
- 
+
+  protected:
     virtual void NotifyGeometryReady(const ISlicedVolume& volume)
     {
       if (slices_.get() == NULL)
@@ -761,7 +780,9 @@
       if (slices_.get() != NULL)
       {
         slice_ = slice;
-        widget_.SetSlice(slices_->GetSlice(slice_).GetGeometry());
+
+        std::auto_ptr<Slice> tmp(slices_->GetSlice(slice_));
+        widget_.SetSlice(tmp->GetGeometry());
       }
     }
   };