changeset 1432:758fb6958c20 loader-injection-feature

Merge from default
author Benjamin Golinvaux <bgo@osimis.io>
date Tue, 19 May 2020 07:39:03 +0200
parents 4e7751a4b603 (diff) 1eaf19af15bf (current diff)
children 49f31fa332b3
files Samples/build-wasm-RtViewer.sh Samples/build-wasm-SingleFrameViewer.sh UnitTestsSources/CMakeLists.txt
diffstat 5 files changed, 303 insertions(+), 108 deletions(-) [+]
line wrap: on
line diff
--- a/Framework/Loaders/DicomStructureSetLoader.cpp	Fri May 15 21:14:35 2020 +0200
+++ b/Framework/Loaders/DicomStructureSetLoader.cpp	Tue May 19 07:39:03 2020 +0200
@@ -48,8 +48,37 @@
   }
 #endif
 
+  // implementation of IInstanceLookupHandler that uses Orthanc REST API calls to retrive the 
+  // geometry of referenced instances
+  class DicomStructureSetLoader::RestInstanceLookupHandler : public DicomStructureSetLoader::IInstanceLookupHandler,
+    public LoaderStateMachine
+  {
+  public:
+    static boost::shared_ptr<RestInstanceLookupHandler > Create(DicomStructureSetLoader& loader)
+    {
+      boost::shared_ptr<RestInstanceLookupHandler> obj(new RestInstanceLookupHandler(loader));
+      obj->LoaderStateMachine::PostConstructor();
+      return obj;
+    }
 
-  class DicomStructureSetLoader::AddReferencedInstance : public LoaderStateMachine::State
+  protected:
+    RestInstanceLookupHandler(DicomStructureSetLoader& loader) 
+      : LoaderStateMachine(loader.loadersContext_)
+      , loader_(loader)
+    {
+    }
+
+    virtual void RetrieveReferencedSlices(const std::set<std::string>& nonEmptyInstances) ORTHANC_OVERRIDE;
+
+  private:
+    // these subclasses hold the loading state
+    class AddReferencedInstance;   // 2nd state
+    class LookupInstance;          // 1st state
+
+    DicomStructureSetLoader& loader_;
+  };
+
+  class DicomStructureSetLoader::RestInstanceLookupHandler::AddReferencedInstance : public LoaderStateMachine::State
   {
   private:
     std::string instanceId_;
@@ -71,27 +100,14 @@
       dicom.FromDicomAsJson(tags);
 
       DicomStructureSetLoader& loader = GetLoader<DicomStructureSetLoader>();
-
-      loader.content_->AddReferencedSlice(dicom);
-      loader.countProcessedInstances_ ++;
-      assert(loader.countProcessedInstances_ <= loader.countReferencedInstances_);
-
-      loader.revision_++;
-      loader.SetStructuresUpdated();
-
-      if (loader.countProcessedInstances_ == loader.countReferencedInstances_)
-      {
-        // All the referenced instances have been loaded, finalize the RT-STRUCT
-        loader.content_->CheckReferencedSlices();
-        loader.revision_++;
-        loader.SetStructuresReady();
-      }
+    
+      loader.AddReferencedSlice(dicom);
     }
   };
 
 
   // State that converts a "SOP Instance UID" to an Orthanc identifier
-  class DicomStructureSetLoader::LookupInstance : public LoaderStateMachine::State
+  class DicomStructureSetLoader::RestInstanceLookupHandler::LookupInstance : public LoaderStateMachine::State
   {
   private:
     std::string  sopInstanceUid_;
@@ -106,9 +122,6 @@
 
     virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message)
     {
-#if 0
-      LOG(TRACE) << "DicomStructureSetLoader::LookupInstance::Handle() (SUCCESS)";
-#endif
       DicomStructureSetLoader& loader = GetLoader<DicomStructureSetLoader>();
 
       Json::Value lookup;
@@ -147,6 +160,21 @@
     }
   };
 
+  void DicomStructureSetLoader::RestInstanceLookupHandler::RetrieveReferencedSlices(
+    const std::set<std::string>& nonEmptyInstances)
+  {
+    for (std::set<std::string>::const_iterator it = nonEmptyInstances.begin(); 
+         it != nonEmptyInstances.end(); 
+         ++it)
+    {
+      std::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand);
+      command->SetUri("/tools/lookup");
+      command->SetMethod(Orthanc::HttpMethod_Post);
+      command->SetBody(*it);
+      command->AcquirePayload(new LookupInstance(loader_, *it));
+      Schedule(command.release());
+    }
+  }
 
   class DicomStructureSetLoader::LoadStructure : public LoaderStateMachine::State
   {
@@ -159,53 +187,28 @@
     virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message)
     {
       DicomStructureSetLoader& loader = GetLoader<DicomStructureSetLoader>();
-        
+
+      // Set the actual structure set content
       {
         OrthancPlugins::FullOrthancDataset dicom(message.GetAnswer());
+
         loader.content_.reset(new OrthancStone::DicomStructureSet(dicom));
-        size_t structureCount = loader.content_->GetStructuresCount();
-        loader.structureVisibility_.resize(structureCount);
-        bool everythingVisible = false;
-        if ((loader.initiallyVisibleStructures_.size() == 1)
-          && (loader.initiallyVisibleStructures_[0].size() == 1)
-          && (loader.initiallyVisibleStructures_[0][0] == '*'))
-        {
-          everythingVisible = true;
-        }
-
-        for (size_t i = 0; i < structureCount; ++i)
-        {
-          // if a single "*" string is supplied, this means we want everything 
-          // to be visible...
-          if(everythingVisible)
-          {
-            loader.structureVisibility_.at(i) = true;
-          }
-          else
-          {
-            // otherwise, we only enable visibility for those structures whose 
-            // names are mentioned in the initiallyVisibleStructures_ array
-            const std::string& structureName = loader.content_->GetStructureName(i);
-
-            std::vector<std::string>::iterator foundIt =
-              std::find(
-                loader.initiallyVisibleStructures_.begin(),
-                loader.initiallyVisibleStructures_.end(),
-                structureName);
-            std::vector<std::string>::iterator endIt = loader.initiallyVisibleStructures_.end();
-            if (foundIt != endIt)
-              loader.structureVisibility_.at(i) = true;
-            else
-              loader.structureVisibility_.at(i) = false;
-          }
-        }
       }
 
+      // initialize visibility flags
+      SetDefaultStructureVisibility();
+
+      // retrieve the (non-empty) referenced instances (the CT slices containing the corresponding structures)
       // Some (admittedly invalid) Dicom files have empty values in the 
       // 0008,1155 tag. We try our best to cope with this.
+      // this is why we use `nonEmptyInstances` and not `instances`
       std::set<std::string> instances;
       std::set<std::string> nonEmptyInstances;
+
+      // this traverses the polygon collection for all structures and retrieve the SOPInstanceUID of 
+      // the referenced instances
       loader.content_->GetReferencedInstances(instances);
+
       for (std::set<std::string>::const_iterator
         it = instances.begin(); it != instances.end(); ++it)
       {
@@ -214,20 +217,55 @@
           nonEmptyInstances.insert(instance);
       }
 
-      loader.countReferencedInstances_ = 
-        static_cast<unsigned int>(nonEmptyInstances.size());
+      loader.RetrieveReferencedSlices(nonEmptyInstances);
+    }
+
+    void SetDefaultStructureVisibility()
+    {
+      DicomStructureSetLoader& loader = GetLoader<DicomStructureSetLoader>();
+
+      size_t structureCount = loader.content_->GetStructuresCount();
 
-      for (std::set<std::string>::const_iterator
-        it = nonEmptyInstances.begin(); it != nonEmptyInstances.end(); ++it)
+      loader.structureVisibility_.resize(structureCount);
+      bool everythingVisible = false;
+      if ((loader.initiallyVisibleStructures_.size() == 1)
+          && (loader.initiallyVisibleStructures_[0].size() == 1)
+          && (loader.initiallyVisibleStructures_[0][0] == '*'))
+      {
+        everythingVisible = true;
+      }
+
+      for (size_t i = 0; i < structureCount; ++i)
       {
-        std::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand);
-        command->SetUri("/tools/lookup");
-        command->SetMethod(Orthanc::HttpMethod_Post);
-        command->SetBody(*it);
-        command->AcquirePayload(new LookupInstance(loader, *it));
-        Schedule(command.release());
+        // if a single "*" string is supplied, this means we want everything 
+        // to be visible...
+        if (everythingVisible)
+        {
+          loader.structureVisibility_.at(i) = true;
+        }
+        else
+        {
+          // otherwise, we only enable visibility for those structures whose 
+          // names are mentioned in the initiallyVisibleStructures_ array
+          const std::string& structureName = loader.content_->GetStructureName(i);
+
+          std::vector<std::string>::iterator foundIt =
+            std::find(
+              loader.initiallyVisibleStructures_.begin(),
+              loader.initiallyVisibleStructures_.end(),
+              structureName);
+          std::vector<std::string>::iterator endIt = loader.initiallyVisibleStructures_.end();
+          if (foundIt != endIt)
+            loader.structureVisibility_.at(i) = true;
+          else
+            loader.structureVisibility_.at(i) = false;
+        }
       }
     }
+
+    private:
+
+
   };
     
 
@@ -347,8 +385,9 @@
     , countReferencedInstances_(0)
     , structuresReady_(false)
   {
+    // the default handler to retrieve slice geometry is RestInstanceLookupHandler
+    instanceLookupHandler_ = RestInstanceLookupHandler::Create(*this);
   }
-   
     
   boost::shared_ptr<OrthancStone::DicomStructureSetLoader> DicomStructureSetLoader::Create(OrthancStone::ILoadersContext& loadersContext)
   {
@@ -357,7 +396,31 @@
         loadersContext));
     obj->LoaderStateMachine::PostConstructor();
     return obj;
+  }
 
+  void DicomStructureSetLoader::AddReferencedSlice(const Orthanc::DicomMap& dicom)
+  {
+    content_->AddReferencedSlice(dicom);
+    countProcessedInstances_ ++;
+    assert(countProcessedInstances_ <= countReferencedInstances_);
+
+    revision_++;
+    SetStructuresUpdated();
+
+    if (countProcessedInstances_ == countReferencedInstances_)
+    {
+      // All the referenced instances have been loaded, finalize the RT-STRUCT
+      content_->CheckReferencedSlices();
+      revision_++;
+      SetStructuresReady();
+    }
+  }
+
+  void DicomStructureSetLoader::RetrieveReferencedSlices(const std::set<std::string>& nonEmptyInstances)
+  {
+    // we set the number of referenced instances. This allows to know, in the method above, when we're done
+    countReferencedInstances_ = static_cast<unsigned int>(nonEmptyInstances.size());
+    instanceLookupHandler_->RetrieveReferencedSlices(nonEmptyInstances);
   }
 
   void DicomStructureSetLoader::SetStructureDisplayState(size_t structureIndex, bool display)
--- a/Framework/Loaders/DicomStructureSetLoader.h	Fri May 15 21:14:35 2020 +0200
+++ b/Framework/Loaders/DicomStructureSetLoader.h	Tue May 19 07:39:03 2020 +0200
@@ -35,12 +35,76 @@
     public OrthancStone::IVolumeSlicer,
     public OrthancStone::IObservable
   {
+  public:
+    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, StructuresReady, DicomStructureSetLoader);
+    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, StructuresUpdated, DicomStructureSetLoader);
+
+    /**
+    
+    Once the structure set has been loaded (the LoadStructure state), we need to fill it with geometry information
+    from the referenced slices (tag (0008,1155) described here:
+    https://dicom.innolitics.com/ciods/rt-structure-set/general-reference/00081140/00081155
+
+    This interface allows to customize how this information can be gathered. By default, the RestInstanceLookupHandler
+    will perform a REST call to the Orthanc API to retrieve this information.
+
+    Injecting another implementation of this interface is useful when where this information can be supplied in 
+    another (faster) way (for instance, if a separate loader for the CT series can be used to supply the slice geometry)
+    */
+    class IInstanceLookupHandler
+    {
+    public:
+      virtual void RetrieveReferencedSlices(const std::set<std::string>& instances) = 0;
+    };
+
+    // predeclaration of the default IInstanceLookupHandler implementation
+    class RestInstanceLookupHandler;
+
+    static boost::shared_ptr<DicomStructureSetLoader> Create(
+      OrthancStone::ILoadersContext& loadersContext);
+
+    void SetInstanceLookupHandler(boost::shared_ptr<IInstanceLookupHandler> instanceLookupHandler)
+    {
+      instanceLookupHandler_ = instanceLookupHandler;
+    }
+
+    OrthancStone::DicomStructureSet* GetContent()
+    {
+      return content_.get();
+    }
+
+    void SetStructureDisplayState(size_t structureIndex, bool display);
+    
+    bool GetStructureDisplayState(size_t structureIndex) const
+    {
+      return structureVisibility_.at(structureIndex);
+    }
+
+    ~DicomStructureSetLoader();
+    
+    void LoadInstance(const std::string& instanceId, 
+                      const std::vector<std::string>& initiallyVisibleStructures = std::vector<std::string>());
+
+    void LoadInstanceFullVisibility(const std::string& instanceId);
+
+
+    virtual IExtractedSlice* ExtractSlice(const OrthancStone::CoordinateSystem3D& cuttingPlane) ORTHANC_OVERRIDE;
+
+    void SetStructuresReady();
+    void SetStructuresUpdated();
+
+    bool AreStructuresReady() const;
+
+    /**
+      Called by the IInstanceLookupHandler when slice referenced instance information is available. 
+      When the last referenced slice is received, this method will perform a final check and will warn observers
+    */
+    void AddReferencedSlice(const Orthanc::DicomMap& dicom);
+
   private:
     class Slice;
 
-    // States of LoaderStateMachine
-    class AddReferencedInstance;   // 3rd state
-    class LookupInstance;          // 2nd state
+    // Only state of LoaderStateMachine
     class LoadStructure;           // 1st state
     
     OrthancStone::ILoadersContext&                    loadersContext_;
@@ -70,41 +134,13 @@
     */
     std::vector<bool>                  structureVisibility_;
 
+
+    boost::shared_ptr<IInstanceLookupHandler> instanceLookupHandler_;
+
+  private:
+    void RetrieveReferencedSlices(const std::set<std::string>& nonEmptyInstances);
+
   protected:
     DicomStructureSetLoader(OrthancStone::ILoadersContext& loadersContext);
-
-  public:
-    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, StructuresReady, DicomStructureSetLoader);
-    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, StructuresUpdated, DicomStructureSetLoader);
-
-    static boost::shared_ptr<DicomStructureSetLoader> Create(
-      OrthancStone::ILoadersContext& loadersContext);
-
-    OrthancStone::DicomStructureSet* GetContent()
-    {
-      return content_.get();
-    }
-
-    void SetStructureDisplayState(size_t structureIndex, bool display);
-    
-    bool GetStructureDisplayState(size_t structureIndex) const
-    {
-      return structureVisibility_.at(structureIndex);
-    }
-
-    ~DicomStructureSetLoader();
-    
-    void LoadInstance(const std::string& instanceId, 
-                      const std::vector<std::string>& initiallyVisibleStructures = std::vector<std::string>());
-
-    void LoadInstanceFullVisibility(const std::string& instanceId);
-
-
-    virtual IExtractedSlice* ExtractSlice(const OrthancStone::CoordinateSystem3D& cuttingPlane) ORTHANC_OVERRIDE;
-
-    void SetStructuresReady();
-    void SetStructuresUpdated();
-
-    bool AreStructuresReady() const;
   };
 }
--- a/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp	Fri May 15 21:14:35 2020 +0200
+++ b/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp	Tue May 19 07:39:03 2020 +0200
@@ -362,6 +362,9 @@
         // the 3D plane corresponding to the slice
         OrthancStone::CoordinateSystem3D geometry = instance->GetGeometry();
         slices.AddSlice(geometry, instance.release());
+
+        if (slicePostProcessor_)
+          slicePostProcessor_->ProcessCTDicomSlice(dicom);
       }
 
       seriesGeometry_.ComputeGeometry(slices);
--- a/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h	Fri May 15 21:14:35 2020 +0200
+++ b/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h	Tue May 19 07:39:03 2020 +0200
@@ -57,6 +57,12 @@
     static const unsigned int QUALITY_02 = 2;
         
     class ExtractedSlice;
+
+    class ISlicePostProcessor
+    {
+    public:
+      virtual void ProcessCTDicomSlice(const Orthanc::DicomMap& dicom) = 0;
+    };
     
     /** Helper class internal to OrthancSeriesVolumeProgressiveLoader */
     class SeriesGeometry : public boost::noncopyable
@@ -121,6 +127,8 @@
     std::vector<unsigned int>     slicesQuality_;
     bool                          volumeImageReadyInHighQuality_;
     
+    boost::shared_ptr<ISlicePostProcessor>  slicePostProcessor_;
+
     OrthancSeriesVolumeProgressiveLoader(
       OrthancStone::ILoadersContext& loadersContext,
       boost::shared_ptr<OrthancStone::DicomVolumeImage> volume,
@@ -141,6 +149,12 @@
 
     void SetSimultaneousDownloads(unsigned int count);
 
+    void SetDicomSlicePostProcessor(boost::shared_ptr<ISlicePostProcessor> slicePostProcessor)
+    {
+      // this will delete the previously stored slice processor, if any
+      slicePostProcessor_ = slicePostProcessor;
+    }
+
     bool IsVolumeImageReadyInHighQuality() const
     {
       return volumeImageReadyInHighQuality_;
--- a/UnitTestsSources/TestStructureSet.cpp	Fri May 15 21:14:35 2020 +0200
+++ b/UnitTestsSources/TestStructureSet.cpp	Tue May 19 07:39:03 2020 +0200
@@ -32,6 +32,11 @@
 #include "Framework/Toolbox/DicomStructureSet2.h"
 #include "Framework/Toolbox/DisjointDataSet.h"
 
+#include "Framework/Loaders/GenericLoadersContext.h"
+#include "Framework/Loaders/DicomStructureSetLoader.h"
+
+#include "boost/date_time/posix_time/posix_time.hpp"
+
 #include <Core/SystemToolbox.h>
 
 #include "gtest/gtest.h"
@@ -5406,9 +5411,83 @@
   const std::vector<DicomStructure2>& structures = structureSet.structures_;
 }
 
-
 #endif 
 // BGO_ENABLE_DICOMSTRUCTURESETLOADER2
 
-
-
+namespace
+{
+  void Initialize(const char* orthancApiUrl, OrthancStone::ILoadersContext& loadersContext)
+  {
+    Orthanc::WebServiceParameters p;
+
+    OrthancStone::GenericLoadersContext& typedLoadersContext =
+      dynamic_cast<OrthancStone::GenericLoadersContext&>(loadersContext);
+    // Default is http://localhost:8042
+    // Here's how you may change it
+    p.SetUrl(orthancApiUrl);
+    p.SetCredentials("orthanc", "orthanc");
+    typedLoadersContext.SetOrthancParameters(p);
+
+    typedLoadersContext.StartOracle();
+  }
+
+  void Exitialize(OrthancStone::ILoadersContext& loadersContext)
+  {
+    OrthancStone::GenericLoadersContext& typedLoadersContext =
+      dynamic_cast<OrthancStone::GenericLoadersContext&>(loadersContext);
+
+    typedLoadersContext.StopOracle();
+  }
+
+
+#if 0
+  class TestObserver : public ObserverBase<TestObserver>
+  {
+  public:
+    TestObserver() {};
+
+    virtual void Handle
+
+  };
+#endif
+
+}
+
+
+TEST(StructureSet, DISABLED_StructureSetLoader_injection_feature_2020_05_10)
+{
+  namespace pt = boost::posix_time;
+
+  std::unique_ptr<OrthancStone::ILoadersContext> loadersContext(new OrthancStone::GenericLoadersContext(1,4,1));
+  Initialize("http://localhost:8042/", *loadersContext);
+
+  boost::shared_ptr<DicomStructureSetLoader> loader = DicomStructureSetLoader::Create(*loadersContext);
+
+  // replace with Orthanc ID of an uploaded RTSTRUCT instance!
+  loader->LoadInstanceFullVisibility("72c773ac-5059f2c4-2e6a9120-4fd4bca1-45701661");
+
+  bool bContinue(true);
+
+  pt::ptime initialTime = pt::second_clock::local_time();
+
+  while (bContinue)
+  {
+    bContinue = !loader->AreStructuresReady();
+    boost::this_thread::sleep_for(boost::chrono::milliseconds(1000));
+
+    {
+      pt::ptime nowTime = pt::second_clock::local_time();
+      pt::time_duration diff = nowTime - initialTime;
+      double seconds = static_cast<double>(diff.total_milliseconds()) * 0.001;
+      std::cout << seconds << " seconds elapsed...\n";
+      if (seconds > 30)
+      {
+        std::cout << "More than 30 seconds elapsed... Aborting test :(\n";
+        //GTEST_FATAL_FAILURE_("More than 30 seconds elapsed... Aborting test :(");
+        //bContinue = false;
+      }
+    }
+  }
+}
+
+