# HG changeset patch # User Sebastien Jodogne # Date 1642183445 -3600 # Node ID ddaee6b96501da42dc58d7aafe112e91bfeaa0c6 # Parent 0c6923982fdd90e68926505e679fc261c89d8d7c retrieving rt-struct info diff -r 0c6923982fdd -r ddaee6b96501 OrthancStone/Sources/Toolbox/DicomStructureSet.cpp --- 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& instances) + void DicomStructureSet::GetReferencedInstances(std::set& 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 >& 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()); + } + } + } } diff -r 0c6923982fdd -r ddaee6b96501 OrthancStone/Sources/Toolbox/DicomStructureSet.h --- 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 @@ -196,7 +197,7 @@ uint8_t& blue, size_t index) const; - void GetReferencedInstances(std::set& instances); + void GetReferencedInstances(std::set& 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 >& target, + size_t structureIndex, + const std::string& sopInstanceUid) const; }; } diff -r 0c6923982fdd -r ddaee6b96501 RenderingPlugin/CMakeLists.txt --- 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} diff -r 0c6923982fdd -r ddaee6b96501 RenderingPlugin/Sources/OrthancPluginConnection.cpp --- /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 . + **/ + + +#include "OrthancPluginConnection.h" + +#include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h" + +#include + +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); + } + } +} diff -r 0c6923982fdd -r ddaee6b96501 RenderingPlugin/Sources/OrthancPluginConnection.h --- /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 . + **/ + + +#pragma once + +#include "../../OrthancStone/Sources/Toolbox/OrthancDatasets/IOrthancConnection.h" + +#include + +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; + }; +} diff -r 0c6923982fdd -r ddaee6b96501 RenderingPlugin/Sources/Plugin.cpp --- 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 @@ -32,10 +34,99 @@ #include #include #include +#include #include +class DicomStructureCache : public boost::noncopyable +{ +private: + boost::mutex mutex_; + std::string instanceId_; + std::unique_ptr 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 sopInstanceUids; + accessor.GetRtStruct().GetReferencedInstances(sopInstanceUids); + + answer[INSTANCES] = Json::arrayValue; + for (std::set::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 > 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("/stone/instances/([^/]+)/frames/([0-9]+)/numpy", true); + OrthancPlugins::RegisterRestCallback("/stone/rt-struct", true); + OrthancPlugins::RegisterRestCallback("/stone/rt-struct/([^/]+)/info", true); + OrthancPlugins::RegisterRestCallback("/stone/rt-struct/([^/]+)/numpy", true); + OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback); } catch (...) {