changeset 1593:b782f78aed42

rendering osirix annotations
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 23 Oct 2020 17:39:16 +0200
parents 0d4b11ba86df
children 74ed691f9c64
files Applications/StoneWebViewer/WebApplication/app.js Applications/StoneWebViewer/WebAssembly/CMakeLists.txt Applications/StoneWebViewer/WebAssembly/JavaScriptLibraries.cmake Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp OrthancStone/Sources/Toolbox/OsiriX/CollectionOfAnnotations.cpp OrthancStone/Sources/Toolbox/OsiriX/CollectionOfAnnotations.h
diffstat 6 files changed, 295 insertions(+), 6 deletions(-) [+]
line wrap: on
line diff
--- a/Applications/StoneWebViewer/WebApplication/app.js	Fri Oct 23 15:15:32 2020 +0200
+++ b/Applications/StoneWebViewer/WebApplication/app.js	Fri Oct 23 17:39:16 2020 +0200
@@ -548,6 +548,31 @@
       app.leftMode = 'full';
     }
   }
+
+
+
+  // TODO - TEST
+  axios.get('length.xml')
+    .then(function (response) {
+      stone.LoadOsiriXAnnotations(response.data, false);
+    });
+  
+  axios.get('angle.xml')
+    .then(function (response) {
+      stone.LoadOsiriXAnnotations(response.data, false);
+    });
+  
+  axios.get('arrow.xml')
+    .then(function (response) {
+      stone.LoadOsiriXAnnotations(response.data, false);
+    });
+  
+  axios.get('text.xml')
+    .then(function (response) {
+      stone.LoadOsiriXAnnotations(response.data, false);
+    });
+  
+  
 });
 
 
--- a/Applications/StoneWebViewer/WebAssembly/CMakeLists.txt	Fri Oct 23 15:15:32 2020 +0200
+++ b/Applications/StoneWebViewer/WebAssembly/CMakeLists.txt	Fri Oct 23 17:39:16 2020 +0200
@@ -79,6 +79,20 @@
   message(FATAL_ERROR "CMAKE_BUILD_TYPE must match either Debug, RelWithDebInfo or Release" )
 endif()
 
+
+# We embed a font to be used for on-screen overlays
+# ---------------------------------------------------------------
+
+DownloadPackage(
+  "a24b8136b8f3bb93f166baf97d9328de"
+  "http://orthanc.osimis.io/ThirdPartyDownloads/ubuntu-font-family-0.83.zip"
+  "${CMAKE_BINARY_DIR}/ubuntu-font-family-0.83")
+
+EmbedResources(
+  UBUNTU_FONT  ${CMAKE_BINARY_DIR}/ubuntu-font-family-0.83/Ubuntu-R.ttf
+  )
+
+
 ################################################################################
 
 
--- a/Applications/StoneWebViewer/WebAssembly/JavaScriptLibraries.cmake	Fri Oct 23 15:15:32 2020 +0200
+++ b/Applications/StoneWebViewer/WebAssembly/JavaScriptLibraries.cmake	Fri Oct 23 17:39:16 2020 +0200
@@ -48,6 +48,7 @@
   FILES
   ${CMAKE_CURRENT_BINARY_DIR}/fontawesome-free-5.14.0-web/css/all.css
   ${CMAKE_CURRENT_BINARY_DIR}/bootstrap-3.4.1-dist/css/bootstrap.css
+  ${CMAKE_CURRENT_BINARY_DIR}/bootstrap-3.4.1-dist/css/bootstrap.css.map
   DESTINATION ${ORTHANC_STONE_INSTALL_PREFIX}/css
   )
 
--- a/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp	Fri Oct 23 15:15:32 2020 +0200
+++ b/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp	Fri Oct 23 17:39:16 2020 +0200
@@ -19,6 +19,7 @@
  **/
 
 
+#include <EmbeddedResources.h>
 #include <emscripten.h>
 
 
@@ -72,10 +73,15 @@
 #include <Scene2D/ColorTextureSceneLayer.h>
 #include <Scene2D/FloatTextureSceneLayer.h>
 #include <Scene2D/PolylineSceneLayer.h>
+#include <Scene2D/TextSceneLayer.h>
 #include <Scene2DViewport/ViewportController.h>
 #include <StoneException.h>
 #include <Toolbox/DicomInstanceParameters.h>
 #include <Toolbox/GeometryToolbox.h>
+#include <Toolbox/OsiriX/AngleAnnotation.h>
+#include <Toolbox/OsiriX/CollectionOfAnnotations.h>
+#include <Toolbox/OsiriX/LineAnnotation.h>
+#include <Toolbox/OsiriX/TextAnnotation.h>
 #include <Toolbox/SortedFrames.h>
 #include <Viewport/DefaultViewportInteractor.h>
 
@@ -946,6 +952,22 @@
       return false;
     }
   }
+
+
+  bool ProjectPoint(double& x,
+                    double& y,
+                    const OrthancStone::Vector& v) const
+  {
+    if (IsValid())
+    {
+      coordinates_.ProjectPoint(x, y, v);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
 };
   
 
@@ -969,6 +991,8 @@
 private:
   static const int LAYER_TEXTURE = 0;
   static const int LAYER_REFERENCE_LINES = 1;
+  static const int LAYER_ANNOTATIONS = 2;
+  static const int LAYER_TEMP = 3;  // TODO - REMOVE
   
   
   class ICommand : public Orthanc::IDynamicObject
@@ -1053,6 +1077,7 @@
     }
   };
 
+
   class SetLowQualityFrame : public ICommand
   {
   private:
@@ -1282,6 +1307,8 @@
   FrameGeometry                                currentFrameGeometry_;
   std::list<PrefetchItem>                      prefetchQueue_;
 
+  boost::shared_ptr<OrthancStone::OsiriX::CollectionOfAnnotations>  annotations_;  
+
   void ScheduleNextPrefetch()
   {
     while (!prefetchQueue_.empty())
@@ -1340,6 +1367,7 @@
     }
   }
 
+
   void DisplayCurrentFrame()
   {
     DisplayedFrameQuality quality = DisplayedFrameQuality_None;
@@ -1401,6 +1429,7 @@
       currentFrameGeometry_ = FrameGeometry();
     }
   }
+  
 
   void ClearViewport()
   {
@@ -1482,6 +1511,96 @@
         pixelSpacingX, pixelSpacingY, frames_->GetFrameTags(index));
       layer->SetPixelSpacing(pixelSpacingX, pixelSpacingY);
 
+
+      /****
+       * BEGINNING OF EXPERIMENTAL CODE
+       ****/
+      
+      std::unique_ptr<OrthancStone::ISceneLayer>  annotationsLayer;  // TODO - Macro layer
+      std::unique_ptr<OrthancStone::ISceneLayer>  tempLayer;  // TODO - Macro layer
+
+      if (annotations_)
+      {
+        std::set<size_t> a;
+        annotations_->LookupSopInstanceUid(a, sopInstanceUid);
+        if (!a.empty())
+        {
+          using namespace OrthancStone::OsiriX;
+
+          std::unique_ptr<OrthancStone::PolylineSceneLayer> layer(new OrthancStone::PolylineSceneLayer);
+          
+          for (std::set<size_t>::const_iterator it = a.begin(); it != a.end(); ++it)
+          {
+            const Annotation& annotation = annotations_->GetAnnotation(*it);
+
+            switch (annotation.GetType())
+            {
+              case Annotation::Type_Line:
+              {
+                const LineAnnotation& line = dynamic_cast<const LineAnnotation&>(annotation);
+                double x1, y1, x2, y2;
+                if (GetCurrentFrameGeometry().ProjectPoint(x1, y1, line.GetPoint1()) &&
+                    GetCurrentFrameGeometry().ProjectPoint(x2, y2, line.GetPoint2()))
+                {
+                  OrthancStone::PolylineSceneLayer::Chain chain;
+                  chain.push_back(OrthancStone::ScenePoint2D(x1, y1));
+                  chain.push_back(OrthancStone::ScenePoint2D(x2, y2));
+
+                  // TODO - IsArrow
+                  
+                  layer->AddChain(chain, false, 0, 255, 0);
+                }
+                break;
+              }
+
+              case Annotation::Type_Angle:
+              {
+                const AngleAnnotation& angle = dynamic_cast<const AngleAnnotation&>(annotation);
+                double x1, y1, x2, y2, x3, y3;
+                if (GetCurrentFrameGeometry().ProjectPoint(x1, y1, angle.GetA()) &&
+                    GetCurrentFrameGeometry().ProjectPoint(x2, y2, angle.GetCenter()) &&
+                    GetCurrentFrameGeometry().ProjectPoint(x3, y3, angle.GetB()))
+                {
+                  OrthancStone::PolylineSceneLayer::Chain chain;
+                  chain.push_back(OrthancStone::ScenePoint2D(x1, y1));
+                  chain.push_back(OrthancStone::ScenePoint2D(x2, y2));
+                  chain.push_back(OrthancStone::ScenePoint2D(x3, y3));
+                  layer->AddChain(chain, false, 0, 255, 0);
+                }
+                break;
+              }
+
+              case Annotation::Type_Text:
+              {
+                const TextAnnotation& text = dynamic_cast<const TextAnnotation&>(annotation);
+                double x, y;
+                OrthancStone::LinearAlgebra::Print(text.GetCenter());
+                if (GetCurrentFrameGeometry().ProjectPoint(x, y, text.GetCenter()))
+                {
+                  std::unique_ptr<OrthancStone::TextSceneLayer> layer2(new OrthancStone::TextSceneLayer());
+                  layer2->SetPosition(x, y);
+                  layer2->SetText(text.GetText());
+                  layer2->SetAnchor(OrthancStone::BitmapAnchor_Center);
+                  layer2->SetColor(255, 0, 0);
+                  tempLayer.reset(layer2.release());
+                }
+                break;
+              }
+
+              default:
+                LOG(ERROR) << "Annotation type not implemented: " << annotation.GetType();
+            }
+          }
+
+          annotationsLayer.reset(layer.release());
+        }
+      }
+
+      /****
+       * END OF EXPERIMENTAL CODE
+       ****/     
+
+      
       if (layer.get() == NULL)
       {
         return false;
@@ -1494,6 +1613,24 @@
 
         scene.SetLayer(LAYER_TEXTURE, layer.release());
 
+        if (annotationsLayer.get() != NULL)
+        {
+          scene.SetLayer(LAYER_ANNOTATIONS, annotationsLayer.release());
+        }
+        else
+        {
+          scene.DeleteLayer(LAYER_ANNOTATIONS);
+        }
+
+        if (tempLayer.get() != NULL)   // TODO - REMOVE
+        {
+          scene.SetLayer(LAYER_TEMP, tempLayer.release());
+        }
+        else
+        {
+          scene.DeleteLayer(LAYER_TEMP);
+        }
+
         if (fitNextContent_)
         {
           lock->RefreshCanvasSize();
@@ -1626,6 +1763,13 @@
       LOG(INFO) << "Creating WebGL viewport in canvas: " << canvas;
       viewport_ = OrthancStone::WebGLViewport::Create(canvas);
     }
+
+    {
+      std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
+      std::string ttf;
+      Orthanc::EmbeddedResources::GetFileResource(ttf, Orthanc::EmbeddedResources::UBUNTU_FONT);
+      lock->GetCompositor().SetFont(0, ttf, 24 /* font size */, Orthanc::Encoding_Latin1);
+    }
     
     emscripten_set_wheel_callback(viewport_->GetCanvasCssSelector().c_str(), this, true, OnWheel);
     emscripten_set_keydown_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, this, false, OnKey);
@@ -1775,6 +1919,17 @@
     }
   }
 
+
+  void UpdateCurrentFrame()
+  {
+    if (cursor_.get() != NULL)
+    {
+      unsigned int quality;
+      DisplayFrame(quality, cursor_->GetCurrentIndex());
+    }
+  }
+
+
   // This method is used when the layout of the HTML page changes,
   // which does not trigger the "emscripten_set_resize_callback()"
   void UpdateSize(bool fitContent)
@@ -1995,6 +2150,12 @@
   {
     viewport_->FitForPrint();
   }
+
+  void SetAnnotations(boost::shared_ptr<OrthancStone::OsiriX::CollectionOfAnnotations> annotations)
+  {
+    annotations_ = annotations;
+  }
+
 };
 
 
@@ -2004,6 +2165,7 @@
 typedef std::map<std::string, boost::shared_ptr<ViewerViewport> >  Viewports;
 static Viewports allViewports_;
 static bool showReferenceLines_ = true;
+static boost::shared_ptr<OrthancStone::OsiriX::CollectionOfAnnotations>  annotations_;
 
 
 static void UpdateReferenceLines()
@@ -2153,6 +2315,7 @@
       ViewerViewport::Create(*lock, source_, canvas, cache_, softwareRendering_));
     viewport->SetMouseButtonActions(leftButtonAction_, middleButtonAction_, rightButtonAction_);
     viewport->AcquireObserver(new WebAssemblyObserver);
+    viewport->SetAnnotations(annotations_);
     allViewports_[canvas] = viewport;
     return viewport;
   }
@@ -2174,6 +2337,7 @@
 
     context_.reset(new OrthancStone::WebAssemblyLoadersContext(1, 4, 1));
     cache_.reset(new FramesCache);
+    annotations_.reset(new OrthancStone::OsiriX::CollectionOfAnnotations);
     
     DISPATCH_JAVASCRIPT_EVENT("StoneInitialized");
   }
@@ -2538,4 +2702,38 @@
     }
     EXTERN_CATCH_EXCEPTIONS;
   }
+
+
+  EMSCRIPTEN_KEEPALIVE
+  int LoadOsiriXAnnotations(const char* xml,
+                            int clearPreviousAnnotations)
+  {
+    try
+    {
+      if (clearPreviousAnnotations)
+      {
+        annotations_->Clear();
+      }
+      
+      annotations_->LoadXml(xml);
+
+      for (Viewports::iterator it = allViewports_.begin(); it != allViewports_.end(); ++it)
+      {
+        // TODO - Check if the viewport contains one of the SOP
+        // Instance UID from the loaded annotations => focus on this
+        // instance
+
+        // TODO - If no viewport contains the instance => monitor the
+        // "ResourcesLoader" as new series get loaded
+
+        assert(it->second != NULL);
+        it->second->UpdateCurrentFrame();
+      }
+
+      LOG(WARNING) << "Loaded " << annotations_->GetSize() << " annotations from OsiriX";
+      return 1;
+    }
+    EXTERN_CATCH_EXCEPTIONS;
+    return 0;
+  }
 }
--- a/OrthancStone/Sources/Toolbox/OsiriX/CollectionOfAnnotations.cpp	Fri Oct 23 15:15:32 2020 +0200
+++ b/OrthancStone/Sources/Toolbox/OsiriX/CollectionOfAnnotations.cpp	Fri Oct 23 17:39:16 2020 +0200
@@ -42,13 +42,16 @@
     }
 
       
-    CollectionOfAnnotations::~CollectionOfAnnotations()
+    void CollectionOfAnnotations::Clear()
     {
       for (size_t i = 0; i < annotations_.size(); i++)
       {
         assert(annotations_[i] != NULL);
         delete annotations_[i];
       }
+
+      annotations_.clear();
+      index_.clear();
     }
 
 
@@ -74,14 +77,44 @@
       }
       else
       {
+        size_t pos = annotations_.size();
         annotations_.push_back(annotation);
+
+        SopInstanceUidIndex::iterator found = index_.find(annotation->GetSopInstanceUid());
+        if (found == index_.end())
+        {
+          std::set<size_t> s;
+          s.insert(pos);
+          index_[annotation->GetSopInstanceUid()] = s;
+        }
+        else
+        {
+          found->second.insert(pos);
+        }
       }
     }
 
-    void CollectionOfAnnotations::ParseXml(const std::string& xml)
+
+    void CollectionOfAnnotations::LookupSopInstanceUid(std::set<size_t>& target,
+                                                       const std::string& sopInstanceUid) const
+    {
+      SopInstanceUidIndex::const_iterator found = index_.find(sopInstanceUid);
+      if (found == index_.end())
+      {
+        target.clear();
+      }
+      else
+      {
+        target = found->second;
+      }
+    }
+    
+
+    void CollectionOfAnnotations::LoadXml(const char* xml,
+                                          size_t size)
     {
       pugi::xml_document doc;
-      pugi::xml_parse_result result = doc.load_buffer(xml.empty() ? NULL : xml.c_str(), xml.size());
+      pugi::xml_parse_result result = doc.load_buffer(xml, size);
       if (!result)
       {
         throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
--- a/OrthancStone/Sources/Toolbox/OsiriX/CollectionOfAnnotations.h	Fri Oct 23 15:15:32 2020 +0200
+++ b/OrthancStone/Sources/Toolbox/OsiriX/CollectionOfAnnotations.h	Fri Oct 23 17:39:16 2020 +0200
@@ -23,6 +23,7 @@
 
 #include "Annotation.h"
 
+#include <set>
 #include <vector>
 
 namespace OrthancStone
@@ -32,11 +33,19 @@
     class CollectionOfAnnotations : public boost::noncopyable
     {
     private:
+      typedef std::map<std::string, std::set<size_t> >  SopInstanceUidIndex;
+      
       std::vector<Annotation*>  annotations_;
+      SopInstanceUidIndex       index_;
 
     public:
-      ~CollectionOfAnnotations();
+      ~CollectionOfAnnotations()
+      {
+        Clear();
+      }
 
+      void Clear();
+      
       size_t GetSize() const
       {
         return annotations_.size();
@@ -46,8 +55,17 @@
 
       void AddAnnotation(Annotation* annotation);  // takes ownership
 
-      // Parse an XML from OsiriX
-      void ParseXml(const std::string& xml);
+      void LookupSopInstanceUid(std::set<size_t>& target,
+                                const std::string& sopInstanceUid) const;
+
+      // Load an XML from OsiriX
+      void LoadXml(const char* xml,
+                    size_t size);
+
+      void LoadXml(const std::string& xml)
+      {
+        LoadXml(xml.empty() ? NULL : xml.c_str(), xml.size());
+      }
     };
   }
 }