changeset 87:4a541cd4fa83 wasm

OrthancVolumeImageLoader
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 26 May 2017 15:31:58 +0200
parents 02c3a7a4938f
children 90bf4116a23c
files Framework/Layers/ILayerSource.h Framework/Layers/LayerSourceBase.cpp Framework/Layers/LayerSourceBase.h Framework/Toolbox/DownloadStack.cpp Framework/Toolbox/DownloadStack.h Framework/Volumes/ImageBuffer3D.cpp Framework/Volumes/ImageBuffer3D.h Framework/Widgets/LayerWidget.cpp Framework/Widgets/LayerWidget.h Platforms/Generic/Oracle.cpp Platforms/Generic/Oracle.h Resources/CMake/OrthancStone.cmake UnitTestsSources/UnitTestsMain.cpp
diffstat 13 files changed, 286 insertions(+), 82 deletions(-) [+]
line wrap: on
line diff
--- a/Framework/Layers/ILayerSource.h	Fri May 26 13:42:50 2017 +0200
+++ b/Framework/Layers/ILayerSource.h	Fri May 26 15:31:58 2017 +0200
@@ -38,25 +38,25 @@
 
       // Triggered as soon as the source has enough information to
       // answer to "GetExtent()"
-      virtual void NotifyGeometryReady(ILayerSource& source) = 0;
+      virtual void NotifyGeometryReady(const ILayerSource& source) = 0;
       
-      virtual void NotifyGeometryError(ILayerSource& source) = 0;
+      virtual void NotifyGeometryError(const ILayerSource& source) = 0;
       
-      // Triggered if the extent or the content of the volume has changed
-      virtual void NotifySourceChange(ILayerSource& source) = 0;
+      // Triggered if the content of the volume has changed
+      virtual void NotifyContentChange(const ILayerSource& source) = 0;
 
       // Triggered if the content of some slice in the source volume has changed
-      virtual void NotifySliceChange(ILayerSource& source,
+      virtual void NotifySliceChange(const ILayerSource& source,
                                      const Slice& slice) = 0;
 
       // The layer must be deleted by the observer. "layer" will never
       // be "NULL", otherwise "NotifyLayerError()" would have been
       // called.
       virtual void NotifyLayerReady(ILayerRenderer *layer,
-                                    ILayerSource& source,
+                                    const ILayerSource& source,
                                     const Slice& slice) = 0;
 
-      virtual void NotifyLayerError(ILayerSource& source,
+      virtual void NotifyLayerError(const ILayerSource& source,
                                     const SliceGeometry& slice) = 0;
     };
     
--- a/Framework/Layers/LayerSourceBase.cpp	Fri May 26 13:42:50 2017 +0200
+++ b/Framework/Layers/LayerSourceBase.cpp	Fri May 26 15:31:58 2017 +0200
@@ -41,11 +41,11 @@
     }
   }  
     
-  void LayerSourceBase::NotifySourceChange()
+  void LayerSourceBase::NotifyContentChange()
   {
     if (observer_ != NULL)
     {
-      observer_->NotifySourceChange(*this);
+      observer_->NotifyContentChange(*this);
     }
   }
 
--- a/Framework/Layers/LayerSourceBase.h	Fri May 26 13:42:50 2017 +0200
+++ b/Framework/Layers/LayerSourceBase.h	Fri May 26 15:31:58 2017 +0200
@@ -35,7 +35,7 @@
     
     void NotifyGeometryError();
     
-    void NotifySourceChange();
+    void NotifyContentChange();
 
     void NotifySliceChange(const Slice& slice);
 
--- a/Framework/Toolbox/DownloadStack.cpp	Fri May 26 13:42:50 2017 +0200
+++ b/Framework/Toolbox/DownloadStack.cpp	Fri May 26 15:31:58 2017 +0200
@@ -174,23 +174,23 @@
   }
 
   
-  void DownloadStack::Writer::SetTopNode(unsigned int value)
+  void DownloadStack::SetTopNode(unsigned int value)
   {
-    if (value >= that_.nodes_.size())
+    if (value >= nodes_.size())
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
     }
 
-    that_.SetTopNodeInternal(value);
+    SetTopNodeInternal(value);
   }
 
 
-  void DownloadStack::Writer::SetTopNodePermissive(int value)
+  void DownloadStack::SetTopNodePermissive(int value)
   {
     if (value >= 0 &&
-        value < static_cast<int>(that_.nodes_.size()))
+        value < static_cast<int>(nodes_.size()))
     {
-      that_.SetTopNodeInternal(value);
+      SetTopNodeInternal(value);
     }
   }
 }
--- a/Framework/Toolbox/DownloadStack.h	Fri May 26 13:42:50 2017 +0200
+++ b/Framework/Toolbox/DownloadStack.h	Fri May 26 15:31:58 2017 +0200
@@ -24,8 +24,6 @@
 #include <vector>
 #include <boost/noncopyable.hpp>
 
-#include <boost/thread/mutex.hpp>   // TODO remove
-
 namespace OrthancStone
 {
   class DownloadStack : public boost::noncopyable
@@ -55,22 +53,8 @@
 
     bool Pop(unsigned int& value);
 
-    class Writer : public boost::noncopyable
-    {
-    private:
-      DownloadStack&              that_;
-      boost::mutex::scoped_lock   lock_;
-      
-    public:
-      Writer(DownloadStack& that) :
-        that_(that) 
-        //lock_(that.mutex_)
-      {
-      }
-      
-      void SetTopNode(unsigned int value);  
+    void SetTopNode(unsigned int value);  
 
-      void SetTopNodePermissive(int value);
-    };
+    void SetTopNodePermissive(int value);
   };
 }
--- a/Framework/Volumes/ImageBuffer3D.cpp	Fri May 26 13:42:50 2017 +0200
+++ b/Framework/Volumes/ImageBuffer3D.cpp	Fri May 26 15:31:58 2017 +0200
@@ -24,6 +24,8 @@
 #include "../../Resources/Orthanc/Core/Images/ImageProcessing.h"
 #include "../../Resources/Orthanc/Core/OrthancException.h"
 
+#include <string.h>
+
 namespace OrthancStone
 {
   Orthanc::ImageAccessor ImageBuffer3D::GetAxialSliceAccessor(unsigned int slice,
@@ -122,14 +124,12 @@
 
   void ImageBuffer3D::Clear()
   {
-    WriteLock lock(mutex_);
-    Orthanc::ImageProcessing::Set(image_, 0);
+    memset(image_.GetBuffer(), 0, image_.GetHeight() * image_.GetPitch());
   }
 
 
   void ImageBuffer3D::SetAxialGeometry(const SliceGeometry& geometry)
   {
-    WriteLock lock(mutex_);
     axialGeometry_ = geometry;
   }
 
@@ -146,7 +146,6 @@
     }
 
     {
-      WriteLock lock(mutex_);
       GeometryToolbox::AssignVector(voxelDimensions_, x, y, z);
     }
   }
@@ -154,8 +153,6 @@
 
   Vector ImageBuffer3D::GetVoxelDimensions(VolumeProjection projection)
   {
-    ReadLock lock(mutex_);
-
     Vector result;
     switch (projection)
     {
@@ -260,8 +257,7 @@
 
   ImageBuffer3D::SliceReader::SliceReader(ImageBuffer3D& that,
                                           VolumeProjection projection,
-                                          unsigned int slice) :
-  lock_(that.mutex_)
+                                          unsigned int slice)
   {
     switch (projection)
     {
@@ -286,10 +282,13 @@
 
   void ImageBuffer3D::SliceWriter::Flush()
   {
-    if (sagittal_.get() != NULL)
+    if (modified_)
     {
-      // TODO
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);          
+      if (sagittal_.get() != NULL)
+      {
+        // TODO
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);          
+      }
     }
   }
 
@@ -297,7 +296,7 @@
   ImageBuffer3D::SliceWriter::SliceWriter(ImageBuffer3D& that,
                                           VolumeProjection projection,
                                           unsigned int slice) :
-  lock_(that.mutex_)
+  modified_(false)
   {
     switch (projection)
     {
--- a/Framework/Volumes/ImageBuffer3D.h	Fri May 26 13:42:50 2017 +0200
+++ b/Framework/Volumes/ImageBuffer3D.h	Fri May 26 15:31:58 2017 +0200
@@ -27,22 +27,11 @@
 
 #include "../../Resources/Orthanc/Core/Images/Image.h"
 
-#include <boost/thread/shared_mutex.hpp>
-
-#if defined(_WIN32)
-#  include <boost/thread/win32/mutex.hpp>
-#endif
-
 namespace OrthancStone
 {
   class ImageBuffer3D : public boost::noncopyable
   {
   private:
-    typedef boost::shared_mutex          Mutex;
-    typedef boost::unique_lock<Mutex>    WriteLock;
-    typedef boost::shared_lock<Mutex>    ReadLock;
-
-    Mutex                  mutex_;
     SliceGeometry          axialGeometry_;
     Vector                 voxelDimensions_;
     Orthanc::Image         image_;
@@ -107,7 +96,6 @@
     class SliceReader : public boost::noncopyable
     {
     private:
-      ReadLock                       lock_;
       Orthanc::ImageAccessor         accessor_;
       std::auto_ptr<Orthanc::Image>  sagittal_;  // Unused for axial and coronal
 
@@ -126,7 +114,7 @@
     class SliceWriter : public boost::noncopyable
     {
     private:
-      WriteLock                      lock_;
+      bool                           modified_;
       Orthanc::ImageAccessor         accessor_;
       std::auto_ptr<Orthanc::Image>  sagittal_;  // Unused for axial and coronal
 
@@ -142,8 +130,14 @@
         Flush();
       }
 
+      const Orthanc::ImageAccessor& GetAccessor() const
+      {
+        return accessor_;
+      }
+
       Orthanc::ImageAccessor& GetAccessor()
       {
+        modified_ = true;
         return accessor_;
       }
     };
--- a/Framework/Widgets/LayerWidget.cpp	Fri May 26 13:42:50 2017 +0200
+++ b/Framework/Widgets/LayerWidget.cpp	Fri May 26 15:31:58 2017 +0200
@@ -144,7 +144,7 @@
 
   
   bool LayerWidget::LookupLayer(size_t& index /* out */,
-                                ILayerSource& layer) const
+                                const ILayerSource& layer) const
   {
     LayersIndex::const_iterator found = layersIndex_.find(&layer);
 
@@ -394,7 +394,7 @@
   }
 
   
-  void LayerWidget::NotifyGeometryReady(ILayerSource& source)
+  void LayerWidget::NotifyGeometryReady(const ILayerSource& source)
   {
     size_t i;
     if (LookupLayer(i, source))
@@ -405,30 +405,38 @@
   }
   
 
-  void LayerWidget::NotifyGeometryError(ILayerSource& source)
+  void LayerWidget::NotifyGeometryError(const ILayerSource& source)
   {
     LOG(ERROR) << "Cannot get geometry";
   }
   
 
-  void LayerWidget::NotifySourceChange(ILayerSource& source)
+  void LayerWidget::NotifyContentChange(const ILayerSource& source)
   {
-    source.ScheduleLayerCreation(slice_);
+    size_t index;
+    if (LookupLayer(index, source))
+    {
+      layers_[index]->ScheduleLayerCreation(slice_);
+    }
   }
   
 
-  void LayerWidget::NotifySliceChange(ILayerSource& source,
+  void LayerWidget::NotifySliceChange(const ILayerSource& source,
                                       const Slice& slice)
   {
     if (slice.ContainsPlane(slice_))
     {
-      source.ScheduleLayerCreation(slice_);
+      size_t index;
+      if (LookupLayer(index, source))
+      {
+        layers_[index]->ScheduleLayerCreation(slice_);
+      }
     }
   }
   
 
   void LayerWidget::NotifyLayerReady(ILayerRenderer* renderer,
-                                     ILayerSource& source,
+                                     const ILayerSource& source,
                                      const Slice& slice)
   {
     std::auto_ptr<ILayerRenderer> tmp(renderer);
@@ -443,7 +451,7 @@
   }
 
   
-  void LayerWidget::NotifyLayerError(ILayerSource& source,
+  void LayerWidget::NotifyLayerError(const ILayerSource& source,
                                      const SliceGeometry& slice)
   {
     size_t index;
@@ -454,7 +462,7 @@
       LOG(INFO) << "Unable to load a slice from layer " << index;
 
       double x1, y1, x2, y2;
-      if (GetAndFixExtent(x1, y1, x2, y2, source))
+      if (GetAndFixExtent(x1, y1, x2, y2, *layers_[index]))
       {
         UpdateLayer(index, new MissingLayerRenderer(x1, y1, x2, y2), Slice(slice, THIN_SLICE_THICKNESS));
       }
--- a/Framework/Widgets/LayerWidget.h	Fri May 26 13:42:50 2017 +0200
+++ b/Framework/Widgets/LayerWidget.h	Fri May 26 15:31:58 2017 +0200
@@ -35,7 +35,7 @@
   private:
     class Scene;
     
-    typedef std::map<ILayerSource*, size_t>  LayersIndex;
+    typedef std::map<const ILayerSource*, size_t>  LayersIndex;
 
     bool                        started_;
     LayersIndex                 layersIndex_;
@@ -47,7 +47,7 @@
 
 
     bool LookupLayer(size_t& index /* out */,
-                     ILayerSource& layer) const;
+                     const ILayerSource& layer) const;
 
     bool GetAndFixExtent(double& x1,
                          double& y1,
@@ -55,20 +55,20 @@
                          double& y2,
                          ILayerSource& source) const;
 
-    virtual void NotifyGeometryReady(ILayerSource& source);
+    virtual void NotifyGeometryReady(const ILayerSource& source);
 
-    virtual void NotifyGeometryError(ILayerSource& source);
+    virtual void NotifyGeometryError(const ILayerSource& source);
 
-    virtual void NotifySourceChange(ILayerSource& source);
+    virtual void NotifyContentChange(const ILayerSource& source);
 
-    virtual void NotifySliceChange(ILayerSource& source,
+    virtual void NotifySliceChange(const ILayerSource& source,
                                    const Slice& slice);
 
     virtual void NotifyLayerReady(ILayerRenderer* renderer,
-                                  ILayerSource& source,
+                                  const ILayerSource& source,
                                   const Slice& slice);
 
-    virtual void NotifyLayerError(ILayerSource& source,
+    virtual void NotifyLayerError(const ILayerSource& source,
                                   const SliceGeometry& slice);
 
         
--- a/Platforms/Generic/Oracle.cpp	Fri May 26 13:42:50 2017 +0200
+++ b/Platforms/Generic/Oracle.cpp	Fri May 26 15:31:58 2017 +0200
@@ -98,6 +98,11 @@
       }
     }
 
+    Orthanc::SharedMessageQueue& GetQueue()
+    {
+      return queue_;
+    }
+
     void Submit(IOracleCommand* command)
     {
       std::auto_ptr<IOracleCommand> protection(command);
@@ -202,4 +207,10 @@
   {
     pimpl_->Stop();
   }
+
+
+  void Oracle::WaitEmpty()
+  {
+    pimpl_->GetQueue().WaitEmpty(50);
+  }
 }
--- a/Platforms/Generic/Oracle.h	Fri May 26 13:42:50 2017 +0200
+++ b/Platforms/Generic/Oracle.h	Fri May 26 15:31:58 2017 +0200
@@ -45,6 +45,8 @@
 
     void Submit(IOracleCommand* command);
         
+    void WaitEmpty();  // For unit tests
+
     void Stop();
   };
 }
--- a/Resources/CMake/OrthancStone.cmake	Fri May 26 13:42:50 2017 +0200
+++ b/Resources/CMake/OrthancStone.cmake	Fri May 26 15:31:58 2017 +0200
@@ -219,10 +219,10 @@
   ${ORTHANC_STONE_DIR}/Framework/Viewport/CairoSurface.cpp
   ${ORTHANC_STONE_DIR}/Framework/Viewport/WidgetViewport.cpp
   ${ORTHANC_STONE_DIR}/Framework/Volumes/ImageBuffer3D.cpp
-  ${ORTHANC_STONE_DIR}/Framework/Volumes/VolumeImage.cpp
-  ${ORTHANC_STONE_DIR}/Framework/Volumes/VolumeImagePolicyBase.cpp
-  ${ORTHANC_STONE_DIR}/Framework/Volumes/VolumeImageProgressivePolicy.cpp
-  ${ORTHANC_STONE_DIR}/Framework/Volumes/VolumeImageSimplePolicy.cpp
+  #${ORTHANC_STONE_DIR}/Framework/Volumes/VolumeImage.cpp
+  #${ORTHANC_STONE_DIR}/Framework/Volumes/VolumeImagePolicyBase.cpp
+  #${ORTHANC_STONE_DIR}/Framework/Volumes/VolumeImageProgressivePolicy.cpp
+  #${ORTHANC_STONE_DIR}/Framework/Volumes/VolumeImageSimplePolicy.cpp
   ${ORTHANC_STONE_DIR}/Framework/Widgets/CairoWidget.cpp
   ${ORTHANC_STONE_DIR}/Framework/Widgets/EmptyWidget.cpp
   ${ORTHANC_STONE_DIR}/Framework/Widgets/LayeredSceneWidget.cpp
--- a/UnitTestsSources/UnitTestsMain.cpp	Fri May 26 13:42:50 2017 +0200
+++ b/UnitTestsSources/UnitTestsMain.cpp	Fri May 26 15:31:58 2017 +0200
@@ -28,6 +28,11 @@
 #include "../Resources/Orthanc/Core/MultiThreading/SharedMessageQueue.h"
 #include "../Resources/Orthanc/Core/OrthancException.h"
 
+#include "../Framework/Toolbox/IVolumeSlicesObserver.h"
+#include "../Framework/Volumes/ImageBuffer3D.h"
+#include "../Framework/Toolbox/DownloadStack.h"
+#include "../Resources/Orthanc/Core/Images/ImageProcessing.h"
+
 #include <boost/lexical_cast.hpp>
 #include <boost/date_time/posix_time/posix_time.hpp>
 #include <boost/thread/thread.hpp> 
@@ -68,10 +73,193 @@
       printf("ERROR 2\n");
     }
   };
+
+
+  class OrthancVolumeImageLoader : private OrthancSlicesLoader::ICallback
+  { 
+  private:
+    OrthancSlicesLoader           loader_;
+    IVolumeSlicesObserver*        observer_;
+    std::auto_ptr<ImageBuffer3D>  image_;
+    std::auto_ptr<DownloadStack>  downloadStack_;
+
+    
+    void ScheduleSliceDownload()
+    {
+      assert(downloadStack_.get() != NULL);
+
+      unsigned int slice;
+      if (downloadStack_->Pop(slice))
+      {
+        loader_.ScheduleLoadSliceImage(slice);
+      }
+    }
+
+
+    static bool IsCompatible(const Slice& a, 
+                             const Slice& b)
+    {
+      if (!GeometryToolbox::IsParallel(a.GetGeometry().GetNormal(),
+                                       b.GetGeometry().GetNormal()))
+      {
+        LOG(ERROR) << "Some slice in the volume image is not parallel to the others";
+        return false;
+      }
+
+      if (a.GetConverter().GetExpectedPixelFormat() != b.GetConverter().GetExpectedPixelFormat())
+      {
+        LOG(ERROR) << "The pixel format changes across the slices of the volume image";
+        return false;
+      }
+
+      if (a.GetWidth() != b.GetWidth() ||
+          a.GetHeight() != b.GetHeight())
+      {
+        LOG(ERROR) << "The width/height of the slices change across the volume image";
+        return false;
+      }
+
+      if (!GeometryToolbox::IsNear(a.GetPixelSpacingX(), b.GetPixelSpacingX()) ||
+          !GeometryToolbox::IsNear(a.GetPixelSpacingY(), b.GetPixelSpacingY()))
+      {
+        LOG(ERROR) << "The pixel spacing of the slices change across the volume image";
+        return false;
+      }
+
+      return true;
+    }
+
+
+    static double GetDistance(const Slice& a, 
+                              const Slice& b)
+    {
+      return fabs(a.GetGeometry().ProjectAlongNormal(a.GetGeometry().GetOrigin()) - 
+                  a.GetGeometry().ProjectAlongNormal(b.GetGeometry().GetOrigin()));
+    }
+
+
+    virtual void NotifyGeometryReady(const OrthancSlicesLoader& loader)
+    {
+      if (loader.GetSliceCount() == 0)
+      {
+        LOG(ERROR) << "Empty volume image";
+        return;
+      }
+
+      for (size_t i = 1; i < loader.GetSliceCount(); i++)
+      {
+        if (!IsCompatible(loader.GetSlice(0), loader.GetSlice(i)))
+        {
+          return;
+        }
+      }
+
+      double spacingZ;
+
+      if (loader.GetSliceCount() > 1)
+      {
+        spacingZ = GetDistance(loader.GetSlice(0), loader.GetSlice(1));
+      }
+      else
+      {
+        // This is a volume with one single slice: Choose a dummy
+        // z-dimension for voxels
+        spacingZ = 1;
+      }
+      
+      for (size_t i = 1; i < loader.GetSliceCount(); i++)
+      {
+        if (!GeometryToolbox::IsNear(spacingZ, GetDistance(loader.GetSlice(i - 1), loader.GetSlice(i))))
+        {
+          LOG(ERROR) << "The distance between successive slices is not constant in a volume image";
+          return;
+        }
+      }
+
+      unsigned int width = loader.GetSlice(0).GetWidth();
+      unsigned int height = loader.GetSlice(0).GetHeight();
+      Orthanc::PixelFormat format = loader.GetSlice(0).GetConverter().GetExpectedPixelFormat();
+      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_->SetAxialGeometry(loader.GetSlice(0).GetGeometry());
+      image_->SetVoxelDimensions(loader.GetSlice(0).GetPixelSpacingX(), 
+                                 loader.GetSlice(0).GetPixelSpacingY(), spacingZ);
+      image_->Clear();
+
+      downloadStack_.reset(new DownloadStack(loader.GetSliceCount()));
+
+      for (unsigned int i = 0; i < 4; i++)  // Limit to 4 simultaneous downloads
+      {
+        ScheduleSliceDownload();
+      }
+    }
+
+    virtual void NotifyGeometryError(const OrthancSlicesLoader& loader)
+    {
+      LOG(ERROR) << "Unable to download a volume image";
+    }
+
+    virtual void NotifySliceImageReady(const OrthancSlicesLoader& loader,
+                                       unsigned int sliceIndex,
+                                       const Slice& slice,
+                                       Orthanc::ImageAccessor* image)
+    {
+      std::auto_ptr<Orthanc::ImageAccessor> protection(image);
+
+      {
+        ImageBuffer3D::SliceWriter writer(*image_, VolumeProjection_Axial, 0);
+        Orthanc::ImageProcessing::Copy(writer.GetAccessor(), *protection);
+      }
+
+      ScheduleSliceDownload();
+    }
+
+    virtual void NotifySliceImageError(const OrthancSlicesLoader& loader,
+                                       unsigned int sliceIndex,
+                                       const Slice& slice)
+    {
+      LOG(ERROR) << "Cannot download slice " << sliceIndex << " in a volume image";
+      ScheduleSliceDownload();
+    }
+
+  public:
+    OrthancVolumeImageLoader(IWebService& orthanc) : 
+      loader_(*this, orthanc),
+      observer_(NULL)
+    {
+    }
+
+    void ScheduleLoadSeries(const std::string& seriesId)
+    {
+      loader_.ScheduleLoadSeries(seriesId);
+    }
+
+    void ScheduleLoadInstance(const std::string& instanceId,
+                              unsigned int frame)
+    {
+      loader_.ScheduleLoadInstance(instanceId, frame);
+    }
+
+    void SetObserver(IVolumeSlicesObserver& observer)
+    {
+      if (observer_ == NULL)
+      {
+        observer_ = &observer;
+      }
+      else
+      {
+        // Cannot add more than one observer
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    }
+
+  };
 }
 
 
-TEST(Toto, Tutu)
+TEST(Toto, DISABLED_Tutu)
 {
   OrthancStone::Oracle oracle(4);
   oracle.Start();
@@ -98,6 +286,24 @@
 }
 
 
+TEST(Toto, Tata)
+{
+  OrthancStone::Oracle oracle(4);
+  oracle.Start();
+
+  Orthanc::WebServiceParameters web;
+  OrthancStone::OracleWebService orthanc(oracle, web);
+  OrthancStone::OrthancVolumeImageLoader volume(orthanc);
+
+  volume.ScheduleLoadInstance("19816330-cb02e1cf-df3a8fe8-bf510623-ccefe9f5", 0);
+  //volume.ScheduleLoadSeries("318603c5-03e8cffc-a82b6ee1-3ccd3c1e-18d7e3bb"); // COMUNIX PET
+  //volume.ScheduleLoadSeries("5990e39c-51e5f201-fe87a54c-31a55943-e59ef80e"); // Delphine sagital
+
+  boost::this_thread::sleep(boost::posix_time::milliseconds(1000));
+
+  oracle.Stop();
+}
+
 
 int main(int argc, char **argv)
 {