changeset 671:28dca6cd827b

Merge default
author Benjamin Golinvaux <bgo@osimis.io>
date Wed, 15 May 2019 18:39:50 +0200
parents 5dd496343fad (current diff) 3805ffa2833d (diff)
children 86930bc676c6
files
diffstat 5 files changed, 421 insertions(+), 204 deletions(-) [+]
line wrap: on
line diff
--- a/Framework/Toolbox/SlicesSorter.cpp	Wed May 15 18:29:42 2019 +0200
+++ b/Framework/Toolbox/SlicesSorter.cpp	Wed May 15 18:39:50 2019 +0200
@@ -285,4 +285,42 @@
 
     return found;
   }
+
+
+  double SlicesSorter::ComputeSpacingBetweenSlices() const
+  {
+    if (GetSlicesCount() <= 1)
+    {
+      // This is a volume that is empty or that contains one single
+      // slice: Choose a dummy z-dimension for voxels
+      return 1.0;
+    }
+    
+    const OrthancStone::CoordinateSystem3D& reference = GetSliceGeometry(0);
+
+    double referencePosition = reference.ProjectAlongNormal(reference.GetOrigin());
+        
+    double p = reference.ProjectAlongNormal(GetSliceGeometry(1).GetOrigin());
+    double spacingZ = p - referencePosition;
+
+    if (spacingZ <= 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls,
+                                      "Please call the Sort() method before");
+    }
+
+    for (size_t i = 1; i < GetSlicesCount(); i++)
+    {
+      OrthancStone::Vector p = reference.GetOrigin() + spacingZ * static_cast<double>(i) * reference.GetNormal();        
+      double d = boost::numeric::ublas::norm_2(p - GetSliceGeometry(i).GetOrigin());
+
+      if (!OrthancStone::LinearAlgebra::IsNear(d, 0, 0.001 /* tolerance expressed in mm */))
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry,
+                                        "The origins of the slices of a volume image are not regularly spaced");
+      }
+    }
+
+    return spacingZ;
+  }
 }
--- a/Framework/Toolbox/SlicesSorter.h	Wed May 15 18:29:42 2019 +0200
+++ b/Framework/Toolbox/SlicesSorter.h	Wed May 15 18:39:50 2019 +0200
@@ -88,5 +88,8 @@
     bool LookupClosestSlice(size_t& index,
                             double& distance,
                             const CoordinateSystem3D& slice) const;
+
+    // WARNING - The slices must have been sorted before calling this method
+    double ComputeSpacingBetweenSlices() const;
   };
 }
--- a/Framework/Volumes/ImageBuffer3D.cpp	Wed May 15 18:29:42 2019 +0200
+++ b/Framework/Volumes/ImageBuffer3D.cpp	Wed May 15 18:39:50 2019 +0200
@@ -118,8 +118,9 @@
   {
     LinearAlgebra::AssignVector(voxelDimensions_, 1, 1, 1);
 
-    LOG(INFO) << "Created an image of "
-              << (GetEstimatedMemorySize() / (1024ll * 1024ll)) << "MB";
+    LOG(INFO) << "Created a 3D image of size " << width << "x" << height
+              << "x" << depth << " in " << Orthanc::EnumerationToString(format)
+              << " (" << (GetEstimatedMemorySize() / (1024ll * 1024ll)) << "MB)";
   }
 
 
--- a/Resources/CMake/OrthancStoneConfiguration.cmake	Wed May 15 18:29:42 2019 +0200
+++ b/Resources/CMake/OrthancStoneConfiguration.cmake	Wed May 15 18:39:50 2019 +0200
@@ -147,7 +147,7 @@
 
 if (ENABLE_OPENGL)
   if (NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
-    # If including "FindOpenGL.cmake" using Emscripten (targetting
+    # If including "FindOpenGL.cmake" using Emscripten (targeting
     # WebAssembly), the "OPENGL_LIBRARIES" value incorrectly includes
     # the "nul" library, which leads to warning message in Emscripten:
     # 'shared:WARNING: emcc: cannot find library "nul"'.
--- a/Samples/Sdl/Loader.cpp	Wed May 15 18:29:42 2019 +0200
+++ b/Samples/Sdl/Loader.cpp	Wed May 15 18:39:50 2019 +0200
@@ -1143,258 +1143,288 @@
     public Orthanc::IDynamicObject  /* to be used as a payload of SlicesSorter */
   {
   private:
-    Orthanc::DicomImageInformation    imageInformation_;
-    OrthancStone::SopClassUid         sopClassUid_;
-    double                            thickness_;
-    double                            pixelSpacingX_;
-    double                            pixelSpacingY_;
-    OrthancStone::CoordinateSystem3D  geometry_;
-    OrthancStone::Vector              frameOffsets_;
-    bool                              isColor_;
-    bool                              hasRescale_;
-    double                            rescaleOffset_;
-    double                            rescaleSlope_;
-    bool                              hasDefaultWindowing_;
-    float                             defaultWindowingCenter_;
-    float                             defaultWindowingWidth_;
-    Orthanc::PixelFormat              expectedPixelFormat_;
+    struct Data   // Struct to ease the copy constructor
+    {
+      Orthanc::DicomImageInformation    imageInformation_;
+      OrthancStone::SopClassUid         sopClassUid_;
+      double                            thickness_;
+      double                            pixelSpacingX_;
+      double                            pixelSpacingY_;
+      OrthancStone::CoordinateSystem3D  geometry_;
+      OrthancStone::Vector              frameOffsets_;
+      bool                              isColor_;
+      bool                              hasRescale_;
+      double                            rescaleOffset_;
+      double                            rescaleSlope_;
+      bool                              hasDefaultWindowing_;
+      float                             defaultWindowingCenter_;
+      float                             defaultWindowingWidth_;
+      Orthanc::PixelFormat              expectedPixelFormat_;
+
+      void ComputeDoseOffsets(const Orthanc::DicomMap& dicom)
+      {
+        // http://dicom.nema.org/medical/Dicom/2016a/output/chtml/part03/sect_C.8.8.3.2.html
+
+        {
+          std::string increment;
 
-    void ComputeDoseOffsets(const Orthanc::DicomMap& dicom)
-    {
-      // http://dicom.nema.org/medical/Dicom/2016a/output/chtml/part03/sect_C.8.8.3.2.html
-
-      {
-        std::string increment;
+          if (dicom.CopyToString(increment, Orthanc::DICOM_TAG_FRAME_INCREMENT_POINTER, false))
+          {
+            Orthanc::Toolbox::ToUpperCase(increment);
+            if (increment != "3004,000C")  // This is the "Grid Frame Offset Vector" tag
+            {
+              LOG(ERROR) << "RT-DOSE: Bad value for the \"FrameIncrementPointer\" tag";
+              return;
+            }
+          }
+        }
 
-        if (dicom.CopyToString(increment, Orthanc::DICOM_TAG_FRAME_INCREMENT_POINTER, false))
+        if (!OrthancStone::LinearAlgebra::ParseVector(frameOffsets_, dicom, Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR) ||
+            frameOffsets_.size() < imageInformation_.GetNumberOfFrames())
+        {
+          LOG(ERROR) << "RT-DOSE: No information about the 3D location of some slice(s)";
+          frameOffsets_.clear();
+        }
+        else
         {
-          Orthanc::Toolbox::ToUpperCase(increment);
-          if (increment != "3004,000C")  // This is the "Grid Frame Offset Vector" tag
+          if (frameOffsets_.size() >= 2)
           {
-            LOG(ERROR) << "RT-DOSE: Bad value for the \"FrameIncrementPointer\" tag";
-            return;
+            thickness_ = frameOffsets_[1] - frameOffsets_[0];
+
+            if (thickness_ < 0)
+            {
+              thickness_ = -thickness_;
+            }
           }
         }
       }
 
-      if (!OrthancStone::LinearAlgebra::ParseVector(frameOffsets_, dicom, Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR) ||
-          frameOffsets_.size() < imageInformation_.GetNumberOfFrames())
-      {
-        LOG(ERROR) << "RT-DOSE: No information about the 3D location of some slice(s)";
-        frameOffsets_.clear();
-      }
-      else
-      {
-        if (frameOffsets_.size() >= 2)
-        {
-          thickness_ = frameOffsets_[1] - frameOffsets_[0];
-
-          if (thickness_ < 0)
-          {
-            thickness_ = -thickness_;
-          }
-        }
-      }
-    }
-
-  public:
-    DicomInstanceParameters(const Orthanc::DicomMap& dicom) :
-      imageInformation_(dicom)
-    {
-      if (imageInformation_.GetNumberOfFrames() <= 0)
+      Data(const Orthanc::DicomMap& dicom) :
+        imageInformation_(dicom)
       {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
-      }
+        if (imageInformation_.GetNumberOfFrames() <= 0)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+        }
             
-      std::string s;
-      if (!dicom.CopyToString(s, Orthanc::DICOM_TAG_SOP_CLASS_UID, false))
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
-      }
-      else
-      {
-        sopClassUid_ = OrthancStone::StringToSopClassUid(s);
-      }
+        std::string s;
+        if (!dicom.CopyToString(s, Orthanc::DICOM_TAG_SOP_CLASS_UID, false))
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+        }
+        else
+        {
+          sopClassUid_ = OrthancStone::StringToSopClassUid(s);
+        }
+
+        if (!dicom.ParseDouble(thickness_, Orthanc::DICOM_TAG_SLICE_THICKNESS))
+        {
+          thickness_ = 100.0 * std::numeric_limits<double>::epsilon();
+        }
+
+        OrthancStone::GeometryToolbox::GetPixelSpacing(pixelSpacingX_, pixelSpacingY_, dicom);
+
+        std::string position, orientation;
+        if (dicom.CopyToString(position, Orthanc::DICOM_TAG_IMAGE_POSITION_PATIENT, false) &&
+            dicom.CopyToString(orientation, Orthanc::DICOM_TAG_IMAGE_ORIENTATION_PATIENT, false))
+        {
+          geometry_ = OrthancStone::CoordinateSystem3D(position, orientation);
+        }
+
+        if (sopClassUid_ == OrthancStone::SopClassUid_RTDose)
+        {
+          ComputeDoseOffsets(dicom);
+        }
+
+        isColor_ = (imageInformation_.GetPhotometricInterpretation() != Orthanc::PhotometricInterpretation_Monochrome1 &&
+                    imageInformation_.GetPhotometricInterpretation() != Orthanc::PhotometricInterpretation_Monochrome2);
+
+        double doseGridScaling;
 
-      if (!dicom.ParseDouble(thickness_, Orthanc::DICOM_TAG_SLICE_THICKNESS))
-      {
-        thickness_ = 100.0 * std::numeric_limits<double>::epsilon();
-      }
-
-      OrthancStone::GeometryToolbox::GetPixelSpacing(pixelSpacingX_, pixelSpacingY_, dicom);
+        if (dicom.ParseDouble(rescaleOffset_, 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))
+        {
+          hasRescale_ = true;
+          rescaleOffset_ = 0;
+          rescaleSlope_ = doseGridScaling;
+        }
+        else
+        {
+          hasRescale_ = false;
+        }
 
-      std::string position, orientation;
-      if (dicom.CopyToString(position, Orthanc::DICOM_TAG_IMAGE_POSITION_PATIENT, false) &&
-          dicom.CopyToString(orientation, Orthanc::DICOM_TAG_IMAGE_ORIENTATION_PATIENT, false))
-      {
-        geometry_ = OrthancStone::CoordinateSystem3D(position, orientation);
-      }
+        OrthancStone::Vector c, w;
+        if (OrthancStone::LinearAlgebra::ParseVector(c, dicom, Orthanc::DICOM_TAG_WINDOW_CENTER) &&
+            OrthancStone::LinearAlgebra::ParseVector(w, dicom, Orthanc::DICOM_TAG_WINDOW_WIDTH) &&
+            c.size() > 0 && 
+            w.size() > 0)
+        {
+          hasDefaultWindowing_ = true;
+          defaultWindowingCenter_ = static_cast<float>(c[0]);
+          defaultWindowingWidth_ = static_cast<float>(w[0]);
+        }
+        else
+        {
+          hasDefaultWindowing_ = false;
+        }
 
-      if (sopClassUid_ == OrthancStone::SopClassUid_RTDose)
-      {
-        ComputeDoseOffsets(dicom);
+        if (sopClassUid_ == OrthancStone::SopClassUid_RTDose)
+        {
+          switch (imageInformation_.GetBitsStored())
+          {
+            case 16:
+              expectedPixelFormat_ = Orthanc::PixelFormat_Grayscale16;
+              break;
+
+            case 32:
+              expectedPixelFormat_ = Orthanc::PixelFormat_Grayscale32;
+              break;
+
+            default:
+              throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+          } 
+        }
+        else if (isColor_)
+        {
+          expectedPixelFormat_ = Orthanc::PixelFormat_RGB24;
+        }
+        else if (imageInformation_.IsSigned())
+        {
+          expectedPixelFormat_ = Orthanc::PixelFormat_SignedGrayscale16;
+        }
+        else
+        {
+          expectedPixelFormat_ = Orthanc::PixelFormat_Grayscale16;
+        }
       }
 
-      isColor_ = (imageInformation_.GetPhotometricInterpretation() != Orthanc::PhotometricInterpretation_Monochrome1 &&
-                  imageInformation_.GetPhotometricInterpretation() != Orthanc::PhotometricInterpretation_Monochrome2);
-
-      double doseGridScaling;
-
-      if (dicom.ParseDouble(rescaleOffset_, Orthanc::DICOM_TAG_RESCALE_INTERCEPT) &&
-          dicom.ParseDouble(rescaleSlope_, Orthanc::DICOM_TAG_RESCALE_SLOPE))
+      OrthancStone::CoordinateSystem3D  GetFrameGeometry(unsigned int frame) const
       {
-        hasRescale_ = true;
-      }
-      else if (dicom.ParseDouble(doseGridScaling, Orthanc::DICOM_TAG_DOSE_GRID_SCALING))
-      {
-        hasRescale_ = true;
-        rescaleOffset_ = 0;
-        rescaleSlope_ = doseGridScaling;
-      }
-      else
-      {
-        hasRescale_ = false;
+        if (frame == 0)
+        {
+          return geometry_;
+        }
+        else if (frame >= imageInformation_.GetNumberOfFrames())
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        }
+        else if (sopClassUid_ == OrthancStone::SopClassUid_RTDose)
+        {
+          if (frame >= frameOffsets_.size())
+          {
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+          }
+
+          return OrthancStone::CoordinateSystem3D(
+            geometry_.GetOrigin() + frameOffsets_[frame] * geometry_.GetNormal(),
+            geometry_.GetAxisX(),
+            geometry_.GetAxisY());
+        }
+        else
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+        }
       }
 
-      OrthancStone::Vector c, w;
-      if (OrthancStone::LinearAlgebra::ParseVector(c, dicom, Orthanc::DICOM_TAG_WINDOW_CENTER) &&
-          OrthancStone::LinearAlgebra::ParseVector(w, dicom, Orthanc::DICOM_TAG_WINDOW_WIDTH) &&
-          c.size() > 0 && 
-          w.size() > 0)
+      // TODO - Is this necessary?
+      bool FrameContainsPlane(unsigned int frame,
+                              const OrthancStone::CoordinateSystem3D& plane) const
       {
-        hasDefaultWindowing_ = true;
-        defaultWindowingCenter_ = static_cast<float>(c[0]);
-        defaultWindowingWidth_ = static_cast<float>(w[0]);
-      }
-      else
-      {
-        hasDefaultWindowing_ = false;
-      }
+        if (frame >= imageInformation_.GetNumberOfFrames())
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        }
 
-      if (sopClassUid_ == OrthancStone::SopClassUid_RTDose)
-      {
-        switch (imageInformation_.GetBitsStored())
+        OrthancStone::CoordinateSystem3D tmp = geometry_;
+
+        if (frame != 0)
         {
-          case 16:
-            expectedPixelFormat_ = Orthanc::PixelFormat_Grayscale16;
-            break;
+          tmp = GetFrameGeometry(frame);
+        }
 
-          case 32:
-            expectedPixelFormat_ = Orthanc::PixelFormat_Grayscale32;
-            break;
+        double distance;
 
-          default:
-            throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-        } 
+        return (OrthancStone::CoordinateSystem3D::GetDistance(distance, tmp, plane) &&
+                distance <= thickness_ / 2.0);
       }
-      else if (isColor_)
-      {
-        expectedPixelFormat_ = Orthanc::PixelFormat_RGB24;
-      }
-      else if (imageInformation_.IsSigned())
-      {
-        expectedPixelFormat_ = Orthanc::PixelFormat_SignedGrayscale16;
-      }
-      else
-      {
-        expectedPixelFormat_ = Orthanc::PixelFormat_Grayscale16;
-      }
+    };
+    
+    Data  data_;
+
+
+  public:
+    DicomInstanceParameters(const DicomInstanceParameters& other) :
+      data_(other.data_)
+    {
+    }
+
+    
+    DicomInstanceParameters(const Orthanc::DicomMap& dicom) :
+      data_(dicom)
+    {
     }
 
     const Orthanc::DicomImageInformation& GetImageInformation() const
     {
-      return imageInformation_;
+      return data_.imageInformation_;
     }
 
     OrthancStone::SopClassUid GetSopClassUid() const
     {
-      return sopClassUid_;
+      return data_.sopClassUid_;
     }
 
     double GetThickness() const
     {
-      return thickness_;
+      return data_.thickness_;
     }
 
     double GetPixelSpacingX() const
     {
-      return pixelSpacingX_;
+      return data_.pixelSpacingX_;
     }
 
     double GetPixelSpacingY() const
     {
-      return pixelSpacingY_;
+      return data_.pixelSpacingY_;
     }
 
     const OrthancStone::CoordinateSystem3D&  GetGeometry() const
     {
-      return geometry_;
+      return data_.geometry_;
     }
 
     OrthancStone::CoordinateSystem3D  GetFrameGeometry(unsigned int frame) const
     {
-      if (frame == 0)
-      {
-        return geometry_;
-      }
-      else if (frame >= imageInformation_.GetNumberOfFrames())
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
-      }
-      else if (sopClassUid_ == OrthancStone::SopClassUid_RTDose)
-      {
-        if (frame >= frameOffsets_.size())
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-        }
-
-        return OrthancStone::CoordinateSystem3D(
-          geometry_.GetOrigin() + frameOffsets_[frame] * geometry_.GetNormal(),
-          geometry_.GetAxisX(),
-          geometry_.GetAxisY());
-      }
-      else
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-      }
+      return data_.GetFrameGeometry(frame);
     }
 
+    // TODO - Is this necessary?
     bool FrameContainsPlane(unsigned int frame,
                             const OrthancStone::CoordinateSystem3D& plane) const
     {
-      if (frame >= imageInformation_.GetNumberOfFrames())
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
-      }
-
-      OrthancStone::CoordinateSystem3D tmp = geometry_;
-
-      if (frame != 0)
-      {
-        tmp = GetFrameGeometry(frame);
-      }
-
-      double distance;
-
-      return (OrthancStone::CoordinateSystem3D::GetDistance(distance, tmp, plane) &&
-              distance <= thickness_ / 2.0);
+      return data_.FrameContainsPlane(frame, plane);
     }
 
     bool IsColor() const
     {
-      return isColor_;
+      return data_.isColor_;
     }
 
     bool HasRescale() const
     {
-      return hasRescale_;
+      return data_.hasRescale_;
     }
 
     double GetRescaleOffset() const
     {
-      if (hasRescale_)
+      if (data_.hasRescale_)
       {
-        return rescaleOffset_;
+        return data_.rescaleOffset_;
       }
       else
       {
@@ -1404,9 +1434,9 @@
 
     double GetRescaleSlope() const
     {
-      if (hasRescale_)
+      if (data_.hasRescale_)
       {
-        return rescaleSlope_;
+        return data_.rescaleSlope_;
       }
       else
       {
@@ -1416,14 +1446,14 @@
 
     bool HasDefaultWindowing() const
     {
-      return hasDefaultWindowing_;
+      return data_.hasDefaultWindowing_;
     }
 
     float GetDefaultWindowingCenter() const
     {
-      if (hasDefaultWindowing_)
+      if (data_.hasDefaultWindowing_)
       {
-        return defaultWindowingCenter_;
+        return data_.defaultWindowingCenter_;
       }
       else
       {
@@ -1433,9 +1463,9 @@
 
     float GetDefaultWindowingWidth() const
     {
-      if (hasDefaultWindowing_)
+      if (data_.hasDefaultWindowing_)
       {
-        return defaultWindowingWidth_;
+        return data_.defaultWindowingWidth_;
       }
       else
       {
@@ -1445,11 +1475,161 @@
 
     Orthanc::PixelFormat GetExpectedPixelFormat() const
     {
-      return expectedPixelFormat_;
+      return data_.expectedPixelFormat_;
     }
   };
 
 
+  class DicomVolumeImage : public boost::noncopyable
+  {
+  private:
+    std::auto_ptr<OrthancStone::ImageBuffer3D>  image_;
+    std::vector<DicomInstanceParameters*>       slices_;
+
+    static const DicomInstanceParameters&
+    GetSliceParameters(const OrthancStone::SlicesSorter& slices,
+                       size_t index)
+    {
+      return dynamic_cast<const DicomInstanceParameters&>(slices.GetSlicePayload(index));
+    }
+
+    static void CheckSlice(const OrthancStone::SlicesSorter& slices,
+                           size_t index,
+                           const OrthancStone::CoordinateSystem3D& reference,
+                           const DicomInstanceParameters& a)
+    {
+      const OrthancStone::CoordinateSystem3D& slice = slices.GetSliceGeometry(index);
+      const DicomInstanceParameters& b = GetSliceParameters(slices, index);
+      
+      if (!OrthancStone::GeometryToolbox::IsParallel(reference.GetNormal(), slice.GetNormal()))
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry,
+                                        "A slice in the volume image is not parallel to the others");
+      }
+
+      if (a.GetExpectedPixelFormat() != b.GetExpectedPixelFormat())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat,
+                                        "The pixel format changes across the slices of the volume image");
+      }
+
+      if (a.GetImageInformation().GetWidth() != b.GetImageInformation().GetWidth() ||
+          a.GetImageInformation().GetHeight() != b.GetImageInformation().GetHeight())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize,
+                                        "The width/height of slices are not constant in the volume image");
+      }
+
+      if (!OrthancStone::LinearAlgebra::IsNear(a.GetPixelSpacingX(), b.GetPixelSpacingX()) ||
+          !OrthancStone::LinearAlgebra::IsNear(a.GetPixelSpacingY(), b.GetPixelSpacingY()))
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry,
+                                        "The pixel spacing of the slices change across the volume image");
+      }
+    }
+
+    
+    static void CheckVolume(const OrthancStone::SlicesSorter& slices)
+    {
+      for (size_t i = 0; i < slices.GetSlicesCount(); i++)
+      {
+        if (GetSliceParameters(slices, i).GetImageInformation().GetNumberOfFrames() != 1)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry,
+                                          "This class does not support multi-frame images");
+        }
+      }
+
+      if (slices.GetSlicesCount() != 0)
+      {
+        const OrthancStone::CoordinateSystem3D& reference = slices.GetSliceGeometry(0);
+        const DicomInstanceParameters& dicom = GetSliceParameters(slices, 0);
+
+        for (size_t i = 1; i < slices.GetSlicesCount(); i++)
+        {
+          CheckSlice(slices, i, reference, dicom);
+        }
+      }
+    }
+
+
+    void Clear()
+    {
+      image_.reset();
+      
+      for (size_t i = 0; i < slices_.size(); i++)
+      {
+        assert(slices_[i] != NULL);
+        delete slices_[i];
+      }
+    }
+
+    
+  public:
+    DicomVolumeImage()
+    {
+    }
+
+    ~DicomVolumeImage()
+    {
+      Clear();
+    }
+
+    // WARNING: The payload of "slices" must be of class "DicomInstanceParameters"
+    void SetGeometry(OrthancStone::SlicesSorter& slices)
+    {
+      Clear();
+      
+      if (!slices.Sort())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
+                                        "Cannot sort the 3D slices of a DICOM series");          
+      }
+
+      slices_.reserve(slices.GetSlicesCount());
+
+      for (size_t i = 0; i < slices.GetSlicesCount(); i++)
+      {
+        slices_.push_back(new DicomInstanceParameters(GetSliceParameters(slices, i)));
+      }
+
+      CheckVolume(slices);
+
+      const double spacingZ = slices.ComputeSpacingBetweenSlices();
+      LOG(INFO) << "Computed spacing between slices: " << spacingZ << "mm";
+      
+      const DicomInstanceParameters& parameters = GetSliceParameters(slices, 0);
+
+      image_.reset(new OrthancStone::ImageBuffer3D(parameters.GetExpectedPixelFormat(),
+                                                   parameters.GetImageInformation().GetWidth(),
+                                                   parameters.GetImageInformation().GetHeight(),
+                                                   slices.GetSlicesCount(), false /* don't compute range */));      
+
+      image_->SetAxialGeometry(slices.GetSliceGeometry(0));
+      image_->SetVoxelDimensions(parameters.GetPixelSpacingX(), parameters.GetPixelSpacingY(), spacingZ);
+      image_->Clear();
+    }
+
+    bool IsGeometryReady() const
+    {
+      return (image_.get() != NULL);
+    }
+
+    const OrthancStone::ImageBuffer3D& GetImage() const
+    {
+      if (!IsGeometryReady())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        return *image_;
+      }
+    }      
+  };
+  
+  
+
   class AxialVolumeOrthancLoader : public OrthancStone::IObserver
   {
   private:
@@ -1488,6 +1668,8 @@
 
         Json::Value::Members instances = value.getMemberNames();
 
+        OrthancStone::SlicesSorter slices;
+        
         for (size_t i = 0; i < instances.size(); i++)
         {
           Orthanc::DicomMap dicom;
@@ -1496,16 +1678,10 @@
           std::auto_ptr<DicomInstanceParameters> instance(new DicomInstanceParameters(dicom));
 
           OrthancStone::CoordinateSystem3D geometry = instance->GetGeometry();
-          that_.slices_.AddSlice(geometry, instance.release());
+          slices.AddSlice(geometry, instance.release());
         }
 
-        if (!that_.slices_.Sort())
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
-                                          "Cannot sort the 3D slices of a DICOM series");          
-        }
-
-        printf("series sorted %d => %d\n", instances.size(), that_.slices_.GetSlicesCount());
+        that_.image_.SetGeometry(slices);
       }
     };
 
@@ -1539,9 +1715,8 @@
     };
 
 
-    bool                                        active_;
-    std::auto_ptr<OrthancStone::ImageBuffer3D>  image_;
-    OrthancStone::SlicesSorter                  slices_;
+    bool              active_;
+    DicomVolumeImage  image_;
 
   public:
     AxialVolumeOrthancLoader(OrthancStone::IObservable& oracle) :