changeset 1279:7ec8fea061b9 broker

integration mainline->broker
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 04 Feb 2020 15:20:08 +0100
parents 0ca50d275b9a (current diff) 398ea4259e65 (diff)
children 68579a31eeb4
files Applications/Generic/GuiAdapter.cpp Applications/Generic/GuiAdapter.h Applications/Generic/NativeStoneApplicationContext.cpp Applications/Generic/NativeStoneApplicationContext.h Applications/Generic/NativeStoneApplicationRunner.cpp Applications/Generic/NativeStoneApplicationRunner.h Applications/IStoneApplication.h Applications/Qt/QCairoWidget.cpp Applications/Samples/SampleApplicationBase.h Applications/Samples/SampleMainNative.cpp Applications/Samples/SimpleViewerApplicationSingleFile.h Applications/Samples/SingleFrameApplication.h Applications/Samples/SingleFrameEditorApplication.h Applications/Sdl/SdlCairoSurface.h Applications/Sdl/SdlEngine.cpp Applications/Sdl/SdlEngine.h Applications/Sdl/SdlOrthancSurface.cpp Applications/Sdl/SdlStoneApplicationRunner.cpp Applications/Sdl/SdlStoneApplicationRunner.h Applications/StoneApplicationContext.cpp Applications/StoneApplicationContext.h Framework/Deprecated/Layers/DicomSeriesVolumeSlicer.cpp Framework/Deprecated/Layers/DicomSeriesVolumeSlicer.h Framework/Deprecated/Layers/DicomStructureSetSlicer.cpp Framework/Deprecated/Layers/DicomStructureSetSlicer.h Framework/Deprecated/Layers/IVolumeSlicer.h Framework/Deprecated/Loaders/DicomStructureSetLoader.cpp Framework/Deprecated/Loaders/DicomStructureSetLoader.h Framework/Deprecated/Loaders/DicomStructureSetLoader2.cpp Framework/Deprecated/Loaders/DicomStructureSetLoader2.h Framework/Deprecated/Loaders/LoaderCache.cpp Framework/Deprecated/Loaders/LoaderCache.h Framework/Deprecated/Loaders/LoaderStateMachine.cpp Framework/Deprecated/Loaders/LoaderStateMachine.h Framework/Deprecated/Loaders/OrthancMultiframeVolumeLoader.cpp Framework/Deprecated/Loaders/OrthancMultiframeVolumeLoader.h Framework/Deprecated/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp Framework/Deprecated/Loaders/OrthancSeriesVolumeProgressiveLoader.h Framework/Deprecated/Messages/LockingEmitter.h Framework/Deprecated/SmartLoader.cpp Framework/Deprecated/SmartLoader.h Framework/Deprecated/Toolbox/IWebService.h Framework/Deprecated/Toolbox/OrthancApiClient.cpp Framework/Deprecated/Toolbox/OrthancApiClient.h Framework/Deprecated/Toolbox/OrthancSlicesLoader.cpp Framework/Deprecated/Toolbox/OrthancSlicesLoader.h Framework/Deprecated/Viewport/IViewport.h Framework/Deprecated/Viewport/WidgetViewport.cpp Framework/Deprecated/Viewport/WidgetViewport.h Framework/Deprecated/Volumes/ISlicedVolume.h Framework/Deprecated/Volumes/IVolumeLoader.h Framework/Deprecated/Volumes/StructureSetLoader.cpp Framework/Deprecated/Volumes/StructureSetLoader.h Framework/Deprecated/Widgets/LayoutWidget.cpp Framework/Deprecated/Widgets/LayoutWidget.h Framework/Deprecated/Widgets/SliceViewerWidget.cpp Framework/Deprecated/Widgets/SliceViewerWidget.h Framework/Fonts/GlyphTextureAlphabet.cpp Framework/Messages/ICallable.h Framework/Messages/IMessage.h Framework/Messages/IMessageEmitter.h Framework/Messages/IObservable.cpp Framework/Messages/IObservable.h Framework/Messages/IObserver.h Framework/OpenGL/OpenGLIncludes.h Framework/OpenGL/SdlOpenGLContext.cpp Framework/OpenGL/SdlOpenGLContext.h Framework/Oracle/GetOrthancImageCommand.cpp Framework/Oracle/GetOrthancImageCommand.h Framework/Oracle/GetOrthancWebViewerJpegCommand.cpp Framework/Oracle/GetOrthancWebViewerJpegCommand.h Framework/Oracle/HttpCommand.cpp Framework/Oracle/HttpCommand.h Framework/Oracle/IOracle.h Framework/Oracle/IOracleCommand.h Framework/Oracle/OracleCommandBase.cpp Framework/Oracle/OracleCommandBase.h Framework/Oracle/OracleCommandExceptionMessage.h Framework/Oracle/OrthancRestApiCommand.cpp Framework/Oracle/OrthancRestApiCommand.h Framework/Oracle/SleepOracleCommand.h Framework/Oracle/ThreadedOracle.cpp Framework/Oracle/ThreadedOracle.h Framework/Oracle/WebAssemblyOracle.cpp Framework/Oracle/WebAssemblyOracle.h Framework/Radiography/RadiographyDicomLayer.cpp Framework/Radiography/RadiographyDicomLayer.h Framework/Radiography/RadiographyLayer.cpp Framework/Radiography/RadiographyLayer.h Framework/Radiography/RadiographyScene.cpp Framework/Radiography/RadiographyScene.h Framework/Radiography/RadiographySceneReader.cpp Framework/Radiography/RadiographySceneReader.h Framework/Radiography/RadiographyWidget.cpp Framework/Radiography/RadiographyWidget.h Framework/Scene2D/CairoCompositor.cpp Framework/Scene2D/CairoCompositor.h Framework/Scene2D/FloatTextureSceneLayer.cpp Framework/Scene2D/FloatTextureSceneLayer.h Framework/Scene2D/GrayscaleStyleConfigurator.cpp Framework/Scene2D/GrayscaleStyleConfigurator.h Framework/Scene2D/Internals/CairoFloatTextureRenderer.cpp Framework/Scene2D/Internals/CairoLookupTableTextureRenderer.cpp Framework/Scene2D/Internals/CompositorHelper.cpp Framework/Scene2D/Internals/CompositorHelper.h Framework/Scene2D/Internals/OpenGLFloatTextureRenderer.cpp Framework/Scene2D/Internals/OpenGLLookupTableTextureRenderer.cpp Framework/Scene2D/LookupTableStyleConfigurator.cpp Framework/Scene2D/LookupTableStyleConfigurator.h Framework/Scene2D/LookupTableTextureSceneLayer.cpp Framework/Scene2D/LookupTableTextureSceneLayer.h Framework/Scene2D/OpenGLCompositor.cpp Framework/Scene2D/OpenGLCompositor.h Framework/Scene2D/PointerEvent.cpp Framework/Scene2D/PointerEvent.h Framework/Scene2D/Scene2D.cpp Framework/Scene2D/Scene2D.h Framework/Scene2D/ScenePoint2D.h Framework/Scene2DViewport/AngleMeasureTool.cpp Framework/Scene2DViewport/AngleMeasureTool.h Framework/Scene2DViewport/CreateAngleMeasureCommand.cpp Framework/Scene2DViewport/CreateAngleMeasureCommand.h Framework/Scene2DViewport/CreateAngleMeasureTracker.cpp Framework/Scene2DViewport/CreateAngleMeasureTracker.h Framework/Scene2DViewport/CreateLineMeasureCommand.cpp Framework/Scene2DViewport/CreateLineMeasureCommand.h Framework/Scene2DViewport/CreateLineMeasureTracker.cpp Framework/Scene2DViewport/CreateLineMeasureTracker.h Framework/Scene2DViewport/EditAngleMeasureCommand.cpp Framework/Scene2DViewport/EditAngleMeasureCommand.h Framework/Scene2DViewport/EditAngleMeasureTracker.cpp Framework/Scene2DViewport/EditAngleMeasureTracker.h Framework/Scene2DViewport/EditLineMeasureCommand.cpp Framework/Scene2DViewport/EditLineMeasureCommand.h Framework/Scene2DViewport/EditLineMeasureTracker.cpp Framework/Scene2DViewport/EditLineMeasureTracker.h Framework/Scene2DViewport/LayerHolder.cpp Framework/Scene2DViewport/LayerHolder.h Framework/Scene2DViewport/LineMeasureTool.cpp Framework/Scene2DViewport/LineMeasureTool.h Framework/Scene2DViewport/MeasureCommands.h Framework/Scene2DViewport/MeasureTool.cpp Framework/Scene2DViewport/MeasureTool.h Framework/Scene2DViewport/MeasureToolsToolbox.cpp Framework/Scene2DViewport/MeasureTrackers.cpp Framework/Scene2DViewport/MeasureTrackers.h Framework/Scene2DViewport/OneGesturePointerTracker.cpp Framework/Scene2DViewport/ViewportController.cpp Framework/Scene2DViewport/ViewportController.h Framework/StoneEnumerations.h Framework/StoneInitialization.cpp Framework/StoneInitialization.h Framework/Toolbox/AffineTransform2D.cpp Framework/Toolbox/AffineTransform2D.h Framework/Toolbox/CoordinateSystem3D.cpp Framework/Toolbox/CoordinateSystem3D.h Framework/Toolbox/DicomInstanceParameters.cpp Framework/Toolbox/DicomInstanceParameters.h Framework/Toolbox/DicomStructureSet.cpp Framework/Toolbox/DicomStructureSet.h Framework/Toolbox/GenericToolbox.h Framework/Toolbox/LinearAlgebra.cpp Framework/Toolbox/SlicesSorter.cpp Framework/Toolbox/SlicesSorter.h Framework/Viewport/IViewport.h Framework/Viewport/SdlViewport.cpp Framework/Viewport/SdlViewport.h Framework/Viewport/SdlWindow.cpp Framework/Viewport/SdlWindow.h Framework/Viewport/WebAssemblyViewport.cpp Framework/Viewport/WebAssemblyViewport.h Framework/Volumes/DicomVolumeImage.h Framework/Volumes/DicomVolumeImageMPRSlicer.cpp Framework/Volumes/VolumeImageGeometry.cpp Platforms/Generic/OracleWebService.h Platforms/Generic/WebServiceCommandBase.cpp Platforms/Generic/WebServiceCommandBase.h Platforms/Generic/WebServiceDeleteCommand.cpp Platforms/Generic/WebServiceDeleteCommand.h Platforms/Generic/WebServiceGetCommand.cpp Platforms/Generic/WebServiceGetCommand.h Platforms/Generic/WebServicePostCommand.cpp Platforms/Generic/WebServicePostCommand.h Resources/CMake/OrthancStoneConfiguration.cmake Resources/CMake/QtConfiguration.cmake UnitTestsSources/GenericToolboxTests.cpp UnitTestsSources/TestMessageBroker.cpp UnitTestsSources/TestStructureSet.cpp UnitTestsSources/UnitTestsMain.cpp
diffstat 20 files changed, 469 insertions(+), 142 deletions(-) [+]
line wrap: on
line diff
--- a/.hgtags	Fri Jan 31 17:34:57 2020 +0100
+++ b/.hgtags	Tue Feb 04 15:20:08 2020 +0100
@@ -40,3 +40,6 @@
 33b0a762e98ade4e1f00e26bf865b417269443fb toa2019110401
 76705b430c78d4ca0a718c6164ac4b73ce007fde toa2019110801
 ce3052f28f2e6e7b29cfad4efae4e3881e70b056 toa2019122001
+5a2d5380148d4068af55ca3e895f5fa18c778bc1 toa2020012701
+ca2058bd74eff429033df3de7a6785ae08a92922 toa2020012702
+5e45322c77249573661bb4f45f035d000db446fc toa2020012703
--- a/Applications/Qt/QCairoWidget.cpp	Fri Jan 31 17:34:57 2020 +0100
+++ b/Applications/Qt/QCairoWidget.cpp	Tue Feb 04 15:20:08 2020 +0100
@@ -181,6 +181,18 @@
       CASE_QT_KEY_TO_ORTHANC(Qt::Key_Down, KeyboardKeys_Down);
       CASE_QT_KEY_TO_ORTHANC(Qt::Key_Left, KeyboardKeys_Left);
       CASE_QT_KEY_TO_ORTHANC(Qt::Key_Right, KeyboardKeys_Right);
+      CASE_QT_KEY_TO_ORTHANC(Qt::Key_F1, KeyboardKeys_F1);
+      CASE_QT_KEY_TO_ORTHANC(Qt::Key_F2, KeyboardKeys_F2);
+      CASE_QT_KEY_TO_ORTHANC(Qt::Key_F3, KeyboardKeys_F3);
+      CASE_QT_KEY_TO_ORTHANC(Qt::Key_F4, KeyboardKeys_F4);
+      CASE_QT_KEY_TO_ORTHANC(Qt::Key_F5, KeyboardKeys_F5);
+      CASE_QT_KEY_TO_ORTHANC(Qt::Key_F6, KeyboardKeys_F6);
+      CASE_QT_KEY_TO_ORTHANC(Qt::Key_F7, KeyboardKeys_F7);
+      CASE_QT_KEY_TO_ORTHANC(Qt::Key_F8, KeyboardKeys_F8);
+      CASE_QT_KEY_TO_ORTHANC(Qt::Key_F9, KeyboardKeys_F9);
+      CASE_QT_KEY_TO_ORTHANC(Qt::Key_F10, KeyboardKeys_F10);
+      CASE_QT_KEY_TO_ORTHANC(Qt::Key_F11, KeyboardKeys_F11);
+      CASE_QT_KEY_TO_ORTHANC(Qt::Key_F12, KeyboardKeys_F12);
     default:
       break;
     }
--- a/Applications/Samples/SingleFrameEditorApplication.h	Fri Jan 31 17:34:57 2020 +0100
+++ b/Applications/Samples/SingleFrameEditorApplication.h	Tue Feb 04 15:20:08 2020 +0100
@@ -315,7 +315,7 @@
                     << snapshot.toStyledString();
 
           boost::shared_ptr<RadiographyScene> scene(new RadiographyScene);
-          RadiographySceneReader reader(*scene, context_->GetOrthancApiClient());
+          RadiographySceneReader reader(*scene, *context_->GetOrthancApiClient());
           reader.Read(snapshot);
 
           widget.SetScene(scene);
--- a/Framework/Deprecated/Loaders/OrthancMultiframeVolumeLoader.cpp	Fri Jan 31 17:34:57 2020 +0100
+++ b/Framework/Deprecated/Loaders/OrthancMultiframeVolumeLoader.cpp	Tue Feb 04 15:20:08 2020 +0100
@@ -263,7 +263,8 @@
   }
 
   template <typename T>
-  void OrthancMultiframeVolumeLoader::CopyPixelData(const std::string& pixelData)
+  void OrthancMultiframeVolumeLoader::CopyPixelDataAndComputeDistribution(
+    const std::string& pixelData, std::map<T,uint64_t>& distribution)
   {
     OrthancStone::ImageBuffer3D& target = volume_->GetPixelData();
       
@@ -283,43 +284,209 @@
       return;
     }
 
-    const uint8_t* source = reinterpret_cast<const uint8_t*>(pixelData.c_str());
-
-    for (unsigned int z = 0; z < depth; z++)
+    // first pass to initialize map
     {
-      OrthancStone::ImageBuffer3D::SliceWriter writer(target, OrthancStone::VolumeProjection_Axial, z);
+      const uint8_t* source = reinterpret_cast<const uint8_t*>(pixelData.c_str());
 
-      assert (writer.GetAccessor().GetWidth() == width &&
-              writer.GetAccessor().GetHeight() == height);
+      for (unsigned int z = 0; z < depth; z++)
+      {
+        for (unsigned int y = 0; y < height; y++)
+        {
+          for (unsigned int x = 0; x < width; x++)
+          {
+            T value;
+            CopyPixel(value, source);
+            distribution[value] = 0;
+            source += bpp;
+          }
+        }
+      }
+    }
+
+    {
+      const uint8_t* source = reinterpret_cast<const uint8_t*>(pixelData.c_str());
 
-      for (unsigned int y = 0; y < height; y++)
+      for (unsigned int z = 0; z < depth; z++)
       {
-        assert(sizeof(T) == Orthanc::GetBytesPerPixel(target.GetFormat()));
+        OrthancStone::ImageBuffer3D::SliceWriter writer(target, OrthancStone::VolumeProjection_Axial, z);
 
-        T* target = reinterpret_cast<T*>(writer.GetAccessor().GetRow(y));
+        assert(writer.GetAccessor().GetWidth() == width &&
+          writer.GetAccessor().GetHeight() == height);
+
+        for (unsigned int y = 0; y < height; y++)
+        {
+          assert(sizeof(T) == Orthanc::GetBytesPerPixel(target.GetFormat()));
 
-        for (unsigned int x = 0; x < width; x++)
-        {
-          CopyPixel(*target, source);
-          target ++;
-          source += bpp;
+          T* target = reinterpret_cast<T*>(writer.GetAccessor().GetRow(y));
+
+          for (unsigned int x = 0; x < width; x++)
+          {
+            CopyPixel(*target, source);
+
+            distribution[*target] += 1;
+
+            target++;
+            source += bpp;
+          }
         }
       }
     }
   }
 
+  template <typename T>
+  void OrthancMultiframeVolumeLoader::ComputeMinMaxWithOutlierRejection(
+    const std::map<T, uint64_t>& distribution)
+  {
+    if (distribution.size() == 0)
+    {
+      LOG(ERROR) << "ComputeMinMaxWithOutlierRejection -- Volume image empty.";
+    }
+    else
+    {
+      OrthancStone::ImageBuffer3D& target = volume_->GetPixelData();
+
+      const uint64_t bpp = target.GetBytesPerPixel();
+      const uint64_t width = target.GetWidth();
+      const uint64_t height = target.GetHeight();
+      const uint64_t depth = target.GetDepth();
+      const uint64_t voxelCount = width * height * depth;
+
+      // now that we have distribution[pixelValue] == numberOfPixelsWithValue
+      // compute number of values and check (assertion) that it is equal to 
+      // width * height * depth 
+      {
+        typename std::map<T, uint64_t>::const_iterator it = distribution.begin();
+        uint64_t totalCount = 0;
+        distributionRawMin_ = static_cast<float>(it->first);
+
+        while (it != distribution.end())
+        {
+          T pixelValue = it->first;
+          uint64_t count = it->second;
+          totalCount += count;
+          it++;
+          if (it == distribution.end())
+            distributionRawMax_ = static_cast<float>(pixelValue);
+        }
+        LOG(INFO) << "Volume image. First distribution value = " 
+          << static_cast<float>(distributionRawMin_) 
+          << " | Last distribution value = " 
+          << static_cast<float>(distributionRawMax_);
+
+        if (totalCount != voxelCount)
+        {
+          LOG(ERROR) << "Internal error in dose distribution computation. TC (" 
+            << totalCount << ") != VoxC (" << voxelCount;
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+      }
+
+      // compute the number of voxels to reject at each end of the distribution
+      uint64_t endRejectionCount = static_cast<uint64_t>(
+        outliersHalfRejectionRate_ * voxelCount);
+
+      if (endRejectionCount > voxelCount)
+      {
+        LOG(ERROR) << "Internal error in dose distribution computation."
+          << " endRejectionCount = " << endRejectionCount
+          << " | voxelCount = " << voxelCount;
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+
+      // this will contain the actual distribution minimum after outlier 
+      // rejection
+      T resultMin = 0;
+
+      // then start from start and remove pixel values up to 
+      // endRejectionCount voxels rejected
+      {
+        typename std::map<T, uint64_t>::const_iterator it = distribution.begin();
+        
+        uint64_t currentCount = 0;
+
+        while (it != distribution.end())
+        {
+          T pixelValue = it->first;
+          uint64_t count = it->second;
+
+          // if this pixelValue crosses the rejection threshold, let's set it
+          // and exit the loop
+          if ((currentCount <= endRejectionCount) &&
+              (currentCount + count > endRejectionCount))
+          {
+            resultMin = pixelValue;
+            break;
+          }
+          else
+          {
+            currentCount += count;
+          }
+          // and continue walking along the distribution
+          it++;
+        }
+      }
+
+      // this will contain the actual distribution maximum after outlier 
+      // rejection
+      T resultMax = 0;
+      // now start from END and remove pixel values up to 
+      // endRejectionCount voxels rejected
+      {
+        typename std::map<T, uint64_t>::const_reverse_iterator it = distribution.rbegin();
+
+        uint64_t currentCount = 0;
+
+        while (it != distribution.rend())
+        {
+          T pixelValue = it->first;
+          uint64_t count = it->second;
+
+          if ((currentCount <= endRejectionCount) &&
+              (currentCount + count > endRejectionCount))
+          {
+            resultMax = pixelValue;
+            break;
+          }
+          else
+          {
+            currentCount += count;
+          }
+          // and continue walking along the distribution
+          it++;
+        }
+      }
+      if (resultMin > resultMax)
+      {
+        LOG(ERROR) << "Internal error in dose distribution computation! " << 
+          "resultMin (" << resultMin << ") > resultMax (" << resultMax << ")";
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+      computedDistributionMin_ = static_cast<float>(resultMin);
+      computedDistributionMax_ = static_cast<float>(resultMax);
+    }
+  }
+
+  template <typename T>
+  void OrthancMultiframeVolumeLoader::CopyPixelDataAndComputeMinMax(
+    const std::string& pixelData)
+  {
+    std::map<T, uint64_t> distribution;
+    CopyPixelDataAndComputeDistribution(pixelData, distribution);
+    ComputeMinMaxWithOutlierRejection(distribution);
+  }
+
   void OrthancMultiframeVolumeLoader::SetUncompressedPixelData(const std::string& pixelData)
   {
     switch (volume_->GetPixelData().GetFormat())
     {
       case Orthanc::PixelFormat_Grayscale32:
-        CopyPixelData<uint32_t>(pixelData);
+        CopyPixelDataAndComputeMinMax<uint32_t>(pixelData);
         break;
       case Orthanc::PixelFormat_Grayscale16:
-        CopyPixelData<uint16_t>(pixelData);
+        CopyPixelDataAndComputeMinMax<uint16_t>(pixelData);
         break;
       case Orthanc::PixelFormat_SignedGrayscale16:
-        CopyPixelData<int16_t>(pixelData);
+        CopyPixelDataAndComputeMinMax<int16_t>(pixelData);
         break;
       default:
         throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
@@ -341,12 +508,19 @@
     return volume_->GetGeometry();
   }
 
-  OrthancMultiframeVolumeLoader::OrthancMultiframeVolumeLoader(boost::shared_ptr<OrthancStone::DicomVolumeImage> volume,
-                                                               OrthancStone::IOracle& oracle,
-                                                               OrthancStone::IObservable& oracleObservable) :
+  OrthancMultiframeVolumeLoader::OrthancMultiframeVolumeLoader(
+    boost::shared_ptr<OrthancStone::DicomVolumeImage> volume,
+    OrthancStone::IOracle& oracle,
+    OrthancStone::IObservable& oracleObservable,
+    float outliersHalfRejectionRate) :
     LoaderStateMachine(oracle, oracleObservable),
     volume_(volume),
-    pixelDataLoaded_(false)
+    pixelDataLoaded_(false),
+    outliersHalfRejectionRate_(outliersHalfRejectionRate),
+    distributionRawMin_(0),
+    distributionRawMax_(0),
+    computedDistributionMin_(0),
+    computedDistributionMax_(0)
   {
     if (volume.get() == NULL)
     {
@@ -359,6 +533,29 @@
     LOG(TRACE) << "OrthancMultiframeVolumeLoader::~OrthancMultiframeVolumeLoader()";
   }
 
+
+  void OrthancMultiframeVolumeLoader::GetDistributionMinMax
+  (float& minValue, float& maxValue) const
+  {
+    if (distributionRawMin_ == 0 && distributionRawMax_ == 0)
+    {
+      LOG(WARNING) << "GetDistributionMinMaxWithOutliersRejection called before computation!";
+    }
+    minValue = distributionRawMin_;
+    maxValue = distributionRawMax_;
+  }
+  
+  void OrthancMultiframeVolumeLoader::GetDistributionMinMaxWithOutliersRejection
+    (float& minValue, float& maxValue) const
+  {
+    if (computedDistributionMin_ == 0 && computedDistributionMax_ == 0)
+    {
+      LOG(WARNING) << "GetDistributionMinMaxWithOutliersRejection called before computation!";
+    }
+    minValue = computedDistributionMin_;
+    maxValue = computedDistributionMax_;
+  }
+
   void OrthancMultiframeVolumeLoader::LoadInstance(const std::string& instanceId)
   {
     Start();
--- a/Framework/Deprecated/Loaders/OrthancMultiframeVolumeLoader.h	Fri Jan 31 17:34:57 2020 +0100
+++ b/Framework/Deprecated/Loaders/OrthancMultiframeVolumeLoader.h	Tue Feb 04 15:20:08 2020 +0100
@@ -42,6 +42,11 @@
     std::string                          instanceId_;
     std::string                          transferSyntaxUid_;
     bool                                 pixelDataLoaded_;
+    float                                outliersHalfRejectionRate_;
+    float                                distributionRawMin_;
+    float                                distributionRawMax_;
+    float                                computedDistributionMin_;
+    float                                computedDistributionMax_;
 
     const std::string& GetInstanceId() const;
 
@@ -51,8 +56,35 @@
 
     void SetGeometry(const Orthanc::DicomMap& dicom);
 
+
+    /**
+    This method will :
+    
+    - copy the pixel values from the response to the volume image
+    - compute the maximum and minimum value while discarding the
+      outliersHalfRejectionRate_ fraction of the outliers from both the start 
+      and the end of the distribution.
+
+      In English, this means that, if the volume dataset contains a few extreme
+      values very different from the rest (outliers) that we want to get rid of,
+      this method allows to do so.
+
+      If you supply 0.005, for instance, it means 1% of the extreme values will
+      be rejected (0.5% on each side of the distribution)
+    */
     template <typename T>
-    void CopyPixelData(const std::string& pixelData);
+    void CopyPixelDataAndComputeMinMax(const std::string& pixelData);
+      
+    /** Service method for CopyPixelDataAndComputeMinMax*/
+    template <typename T>
+    void CopyPixelDataAndComputeDistribution(
+      const std::string& pixelData, 
+      std::map<T, uint64_t>& distribution);
+
+    /** Service method for CopyPixelDataAndComputeMinMax*/
+    template <typename T>
+    void ComputeMinMaxWithOutlierRejection(
+      const std::map<T, uint64_t>& distribution);
 
     void SetUncompressedPixelData(const std::string& pixelData);
 
@@ -62,7 +94,8 @@
   public:
     OrthancMultiframeVolumeLoader(boost::shared_ptr<OrthancStone::DicomVolumeImage> volume,
                                   OrthancStone::IOracle& oracle,
-                                  OrthancStone::IObservable& oracleObservable);
+                                  OrthancStone::IObservable& oracleObservable,
+                                  float outliersHalfRejectionRate = 0.0005);
     
     virtual ~OrthancMultiframeVolumeLoader();
 
@@ -71,6 +104,12 @@
       return pixelDataLoaded_;
     }
 
+    void GetDistributionMinMax
+      (float& minValue, float& maxValue) const;
+
+    void GetDistributionMinMaxWithOutliersRejection
+      (float& minValue, float& maxValue) const;
+
     void LoadInstance(const std::string& instanceId);
   };
 }
--- a/Framework/Messages/ICallable.h	Fri Jan 31 17:34:57 2020 +0100
+++ b/Framework/Messages/ICallable.h	Tue Feb 04 15:20:08 2020 +0100
@@ -30,6 +30,7 @@
 #include <boost/weak_ptr.hpp>
 
 #include <string>
+#include <stdint.h>
 
 namespace OrthancStone 
 {
--- a/Framework/Messages/IObserver.h	Fri Jan 31 17:34:57 2020 +0100
+++ b/Framework/Messages/IObserver.h	Tue Feb 04 15:20:08 2020 +0100
@@ -23,6 +23,8 @@
 
 #include <boost/noncopyable.hpp>
 
+#include <stdint.h>
+
 namespace OrthancStone 
 {
   class IObserver : public boost::noncopyable
--- a/Framework/Radiography/RadiographyDicomLayer.cpp	Fri Jan 31 17:34:57 2020 +0100
+++ b/Framework/Radiography/RadiographyDicomLayer.cpp	Tue Feb 04 15:20:08 2020 +0100
@@ -113,7 +113,7 @@
     BroadcastMessage(RadiographyLayer::LayerEditedMessage(*this));
   }
 
-  void RadiographyDicomLayer::SetSourceImage(Orthanc::ImageAccessor* image, double newPixelSpacingX, double newPixelSpacingY)   // Takes ownership
+  void RadiographyDicomLayer::SetSourceImage(Orthanc::ImageAccessor* image, double newPixelSpacingX, double newPixelSpacingY, bool emitLayerEditedEvent)   // Takes ownership
   {
     std::auto_ptr<Orthanc::ImageAccessor> raii(image);
 
@@ -122,14 +122,17 @@
       throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
     }
 
-    SetSize(image->GetWidth(), image->GetHeight());
+    SetSize(image->GetWidth(), image->GetHeight(), false);
 
     source_ = raii;
     ApplyConverter();
 
     SetPixelSpacing(newPixelSpacingX, newPixelSpacingY, false);
 
-    BroadcastMessage(RadiographyLayer::LayerEditedMessage(*this));
+    if (emitLayerEditedEvent)
+    {
+      BroadcastMessage(RadiographyLayer::LayerEditedMessage(*this));
+    }
   }
 
 
--- a/Framework/Radiography/RadiographyDicomLayer.h	Fri Jan 31 17:34:57 2020 +0100
+++ b/Framework/Radiography/RadiographyDicomLayer.h	Tue Feb 04 15:20:08 2020 +0100
@@ -80,7 +80,7 @@
 
     void SetSourceImage(Orthanc::ImageAccessor* image);   // Takes ownership
 
-    void SetSourceImage(Orthanc::ImageAccessor* image, double newPixelSpacingX, double newPixelSpacingY);   // Takes ownership
+    void SetSourceImage(Orthanc::ImageAccessor* image, double newPixelSpacingX, double newPixelSpacingY, bool emitLayerEditedEvent = true);   // Takes ownership
 
     const Orthanc::ImageAccessor* GetSourceImage() const {return source_.get();}  // currently need this access to serialize scene in plain old data to send to a WASM worker
 
--- a/Framework/Radiography/RadiographyLayer.cpp	Fri Jan 31 17:34:57 2020 +0100
+++ b/Framework/Radiography/RadiographyLayer.cpp	Tue Feb 04 15:20:08 2020 +0100
@@ -59,15 +59,15 @@
 
   void RadiographyLayer::UpdateTransform()
   {
+    // important to update transform_ before getting the center to use the right scaling !!!
     transform_ = AffineTransform2D::CreateScaling(geometry_.GetScalingX(), geometry_.GetScalingY());
 
     double centerX, centerY;
     GetCenter(centerX, centerY);
 
     transform_ = AffineTransform2D::Combine(
-          AffineTransform2D::CreateOffset(geometry_.GetPanX() + centerX, geometry_.GetPanY() + centerY),
-          AffineTransform2D::CreateRotation(geometry_.GetAngle()),
-          AffineTransform2D::CreateOffset(-centerX, -centerY),
+          AffineTransform2D::CreateOffset(geometry_.GetPanX(), geometry_.GetPanY()),
+          AffineTransform2D::CreateRotation(geometry_.GetAngle(), centerX, centerY),
           transform_);
 
     transformInverse_ = AffineTransform2D::Invert(transform_);
@@ -222,14 +222,19 @@
   }
 
   void RadiographyLayer::SetSize(unsigned int width,
-                                 unsigned int height)
+                                 unsigned int height,
+                                 bool emitLayerEditedEvent)
   {
     hasSize_ = true;
     width_ = width;
     height_ = height;
 
     UpdateTransform();
-    BroadcastMessage(RadiographyLayer::LayerEditedMessage(*this));
+
+    if (emitLayerEditedEvent)
+    {
+      BroadcastMessage(RadiographyLayer::LayerEditedMessage(*this));
+    }
   }
 
 
--- a/Framework/Radiography/RadiographyLayer.h	Fri Jan 31 17:34:57 2020 +0100
+++ b/Framework/Radiography/RadiographyLayer.h	Tue Feb 04 15:20:08 2020 +0100
@@ -217,16 +217,6 @@
     const RadiographyScene&   scene_;
 
   protected:
-    virtual const AffineTransform2D& GetTransform() const
-    {
-      return transform_;
-    }
-
-    virtual const AffineTransform2D& GetTransformInverse() const
-    {
-      return transformInverse_;
-    }
-
     void SetPreferredPhotomotricDisplayMode(RadiographyPhotometricDisplayMode  prefferedPhotometricDisplayMode);
 
   private:
@@ -254,6 +244,16 @@
     {
     }
 
+    virtual const AffineTransform2D& GetTransform() const
+    {
+      return transform_;
+    }
+
+    virtual const AffineTransform2D& GetTransformInverse() const
+    {
+      return transformInverse_;
+    }
+
     size_t GetIndex() const
     {
       return index_;
@@ -298,7 +298,8 @@
     }
 
     void SetSize(unsigned int width,
-                 unsigned int height);
+                 unsigned int height,
+                 bool emitLayerEditedEvent = true);
 
     bool HasSize() const
     {
@@ -358,8 +359,6 @@
     virtual bool GetRange(float& minValue,
                           float& maxValue) const = 0;
 
-    friend class RadiographyMaskLayer; // because it needs to GetTransform on the dicomLayer it relates to
-
     virtual size_t GetApproximateMemoryUsage() const // this is used to limit the number of scenes loaded in RAM when resources are limited (we actually only count the size used by the images, not the C structs)
     {
       return 0;
--- a/Framework/Radiography/RadiographyMaskLayer.cpp	Fri Jan 31 17:34:57 2020 +0100
+++ b/Framework/Radiography/RadiographyMaskLayer.cpp	Tue Feb 04 15:20:08 2020 +0100
@@ -82,7 +82,7 @@
                                     float windowWidth,
                                     bool applyWindowing) const
   {
-    if (dicomLayer_.GetWidth() == 0) // nothing to do if the DICOM layer is not displayed (or not loaded)
+    if (dicomLayer_.GetWidth() == 0 || dicomLayer_.GetSourceImage() == NULL) // nothing to do if the DICOM layer is not displayed (or not loaded)
       return;
 
     if (invalidated_)
--- a/Framework/Radiography/RadiographyScene.cpp	Fri Jan 31 17:34:57 2020 +0100
+++ b/Framework/Radiography/RadiographyScene.cpp	Tue Feb 04 15:20:08 2020 +0100
@@ -125,6 +125,17 @@
     }
   }
 
+  void RadiographyScene::_RegisterLayer(RadiographyLayer* layer)
+  {
+    std::auto_ptr<RadiographyLayer> raii(layer);
+
+    // LOG(INFO) << "Registering layer: " << countLayers_;
+
+    size_t index = nextLayerIndex_++;
+    raii->SetIndex(index);
+    layers_[index] = raii.release();
+  }
+
   RadiographyLayer& RadiographyScene::RegisterLayer(RadiographyLayer* layer)
   {
     if (layer == NULL)
@@ -132,11 +143,7 @@
       throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
     }
 
-    std::auto_ptr<RadiographyLayer> raii(layer);
-
-    size_t index = nextLayerIndex_++;
-    raii->SetIndex(index);
-    layers_[index] = raii.release();
+    _RegisterLayer(layer);
 
     BroadcastMessage(GeometryChangedMessage(*this, *layer));
     BroadcastMessage(ContentChangedMessage(*this, *layer));
@@ -221,6 +228,8 @@
       
       LOG(INFO) << "Removing layer, there are now : " << layers_.size() << " layers";
 
+      _OnLayerRemoved();
+
       BroadcastMessage(RadiographyScene::LayerRemovedMessage(*this, layerIndex));
     }
   }
@@ -556,11 +565,24 @@
     // Render layers in the background-to-foreground order
     for (size_t index = 0; index < nextLayerIndex_; index++)
     {
-      Layers::const_iterator it = layers_.find(index);
-      if (it != layers_.end())
+      try
       {
-        assert(it->second != NULL);
-        it->second->Render(buffer, viewTransform, interpolation, windowingCenter_, windowingWidth_, applyWindowing);
+        Layers::const_iterator it = layers_.find(index);
+        if (it != layers_.end())
+        {
+          assert(it->second != NULL);
+          it->second->Render(buffer, viewTransform, interpolation, windowingCenter_, windowingWidth_, applyWindowing);
+        }
+      }
+      catch (Orthanc::OrthancException& ex)
+      {
+        LOG(ERROR) << "RadiographyScene::Render: " << index << ", OrthancException: " << ex.GetDetails();
+        throw ex; // rethrow because we want it to crash to see there's a problem !
+      }
+      catch (...)
+      {
+        LOG(ERROR) << "RadiographyScene::Render: " << index << ", unkown exception: ";
+        throw; // rethrow because we want it to crash to see there's a problem !
       }
     }
   }
@@ -637,6 +659,28 @@
     }
   }
 
+  void RadiographyScene::ExtractLayerFromRenderedScene(Orthanc::ImageAccessor& layer,
+                                                       const Orthanc::ImageAccessor& renderedScene,
+                                                       size_t layerIndex,
+                                                       ImageInterpolation interpolation)
+  {
+    Extent2D sceneExtent = GetSceneExtent();
+
+    double pixelSpacingX = sceneExtent.GetWidth() / renderedScene.GetWidth();
+    double pixelSpacingY = sceneExtent.GetHeight() / renderedScene.GetHeight();
+
+    AffineTransform2D view = AffineTransform2D::Combine(
+          AffineTransform2D::CreateScaling(1.0 / pixelSpacingX, 1.0 / pixelSpacingY),
+          AffineTransform2D::CreateOffset(-sceneExtent.GetX1(), -sceneExtent.GetY1()));
+
+    AffineTransform2D layerToSceneTransform = AffineTransform2D::Combine(
+          view,
+          GetLayer(layerIndex).GetTransform());
+
+    AffineTransform2D sceneToLayerTransform = AffineTransform2D::Invert(layerToSceneTransform);
+    sceneToLayerTransform.Apply(layer, renderedScene, interpolation, false);
+  }
+
   Orthanc::Image* RadiographyScene::ExportToImage(double pixelSpacingX,
                                                   double pixelSpacingY,
                                                   ImageInterpolation interpolation,
@@ -669,7 +713,14 @@
           AffineTransform2D::CreateOffset(-extent.GetX1(), -extent.GetY1()));
 
     // wipe background before rendering
-    Orthanc::ImageProcessing::Set(layers, 0);
+    if (GetPreferredPhotomotricDisplayMode() == RadiographyPhotometricDisplayMode_Monochrome1)
+    {
+      Orthanc::ImageProcessing::Set(layers, 65535.0f);
+    }
+    else
+    {
+      Orthanc::ImageProcessing::Set(layers, 0);
+    }
 
     Render(layers, view, interpolation, applyWindowing);
 
--- a/Framework/Radiography/RadiographyScene.h	Fri Jan 31 17:34:57 2020 +0100
+++ b/Framework/Radiography/RadiographyScene.h	Tue Feb 04 15:20:08 2020 +0100
@@ -37,6 +37,7 @@
     public ObserverBase<RadiographyScene>,
     public IObservable
   {
+    friend class RadiographySceneGeometryReader;
   public:
     class GeometryChangedMessage : public OriginMessage<RadiographyScene>
     {
@@ -168,8 +169,17 @@
     float   windowingWidth_;
     Layers  layers_;
 
+  public:
+    RadiographyLayer& RegisterLayer(RadiographyLayer* layer);
+
   protected:
-    RadiographyLayer& RegisterLayer(RadiographyLayer* layer);
+    virtual void _RegisterLayer(RadiographyLayer* layer);
+    virtual void _OnLayerRemoved() {}
+
+    void SetLayerIndex(RadiographyLayer* layer, size_t index)
+    {
+      layer->SetIndex(index);
+    }
 
     virtual void OnTagsReceived(const Deprecated::OrthancApiClient::BinaryResponseReadyMessage& message);
 
@@ -341,5 +351,9 @@
                                   int64_t maxValue /* for inversion */,
                                   bool applyWindowing);
 
+    void ExtractLayerFromRenderedScene(Orthanc::ImageAccessor& layer,
+                                       const Orthanc::ImageAccessor& renderedScene,
+                                       size_t layerIndex,
+                                       ImageInterpolation interpolation);
   };
 }
--- a/Framework/Radiography/RadiographySceneReader.cpp	Fri Jan 31 17:34:57 2020 +0100
+++ b/Framework/Radiography/RadiographySceneReader.cpp	Tue Feb 04 15:20:08 2020 +0100
@@ -50,7 +50,17 @@
 
   RadiographyDicomLayer* RadiographySceneReader::LoadDicom(const std::string& instanceId, unsigned int frame, RadiographyLayer::Geometry* geometry)
   {
-    return dynamic_cast<RadiographyDicomLayer*>(&(scene_.LoadDicomFrame(*orthancApiClient_, instanceId, frame, false, geometry)));
+    return dynamic_cast<RadiographyDicomLayer*>(&(scene_.LoadDicomFrame(orthancApiClient_, instanceId, frame, false, geometry)));
+  }
+
+  RadiographyDicomLayer* RadiographySceneGeometryReader::LoadDicom(const std::string& instanceId, unsigned int frame, RadiographyLayer::Geometry* geometry)
+  {
+    std::auto_ptr<RadiographyPlaceholderLayer>  layer(new RadiographyPlaceholderLayer(scene_));
+    layer->SetGeometry(*geometry);
+    layer->SetSize(dicomImageWidth_, dicomImageHeight_);
+    scene_.RegisterLayer(layer.get());
+
+    return layer.release();
   }
 
   void RadiographySceneBuilder::Read(const Json::Value& input)
@@ -130,82 +140,8 @@
     }
   }
 
-  void RadiographySceneReader::Read(const Json::Value& input)
-  {
-    unsigned int version = input["version"].asUInt();
-
-    if (version != 1)
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-
-    if (input.isMember("hasWindowing") && input["hasWindowing"].asBool())
-    {
-      scene_.SetWindowing(input["windowCenter"].asFloat(), input["windowWidth"].asFloat());
-    }
-
-    RadiographyDicomLayer* dicomLayer = NULL;
-    for(size_t layerIndex = 0; layerIndex < input["layers"].size(); layerIndex++)
-    {
-      const Json::Value& jsonLayer = input["layers"][(int)layerIndex];
-      RadiographyLayer::Geometry geometry;
-
-      if (jsonLayer["type"].asString() == "dicom")
-      {
-        ReadLayerGeometry(geometry, jsonLayer);
-        dicomLayer = dynamic_cast<RadiographyDicomLayer*>(&(scene_.LoadDicomFrame(*orthancApiClient_, jsonLayer["instanceId"].asString(), jsonLayer["frame"].asUInt(), false, &geometry)));
-      }
-      else if (jsonLayer["type"].asString() == "mask")
-      {
-        if (dicomLayer == NULL)
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); // we always assumed the dicom layer was read before the mask
-        }
-        ReadLayerGeometry(geometry, jsonLayer);
 
-        float foreground = jsonLayer["foreground"].asFloat();
-        std::vector<Orthanc::ImageProcessing::ImagePoint> corners;
-        for (size_t i = 0; i < jsonLayer["corners"].size(); i++)
-        {
-          Orthanc::ImageProcessing::ImagePoint corner(jsonLayer["corners"][(int)i]["x"].asInt(),
-              jsonLayer["corners"][(int)i]["y"].asInt());
-          corners.push_back(corner);
-        }
 
-        scene_.LoadMask(corners, *dicomLayer, foreground, &geometry);
-      }
-      else if (jsonLayer["type"].asString() == "text")
-      {
-        ReadLayerGeometry(geometry, jsonLayer);
-        scene_.LoadText(jsonLayer["text"].asString(), jsonLayer["font"].asString(), jsonLayer["fontSize"].asUInt(), static_cast<uint8_t>(jsonLayer["foreground"].asUInt()), &geometry, false);
-      }
-      else if (jsonLayer["type"].asString() == "alpha")
-      {
-        ReadLayerGeometry(geometry, jsonLayer);
-
-        const std::string& pngContentBase64 = jsonLayer["content"].asString();
-        std::string pngContent;
-        std::string mimeType;
-        Orthanc::Toolbox::DecodeDataUriScheme(mimeType, pngContent, pngContentBase64);
-
-        std::auto_ptr<Orthanc::ImageAccessor>  image;
-        if (mimeType == "image/png")
-        {
-          image.reset(new Orthanc::PngReader());
-          dynamic_cast<Orthanc::PngReader*>(image.get())->ReadFromMemory(pngContent);
-        }
-        else
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-
-        RadiographyAlphaLayer& layer = dynamic_cast<RadiographyAlphaLayer&>(scene_.LoadAlphaBitmap(image.release(), &geometry));
-
-        if (!jsonLayer["isUsingWindowing"].asBool())
-        {
-          layer.SetForegroundValue((float)(jsonLayer["foreground"].asDouble()));
-        }
-      }
-      else
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-    }
-  }
 
   void RadiographySceneBuilder::ReadDicomLayerGeometry(RadiographyLayer::Geometry& geometry, const Json::Value& input)
   {
--- a/Framework/Radiography/RadiographySceneReader.h	Fri Jan 31 17:34:57 2020 +0100
+++ b/Framework/Radiography/RadiographySceneReader.h	Tue Feb 04 15:20:08 2020 +0100
@@ -33,6 +33,18 @@
 
 namespace OrthancStone
 {
+  // a layer containing only the geometry of a DICOM layer (bit hacky !)
+  class RadiographyPlaceholderLayer : public RadiographyDicomLayer
+  {
+  public:
+    RadiographyPlaceholderLayer(const RadiographyScene& scene) :
+      RadiographyDicomLayer(scene)
+    {
+    }
+
+  };
+
+
   // HACK: I had to introduce this builder class in order to be able to recreate a RadiographyScene
   // from a serialized scene that is passed to web-workers.
   // It needs some architecturing...
@@ -68,18 +80,32 @@
 
   class RadiographySceneReader : public RadiographySceneBuilder
   {
-  private:
-    boost::shared_ptr<Deprecated::OrthancApiClient>  orthancApiClient_;
+    Deprecated::OrthancApiClient&             orthancApiClient_;
 
   public:
-    RadiographySceneReader(RadiographyScene& scene,
-                           boost::shared_ptr<Deprecated::OrthancApiClient> orthancApiClient) :
+    RadiographySceneReader(RadiographyScene& scene, Deprecated::OrthancApiClient& orthancApiClient) :
       RadiographySceneBuilder(scene),
       orthancApiClient_(orthancApiClient)
     {
     }
 
-    void Read(const Json::Value& input);
+  protected:
+    virtual RadiographyDicomLayer*  LoadDicom(const std::string& instanceId, unsigned int frame, RadiographyLayer::Geometry* geometry);
+  };
+
+  // reads the whole scene but the DICOM image such that we have the full geometry
+  class RadiographySceneGeometryReader : public RadiographySceneBuilder
+  {
+    unsigned int dicomImageWidth_;
+    unsigned int dicomImageHeight_;
+
+  public:
+    RadiographySceneGeometryReader(RadiographyScene& scene, unsigned int dicomImageWidth, unsigned int dicomImageHeight) :
+      RadiographySceneBuilder(scene),
+      dicomImageWidth_(dicomImageWidth),
+      dicomImageHeight_(dicomImageHeight)
+    {
+    }
 
   protected:
     virtual RadiographyDicomLayer*  LoadDicom(const std::string& instanceId, unsigned int frame, RadiographyLayer::Geometry* geometry);
--- a/Framework/Radiography/RadiographyWidget.cpp	Fri Jan 31 17:34:57 2020 +0100
+++ b/Framework/Radiography/RadiographyWidget.cpp	Tue Feb 04 15:20:08 2020 +0100
@@ -76,6 +76,11 @@
     {
       floatBuffer_.reset(new Orthanc::Image(
         Orthanc::PixelFormat_Float32, width, height, false));
+
+      if (floatBuffer_.get() == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotEnoughMemory, "RadiographyWidget::RenderInternal: unable to allocate float buffer");
+      }
     }
 
     if (cairoBuffer_.get() == NULL ||
@@ -83,6 +88,11 @@
         cairoBuffer_->GetHeight() != height)
     {
       cairoBuffer_.reset(new CairoSurface(width, height, false /* no alpha */));
+
+      if (cairoBuffer_.get() == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotEnoughMemory, "RadiographyWidget::RenderInternal: unable to allocate cairo buffer");
+      }
     }
 
     RenderBackground(*floatBuffer_, 0.0, 65535.0);
@@ -188,6 +198,8 @@
   void RadiographyWidget::Unselect()
   {
     hasSelection_ = false;
+
+    NotifyContentChanged();
     BroadcastMessage(SelectionChangedMessage(*this));
   }
 
--- a/Framework/StoneEnumerations.h	Fri Jan 31 17:34:57 2020 +0100
+++ b/Framework/StoneEnumerations.h	Tue Feb 04 15:20:08 2020 +0100
@@ -86,7 +86,20 @@
     KeyboardKeys_Up = 38,
     KeyboardKeys_Right = 39,
     KeyboardKeys_Down = 40,
-    KeyboardKeys_Delete = 46
+    KeyboardKeys_Delete = 46,
+
+    KeyboardKeys_F1 = 112,
+    KeyboardKeys_F2 = 113,
+    KeyboardKeys_F3 = 114,
+    KeyboardKeys_F4 = 115,
+    KeyboardKeys_F5 = 116,
+    KeyboardKeys_F6 = 117,
+    KeyboardKeys_F7 = 118,
+    KeyboardKeys_F8 = 119,
+    KeyboardKeys_F9 = 120,
+    KeyboardKeys_F10 = 121,
+    KeyboardKeys_F11 = 122,
+    KeyboardKeys_F12 = 123,
   };
 
   enum SopClassUid
--- a/Framework/Toolbox/AffineTransform2D.cpp	Fri Jan 31 17:34:57 2020 +0100
+++ b/Framework/Toolbox/AffineTransform2D.cpp	Tue Feb 04 15:20:08 2020 +0100
@@ -246,6 +246,16 @@
     return t;
   }
 
+  AffineTransform2D AffineTransform2D::CreateRotation(double angle, // CW rotation
+                                                      double cx,    // rotation center
+                                                      double cy)    // rotation center
+  {
+    return Combine(
+          CreateOffset(cx, cy),
+          CreateRotation(angle),
+          CreateOffset(-cx, -cy)
+          );
+  }
 
   AffineTransform2D AffineTransform2D::CreateOpenGLClipspace(unsigned int canvasWidth,
                                                              unsigned int canvasHeight)
--- a/Framework/Toolbox/AffineTransform2D.h	Fri Jan 31 17:34:57 2020 +0100
+++ b/Framework/Toolbox/AffineTransform2D.h	Tue Feb 04 15:20:08 2020 +0100
@@ -90,8 +90,12 @@
 
     static AffineTransform2D CreateScaling(double sx,
                                            double sy);
-    
-    static AffineTransform2D CreateRotation(double angle);
+
+    static AffineTransform2D CreateRotation(double angle); // CW rotation in radians
+
+    static AffineTransform2D CreateRotation(double angle, // CW rotation in radians
+                                            double cx,    // rotation center
+                                            double cy);   // rotation center
 
     static AffineTransform2D CreateOpenGLClipspace(unsigned int canvasWidth,
                                                    unsigned int canvasHeight);