changeset 1885:ddaee6b96501

retrieving rt-struct info
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 14 Jan 2022 19:04:05 +0100
parents 0c6923982fdd
children ca89fec8c48f
files OrthancStone/Sources/Toolbox/DicomStructureSet.cpp OrthancStone/Sources/Toolbox/DicomStructureSet.h RenderingPlugin/CMakeLists.txt RenderingPlugin/Sources/OrthancPluginConnection.cpp RenderingPlugin/Sources/OrthancPluginConnection.h RenderingPlugin/Sources/Plugin.cpp
diffstat 6 files changed, 502 insertions(+), 3 deletions(-) [+]
line wrap: on
line diff
--- a/OrthancStone/Sources/Toolbox/DicomStructureSet.cpp	Fri Jan 14 14:14:25 2022 +0100
+++ b/OrthancStone/Sources/Toolbox/DicomStructureSet.cpp	Fri Jan 14 19:04:05 2022 +0100
@@ -700,7 +700,7 @@
   }
 
 
-  void DicomStructureSet::GetReferencedInstances(std::set<std::string>& instances)
+  void DicomStructureSet::GetReferencedInstances(std::set<std::string>& instances) const
   {
     for (Structures::const_iterator structure = structures_.begin();
          structure != structures_.end(); ++structure)
@@ -1087,4 +1087,26 @@
     }
 #endif
   }
+
+
+  void DicomStructureSet::GetStructurePoints(std::list< std::vector<Vector> >& target,
+                                             size_t structureIndex,
+                                             const std::string& sopInstanceUid) const
+  {
+    target.clear();
+    
+    const Structure& structure = GetStructure(structureIndex);
+
+    // TODO - Could be optimized by adding a multimap on "Structure", mapping
+    // from SOP Instance UID to polygons
+    
+    for (Polygons::const_iterator it = structure.polygons_.begin();
+         it != structure.polygons_.end(); ++it)
+    {
+      if (it->GetSopInstanceUid() == sopInstanceUid)
+      {
+        target.push_back(it->GetPoints());
+      }
+    }
+  }
 }
--- a/OrthancStone/Sources/Toolbox/DicomStructureSet.h	Fri Jan 14 14:14:25 2022 +0100
+++ b/OrthancStone/Sources/Toolbox/DicomStructureSet.h	Fri Jan 14 19:04:05 2022 +0100
@@ -35,6 +35,7 @@
 #include "OrthancDatasets/FullOrthancDataset.h"
 #include "../Scene2D/Color.h"
 #include "../Scene2D/PolylineSceneLayer.h"
+#include "../Scene2D/ScenePoint2D.h"
 
 #if ORTHANC_ENABLE_DCMTK == 1
 #  include <DicomParsing/ParsedDicomFile.h>
@@ -196,7 +197,7 @@
                            uint8_t& blue,
                            size_t index) const;
 
-    void GetReferencedInstances(std::set<std::string>& instances);
+    void GetReferencedInstances(std::set<std::string>& instances) const;
 
     void AddReferencedSlice(const std::string& sopInstanceUid,
                             const std::string& seriesInstanceUid,
@@ -236,5 +237,9 @@
     {
       ProjectOntoLayer(layer, plane, structureIndex, GetStructureColor(structureIndex));
     }
+
+    void GetStructurePoints(std::list< std::vector<Vector> >& target,
+                            size_t structureIndex,
+                            const std::string& sopInstanceUid) const;
   };
 }
--- a/RenderingPlugin/CMakeLists.txt	Fri Jan 14 14:14:25 2022 +0100
+++ b/RenderingPlugin/CMakeLists.txt	Fri Jan 14 19:04:05 2022 +0100
@@ -85,7 +85,9 @@
 
 
 add_library(StoneRendering SHARED
+  Sources/OrthancPluginConnection.cpp
   Sources/Plugin.cpp
+
   ${AUTOGENERATED_SOURCES}
   ${CMAKE_SOURCE_DIR}/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp
   ${ORTHANC_CORE_SOURCES}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/RenderingPlugin/Sources/OrthancPluginConnection.cpp	Fri Jan 14 19:04:05 2022 +0100
@@ -0,0 +1,88 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OrthancPluginConnection.h"
+
+#include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h"
+
+#include <OrthancException.h>
+
+namespace OrthancStone
+{
+  void OrthancPluginConnection::RestApiGet(std::string& result,
+                                           const std::string& uri)
+  {
+    OrthancPlugins::MemoryBuffer tmp;
+    
+    if (tmp.RestApiGet(uri, false))
+    {
+      tmp.ToString(result);
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+    }
+  }
+
+  
+  void OrthancPluginConnection::RestApiPost(std::string& result,
+                                            const std::string& uri,
+                                            const std::string& body)
+  {
+    OrthancPlugins::MemoryBuffer tmp;
+    
+    if (tmp.RestApiPost(uri, body.empty() ? NULL : body.c_str(), body.size(), false))
+    {
+      tmp.ToString(result);
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+    }
+  }
+   
+
+  void OrthancPluginConnection::RestApiPut(std::string& result,
+                                           const std::string& uri,
+                                           const std::string& body)
+  {
+    OrthancPlugins::MemoryBuffer tmp;
+    
+    if (tmp.RestApiPut(uri, body.empty() ? NULL : body.c_str(), body.size(), false))
+    {
+      tmp.ToString(result);
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+    }
+  }
+
+  
+  void OrthancPluginConnection::RestApiDelete(const std::string& uri)
+  {
+    if (!OrthancPlugins::RestApiDelete(uri, false))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+    }   
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/RenderingPlugin/Sources/OrthancPluginConnection.h	Fri Jan 14 19:04:05 2022 +0100
@@ -0,0 +1,47 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../OrthancStone/Sources/Toolbox/OrthancDatasets/IOrthancConnection.h"
+
+#include <Compatibility.h>
+
+namespace OrthancStone
+{
+  class OrthancPluginConnection : public IOrthancConnection
+  {
+  public:
+    virtual void RestApiGet(std::string& result,
+                            const std::string& uri) ORTHANC_OVERRIDE;
+
+    virtual void RestApiPost(std::string& result,
+                             const std::string& uri,
+                             const std::string& body) ORTHANC_OVERRIDE;
+
+    virtual void RestApiPut(std::string& result,
+                            const std::string& uri,
+                            const std::string& body) ORTHANC_OVERRIDE;
+
+    virtual void RestApiDelete(const std::string& uri) ORTHANC_OVERRIDE;
+  };
+}
--- a/RenderingPlugin/Sources/Plugin.cpp	Fri Jan 14 14:14:25 2022 +0100
+++ b/RenderingPlugin/Sources/Plugin.cpp	Fri Jan 14 19:04:05 2022 +0100
@@ -20,10 +20,12 @@
  **/
 
 
+#include "OrthancPluginConnection.h"
 #include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h"
 
 #include "../../OrthancStone/Sources/Toolbox/AffineTransform2D.h"
 #include "../../OrthancStone/Sources/Toolbox/DicomInstanceParameters.h"
+#include "../../OrthancStone/Sources/Toolbox/DicomStructureSet.h"
 
 #include <EmbeddedResources.h>
 
@@ -32,10 +34,99 @@
 #include <Images/NumpyWriter.h>
 #include <Logging.h>
 #include <SerializationToolbox.h>
+#include <Toolbox.h>
 
 #include <boost/math/constants/constants.hpp>
 
 
+class DicomStructureCache : public boost::noncopyable
+{
+private:
+  boost::mutex   mutex_;
+  std::string    instanceId_;
+  std::unique_ptr<OrthancStone::DicomStructureSet> rtstruct_;
+
+  DicomStructureCache()  // Singleton design pattern
+  {
+  }
+  
+public:
+  void Invalidate(const std::string& instanceId)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    if (instanceId_ == instanceId)
+    {
+      rtstruct_.reset(NULL);
+    }
+  }
+
+  static DicomStructureCache& GetSingleton()
+  {
+    static DicomStructureCache instance;
+    return instance;
+  }
+
+  class Accessor : public boost::noncopyable
+  {
+  private:
+    boost::mutex::scoped_lock               lock_;
+    std::string                             instanceId_;
+    const OrthancStone::DicomStructureSet*  rtstruct_;
+
+  public:
+    Accessor(DicomStructureCache& that,
+             const std::string& instanceId) :
+      lock_(that.mutex_),
+      instanceId_(instanceId),
+      rtstruct_(NULL)
+    {
+      if (that.instanceId_ == instanceId &&
+          that.rtstruct_.get() != NULL)
+      {
+        rtstruct_ = that.rtstruct_.get();
+      }
+      else
+      {
+        try
+        {
+          OrthancStone::OrthancPluginConnection connection;
+          OrthancStone::FullOrthancDataset dataset(connection, "/instances/" + instanceId + "/tags?ignore-length=3006-0050");
+          that.rtstruct_.reset(new OrthancStone::DicomStructureSet(dataset));
+          that.instanceId_ = instanceId;
+          rtstruct_ = that.rtstruct_.get();
+        }
+        catch (Orthanc::OrthancException&)
+        {
+        }
+      }
+    }
+
+    const std::string& GetInstanceId() const
+    {
+      return instanceId_;
+    }
+
+    bool IsValid() const
+    {
+      return rtstruct_ != NULL;
+    }
+
+    const OrthancStone::DicomStructureSet& GetRtStruct() const
+    {
+      if (IsValid())
+      {
+        return *rtstruct_;
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    }
+  };
+};
+
+
 static Orthanc::PixelFormat Convert(OrthancPluginPixelFormat format)
 {
   switch (format)
@@ -206,7 +297,247 @@
   Orthanc::IImageWriter::WriteToMemory(writer, answer, *modified);
   
   OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output,
-                            answer.c_str(), answer.size(), "text/plain");
+                            answer.c_str(), answer.size(), "application/octet-stream");
+}
+
+
+static bool IsRtStruct(const std::string& instanceId)
+{
+  static const char* SOP_CLASS_UID = "0008,0016";
+  static const char* RT_STRUCT_IOD = "1.2.840.10008.5.1.4.1.1.481.3";
+      
+  std::string s;
+  if (OrthancPlugins::RestApiGetString(s, "/instances/" + instanceId + "/content/" + SOP_CLASS_UID, false) &&
+      !s.empty())
+  {
+    if (s[s.size() - 1] == '\0')  // Deal with DICOM padding
+    {
+      s.resize(s.size() - 1);
+    }
+    
+    return s == RT_STRUCT_IOD;
+  }
+  else
+  {
+    return false;
+  }
+}
+
+
+static void ListRtStruct(OrthancPluginRestOutput* output,
+                         const char* url,
+                         const OrthancPluginHttpRequest* request)
+{
+  // This is a quick version of "/tools/find" on "SOPClassUID" (the
+  // latter would load all the DICOM files from disk)
+
+  static const char* INSTANCES = "Instances";
+  
+  Json::Value series;
+  OrthancPlugins::RestApiGet(series, "/series?expand", false);
+
+  if (series.type() != Json::arrayValue)
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+  }
+
+  Json::Value answer = Json::arrayValue;
+
+  for (Json::Value::ArrayIndex i = 0; i < series.size(); i++)
+  {
+    if (series[i].type() != Json::objectValue ||
+        !series[i].isMember(INSTANCES) ||
+        series[i][INSTANCES].type() != Json::arrayValue)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+    
+    const Json::Value& instances = series[i][INSTANCES];
+
+    for (Json::Value::ArrayIndex j = 0; j < instances.size(); j++)
+    {
+      if (instances[j].type() != Json::stringValue)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+    }
+    
+    if (instances.size() > 0 &&
+        IsRtStruct(instances[0].asString()))
+    {
+      for (Json::Value::ArrayIndex j = 0; j < instances.size(); j++)
+      {
+        answer.append(instances[j].asString());
+      }
+    }
+  }
+
+  std::string s = answer.toStyledString();
+  OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, s.c_str(), s.size(), "application/json");
+}
+
+
+static void GetRtStruct(OrthancPluginRestOutput* output,
+                        const char* url,
+                        const OrthancPluginHttpRequest* request)
+{
+  static const char* STRUCTURES = "Structures";
+  static const char* INSTANCES = "Instances";
+  
+  DicomStructureCache::Accessor accessor(DicomStructureCache::GetSingleton(), request->groups[0]);
+
+  if (!accessor.IsValid())
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem);
+  }
+
+  Json::Value answer;
+  answer[STRUCTURES] = Json::arrayValue;
+
+  for (size_t i = 0; i < accessor.GetRtStruct().GetStructuresCount(); i++)
+  {
+    Json::Value color = Json::arrayValue;
+    color.append(accessor.GetRtStruct().GetStructureColor(i).GetRed());
+    color.append(accessor.GetRtStruct().GetStructureColor(i).GetGreen());
+    color.append(accessor.GetRtStruct().GetStructureColor(i).GetBlue());
+    
+    Json::Value structure;
+    structure["Name"] = accessor.GetRtStruct().GetStructureName(i);
+    structure["Interpretation"] = accessor.GetRtStruct().GetStructureInterpretation(i);
+    structure["Color"] = color;
+    
+    answer[STRUCTURES].append(structure);
+  }
+
+  std::set<std::string> sopInstanceUids;
+  accessor.GetRtStruct().GetReferencedInstances(sopInstanceUids);
+
+  answer[INSTANCES] = Json::arrayValue;
+  for (std::set<std::string>::const_iterator it = sopInstanceUids.begin(); it != sopInstanceUids.end(); ++it)
+  {
+    OrthancPlugins::OrthancString s;
+    s.Assign(OrthancPluginLookupInstance(OrthancPlugins::GetGlobalContext(), it->c_str()));
+
+    std::string t;
+    s.ToString(t);
+
+    answer[INSTANCES].append(t);
+  }  
+
+  std::string s = answer.toStyledString();
+  OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, s.c_str(), s.size(), "application/json");
+}
+
+
+static void RenderRtStruct(OrthancPluginRestOutput* output,
+                           const char* url,
+                           const OrthancPluginHttpRequest* request)
+{
+  static const char* MAIN_DICOM_TAGS = "MainDicomTags";
+  
+  DicomStructureCache::Accessor accessor(DicomStructureCache::GetSingleton(), request->groups[0]);
+
+  if (!accessor.IsValid())
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem);
+  }
+
+  std::string structureName;
+  std::string instanceId;
+
+  for (uint32_t i = 0; i < request->getCount; i++)
+  {
+    std::string key(request->getKeys[i]);
+    std::string value(request->getValues[i]);
+
+    if (key == "structure")
+    {
+      structureName = value;
+    }
+    else if (key == "instance")
+    {
+      instanceId = value;
+    }
+    else
+    {
+      LOG(WARNING) << "Unsupported option: " << key;
+    }
+  }
+
+  if (structureName.empty())
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol,
+                                    "Missing option \"structure\" to provide the structure name");
+  }
+
+  if (instanceId.empty())
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol,
+                                    "Missing option \"instance\" to provide the Orthanc identifier of the instance of interest");
+  }
+
+  size_t structureIndex;
+  bool found = false;
+  for (size_t i = 0; i < accessor.GetRtStruct().GetStructuresCount(); i++)
+  {
+    if (accessor.GetRtStruct().GetStructureName(i) == structureName)
+    {
+      structureIndex = i;
+      found = true;
+      break;
+    }
+  }
+
+  if (!found)
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem,
+                                    "Unknown structure name: " + structureName);
+  }
+
+  Json::Value instance;
+  if (!OrthancPlugins::RestApiGet(instance, "/instances/" + instanceId, false))
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem,
+                                    "Unknown instance with Orthanc ID: " + instanceId);
+  }
+
+  if (instance.type() != Json::objectValue ||
+      !instance.isMember(MAIN_DICOM_TAGS) ||
+      instance[MAIN_DICOM_TAGS].type() != Json::objectValue)
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+  }
+
+  std::string sopInstanceUid = Orthanc::SerializationToolbox::ReadString(instance[MAIN_DICOM_TAGS], "SOPInstanceUID");
+  
+
+  std::list< std::vector<OrthancStone::Vector> > p;
+  accessor.GetRtStruct().GetStructurePoints(p, structureIndex, sopInstanceUid);
+
+  std::string s = "hello";
+  OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, s.c_str(), s.size(), "application/json");
+}
+
+
+OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType,
+                                        OrthancPluginResourceType resourceType,
+                                        const char* resourceId)
+{
+  switch (changeType)
+  {
+    case OrthancPluginChangeType_Deleted:
+      if (resourceType == OrthancPluginResourceType_Instance)
+      {
+        DicomStructureCache::GetSingleton().Invalidate(resourceId);
+      }
+      
+      break;
+
+    default:
+      break;
+  }
+
+  return OrthancPluginErrorCode_Success;
 }
 
 
@@ -238,6 +569,10 @@
     try
     {
       OrthancPlugins::RegisterRestCallback<RenderNumpyFrame>("/stone/instances/([^/]+)/frames/([0-9]+)/numpy", true);
+      OrthancPlugins::RegisterRestCallback<ListRtStruct>("/stone/rt-struct", true);
+      OrthancPlugins::RegisterRestCallback<GetRtStruct>("/stone/rt-struct/([^/]+)/info", true);
+      OrthancPlugins::RegisterRestCallback<RenderRtStruct>("/stone/rt-struct/([^/]+)/numpy", true);
+      OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback);
     }
     catch (...)
     {