changeset 421:f87f28624b96 cache-in-radiography

tentative to make SmartLoader and RadiographyScene work together (not really working)
author am@osimis.io
date Tue, 20 Nov 2018 16:35:29 +0100
parents 8bf717c4e497
children 6125640bffd6
files Applications/Samples/SingleFrameEditorApplication.h Framework/Layers/DicomSeriesVolumeSlicer.cpp Framework/Layers/DicomSeriesVolumeSlicer.h Framework/Layers/IVolumeSlicer.h Framework/Radiography/RadiographyScene.cpp Framework/Radiography/RadiographyScene.h Framework/SmartLoader.cpp Framework/SmartLoader.h Framework/StoneEnumerations.h Framework/Toolbox/OrthancSlicesLoader.cpp Framework/Toolbox/OrthancSlicesLoader.h Framework/Volumes/ISlicedVolume.h Resources/CMake/OrthancStoneConfiguration.cmake
diffstat 13 files changed, 319 insertions(+), 132 deletions(-) [+]
line wrap: on
line diff
--- a/Applications/Samples/SingleFrameEditorApplication.h	Mon Nov 19 12:45:37 2018 +0100
+++ b/Applications/Samples/SingleFrameEditorApplication.h	Tue Nov 20 16:35:29 2018 +0100
@@ -31,6 +31,7 @@
 #include "../../Framework/Radiography/RadiographySceneCommand.h"
 #include "../../Framework/Radiography/RadiographyWidget.h"
 #include "../../Framework/Radiography/RadiographyWindowingTracker.h"
+#include "../../Framework/SmartLoader.h"
 
 #include <Core/HttpClient.h>
 #include <Core/Images/FontRegistry.h>
@@ -382,6 +383,7 @@
     private:
       std::auto_ptr<RadiographyScene>  scene_;
       RadiographyEditorInteractor      interactor_;
+      std::auto_ptr<SmartLoader>       smartLoader_;
 
     public:
       SingleFrameEditorApplication(MessageBroker& broker) :
@@ -443,9 +445,13 @@
         Orthanc::FontRegistry fonts;
         fonts.AddFromResource(Orthanc::EmbeddedResources::FONT_UBUNTU_MONO_BOLD_16);
         
+        smartLoader_.reset(new SmartLoader(IObserver::GetBroker(), context->GetOrthancApiClient()));
+        smartLoader_->SetImageQuality(SliceImageQuality_FullPam);
+
         scene_.reset(new RadiographyScene(GetBroker()));
         //scene_->LoadDicomFrame(instance, frame, false); //.SetPan(200, 0);
-        scene_->LoadDicomFrame(context->GetOrthancApiClient(), "61f3143e-96f34791-ad6bbb8d-62559e75-45943e1b", 0, false);
+        //scene_->LoadDicomFrame(context->GetOrthancApiClient(), "61f3143e-96f34791-ad6bbb8d-62559e75-45943e1b", 0, false);
+        smartLoader_->SetFrameInRadiographyScene(*scene_, "61f3143e-96f34791-ad6bbb8d-62559e75-45943e1b", 0);
 
 #if !defined(ORTHANC_ENABLE_WASM) || ORTHANC_ENABLE_WASM != 1
         Orthanc::HttpClient::ConfigureSsl(true, "/etc/ssl/certs/ca-certificates.crt");
--- a/Framework/Layers/DicomSeriesVolumeSlicer.cpp	Mon Nov 19 12:45:37 2018 +0100
+++ b/Framework/Layers/DicomSeriesVolumeSlicer.cpp	Tue Nov 20 16:35:29 2018 +0100
@@ -32,6 +32,11 @@
 namespace OrthancStone
 {
 
+  void DicomSeriesVolumeSlicer::OnSliceTagsReady(const OrthancSlicesLoader::SliceTagsReadyMessage& message)
+  {
+    EmitMessage(IVolumeSlicer::TagsReadyMessage(*this, message.GetDicomTags()));
+  }
+
   void DicomSeriesVolumeSlicer::OnSliceGeometryReady(const OrthancSlicesLoader::SliceGeometryReadyMessage& message)
   {
     if (message.GetOrigin().GetSliceCount() > 0)
@@ -94,6 +99,7 @@
     quality_(SliceImageQuality_FullPng)
   {
     loader_.RegisterObserverCallback(new Callable<DicomSeriesVolumeSlicer, OrthancSlicesLoader::SliceGeometryReadyMessage>(*this, &DicomSeriesVolumeSlicer::OnSliceGeometryReady));
+    loader_.RegisterObserverCallback(new Callable<DicomSeriesVolumeSlicer, OrthancSlicesLoader::SliceTagsReadyMessage>(*this, &DicomSeriesVolumeSlicer::OnSliceTagsReady));
     loader_.RegisterObserverCallback(new Callable<DicomSeriesVolumeSlicer, OrthancSlicesLoader::SliceGeometryErrorMessage>(*this, &DicomSeriesVolumeSlicer::OnSliceGeometryError));
     loader_.RegisterObserverCallback(new Callable<DicomSeriesVolumeSlicer, OrthancSlicesLoader::SliceImageReadyMessage>(*this, &DicomSeriesVolumeSlicer::OnSliceImageReady));
     loader_.RegisterObserverCallback(new Callable<DicomSeriesVolumeSlicer, OrthancSlicesLoader::SliceImageErrorMessage>(*this, &DicomSeriesVolumeSlicer::OnSliceImageError));
--- a/Framework/Layers/DicomSeriesVolumeSlicer.h	Mon Nov 19 12:45:37 2018 +0100
+++ b/Framework/Layers/DicomSeriesVolumeSlicer.h	Tue Nov 20 16:35:29 2018 +0100
@@ -117,6 +117,7 @@
 
 protected:
     void OnSliceGeometryReady(const OrthancSlicesLoader::SliceGeometryReadyMessage& message);
+    void OnSliceTagsReady(const OrthancSlicesLoader::SliceTagsReadyMessage& message);
     void OnSliceGeometryError(const OrthancSlicesLoader::SliceGeometryErrorMessage& message);
     void OnSliceImageReady(const OrthancSlicesLoader::SliceImageReadyMessage& message);
     void OnSliceImageError(const OrthancSlicesLoader::SliceImageErrorMessage& message);
--- a/Framework/Layers/IVolumeSlicer.h	Mon Nov 19 12:45:37 2018 +0100
+++ b/Framework/Layers/IVolumeSlicer.h	Tue Nov 20 16:35:29 2018 +0100
@@ -26,10 +26,12 @@
 #include "../../Framework/Messages/IObservable.h"
 #include "../../Framework/Messages/IMessage.h"
 #include "Core/Images/Image.h"
+#include <Plugins/Samples/Common/FullOrthancDataset.h>
 #include <boost/shared_ptr.hpp>
 
 namespace OrthancStone
 {
+
   class IVolumeSlicer : public IObservable
   {
   public:
@@ -37,6 +39,41 @@
     typedef OriginMessage<MessageType_VolumeSlicer_GeometryError, IVolumeSlicer>  GeometryErrorMessage;
     typedef OriginMessage<MessageType_VolumeSlicer_ContentChanged, IVolumeSlicer> ContentChangedMessage;
 
+    class TagsReadyMessage : public OriginMessage<MessageType_VolumeSlicer_TagsReady, IVolumeSlicer>
+    {
+    private:
+      const OrthancPlugins::FullOrthancDataset& dicomTags_;
+    public:
+      TagsReadyMessage(IVolumeSlicer& origin,
+                       const OrthancPlugins::FullOrthancDataset& dicomTags) :
+        OriginMessage(origin),
+        dicomTags_(dicomTags)
+      {
+      }
+
+      const OrthancPlugins::FullOrthancDataset& GetDicomTags() const
+      {
+        return dicomTags_;
+      }
+    };
+
+    class FrameReadyMessage : public OriginMessage<MessageType_VolumeSlicer_FrameReady, IVolumeSlicer>
+    {
+    private:
+      boost::shared_ptr<Orthanc::ImageAccessor> image_;
+    public:
+      FrameReadyMessage(IVolumeSlicer& origin, boost::shared_ptr<Orthanc::ImageAccessor> image) :
+        OriginMessage(origin),
+        image_(image)
+      {
+      }
+
+      boost::shared_ptr<Orthanc::ImageAccessor> GetImage() const
+      {
+        return image_;
+      }
+    };
+
     class SliceContentChangedMessage : public OriginMessage<MessageType_VolumeSlicer_SliceChanged, IVolumeSlicer>
     {
     private:
@@ -44,7 +81,7 @@
 
     public:
       SliceContentChangedMessage(IVolumeSlicer& origin,
-                          const Slice& slice) :
+                                 const Slice& slice) :
         OriginMessage(origin),
         slice_(slice)
       {
@@ -69,7 +106,7 @@
 
         virtual ILayerRenderer* CreateRenderer() const = 0;
       };
-    
+
     private:
       const IRendererFactory&    factory_;
       const CoordinateSystem3D&  slice_;
--- a/Framework/Radiography/RadiographyScene.cpp	Mon Nov 19 12:45:37 2018 +0100
+++ b/Framework/Radiography/RadiographyScene.cpp	Tue Nov 20 16:35:29 2018 +0100
@@ -55,7 +55,7 @@
     }
   }
 
-      
+
   RadiographyScene::LayerAccessor::LayerAccessor(RadiographyScene& scene,
                                                  double x,
                                                  double y) :
@@ -65,7 +65,7 @@
     if (scene.LookupLayer(index_, x, y))
     {
       Layers::iterator layer = scene.layers_.find(index_);
-          
+
       if (layer == scene.layers_.end())
       {
         throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
@@ -119,7 +119,7 @@
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
     }
-  }    
+  }
 
 
 
@@ -145,12 +145,12 @@
       useWindowing_ = false;
       foreground_ = foreground;
     }
-      
-      
+
+
     void SetAlpha(Orthanc::ImageAccessor* image)
     {
       std::auto_ptr<Orthanc::ImageAccessor> raii(image);
-        
+
       if (image == NULL)
       {
         throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
@@ -170,7 +170,7 @@
                   const std::string& utf8)
     {
       SetAlpha(font.RenderAlpha(utf8));
-    }                   
+    }
 
 
     virtual bool GetDefaultWindowing(float& center,
@@ -178,7 +178,7 @@
     {
       return false;
     }
-      
+
 
     virtual void Render(Orthanc::ImageAccessor& buffer,
                         const AffineTransform2D& viewTransform,
@@ -188,7 +188,7 @@
       {
         return;
       }
-        
+
       if (buffer.GetFormat() != Orthanc::PixelFormat_Float32)
       {
         throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
@@ -198,12 +198,12 @@
       GetCrop(cropX, cropY, cropWidth, cropHeight);
 
       const AffineTransform2D t = AffineTransform2D::Combine(
-        viewTransform, GetTransform(),
-        AffineTransform2D::CreateOffset(cropX, cropY));
+            viewTransform, GetTransform(),
+            AffineTransform2D::CreateOffset(cropX, cropY));
 
       Orthanc::ImageAccessor cropped;
       alpha_->GetRegion(cropped, cropX, cropY, cropWidth, cropHeight);
-        
+
       Orthanc::Image tmp(Orthanc::PixelFormat_Grayscale8, buffer.GetWidth(), buffer.GetHeight(), false);
       
       t.Apply(tmp, cropped, interpolation, true /* clear */);
@@ -213,7 +213,7 @@
       const unsigned int height = buffer.GetHeight();
 
       float value = foreground_;
-        
+
       if (useWindowing_)
       {
         float center, width;
@@ -222,7 +222,7 @@
           value = center + width / 2.0f;
         }
       }
-        
+
       for (unsigned int y = 0; y < height; y++)
       {
         float *q = reinterpret_cast<float*>(buffer.GetRow(y));
@@ -231,13 +231,13 @@
         for (unsigned int x = 0; x < width; x++, p++, q++)
         {
           float a = static_cast<float>(*p) / 255.0f;
-            
+
           *q = (a * value + (1.0f - a) * (*q));
         }
-      }        
+      }
     }
 
-      
+
     virtual bool GetRange(float& minValue,
                           float& maxValue) const
     {
@@ -264,13 +264,13 @@
       }
     }
   };
-    
-    
+
+
 
   class RadiographyScene::DicomLayer : public RadiographyLayer
   {
   private:
-    std::auto_ptr<Orthanc::ImageAccessor>  source_;  // Content of PixelData
+    boost::shared_ptr<Orthanc::ImageAccessor>  source_;  // Content of PixelData
     std::auto_ptr<DicomFrameConverter>     converter_;
     std::auto_ptr<Orthanc::ImageAccessor>  converted_;  // Float32
 
@@ -278,7 +278,7 @@
     {
       return OrthancPlugins::DicomTag(tag.GetGroup(), tag.GetElement());
     }
-      
+
 
     void ApplyConverter()
     {
@@ -288,7 +288,7 @@
         converted_.reset(converter_->ConvertFrame(*source_));
       }
     }
-      
+
   public:
     void SetDicomTags(const OrthancPlugins::FullOrthancDataset& dataset)
     {
@@ -298,7 +298,7 @@
 
       std::string tmp;
       Vector pixelSpacing;
-        
+
       if (dataset.GetStringValue(tmp, ConvertTag(Orthanc::DICOM_TAG_PIXEL_SPACING)) &&
           LinearAlgebra::ParseVector(pixelSpacing, tmp) &&
           pixelSpacing.size() == 2)
@@ -322,23 +322,21 @@
       }
     }
 
-      
-    void SetSourceImage(Orthanc::ImageAccessor* image)   // Takes ownership
+
+    void SetSourceImage(boost::shared_ptr<Orthanc::ImageAccessor> image)
     {
-      std::auto_ptr<Orthanc::ImageAccessor> raii(image);
-        
-      if (image == NULL)
+      if (image.get() == NULL)
       {
         throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
       }
 
       SetSize(image->GetWidth(), image->GetHeight());
-        
-      source_ = raii;
+
+      source_ = image;
       ApplyConverter();
     }
 
-      
+
     virtual void Render(Orthanc::ImageAccessor& buffer,
                         const AffineTransform2D& viewTransform,
                         ImageInterpolation interpolation) const
@@ -354,8 +352,8 @@
         GetCrop(cropX, cropY, cropWidth, cropHeight);
 
         AffineTransform2D t = AffineTransform2D::Combine(
-          viewTransform, GetTransform(),
-          AffineTransform2D::CreateOffset(cropX, cropY));
+              viewTransform, GetTransform(),
+              AffineTransform2D::CreateOffset(cropX, cropY));
 
         Orthanc::ImageAccessor cropped;
         converted_->GetRegion(cropped, cropX, cropY, cropWidth, cropHeight);
@@ -411,7 +409,7 @@
     }
 
     std::auto_ptr<RadiographyLayer> raii(layer);
-      
+
     size_t index = countLayers_++;
     raii->SetIndex(index);
     layers_[index] = raii.release();
@@ -421,7 +419,7 @@
 
     return *layer;
   }
-    
+
 
   RadiographyScene::RadiographyScene(MessageBroker& broker) :
     IObserver(broker),
@@ -489,7 +487,7 @@
     return RegisterLayer(alpha.release());
   }
 
-    
+
   RadiographyLayer& RadiographyScene::LoadTestBlock(unsigned int width,
                                                     unsigned int height)
   {
@@ -520,7 +518,7 @@
     return RegisterLayer(alpha.release());
   }
 
-    
+
   RadiographyLayer& RadiographyScene::LoadDicomFrame(OrthancApiClient& orthanc,
                                                      const std::string& instance,
                                                      unsigned int frame,
@@ -531,12 +529,12 @@
     {
       IWebService::HttpHeaders headers;
       std::string uri = "/instances/" + instance + "/tags";
-        
+
       orthanc.GetBinaryAsync(
-        uri, headers,
-        new Callable<RadiographyScene, OrthancApiClient::BinaryResponseReadyMessage>
-        (*this, &RadiographyScene::OnTagsReceived), NULL,
-        new Orthanc::SingleValueObject<size_t>(layer.GetIndex()));
+            uri, headers,
+            new Callable<RadiographyScene, OrthancApiClient::BinaryResponseReadyMessage>
+            (*this, &RadiographyScene::OnTagsReceived), NULL,
+            new Orthanc::SingleValueObject<size_t>(layer.GetIndex()));
     }
 
     {
@@ -547,15 +545,15 @@
       {
         headers["Accept-Encoding"] = "gzip";
       }
-        
+
       std::string uri = ("/instances/" + instance + "/frames/" +
                          boost::lexical_cast<std::string>(frame) + "/image-uint16");
-        
+
       orthanc.GetBinaryAsync(
-        uri, headers,
-        new Callable<RadiographyScene, OrthancApiClient::BinaryResponseReadyMessage>
-        (*this, &RadiographyScene::OnFrameReceived), NULL,
-        new Orthanc::SingleValueObject<size_t>(layer.GetIndex()));
+            uri, headers,
+            new Callable<RadiographyScene, OrthancApiClient::BinaryResponseReadyMessage>
+            (*this, &RadiographyScene::OnFrameReceived), NULL,
+            new Orthanc::SingleValueObject<size_t>(layer.GetIndex()));
     }
 
     return layer;
@@ -566,25 +564,67 @@
   {
     RadiographyLayer& layer = RegisterLayer(new DicomLayer);
 
-      
+
+    return layer;
+  }
+
+  RadiographyLayer& RadiographyScene::SetFrame(IVolumeSlicer* slice)
+  {
+    RadiographyLayer& layer = RegisterLayer(new DicomLayer);
+    layersIndexBySlice_[slice] = layer.GetIndex();
+
+    slice->RegisterObserverCallback(new Callable<RadiographyScene, IVolumeSlicer::TagsReadyMessage>
+                                    (*this, &RadiographyScene::OnTagsReady));
+    slice->RegisterObserverCallback(new Callable<RadiographyScene, IVolumeSlicer::FrameReadyMessage>
+                                    (*this, &RadiographyScene::OnImageReady));
+
+    // TODO: register failures
+
     return layer;
   }
 
+  void RadiographyScene::OnTagsReady(const IVolumeSlicer::TagsReadyMessage &message)
+  {
+    size_t layerIndex = layersIndexBySlice_[&(message.GetOrigin())];
+    Layers::iterator layer = layers_.find(layerIndex);
 
-    
+    if (layer != layers_.end())
+    {
+      assert(layer->second != NULL);
+
+      const OrthancPlugins::FullOrthancDataset& dicom = message.GetDicomTags();
+      dynamic_cast<DicomLayer*>(layer->second)->SetDicomTags(dicom);
+
+      float c, w;
+      if (!hasWindowing_ &&
+          layer->second->GetDefaultWindowing(c, w))
+      {
+        hasWindowing_ = true;
+        windowingCenter_ = c;
+        windowingWidth_ = w;
+      }
+
+      EmitMessage(GeometryChangedMessage(*this));
+
+      CoordinateSystem3D dummy;
+      message.GetOrigin().ScheduleLayerCreation(dummy);
+    }
+
+  }
+
   void RadiographyScene::OnTagsReceived(const OrthancApiClient::BinaryResponseReadyMessage& message)
   {
     size_t index = dynamic_cast<const Orthanc::SingleValueObject<size_t>&>
-      (message.GetPayload()).GetValue();
+        (message.GetPayload()).GetValue();
 
     LOG(INFO) << "JSON received: " << message.GetUri().c_str()
               << " (" << message.GetAnswerSize() << " bytes) for layer " << index;
-      
+
     Layers::iterator layer = layers_.find(index);
     if (layer != layers_.end())
     {
       assert(layer->second != NULL);
-        
+
       OrthancPlugins::FullOrthancDataset dicom(message.GetAnswer(), message.GetAnswerSize());
       dynamic_cast<DicomLayer*>(layer->second)->SetDicomTags(dicom);
 
@@ -600,15 +640,30 @@
       EmitMessage(GeometryChangedMessage(*this));
     }
   }
-    
+
+
+  void RadiographyScene::OnImageReady(const IVolumeSlicer::FrameReadyMessage &message)
+  {
+    size_t layerIndex = layersIndexBySlice_[&(message.GetOrigin())];
+    Layers::iterator layer = layers_.find(layerIndex);
+
+    if (layer != layers_.end())
+    {
+      assert(layer->second != NULL);
+
+      dynamic_cast<DicomLayer*>(layer->second)->SetSourceImage(message.GetImage());
+
+      EmitMessage(ContentChangedMessage(*this));
+    }
+  }
 
   void RadiographyScene::OnFrameReceived(const OrthancApiClient::BinaryResponseReadyMessage& message)
   {
     size_t index = dynamic_cast<const Orthanc::SingleValueObject<size_t>&>(message.GetPayload()).GetValue();
-      
+
     LOG(INFO) << "DICOM frame received: " << message.GetUri().c_str()
               << " (" << message.GetAnswerSize() << " bytes) for layer " << index;
-      
+
     Layers::iterator layer = layers_.find(index);
     if (layer != layers_.end())
     {
@@ -619,10 +674,10 @@
       {
         content.assign(reinterpret_cast<const char*>(message.GetAnswer()), message.GetAnswerSize());
       }
-        
-      std::auto_ptr<Orthanc::PamReader> reader(new Orthanc::PamReader);
+
+      boost::shared_ptr<Orthanc::PamReader> reader(new Orthanc::PamReader);
       reader->ReadFromMemory(content);
-      dynamic_cast<DicomLayer*>(layer->second)->SetSourceImage(reader.release());
+      dynamic_cast<DicomLayer*>(layer->second)->SetSourceImage(reader);
 
       EmitMessage(ContentChangedMessage(*this));
     }
@@ -642,7 +697,7 @@
 
     return extent;
   }
-    
+
 
   void RadiographyScene::Render(Orthanc::ImageAccessor& buffer,
                                 const AffineTransform2D& viewTransform,
@@ -685,13 +740,13 @@
     return false;
   }
 
-    
+
   void RadiographyScene::DrawBorder(CairoContext& context,
                                     unsigned int layer,
                                     double zoom)
   {
     Layers::const_iterator found = layers_.find(layer);
-        
+
     if (found != layers_.end())
     {
       context.SetSourceColor(255, 0, 0);
@@ -704,7 +759,7 @@
                                   float& maxValue) const
   {
     bool first = true;
-      
+
     for (Layers::const_iterator it = layers_.begin();
          it != layers_.end(); it++)
     {
@@ -750,7 +805,7 @@
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
     }
-      
+
     LOG(INFO) << "Exporting DICOM";
 
     Extent2D extent = GetSceneExtent();
@@ -768,9 +823,9 @@
                           static_cast<unsigned int>(h), false);
 
     AffineTransform2D view = AffineTransform2D::Combine(
-      AffineTransform2D::CreateScaling(1.0 / pixelSpacingX, 1.0 / pixelSpacingY),
-      AffineTransform2D::CreateOffset(-extent.GetX1(), -extent.GetY1()));
-      
+          AffineTransform2D::CreateScaling(1.0 / pixelSpacingX, 1.0 / pixelSpacingY),
+          AffineTransform2D::CreateOffset(-extent.GetX1(), -extent.GetY1()));
+
     Render(layers, view, interpolation);
 
     Orthanc::Image rendered(Orthanc::PixelFormat_Grayscale16,
@@ -801,9 +856,9 @@
 
     Json::Value json = Json::objectValue;
     json["Tags"] = Json::objectValue;
-           
+
     for (std::set<Orthanc::DicomTag>::const_iterator
-           tag = tags.begin(); tag != tags.end(); ++tag)
+         tag = tags.begin(); tag != tags.end(); ++tag)
     {
       const Orthanc::DicomValue& value = dicom.GetValue(*tag);
       if (!value.IsNull() &&
@@ -814,7 +869,7 @@
     }
 
     json["Tags"][Orthanc::DICOM_TAG_PHOTOMETRIC_INTERPRETATION.Format()] =
-      (invert ? "MONOCHROME1" : "MONOCHROME2");
+        (invert ? "MONOCHROME1" : "MONOCHROME2");
 
     // WARNING: The order of PixelSpacing is Y/X. We use "%0.8f" to
     // avoid floating-point numbers to grow over 16 characters,
@@ -822,17 +877,17 @@
     // ("dciodvfy" would complain).
     char buf[32];
     sprintf(buf, "%0.8f\\%0.8f", pixelSpacingY, pixelSpacingX);
-      
+
     json["Tags"][Orthanc::DICOM_TAG_PIXEL_SPACING.Format()] = buf;
 
     float center, width;
     if (GetWindowing(center, width))
     {
       json["Tags"][Orthanc::DICOM_TAG_WINDOW_CENTER.Format()] =
-        boost::lexical_cast<std::string>(boost::math::iround(center));
+          boost::lexical_cast<std::string>(boost::math::iround(center));
 
       json["Tags"][Orthanc::DICOM_TAG_WINDOW_WIDTH.Format()] =
-        boost::lexical_cast<std::string>(boost::math::iround(width));
+          boost::lexical_cast<std::string>(boost::math::iround(width));
     }
 
     // This is Data URI scheme: https://en.wikipedia.org/wiki/Data_URI_scheme
@@ -841,10 +896,10 @@
                        ";base64," + base64);
 
     orthanc.PostJsonAsyncExpectJson(
-      "/tools/create-dicom", json,
-      new Callable<RadiographyScene, OrthancApiClient::JsonResponseReadyMessage>
-      (*this, &RadiographyScene::OnDicomExported),
-      NULL, NULL);
+          "/tools/create-dicom", json,
+          new Callable<RadiographyScene, OrthancApiClient::JsonResponseReadyMessage>
+          (*this, &RadiographyScene::OnDicomExported),
+          NULL, NULL);
   }
 
 
@@ -861,7 +916,7 @@
 
     const IWebService::HttpHeaders& h = message.GetAnswerHttpHeaders();
     for (IWebService::HttpHeaders::const_iterator
-           it = h.begin(); it != h.end(); ++it)
+         it = h.begin(); it != h.end(); ++it)
     {
       printf("[%s] = [%s]\n", it->first.c_str(), it->second.c_str());
     }
--- a/Framework/Radiography/RadiographyScene.h	Mon Nov 19 12:45:37 2018 +0100
+++ b/Framework/Radiography/RadiographyScene.h	Tue Nov 20 16:35:29 2018 +0100
@@ -22,11 +22,14 @@
 #pragma once
 
 #include "RadiographyLayer.h"
+#include <Framework/Layers/IVolumeSlicer.h>
 #include "../Toolbox/OrthancApiClient.h"
 
 
 namespace OrthancStone
 {
+  class IVolumeSlicer;
+
   class RadiographyScene :
     public IObserver,
     public IObservable
@@ -80,6 +83,8 @@
     float   windowingWidth_;
     Layers  layers_;
 
+    std::map<const IVolumeSlicer*, size_t> layersIndexBySlice_;
+
     RadiographyLayer& RegisterLayer(RadiographyLayer* layer);
 
     void OnTagsReceived(const OrthancApiClient::BinaryResponseReadyMessage& message);
@@ -90,6 +95,10 @@
 
     void OnDicomWebReceived(const IWebService::HttpRequestSuccessMessage& message);
 
+    void OnTagsReady(const IVolumeSlicer::TagsReadyMessage& message);
+
+    void OnImageReady(const IVolumeSlicer::FrameReadyMessage& message);
+
   public:
     RadiographyScene(MessageBroker& broker);
     
@@ -117,6 +126,8 @@
 
     RadiographyLayer& LoadDicomWebFrame(IWebService& web);
 
+    RadiographyLayer& SetFrame(IVolumeSlicer* slice);
+
     Extent2D GetSceneExtent() const;
 
     void Render(Orthanc::ImageAccessor& buffer,
--- a/Framework/SmartLoader.cpp	Mon Nov 19 12:45:37 2018 +0100
+++ b/Framework/SmartLoader.cpp	Tue Nov 20 16:35:29 2018 +0100
@@ -27,6 +27,7 @@
 #include "Framework/StoneException.h"
 #include "Framework/Layers/FrameRenderer.h"
 #include "Core/Logging.h"
+#include "Radiography/RadiographyScene.h"
 
 namespace OrthancStone
 {
@@ -62,6 +63,8 @@
     
     unsigned int                    sliceIndex_;
     std::auto_ptr<Slice>            slice_;
+    std::auto_ptr<OrthancPlugins::FullOrthancDataset> dicomTags_;
+
     boost::shared_ptr<Orthanc::ImageAccessor>   image_;
     SliceImageQuality               effectiveQuality_;
     CachedSliceStatus               status_;
@@ -95,6 +98,7 @@
         LOG(WARNING) << "ScheduleLayerCreation for CachedSlice (image is loaded): " << slice_->GetOrthancInstanceId();
 
         RendererFactory factory(*this);   
+        EmitMessage(IVolumeSlicer::FrameReadyMessage(*this, image_));
         EmitMessage(IVolumeSlicer::LayerReadyMessage(*this, factory, slice_->GetGeometry()));
       }
       else
@@ -127,38 +131,69 @@
   {
   }
 
+  void SmartLoader::SetFrameInRadiographyScene(RadiographyScene& scene, const std::string& instanceId, unsigned int frame)
+  {
+    // create the frame loader
+    std::auto_ptr<IVolumeSlicer> layerSource(GetFrameLoader(instanceId, frame));
+    IVolumeSlicer* layerSource2 = layerSource.get();
+
+    // make sure that the widget registers the events before we trigger them (once we start loading the frame)
+    scene.SetFrame(layerSource.release());
+
+    // start loading
+    LoadFrame(layerSource2, instanceId, frame);
+  }
+
+
+  IVolumeSlicer* SmartLoader::GetFrameLoader(const std::string &instanceId, unsigned int frame)
+  {
+    std::auto_ptr<IVolumeSlicer> layerSource;
+    std::string sliceKeyId = instanceId + ":" + boost::lexical_cast<std::string>(frame);
+
+
+    if (cachedSlices_.find(sliceKeyId) != cachedSlices_.end()) // && cachedSlices_[sliceKeyId]->status_ == CachedSliceStatus_Loaded)
+    { // if the image is cached, return a clone of the cached image
+      layerSource.reset(cachedSlices_[sliceKeyId]->Clone());
+    }
+    else
+    { // return a standard frame loader
+      layerSource.reset(new DicomSeriesVolumeSlicer(IObserver::GetBroker(), orthancApiClient_));
+      dynamic_cast<DicomSeriesVolumeSlicer*>(layerSource.get())->SetImageQuality(imageQuality_);
+      layerSource->RegisterObserverCallback(new Callable<SmartLoader, IVolumeSlicer::TagsReadyMessage>(*this, &SmartLoader::OnLayerTagsReady));
+      layerSource->RegisterObserverCallback(new Callable<SmartLoader, DicomSeriesVolumeSlicer::FrameReadyMessage>(*this, &SmartLoader::OnFrameReady));
+      layerSource->RegisterObserverCallback(new Callable<SmartLoader, IVolumeSlicer::LayerReadyMessage>(*this, &SmartLoader::OnLayerReady));
+    }
+
+    return layerSource.release();
+  }
+
+  void SmartLoader::LoadFrame(IVolumeSlicer *frameLoader, const std::string &instanceId, unsigned int frame)
+  {
+    SmartLoader::CachedSlice* cachedSlice = dynamic_cast<SmartLoader::CachedSlice*>(frameLoader);
+
+    if (cachedSlice != NULL)
+    {
+      EmitMessage(IVolumeSlicer::GeometryReadyMessage(*cachedSlice));
+      EmitMessage(IVolumeSlicer::TagsReadyMessage(*cachedSlice, *(cachedSlice->dicomTags_.get())));
+    }
+    else
+    {
+      DicomSeriesVolumeSlicer* volumeLoader = dynamic_cast<DicomSeriesVolumeSlicer*>(frameLoader);
+      volumeLoader->LoadFrame(instanceId, frame);
+    }
+
+  }
+
   void SmartLoader::SetFrameInWidget(SliceViewerWidget& sliceViewer, 
                                      size_t layerIndex, 
                                      const std::string& instanceId, 
                                      unsigned int frame)
   {
-    // TODO: check if this frame has already been loaded or is already being loaded.
-    // - if already loaded: create a "clone" that will emit the GeometryReady/ImageReady messages "immediately"
-    //   (it can not be immediate because Observers needs to register first and this is done after this method returns)
-    // - if currently loading, we need to return an object that will observe the existing VolumeSlicer and forward
-    //   the messages to its observables
-    // in both cases, we must be carefull about objects lifecycle !!!
-
-    std::auto_ptr<IVolumeSlicer> layerSource;
-    std::string sliceKeyId = instanceId + ":" + boost::lexical_cast<std::string>(frame);
-    SmartLoader::CachedSlice* cachedSlice = NULL;
+    // create the frame loader
+    std::auto_ptr<IVolumeSlicer> layerSource(GetFrameLoader(instanceId, frame));
+    IVolumeSlicer* layerSource2 = layerSource.get();
 
-    if (cachedSlices_.find(sliceKeyId) != cachedSlices_.end()) // && cachedSlices_[sliceKeyId]->status_ == CachedSliceStatus_Loaded)
-    {
-      layerSource.reset(cachedSlices_[sliceKeyId]->Clone());
-      cachedSlice = dynamic_cast<SmartLoader::CachedSlice*>(layerSource.get());
-    }
-    else
-    {
-      layerSource.reset(new DicomSeriesVolumeSlicer(IObserver::GetBroker(), orthancApiClient_));
-      dynamic_cast<DicomSeriesVolumeSlicer*>(layerSource.get())->SetImageQuality(imageQuality_);
-      layerSource->RegisterObserverCallback(new Callable<SmartLoader, IVolumeSlicer::GeometryReadyMessage>(*this, &SmartLoader::OnLayerGeometryReady));
-      layerSource->RegisterObserverCallback(new Callable<SmartLoader, DicomSeriesVolumeSlicer::FrameReadyMessage>(*this, &SmartLoader::OnFrameReady));
-      layerSource->RegisterObserverCallback(new Callable<SmartLoader, IVolumeSlicer::LayerReadyMessage>(*this, &SmartLoader::OnLayerReady));
-      dynamic_cast<DicomSeriesVolumeSlicer*>(layerSource.get())->LoadFrame(instanceId, frame);
-    }
-
-    // make sure that the widget registers the events before we trigger them
+    // make sure that the widget registers the events before we trigger them (once we start loading the frame)
     if (sliceViewer.GetLayerCount() == layerIndex)
     {
       sliceViewer.AddLayer(layerSource.release());
@@ -172,11 +207,8 @@
       throw StoneException(ErrorCode_CanOnlyAddOneLayerAtATime);
     }
 
-    if (cachedSlice != NULL)
-    {
-      EmitMessage(IVolumeSlicer::GeometryReadyMessage(*cachedSlice));
-    }
-
+    // start loading
+    LoadFrame(layerSource2, instanceId, frame);
   }
 
   void SmartLoader::PreloadSlice(const std::string instanceId, 
@@ -201,7 +233,7 @@
     std::auto_ptr<IVolumeSlicer> layerSource(new DicomSeriesVolumeSlicer(IObserver::GetBroker(), orthancApiClient_));
 
     dynamic_cast<DicomSeriesVolumeSlicer*>(layerSource.get())->SetImageQuality(imageQuality_);
-    layerSource->RegisterObserverCallback(new Callable<SmartLoader, IVolumeSlicer::GeometryReadyMessage>(*this, &SmartLoader::OnLayerGeometryReady));
+    layerSource->RegisterObserverCallback(new Callable<SmartLoader, IVolumeSlicer::TagsReadyMessage>(*this, &SmartLoader::OnLayerTagsReady));
     layerSource->RegisterObserverCallback(new Callable<SmartLoader, DicomSeriesVolumeSlicer::FrameReadyMessage>(*this, &SmartLoader::OnFrameReady));
     layerSource->RegisterObserverCallback(new Callable<SmartLoader, IVolumeSlicer::LayerReadyMessage>(*this, &SmartLoader::OnLayerReady));
     dynamic_cast<DicomSeriesVolumeSlicer*>(layerSource.get())->LoadFrame(instanceId, frame);
@@ -222,7 +254,7 @@
 //  }
 
 
-  void SmartLoader::OnLayerGeometryReady(const IVolumeSlicer::GeometryReadyMessage& message)
+  void SmartLoader::OnLayerTagsReady(const IVolumeSlicer::TagsReadyMessage& message)
   {
     const DicomSeriesVolumeSlicer& source =
       dynamic_cast<const DicomSeriesVolumeSlicer&>(message.GetOrigin());
@@ -238,11 +270,12 @@
     cachedSlice->slice_.reset(slice.Clone());
     cachedSlice->effectiveQuality_ = source.GetImageQuality();
     cachedSlice->status_ = CachedSliceStatus_GeometryLoaded;
+    cachedSlice->dicomTags_.reset(message.GetDicomTags().Clone());
 
     cachedSlices_[sliceKeyId] = boost::shared_ptr<CachedSlice>(cachedSlice);
 
     // re-emit original Layer message to observers
-    EmitMessage(message);
+    // EmitMessage(message);
   }
 
 
@@ -264,7 +297,7 @@
     cachedSlices_[sliceKeyId] = cachedSlice;
 
     // re-emit original Layer message to observers
-    EmitMessage(message);
+//    EmitMessage(IVolumeSlicer::FrameReadyMessage(*(cachedSlice.get()), cachedSlice->image_));
   }
 
 
@@ -286,6 +319,6 @@
     }
 
     // re-emit original Layer message to observers
-    EmitMessage(message);
+//    EmitMessage(message);
   }
 }
--- a/Framework/SmartLoader.h	Mon Nov 19 12:45:37 2018 +0100
+++ b/Framework/SmartLoader.h	Tue Nov 20 16:35:29 2018 +0100
@@ -29,6 +29,7 @@
 namespace OrthancStone
 {
   class SliceViewerWidget;
+  class RadiographyScene;
 
   class SmartLoader : public IObservable, public IObserver
   {
@@ -57,11 +58,15 @@
 
     void GetFirstInstanceIdForSeries(std::string& output, const std::string& seriesId);
 
+    void SetFrameInRadiographyScene(RadiographyScene& scene, const std::string& instanceId, unsigned int frame);
+
   private:
-    void OnLayerGeometryReady(const IVolumeSlicer::GeometryReadyMessage& message);
+    void OnLayerTagsReady(const IVolumeSlicer::TagsReadyMessage& message);
     void OnFrameReady(const DicomSeriesVolumeSlicer::FrameReadyMessage& message);
     void OnLayerReady(const IVolumeSlicer::LayerReadyMessage& message);
 
+    IVolumeSlicer* GetFrameLoader(const std::string& instanceId, unsigned int frame);
+    void LoadFrame(IVolumeSlicer* frameLoader, const std::string &instanceId, unsigned int frame);
   };
 
 }
--- a/Framework/StoneEnumerations.h	Mon Nov 19 12:45:37 2018 +0100
+++ b/Framework/StoneEnumerations.h	Tue Nov 20 16:35:29 2018 +0100
@@ -120,7 +120,9 @@
     MessageType_Widget_GeometryChanged,
     MessageType_Widget_ContentChanged,
 
-    MessageType_VolumeSlicer_GeometryReady,   // instance tags have been loaded
+    MessageType_VolumeSlicer_GeometryReady,   // instance geometry is available
+    MessageType_VolumeSlicer_TagsReady,       // instance tags are available
+    MessageType_VolumeSlicer_FrameReady,      // pixels data are available
     MessageType_VolumeSlicer_GeometryError,
     MessageType_VolumeSlicer_ContentChanged,
     MessageType_VolumeSlicer_SliceChanged,
@@ -132,6 +134,7 @@
     MessageType_SliceViewerWidget_DisplayedSlice,  // The displayed slice has changed
 
     MessageType_SliceLoader_GeometryReady,
+    MessageType_SliceLoader_TagsReady,
     MessageType_SliceLoader_GeometryError,
     MessageType_SliceLoader_ImageReady,
     MessageType_SliceLoader_ImageError,
--- a/Framework/Toolbox/OrthancSlicesLoader.cpp	Mon Nov 19 12:45:37 2018 +0100
+++ b/Framework/Toolbox/OrthancSlicesLoader.cpp	Tue Nov 20 16:35:29 2018 +0100
@@ -189,7 +189,7 @@
   }
   
   
-  void OrthancSlicesLoader::SortAndFinalizeSlices()
+  void OrthancSlicesLoader::SortAndFinalizeSlices(const OrthancPlugins::FullOrthancDataset& dicomTags)
   {
     bool ok = false;
     
@@ -211,6 +211,7 @@
     {
       LOG(INFO) << "Loaded a series with " << slices_.GetSliceCount() << " slice(s)";
       EmitMessage(SliceGeometryReadyMessage(*this));
+      EmitMessage(SliceTagsReadyMessage(*this, dicomTags));
     }
     else
     {
@@ -236,6 +237,12 @@
     const Json::Value& series = message.GetJson();
     Json::Value::Members instances = series.getMemberNames();
     
+    if (instances.size() < 1)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem);
+    }
+    OrthancPlugins::FullOrthancDataset firstInstanceDataSet(series[instances[0]]);
+
     slices_.Reserve(instances.size());
     
     for (size_t i = 0; i < instances.size(); i++)
@@ -264,8 +271,8 @@
         }
       }
     }
-    
-    SortAndFinalizeSlices();
+
+    SortAndFinalizeSlices(firstInstanceDataSet);
   }
   
   void OrthancSlicesLoader::ParseInstanceGeometry(const OrthancApiClient::JsonResponseReadyMessage& message)
@@ -301,7 +308,7 @@
       }
     }
     
-    SortAndFinalizeSlices();
+    SortAndFinalizeSlices(dataset);
   }
   
   
@@ -324,6 +331,7 @@
       LOG(INFO) << "Loaded instance geometry " << instanceId;
       slices_.AddSlice(slice.release());
       EmitMessage(SliceGeometryReadyMessage(*this));
+      EmitMessage(SliceTagsReadyMessage(*this, dataset));
     }
     else
     {
--- a/Framework/Toolbox/OrthancSlicesLoader.h	Mon Nov 19 12:45:37 2018 +0100
+++ b/Framework/Toolbox/OrthancSlicesLoader.h	Tue Nov 20 16:35:29 2018 +0100
@@ -28,6 +28,7 @@
 #include "SlicesSorter.h"
 
 #include <Core/Images/Image.h>
+#include <Plugins/Samples/Common/FullOrthancDataset.h>
 
 
 namespace OrthancStone
@@ -39,8 +40,26 @@
     typedef OriginMessage<MessageType_SliceLoader_GeometryReady, OrthancSlicesLoader> SliceGeometryReadyMessage;
     typedef OriginMessage<MessageType_SliceLoader_GeometryError, OrthancSlicesLoader> SliceGeometryErrorMessage;
 
+    class SliceTagsReadyMessage : public OriginMessage<MessageType_SliceLoader_TagsReady, OrthancSlicesLoader>
+    {
+    private:
+      const OrthancPlugins::FullOrthancDataset& dicomTags_;
+    public:
+      SliceTagsReadyMessage(OrthancSlicesLoader& origin,
+                                const OrthancPlugins::FullOrthancDataset& dicomTags) :
+        OriginMessage(origin),
+        dicomTags_(dicomTags)
+      {
+      }
+
+      const OrthancPlugins::FullOrthancDataset& GetDicomTags() const
+      {
+        return dicomTags_;
+      }
+    };
+
     class SliceImageReadyMessage :
-      public OriginMessage<MessageType_SliceLoader_ImageReady, OrthancSlicesLoader>
+        public OriginMessage<MessageType_SliceLoader_ImageReady, OrthancSlicesLoader>
     {
     private:
       unsigned int                   sliceIndex_;
@@ -80,12 +99,12 @@
       SliceImageQuality GetEffectiveQuality() const
       {
         return effectiveQuality_;
-      }        
+      }
     };
     
 
-    class SliceImageErrorMessage : 
-      public OriginMessage<MessageType_SliceLoader_ImageError, OrthancSlicesLoader>
+    class SliceImageErrorMessage :
+        public OriginMessage<MessageType_SliceLoader_ImageError, OrthancSlicesLoader>
     {
     private:
       const Slice&       slice_;
@@ -116,7 +135,7 @@
       SliceImageQuality GetEffectiveQuality() const
       {
         return effectiveQuality_;
-      }        
+      }
     };
     
   private:
@@ -177,7 +196,7 @@
                                 size_t index,
                                 SliceImageQuality quality);
 
-    void SortAndFinalizeSlices();
+    void SortAndFinalizeSlices(const OrthancPlugins::FullOrthancDataset& dicomTags);
     
   public:
     OrthancSlicesLoader(MessageBroker& broker,
--- a/Framework/Volumes/ISlicedVolume.h	Mon Nov 19 12:45:37 2018 +0100
+++ b/Framework/Volumes/ISlicedVolume.h	Tue Nov 20 16:35:29 2018 +0100
@@ -13,7 +13,7 @@
  * WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  * Affero General Public License for more details.
- * 
+ *
  * You should have received a copy of the GNU Affero General Public License
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  **/
@@ -23,6 +23,7 @@
 
 #include "../Messages/IObservable.h"
 #include "../Toolbox/Slice.h"
+#include <Plugins/Samples/Common/FullOrthancDataset.h>
 
 namespace OrthancStone
 {
@@ -30,12 +31,12 @@
   {
   public:
     typedef OriginMessage<MessageType_SlicedVolume_ContentChanged, ISlicedVolume> ContentChangedMessage;
+    typedef OriginMessage<MessageType_SlicedVolume_GeometryReady, ISlicedVolume> GeometryReadyMessage;
     typedef OriginMessage<MessageType_SlicedVolume_GeometryError, ISlicedVolume> GeometryErrorMessage;
-    typedef OriginMessage<MessageType_SlicedVolume_GeometryReady, ISlicedVolume> GeometryReadyMessage;
     typedef OriginMessage<MessageType_SlicedVolume_VolumeReady, ISlicedVolume> VolumeReadyMessage;
 
     class SliceContentChangedMessage :
-      public OriginMessage<MessageType_SlicedVolume_SliceContentChanged, ISlicedVolume>
+        public OriginMessage<MessageType_SlicedVolume_SliceContentChanged, ISlicedVolume>
     {
     private:
       size_t        sliceIndex_;
--- a/Resources/CMake/OrthancStoneConfiguration.cmake	Mon Nov 19 12:45:37 2018 +0100
+++ b/Resources/CMake/OrthancStoneConfiguration.cmake	Tue Nov 20 16:35:29 2018 +0100
@@ -235,6 +235,7 @@
   #${ORTHANC_STONE_ROOT}/Framework/Layers/SeriesFrameRendererFactory.cpp
   #${ORTHANC_STONE_ROOT}/Framework/Layers/SingleFrameRendererFactory.cpp
 
+  ${ORTHANC_STONE_ROOT}/Framework/dev.h
   ${ORTHANC_STONE_ROOT}/Framework/Layers/CircleMeasureTracker.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Layers/ColorFrameRenderer.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Layers/DicomSeriesVolumeSlicer.cpp
@@ -286,6 +287,7 @@
   ${ORTHANC_STONE_ROOT}/Framework/Viewport/IViewport.h
   ${ORTHANC_STONE_ROOT}/Framework/Viewport/WidgetViewport.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Volumes/ImageBuffer3D.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Volumes/ISlicedVolume.h
   ${ORTHANC_STONE_ROOT}/Framework/Volumes/StructureSetLoader.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Volumes/VolumeReslicer.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Widgets/CairoWidget.cpp