diff OrthancStone/Sources/Deprecated/dev.h @ 1512:244ad1e4e76a

reorganization of folders
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 07 Jul 2020 16:21:02 +0200
parents Framework/Deprecated/dev.h@30deba7bc8e2
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Deprecated/dev.h	Tue Jul 07 16:21:02 2020 +0200
@@ -0,0 +1,958 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * 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/>.
+ **/
+
+
+#pragma once
+
+#include "Layers/FrameRenderer.h"
+#include "Layers/LineLayerRenderer.h"
+#include "Layers/SliceOutlineRenderer.h"
+#include "Toolbox/DownloadStack.h"
+#include "Toolbox/GeometryToolbox.h"
+#include "Toolbox/OrthancSlicesLoader.h"
+#include "Volumes/ISlicedVolume.h"
+#include "Volumes/ImageBuffer3D.h"
+#include "Widgets/SliceViewerWidget.h"
+
+#include <Logging.h>
+#include <Images/ImageProcessing.h>
+#include <OrthancException.h>
+
+#include <boost/math/special_functions/round.hpp>
+
+
+namespace Deprecated
+{
+  // TODO: Handle errors while loading
+  class OrthancVolumeImage :
+    public ISlicedVolume,
+    public OrthancStone::IObserver
+  {
+  private:
+    OrthancSlicesLoader           loader_;
+    std::unique_ptr<OrthancStone::ImageBuffer3D>  image_;
+    std::unique_ptr<DownloadStack>  downloadStack_;
+    bool                          computeRange_;
+    size_t                        pendingSlices_;
+
+    void ScheduleSliceDownload()
+    {
+      assert(downloadStack_.get() != NULL);
+
+      unsigned int slice;
+      if (downloadStack_->Pop(slice))
+      {
+        loader_.ScheduleLoadSliceImage(slice, OrthancStone::SliceImageQuality_Jpeg90);
+      }
+    }
+
+
+    static bool IsCompatible(const Slice& a,
+                             const Slice& b)
+    {
+      if (!OrthancStone::GeometryToolbox::IsParallel(a.GetGeometry().GetNormal(),
+                                                     b.GetGeometry().GetNormal()))
+      {
+        LOG(ERROR) << "A 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 slices dimensions (width/height) are varying throughout the volume image";
+        return false;
+      }
+
+      if (!OrthancStone::LinearAlgebra::IsNear(a.GetPixelSpacingX(), b.GetPixelSpacingX()) ||
+          !OrthancStone::LinearAlgebra::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()));
+    }
+
+
+    void OnSliceGeometryReady(const OrthancSlicesLoader::SliceGeometryReadyMessage& message)
+    {
+      assert(&message.GetOrigin() == &loader_);
+
+      if (loader_.GetSlicesCount() == 0)
+      {
+        LOG(ERROR) << "Empty volume image";
+        BroadcastMessage(ISlicedVolume::GeometryErrorMessage(*this));
+        return;
+      }
+
+      for (size_t i = 1; i < loader_.GetSlicesCount(); i++)
+      {
+        if (!IsCompatible(loader_.GetSlice(0), loader_.GetSlice(i)))
+        {
+          BroadcastMessage(ISlicedVolume::GeometryErrorMessage(*this));
+          return;
+        }
+      }
+
+      double spacingZ;
+
+      if (loader_.GetSlicesCount() > 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_.GetSlicesCount(); i++)
+      {
+        if (!OrthancStone::LinearAlgebra::IsNear(spacingZ, GetDistance(loader_.GetSlice(i - 1), loader_.GetSlice(i)),
+                                                 0.001 /* this is expressed in mm */))
+        {
+          LOG(ERROR) << "The distance between successive slices is not constant in a volume image";
+          BroadcastMessage(ISlicedVolume::GeometryErrorMessage(*this));
+          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_.GetSlicesCount() << " in " << Orthanc::EnumerationToString(format);
+
+      image_.reset(new OrthancStone::ImageBuffer3D(format, width, height, static_cast<unsigned int>(loader_.GetSlicesCount()), computeRange_));
+      image_->GetGeometry().SetAxialGeometry(loader_.GetSlice(0).GetGeometry());
+      image_->GetGeometry().SetVoxelDimensions(loader_.GetSlice(0).GetPixelSpacingX(),
+                                               loader_.GetSlice(0).GetPixelSpacingY(), spacingZ);
+      image_->Clear();
+
+      downloadStack_.reset(new DownloadStack(static_cast<unsigned int>(loader_.GetSlicesCount())));
+      pendingSlices_ = loader_.GetSlicesCount();
+
+      for (unsigned int i = 0; i < 4; i++)  // Limit to 4 simultaneous downloads
+      {
+        ScheduleSliceDownload();
+      }
+
+      // TODO Check the DicomFrameConverter are constant
+
+      BroadcastMessage(ISlicedVolume::GeometryReadyMessage(*this));
+    }
+
+
+    void OnSliceGeometryError(const OrthancSlicesLoader::SliceGeometryErrorMessage& message)
+    {
+      assert(&message.GetOrigin() == &loader_);
+
+      LOG(ERROR) << "Unable to download a volume image";
+      BroadcastMessage(ISlicedVolume::GeometryErrorMessage(*this));
+    }
+
+
+    void OnSliceImageReady(const OrthancSlicesLoader::SliceImageReadyMessage& message)
+    {
+      assert(&message.GetOrigin() == &loader_);
+
+      {
+        OrthancStone::ImageBuffer3D::SliceWriter writer(*image_, OrthancStone::VolumeProjection_Axial, message.GetSliceIndex());
+        Orthanc::ImageProcessing::Copy(writer.GetAccessor(), message.GetImage());
+      }
+
+      BroadcastMessage(ISlicedVolume::SliceContentChangedMessage
+                       (*this, message.GetSliceIndex(), message.GetSlice()));
+
+      if (pendingSlices_ == 1)
+      {
+        BroadcastMessage(ISlicedVolume::VolumeReadyMessage(*this));
+        pendingSlices_ = 0;
+      }
+      else if (pendingSlices_ > 1)
+      {
+        pendingSlices_ -= 1;
+      }
+
+      ScheduleSliceDownload();
+    }
+
+
+    void OnSliceImageError(const OrthancSlicesLoader::SliceImageErrorMessage& message)
+    {
+      assert(&message.GetOrigin() == &loader_);
+
+      LOG(ERROR) << "Cannot download slice " << message.GetSliceIndex() << " in a volume image";
+      ScheduleSliceDownload();
+    }
+
+
+  public:
+    OrthancVolumeImage(OrthancStone::MessageBroker& broker,
+                       OrthancApiClient& orthanc,
+                       bool computeRange) :
+      ISlicedVolume(broker),
+      IObserver(broker),
+      loader_(broker, orthanc),
+      computeRange_(computeRange),
+      pendingSlices_(0)
+    {
+      loader_.RegisterObserverCallback(
+        new OrthancStone::Callable<OrthancVolumeImage, OrthancSlicesLoader::SliceGeometryReadyMessage>
+        (*this, &OrthancVolumeImage::OnSliceGeometryReady));
+
+      loader_.RegisterObserverCallback(
+        new OrthancStone::Callable<OrthancVolumeImage, OrthancSlicesLoader::SliceGeometryErrorMessage>
+        (*this, &OrthancVolumeImage::OnSliceGeometryError));
+
+      loader_.RegisterObserverCallback(
+        new OrthancStone::Callable<OrthancVolumeImage, OrthancSlicesLoader::SliceImageReadyMessage>
+        (*this, &OrthancVolumeImage::OnSliceImageReady));
+
+      loader_.RegisterObserverCallback(
+        new OrthancStone::Callable<OrthancVolumeImage, OrthancSlicesLoader::SliceImageErrorMessage>
+        (*this, &OrthancVolumeImage::OnSliceImageError));
+    }
+
+    void ScheduleLoadSeries(const std::string& seriesId)
+    {
+      loader_.ScheduleLoadSeries(seriesId);
+    }
+
+    void ScheduleLoadInstance(const std::string& instanceId)
+    {
+      loader_.ScheduleLoadInstance(instanceId);
+    }
+
+    void ScheduleLoadFrame(const std::string& instanceId,
+                           unsigned int frame)
+    {
+      loader_.ScheduleLoadFrame(instanceId, frame);
+    }
+
+    virtual size_t GetSlicesCount() const
+    {
+      return loader_.GetSlicesCount();
+    }
+
+    virtual const Slice& GetSlice(size_t index) const
+    {
+      return loader_.GetSlice(index);
+    }
+
+    OrthancStone::ImageBuffer3D& GetImage() const
+    {
+      if (image_.get() == NULL)
+      {
+        // The geometry is not ready yet
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        return *image_;
+      }
+    }
+
+    bool FitWindowingToRange(RenderStyle& style,
+                             const DicomFrameConverter& converter) const
+    {
+      if (image_.get() == NULL)
+      {
+        return false;
+      }
+      else
+      {
+        return image_->FitWindowingToRange(style, converter);
+      }
+    }
+  };
+
+
+  class VolumeImageGeometry
+  {
+  private:
+    unsigned int         width_;
+    unsigned int         height_;
+    size_t               depth_;
+    double               pixelSpacingX_;
+    double               pixelSpacingY_;
+    double               sliceThickness_;
+    OrthancStone::CoordinateSystem3D   reference_;
+    DicomFrameConverter  converter_;
+
+    double ComputeAxialThickness(const OrthancVolumeImage& volume) const
+    {
+      double thickness;
+
+      size_t n = volume.GetSlicesCount();
+      if (n > 1)
+      {
+        const Slice& a = volume.GetSlice(0);
+        const Slice& b = volume.GetSlice(n - 1);
+        thickness = ((reference_.ProjectAlongNormal(b.GetGeometry().GetOrigin()) -
+                      reference_.ProjectAlongNormal(a.GetGeometry().GetOrigin())) /
+                     (static_cast<double>(n) - 1.0));
+      }
+      else
+      {
+        thickness = volume.GetSlice(0).GetThickness();
+      }
+
+      if (thickness <= 0)
+      {
+        // The slices should have been sorted with increasing Z
+        // (along the normal) by the OrthancSlicesLoader
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
+      else
+      {
+        return thickness;
+      }
+    }
+
+    void SetupAxial(const OrthancVolumeImage& volume)
+    {
+      const Slice& axial = volume.GetSlice(0);
+
+      width_ = axial.GetWidth();
+      height_ = axial.GetHeight();
+      depth_ = volume.GetSlicesCount();
+
+      pixelSpacingX_ = axial.GetPixelSpacingX();
+      pixelSpacingY_ = axial.GetPixelSpacingY();
+      sliceThickness_ = ComputeAxialThickness(volume);
+
+      reference_ = axial.GetGeometry();
+    }
+
+    void SetupCoronal(const OrthancVolumeImage& volume)
+    {
+      const Slice& axial = volume.GetSlice(0);
+      double axialThickness = ComputeAxialThickness(volume);
+
+      width_ = axial.GetWidth();
+      height_ = static_cast<unsigned int>(volume.GetSlicesCount());
+      depth_ = axial.GetHeight();
+
+      pixelSpacingX_ = axial.GetPixelSpacingX();
+      pixelSpacingY_ = axialThickness;
+      sliceThickness_ = axial.GetPixelSpacingY();
+
+      OrthancStone::Vector origin = axial.GetGeometry().GetOrigin();
+      origin += (static_cast<double>(volume.GetSlicesCount() - 1) *
+                 axialThickness * axial.GetGeometry().GetNormal());
+
+      reference_ = OrthancStone::CoordinateSystem3D(origin,
+                                                    axial.GetGeometry().GetAxisX(),
+                                                    - axial.GetGeometry().GetNormal());
+    }
+
+    void SetupSagittal(const OrthancVolumeImage& volume)
+    {
+      const Slice& axial = volume.GetSlice(0);
+      double axialThickness = ComputeAxialThickness(volume);
+
+      width_ = axial.GetHeight();
+      height_ = static_cast<unsigned int>(volume.GetSlicesCount());
+      depth_ = axial.GetWidth();
+
+      pixelSpacingX_ = axial.GetPixelSpacingY();
+      pixelSpacingY_ = axialThickness;
+      sliceThickness_ = axial.GetPixelSpacingX();
+
+      OrthancStone::Vector origin = axial.GetGeometry().GetOrigin();
+      origin += (static_cast<double>(volume.GetSlicesCount() - 1) *
+                 axialThickness * axial.GetGeometry().GetNormal());
+
+      reference_ = OrthancStone::CoordinateSystem3D(origin,
+                                                    axial.GetGeometry().GetAxisY(),
+                                                    axial.GetGeometry().GetNormal());
+    }
+
+  public:
+    VolumeImageGeometry(const OrthancVolumeImage& volume,
+                        OrthancStone::VolumeProjection projection)
+    {
+      if (volume.GetSlicesCount() == 0)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+
+      converter_ = volume.GetSlice(0).GetConverter();
+
+      switch (projection)
+      {
+        case OrthancStone::VolumeProjection_Axial:
+          SetupAxial(volume);
+          break;
+
+        case OrthancStone::VolumeProjection_Coronal:
+          SetupCoronal(volume);
+          break;
+
+        case OrthancStone::VolumeProjection_Sagittal:
+          SetupSagittal(volume);
+          break;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+    size_t GetSlicesCount() const
+    {
+      return depth_;
+    }
+
+    const OrthancStone::Vector& GetNormal() const
+    {
+      return reference_.GetNormal();
+    }
+
+    bool LookupSlice(size_t& index,
+                     const OrthancStone::CoordinateSystem3D& slice) const
+    {
+      bool opposite;
+      if (!OrthancStone::GeometryToolbox::IsParallelOrOpposite(opposite,
+                                                               reference_.GetNormal(),
+                                                               slice.GetNormal()))
+      {
+        return false;
+      }
+
+      double z = (reference_.ProjectAlongNormal(slice.GetOrigin()) -
+                  reference_.ProjectAlongNormal(reference_.GetOrigin())) / sliceThickness_;
+
+      int s = static_cast<int>(boost::math::iround(z));
+
+      if (s < 0 ||
+          s >= static_cast<int>(depth_))
+      {
+        return false;
+      }
+      else
+      {
+        index = static_cast<size_t>(s);
+        return true;
+      }
+    }
+
+    Slice* GetSlice(size_t slice) const
+    {
+      if (slice >= depth_)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+      else
+      {
+        OrthancStone::CoordinateSystem3D origin(reference_.GetOrigin() +
+                                                static_cast<double>(slice) * sliceThickness_ * reference_.GetNormal(),
+                                                reference_.GetAxisX(),
+                                                reference_.GetAxisY());
+
+        return new Slice(origin, pixelSpacingX_, pixelSpacingY_, sliceThickness_,
+                         width_, height_, converter_);
+      }
+    }
+  };
+
+
+
+  class VolumeImageMPRSlicer :
+    public IVolumeSlicer,
+    public OrthancStone::IObserver
+  {
+  private:
+    class RendererFactory : public LayerReadyMessage::IRendererFactory
+    {
+    private:
+      const Orthanc::ImageAccessor&  frame_;
+      const Slice&                   slice_;
+      bool                           isFullQuality_;
+
+    public:
+      RendererFactory(const Orthanc::ImageAccessor& frame,
+                      const Slice& slice,
+                      bool isFullQuality) :
+        frame_(frame),
+        slice_(slice),
+        isFullQuality_(isFullQuality)
+      {
+      }
+
+      virtual ILayerRenderer* CreateRenderer() const
+      {
+        return FrameRenderer::CreateRenderer(frame_, slice_, isFullQuality_);
+      }
+    };
+
+
+    OrthancVolumeImage&                 volume_;
+    std::unique_ptr<VolumeImageGeometry>  axialGeometry_;
+    std::unique_ptr<VolumeImageGeometry>  coronalGeometry_;
+    std::unique_ptr<VolumeImageGeometry>  sagittalGeometry_;
+
+
+    bool IsGeometryReady() const
+    {
+      return axialGeometry_.get() != NULL;
+    }
+
+    void OnGeometryReady(const ISlicedVolume::GeometryReadyMessage& message)
+    {
+      assert(&message.GetOrigin() == &volume_);
+
+      // These 3 values are only used to speed up the IVolumeSlicer
+      axialGeometry_.reset(new VolumeImageGeometry(volume_, OrthancStone::VolumeProjection_Axial));
+      coronalGeometry_.reset(new VolumeImageGeometry(volume_, OrthancStone::VolumeProjection_Coronal));
+      sagittalGeometry_.reset(new VolumeImageGeometry(volume_, OrthancStone::VolumeProjection_Sagittal));
+
+      BroadcastMessage(IVolumeSlicer::GeometryReadyMessage(*this));
+    }
+
+    void OnGeometryError(const ISlicedVolume::GeometryErrorMessage& message)
+    {
+      assert(&message.GetOrigin() == &volume_);
+
+      BroadcastMessage(IVolumeSlicer::GeometryErrorMessage(*this));
+    }
+
+    void OnContentChanged(const ISlicedVolume::ContentChangedMessage& message)
+    {
+      assert(&message.GetOrigin() == &volume_);
+
+      BroadcastMessage(IVolumeSlicer::ContentChangedMessage(*this));
+    }
+
+    void OnSliceContentChanged(const ISlicedVolume::SliceContentChangedMessage& message)
+    {
+      assert(&message.GetOrigin() == &volume_);
+
+      //IVolumeSlicer::OnSliceContentChange(slice);
+
+      // TODO Improve this?
+      BroadcastMessage(IVolumeSlicer::ContentChangedMessage(*this));
+    }
+
+    const VolumeImageGeometry& GetProjectionGeometry(OrthancStone::VolumeProjection projection)
+    {
+      if (!IsGeometryReady())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+
+      switch (projection)
+      {
+        case OrthancStone::VolumeProjection_Axial:
+          return *axialGeometry_;
+
+        case OrthancStone::VolumeProjection_Sagittal:
+          return *sagittalGeometry_;
+
+        case OrthancStone::VolumeProjection_Coronal:
+          return *coronalGeometry_;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+    }
+
+
+    bool DetectProjection(OrthancStone::VolumeProjection& projection,
+                          const OrthancStone::CoordinateSystem3D& viewportSlice)
+    {
+      bool isOpposite;  // Ignored
+
+      if (OrthancStone::GeometryToolbox::IsParallelOrOpposite(isOpposite,
+                                                              viewportSlice.GetNormal(),
+                                                              axialGeometry_->GetNormal()))
+      {
+        projection = OrthancStone::VolumeProjection_Axial;
+        return true;
+      }
+      else if (OrthancStone::GeometryToolbox::IsParallelOrOpposite(isOpposite,
+                                                                   viewportSlice.GetNormal(),
+                                                                   sagittalGeometry_->GetNormal()))
+      {
+        projection = OrthancStone::VolumeProjection_Sagittal;
+        return true;
+      }
+      else if (OrthancStone::GeometryToolbox::IsParallelOrOpposite(isOpposite,
+                                                                   viewportSlice.GetNormal(),
+                                                                   coronalGeometry_->GetNormal()))
+      {
+        projection = OrthancStone::VolumeProjection_Coronal;
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }
+
+
+  public:
+    VolumeImageMPRSlicer(OrthancStone::MessageBroker& broker,
+                         OrthancVolumeImage&  volume) :
+      IVolumeSlicer(broker),
+      IObserver(broker),
+      volume_(volume)
+    {
+      volume_.RegisterObserverCallback(
+        new OrthancStone::Callable<VolumeImageMPRSlicer, ISlicedVolume::GeometryReadyMessage>
+        (*this, &VolumeImageMPRSlicer::OnGeometryReady));
+
+      volume_.RegisterObserverCallback(
+        new OrthancStone::Callable<VolumeImageMPRSlicer, ISlicedVolume::GeometryErrorMessage>
+        (*this, &VolumeImageMPRSlicer::OnGeometryError));
+
+      volume_.RegisterObserverCallback(
+        new OrthancStone::Callable<VolumeImageMPRSlicer, ISlicedVolume::ContentChangedMessage>
+        (*this, &VolumeImageMPRSlicer::OnContentChanged));
+
+      volume_.RegisterObserverCallback(
+        new OrthancStone::Callable<VolumeImageMPRSlicer, ISlicedVolume::SliceContentChangedMessage>
+        (*this, &VolumeImageMPRSlicer::OnSliceContentChanged));
+    }
+
+    virtual bool GetExtent(std::vector<OrthancStone::Vector>& points,
+                           const OrthancStone::CoordinateSystem3D& viewportSlice) ORTHANC_OVERRIDE
+    {
+      OrthancStone::VolumeProjection projection;
+
+      if (!IsGeometryReady() ||
+          !DetectProjection(projection, viewportSlice))
+      {
+        return false;
+      }
+      else
+      {
+        // As the slices of the volumic image are arranged in a box,
+        // we only consider one single reference slice (the one with index 0).
+        std::unique_ptr<Slice> slice(GetProjectionGeometry(projection).GetSlice(0));
+        slice->GetExtent(points);
+
+        return true;
+      }
+    }
+
+    virtual void ScheduleLayerCreation(const OrthancStone::CoordinateSystem3D& viewportSlice) ORTHANC_OVERRIDE
+    {
+      OrthancStone::VolumeProjection projection;
+
+      if (IsGeometryReady() &&
+          DetectProjection(projection, viewportSlice))
+      {
+        const VolumeImageGeometry& geometry = GetProjectionGeometry(projection);
+
+        size_t closest;
+
+        if (geometry.LookupSlice(closest, viewportSlice))
+        {
+          bool isFullQuality = true;  // TODO
+
+          std::unique_ptr<Orthanc::Image> frame;
+
+          {
+            OrthancStone::ImageBuffer3D::SliceReader reader(volume_.GetImage(), projection, static_cast<unsigned int>(closest));
+
+            // TODO Transfer ownership if non-axial, to avoid memcpy
+            frame.reset(Orthanc::Image::Clone(reader.GetAccessor()));
+          }
+
+          std::unique_ptr<Slice> slice(geometry.GetSlice(closest));
+
+          RendererFactory factory(*frame, *slice, isFullQuality);
+
+          BroadcastMessage(IVolumeSlicer::LayerReadyMessage(*this, factory, slice->GetGeometry()));
+          return;
+        }
+      }
+
+      // Error
+      OrthancStone::CoordinateSystem3D slice;
+      BroadcastMessage(IVolumeSlicer::LayerErrorMessage(*this, slice));
+    }
+  };
+
+
+  class VolumeImageInteractor :
+    public IWorldSceneInteractor,
+    public OrthancStone::IObserver
+  {
+  private:
+    SliceViewerWidget&                  widget_;
+    OrthancStone::VolumeProjection      projection_;
+    std::unique_ptr<VolumeImageGeometry>  slices_;
+    size_t                              slice_;
+
+  protected:
+    void OnGeometryReady(const ISlicedVolume::GeometryReadyMessage& message)
+    {
+      if (slices_.get() == NULL)
+      {
+        const OrthancVolumeImage& image =
+          dynamic_cast<const OrthancVolumeImage&>(message.GetOrigin());
+
+        slices_.reset(new VolumeImageGeometry(image, projection_));
+        SetSlice(slices_->GetSlicesCount() / 2);
+
+        widget_.FitContent();
+      }
+    }
+
+    virtual IWorldSceneMouseTracker* CreateMouseTracker(WorldSceneWidget& widget,
+                                                        const ViewportGeometry& view,
+                                                        OrthancStone::MouseButton button,
+                                                        OrthancStone::KeyboardModifiers modifiers,
+                                                        int viewportX,
+                                                        int viewportY,
+                                                        double x,
+                                                        double y,
+                                                        IStatusBar* statusBar,
+                                                        const std::vector<Touch>& touches) ORTHANC_OVERRIDE
+    {
+      return  NULL;
+    }
+
+    virtual void MouseOver(OrthancStone::CairoContext& context,
+                           WorldSceneWidget& widget,
+                           const ViewportGeometry& view,
+                           double x,
+                           double y,
+                           IStatusBar* statusBar) ORTHANC_OVERRIDE
+    {
+    }
+
+    virtual void MouseWheel(WorldSceneWidget& widget,
+                            OrthancStone::MouseWheelDirection direction,
+                            OrthancStone::KeyboardModifiers modifiers,
+                            IStatusBar* statusBar) ORTHANC_OVERRIDE
+    {
+      int scale = (modifiers & OrthancStone::KeyboardModifiers_Control ? 10 : 1);
+
+      switch (direction)
+      {
+        case OrthancStone::MouseWheelDirection_Up:
+          OffsetSlice(-scale);
+          break;
+
+        case OrthancStone::MouseWheelDirection_Down:
+          OffsetSlice(scale);
+          break;
+
+        default:
+          break;
+      }
+    }
+
+    virtual void KeyPressed(WorldSceneWidget& widget,
+                            OrthancStone::KeyboardKeys key,
+                            char keyChar,
+                            OrthancStone::KeyboardModifiers modifiers,
+                            IStatusBar* statusBar) ORTHANC_OVERRIDE
+    {
+      switch (keyChar)
+      {
+        case 's':
+          widget.FitContent();
+          break;
+
+        default:
+          break;
+      }
+    }
+
+  public:
+    VolumeImageInteractor(OrthancStone::MessageBroker& broker,
+                          OrthancVolumeImage& volume,
+                          SliceViewerWidget& widget,
+                          OrthancStone::VolumeProjection projection) :
+      IObserver(broker),
+      widget_(widget),
+      projection_(projection)
+    {
+      widget.SetInteractor(*this);
+
+      volume.RegisterObserverCallback(
+        new OrthancStone::Callable<VolumeImageInteractor, ISlicedVolume::GeometryReadyMessage>
+        (*this, &VolumeImageInteractor::OnGeometryReady));
+    }
+
+    bool IsGeometryReady() const
+    {
+      return slices_.get() != NULL;
+    }
+
+    size_t GetSlicesCount() const
+    {
+      if (slices_.get() == NULL)
+      {
+        return 0;
+      }
+      else
+      {
+        return slices_->GetSlicesCount();
+      }
+    }
+
+    void OffsetSlice(int offset)
+    {
+      if (slices_.get() != NULL)
+      {
+        int slice = static_cast<int>(slice_) + offset;
+
+        if (slice < 0)
+        {
+          slice = 0;
+        }
+
+        if (slice >= static_cast<int>(slices_->GetSlicesCount()))
+        {
+          slice = static_cast<unsigned int>(slices_->GetSlicesCount()) - 1;
+        }
+
+        if (slice != static_cast<int>(slice_))
+        {
+          SetSlice(slice);
+        }
+      }
+    }
+
+    void SetSlice(size_t slice)
+    {
+      if (slices_.get() != NULL)
+      {
+        slice_ = slice;
+
+        std::unique_ptr<Slice> tmp(slices_->GetSlice(slice_));
+        widget_.SetSlice(tmp->GetGeometry());
+      }
+    }
+  };
+
+
+
+  class ReferenceLineSource : public IVolumeSlicer
+  {
+  private:
+    class RendererFactory : public LayerReadyMessage::IRendererFactory
+    {
+    private:
+      double                     x1_;
+      double                     y1_;
+      double                     x2_;
+      double                     y2_;
+      const OrthancStone::CoordinateSystem3D&  slice_;
+
+    public:
+      RendererFactory(double x1,
+                      double y1,
+                      double x2,
+                      double y2,
+                      const OrthancStone::CoordinateSystem3D& slice) :
+        x1_(x1),
+        y1_(y1),
+        x2_(x2),
+        y2_(y2),
+        slice_(slice)
+      {
+      }
+
+      virtual ILayerRenderer* CreateRenderer() const
+      {
+        return new LineLayerRenderer(x1_, y1_, x2_, y2_, slice_);
+      }
+    };
+
+    SliceViewerWidget&  otherPlane_;
+
+  public:
+    ReferenceLineSource(OrthancStone::MessageBroker& broker,
+                        SliceViewerWidget&  otherPlane) :
+      IVolumeSlicer(broker),
+      otherPlane_(otherPlane)
+    {
+      BroadcastMessage(IVolumeSlicer::GeometryReadyMessage(*this));
+    }
+
+    virtual bool GetExtent(std::vector<OrthancStone::Vector>& points,
+                           const OrthancStone::CoordinateSystem3D& viewportSlice)
+    {
+      return false;
+    }
+
+    virtual void ScheduleLayerCreation(const OrthancStone::CoordinateSystem3D& viewportSlice)
+    {
+      Slice reference(viewportSlice, 0.001);
+
+      OrthancStone::Vector p, d;
+
+      const OrthancStone::CoordinateSystem3D& slice = otherPlane_.GetSlice();
+
+      // Compute the line of intersection between the two slices
+      if (!OrthancStone::GeometryToolbox::IntersectTwoPlanes(p, d,
+                                                             slice.GetOrigin(), slice.GetNormal(),
+                                                             viewportSlice.GetOrigin(), viewportSlice.GetNormal()))
+      {
+        // The two slice are parallel, don't try and display the intersection
+        BroadcastMessage(IVolumeSlicer::LayerErrorMessage(*this, reference.GetGeometry()));
+      }
+      else
+      {
+        double x1, y1, x2, y2;
+        viewportSlice.ProjectPoint(x1, y1, p);
+        viewportSlice.ProjectPoint(x2, y2, p + 1000.0 * d);
+
+        const OrthancStone::Extent2D extent = otherPlane_.GetSceneExtent();
+
+        if (OrthancStone::GeometryToolbox::ClipLineToRectangle(x1, y1, x2, y2,
+                                                               x1, y1, x2, y2,
+                                                               extent.GetX1(), extent.GetY1(),
+                                                               extent.GetX2(), extent.GetY2()))
+        {
+          RendererFactory factory(x1, y1, x2, y2, slice);
+          BroadcastMessage(IVolumeSlicer::LayerReadyMessage(*this, factory, reference.GetGeometry()));
+        }
+        else
+        {
+          // Error: Parallel slices
+          BroadcastMessage(IVolumeSlicer::LayerErrorMessage(*this, reference.GetGeometry()));
+        }
+      }
+    }
+  };
+}