changeset 2102:9f7604d6b581 deep-learning

integration mainline->deep-learning
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 14 Nov 2023 11:45:17 +0100
parents a6d5373e471c (current diff) 517ef20aef74 (diff)
children 5144d59d0fb2
files Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp OrthancStone/Resources/CMake/OrthancStoneConfiguration.cmake
diffstat 17 files changed, 1088 insertions(+), 51 deletions(-) [+]
line wrap: on
line diff
--- a/Applications/Samples/WebAssembly/docker-build.sh	Wed Oct 11 17:10:45 2023 +0200
+++ b/Applications/Samples/WebAssembly/docker-build.sh	Tue Nov 14 11:45:17 2023 +0100
@@ -43,6 +43,7 @@
 mkdir -p ${ROOT_DIR}/wasm-binaries
 
 docker run -t ${DOCKER_FLAGS} --rm \
+    --dns=8.8.8.8 \
     --user $(id -u):$(id -g) \
     -v ${ROOT_DIR}:/source:ro \
     -v ${ROOT_DIR}/wasm-binaries:/target:rw ${IMAGE} \
--- a/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp	Wed Oct 11 17:10:45 2023 +0200
+++ b/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp	Tue Nov 14 11:45:17 2023 +0100
@@ -204,6 +204,84 @@
 
 
 
+class IFramesCollection : public boost::noncopyable
+{
+public:
+  virtual ~IFramesCollection()
+  {
+  }
+
+  virtual size_t GetFramesCount() const = 0;
+
+  virtual const OrthancStone::DicomInstanceParameters& GetInstanceOfFrame(size_t frameIndex) const = 0;
+
+  virtual unsigned int GetFrameNumberInInstance(size_t frameIndex) const = 0;
+
+  virtual bool LookupFrame(size_t& frameIndex,
+                           const std::string& sopInstanceUid,
+                           unsigned int frameNumber) const = 0;
+
+  virtual bool FindClosestFrame(size_t& frameIndex,
+                                const OrthancStone::Vector& point,
+                                double maximumDistance) const = 0;
+
+  static OrthancStone::CoordinateSystem3D GetFrameGeometry(const IFramesCollection& frames,
+                                                           size_t frameIndex)
+  {
+    return frames.GetInstanceOfFrame(frameIndex).GetFrameGeometry(frames.GetFrameNumberInInstance(frameIndex));
+  }
+};
+
+
+class SortedFramesCollection : public IFramesCollection
+{
+private:
+  std::unique_ptr<OrthancStone::SortedFrames>  frames_;
+
+public:
+  SortedFramesCollection(OrthancStone::SortedFrames* frames)
+  {
+    if (frames == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+    else
+    {
+      frames_.reset(frames);
+    }
+  }
+
+  virtual size_t GetFramesCount() const ORTHANC_OVERRIDE
+  {
+    return frames_->GetFramesCount();
+  }
+
+  const OrthancStone::DicomInstanceParameters& GetInstanceOfFrame(size_t frameIndex) const ORTHANC_OVERRIDE
+  {
+    return frames_->GetInstanceOfFrame(frameIndex);
+  }
+
+  virtual unsigned int GetFrameNumberInInstance(size_t frameIndex) const ORTHANC_OVERRIDE
+  {
+    return frames_->GetFrameNumberInInstance(frameIndex);
+  }
+
+  virtual bool LookupFrame(size_t& frameIndex,
+                           const std::string& sopInstanceUid,
+                           unsigned int frameNumber) const ORTHANC_OVERRIDE
+  {
+    return frames_->LookupFrame(frameIndex, sopInstanceUid, frameNumber);
+  }
+
+  virtual bool FindClosestFrame(size_t& frameIndex,
+                                const OrthancStone::Vector& point,
+                                double maximumDistance) const ORTHANC_OVERRIDE
+  {
+    return frames_->FindClosestFrame(frameIndex, point, maximumDistance);
+  };
+};
+
+
 class VirtualSeries : public boost::noncopyable
 {
 private:
@@ -741,32 +819,31 @@
     }
   }
 
-  bool SortSeriesFrames(OrthancStone::SortedFrames& target,
-                        const std::string& seriesInstanceUid) const
+  IFramesCollection* GetSeriesFrames(const std::string& seriesInstanceUid) const
   {
     OrthancStone::SeriesMetadataLoader::Accessor accessor(*metadataLoader_, seriesInstanceUid);
     
     if (accessor.IsComplete())
     {
-      target.Clear();
+      std::unique_ptr<OrthancStone::SortedFrames> target(new OrthancStone::SortedFrames);
+      target->Clear();
 
       for (size_t i = 0; i < accessor.GetInstancesCount(); i++)
       {
-        target.AddInstance(accessor.GetInstance(i));
+        target->AddInstance(accessor.GetInstance(i));
       }
 
-      target.Sort();
+      target->Sort();
       
-      return true;
+      return new SortedFramesCollection(target.release());
     }
     else
     {
-      return false;
-    }
-  }
-
-  bool SortVirtualSeriesFrames(OrthancStone::SortedFrames& target,
-                               const std::string& virtualSeriesId) const
+      return NULL;
+    }
+  }
+
+  IFramesCollection* GetVirtualSeriesFrames(const std::string& virtualSeriesId) const
   {
     const std::string& seriesInstanceUid = virtualSeries_.GetSeriesInstanceUid(virtualSeriesId);
     
@@ -776,7 +853,8 @@
     {
       const std::list<std::string>& sopInstanceUids = virtualSeries_.GetSopInstanceUids(virtualSeriesId);
 
-      target.Clear();
+      std::unique_ptr<OrthancStone::SortedFrames> target(new OrthancStone::SortedFrames);
+      target->Clear();
 
       for (std::list<std::string>::const_iterator
              it = sopInstanceUids.begin(); it != sopInstanceUids.end(); ++it)
@@ -784,7 +862,7 @@
         Orthanc::DicomMap instance;
         if (accessor.LookupInstance(instance, *it))
         {
-          target.AddInstance(instance);
+          target->AddInstance(instance);
         }
         else
         {
@@ -792,12 +870,13 @@
         }
       }
       
-      target.Sort();
-      return true;
+      target->Sort();
+
+      return new SortedFramesCollection(target.release());
     }
     else
     {
-      return false;
+      return NULL;
     }
   }
 
@@ -1557,7 +1636,6 @@
 
 
 
-
 class ViewerViewport : public OrthancStone::ObserverBase<ViewerViewport>
 {
 public:
@@ -1840,6 +1918,8 @@
   class SetFullDicomFrame : public ICommand
   {
   private:
+    std::string   studyInstanceUid_;
+    std::string   seriesInstanceUid_;
     std::string   sopInstanceUid_;
     unsigned int  frameNumber_;
     int           priority_;
@@ -1848,12 +1928,16 @@
     
   public:
     SetFullDicomFrame(boost::shared_ptr<ViewerViewport> viewport,
+                      const std::string& studyInstanceUid,
+                      const std::string& seriesInstanceUid,
                       const std::string& sopInstanceUid,
                       unsigned int frameNumber,
                       int priority,
                       bool isPrefetch,
                       bool serverSideTranscoding) :
       ICommand(viewport),
+      studyInstanceUid_(studyInstanceUid),
+      seriesInstanceUid_(seriesInstanceUid),
       sopInstanceUid_(sopInstanceUid),
       frameNumber_(frameNumber),
       priority_(priority),
@@ -1879,7 +1963,7 @@
             // If we haven't tried server-side rendering yet, give it a try
             LOG(INFO) << "Switching to server-side transcoding";
             GetViewport().serverSideTranscoding_ = true;
-            GetViewport().ScheduleLoadFullDicomFrame(sopInstanceUid_, frameNumber_, priority_, isPrefetch_);
+            GetViewport().ScheduleLoadFullDicomFrame(studyInstanceUid_, seriesInstanceUid_, sopInstanceUid_, frameNumber_, priority_, isPrefetch_);
           }
           return;
         }
@@ -1997,7 +2081,7 @@
   boost::shared_ptr<OrthancStone::DicomResourcesLoader> loader_;
   OrthancStone::DicomSource                    source_;
   boost::shared_ptr<FramesCache>               framesCache_;  
-  std::unique_ptr<OrthancStone::SortedFrames>  frames_;
+  std::unique_ptr<IFramesCollection>           frames_;
   std::unique_ptr<SeriesCursor>                cursor_;
   float                                        windowingCenter_;
   float                                        windowingWidth_;
@@ -2387,7 +2471,7 @@
       if (instance.GetSopInstanceUid() == loadedSopInstanceUid &&
           frameNumber == loadedFrameNumber)
       {
-        const OrthancStone::CoordinateSystem3D plane = frames_->GetFrameGeometry(cursorIndex);
+        const OrthancStone::CoordinateSystem3D plane = IFramesCollection::GetFrameGeometry(*frames_, cursorIndex);
         
         if (quality == DisplayedFrameQuality_Low)
         {
@@ -2426,7 +2510,9 @@
     }
   }
 
-  void ScheduleLoadFullDicomFrame(const std::string& sopInstanceUid,
+  void ScheduleLoadFullDicomFrame(const std::string& studyInstanceUid,
+                                  const std::string& seriesInstanceUid,
+                                  const std::string& sopInstanceUid,
                                   unsigned int frameNumber,
                                   int priority,
                                   bool isPrefetch)
@@ -2436,10 +2522,10 @@
       std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(context_.Lock());
       lock->Schedule(
         GetSharedObserver(), priority, OrthancStone::ParseDicomFromWadoCommand::Create(
-          source_, frames_->GetStudyInstanceUid(), frames_->GetSeriesInstanceUid(),
-          sopInstanceUid, serverSideTranscoding_,
+          source_, studyInstanceUid, seriesInstanceUid, sopInstanceUid, serverSideTranscoding_,
           Orthanc::DicomTransferSyntax_LittleEndianExplicit,
-          new SetFullDicomFrame(GetSharedObserver(), sopInstanceUid, frameNumber, priority, isPrefetch, serverSideTranscoding_)));
+          new SetFullDicomFrame(GetSharedObserver(), studyInstanceUid, seriesInstanceUid,
+                                sopInstanceUid, frameNumber, priority, isPrefetch, serverSideTranscoding_)));
     }
   }
 
@@ -2449,9 +2535,11 @@
   {
     if (frames_.get() != NULL)
     {
+      std::string studyInstanceUid = frames_->GetInstanceOfFrame(cursorIndex).GetStudyInstanceUid();
+      std::string seriesInstanceUid = frames_->GetInstanceOfFrame(cursorIndex).GetSeriesInstanceUid();
       std::string sopInstanceUid = frames_->GetInstanceOfFrame(cursorIndex).GetSopInstanceUid();
       unsigned int frameNumber = frames_->GetFrameNumberInInstance(cursorIndex);
-      ScheduleLoadFullDicomFrame(sopInstanceUid, frameNumber, priority, isPrefetch);
+      ScheduleLoadFullDicomFrame(studyInstanceUid, seriesInstanceUid, sopInstanceUid, frameNumber, priority, isPrefetch);
     }
   }
 
@@ -2500,8 +2588,8 @@
       bool isMonochrome1 = (instance.GetImageInformation().GetPhotometricInterpretation() ==
                             Orthanc::PhotometricInterpretation_Monochrome1);
 
-      const std::string uri = ("studies/" + frames_->GetStudyInstanceUid() +
-                               "/series/" + frames_->GetSeriesInstanceUid() +
+      const std::string uri = ("studies/" + instance.GetStudyInstanceUid() +
+                               "/series/" + instance.GetSeriesInstanceUid() +
                                "/instances/" + instance.GetSopInstanceUid() +
                                "/frames/" + boost::lexical_cast<std::string>(frameNumber + 1) + "/rendered");
 
@@ -2542,7 +2630,6 @@
     }
   }
   
-
   ViewerViewport(OrthancStone::WebAssemblyLoadersContext& context,
                  const OrthancStone::DicomSource& source,
                  const std::string& canvas,
@@ -2637,14 +2724,12 @@
       {
         const size_t currentCursorIndex = that.cursor_->GetCurrentIndex();
 
-        const OrthancStone::CoordinateSystem3D current =
-          that.frames_->GetFrameGeometry(currentCursorIndex);
+        const OrthancStone::CoordinateSystem3D current = IFramesCollection::GetFrameGeometry(*that.frames_, currentCursorIndex);
       
         if (isShift &&
             previousCursorIndex != currentCursorIndex)
         {
-          const OrthancStone::CoordinateSystem3D previous =
-            that.frames_->GetFrameGeometry(previousCursorIndex);
+          const OrthancStone::CoordinateSystem3D previous = IFramesCollection::GetFrameGeometry(*that.frames_, previousCursorIndex);
           that.synchronizationOffset_ += previous.GetOrigin() - current.GetOrigin();
         }
 
@@ -2791,7 +2876,7 @@
     return viewport;    
   }
 
-  void SetFrames(OrthancStone::SortedFrames* frames)
+  void SetFrames(IFramesCollection* frames)
   {
     if (frames == NULL)
     {
@@ -2861,8 +2946,8 @@
           GetSeriesThumbnailType(uid) != OrthancStone::SeriesThumbnailType_Video)
       {
         // Fetch the details of the series from the central instance
-        const std::string uri = ("studies/" + frames_->GetStudyInstanceUid() +
-                                 "/series/" + frames_->GetSeriesInstanceUid() +
+        const std::string uri = ("studies/" + centralInstance.GetStudyInstanceUid() +
+                                 "/series/" + centralInstance.GetSeriesInstanceUid() +
                                  "/instances/" + centralInstance.GetSopInstanceUid() + "/metadata");
         
         loader_->ScheduleGetDicomWeb(
@@ -2918,7 +3003,7 @@
       FramesCache::Accessor accessor(*framesCache_, instance.GetSopInstanceUid(), frameNumber);
       if (accessor.IsValid())
       {
-        RenderCurrentScene(accessor.GetImage(), instance, frameNumber, frames_->GetFrameGeometry(cursorIndex));
+        RenderCurrentScene(accessor.GetImage(), instance, frameNumber, IFramesCollection::GetFrameGeometry(*frames_, cursorIndex));
 
         DisplayedFrameQuality quality;
         
@@ -3029,7 +3114,7 @@
     if (cursor_.get() != NULL &&
         frames_.get() != NULL)
     {
-      plane = frames_->GetFrameGeometry(cursor_->GetCurrentIndex());      
+      plane = IFramesCollection::GetFrameGeometry(*frames_, cursor_->GetCurrentIndex());
       return true;
     }
     else
@@ -3103,7 +3188,7 @@
   void SetWindowingPreset()
   {
     assert(windowingPresetCenters_.size() == windowingPresetWidths_.size());
-    
+
     if (windowingPresetCenters_.empty())
     {
       SetWindowing(128, 256);
@@ -3568,8 +3653,7 @@
     {
       const size_t currentCursorIndex = cursor_->GetCurrentIndex();
 
-      const OrthancStone::CoordinateSystem3D current =
-        frames_->GetFrameGeometry(currentCursorIndex);
+      const OrthancStone::CoordinateSystem3D current = IFramesCollection::GetFrameGeometry(*frames_, currentCursorIndex);
       
       observer_->SignalSynchronizedBrowsing(
         *this, current.GetOrigin() + synchronizationOffset_, current.GetNormal());
@@ -4476,9 +4560,9 @@
   {
     try
     {
-      std::unique_ptr<OrthancStone::SortedFrames> frames(new OrthancStone::SortedFrames);
-      
-      if (GetResourcesLoader().SortSeriesFrames(*frames, seriesInstanceUid))
+      std::unique_ptr<IFramesCollection> frames(GetResourcesLoader().GetSeriesFrames(seriesInstanceUid));
+
+      if (frames.get() != NULL)
       {
         GetViewport(canvas)->SetFrames(frames.release());
         return 1;
@@ -4499,9 +4583,9 @@
   {
     try
     {
-      std::unique_ptr<OrthancStone::SortedFrames> frames(new OrthancStone::SortedFrames);
-
-      if (GetResourcesLoader().SortVirtualSeriesFrames(*frames, virtualSeriesId))
+      std::unique_ptr<IFramesCollection> frames(GetResourcesLoader().GetVirtualSeriesFrames(virtualSeriesId));
+
+      if (frames.get() != NULL)
       {
         GetViewport(canvas)->SetFrames(frames.release());
         return 1;
--- a/Applications/StoneWebViewer/WebAssembly/docker-build.sh	Wed Oct 11 17:10:45 2023 +0200
+++ b/Applications/StoneWebViewer/WebAssembly/docker-build.sh	Tue Nov 14 11:45:17 2023 +0100
@@ -50,6 +50,7 @@
 mkdir -p ${ROOT_DIR}/wasm-binaries
 
 docker run -t ${DOCKER_FLAGS} --rm \
+    --dns=8.8.8.8 \
     --user $(id -u):$(id -g) \
     -e STONE_BRANCH=${STONE_BRANCH} \
     -v ${ROOT_DIR}:/source:ro \
--- a/Applications/StoneWebViewer/WebAssembly/docker-internal.sh	Wed Oct 11 17:10:45 2023 +0200
+++ b/Applications/StoneWebViewer/WebAssembly/docker-internal.sh	Tue Nov 14 11:45:17 2023 +0100
@@ -44,6 +44,7 @@
       -DORTHANC_STONE_INSTALL_PREFIX=/target/StoneWebViewer \
       -DCMAKE_TOOLCHAIN_FILE=${EMSDK}/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake \
       -DSTATIC_BUILD=ON \
+      -DLIBCLANG=/usr/lib/llvm-4.0/lib/libclang-4.0.so \
       -G Ninja
 
 ninja -j2 install
--- a/OrthancStone/Resources/CMake/OrthancStoneConfiguration.cmake	Wed Oct 11 17:10:45 2023 +0200
+++ b/OrthancStone/Resources/CMake/OrthancStoneConfiguration.cmake	Tue Nov 14 11:45:17 2023 +0100
@@ -189,6 +189,7 @@
 if (ENABLE_DCMTK)
   list(APPEND ORTHANC_STONE_SOURCES
     ${ORTHANC_STONE_ROOT}/Oracle/ParseDicomSuccessMessage.cpp
+    ${ORTHANC_STONE_ROOT}/Toolbox/DicomStructuredReport.cpp
     ${ORTHANC_STONE_ROOT}/Toolbox/OrthancDatasets/SimplifiedOrthancDataset.cpp
     ${ORTHANC_STONE_ROOT}/Toolbox/ParsedDicomCache.cpp
     ${ORTHANC_STONE_ROOT}/Toolbox/ParsedDicomDataset.cpp
--- a/OrthancStone/Resources/Orthanc/CMake/DownloadOrthancFramework.cmake	Wed Oct 11 17:10:45 2023 +0200
+++ b/OrthancStone/Resources/Orthanc/CMake/DownloadOrthancFramework.cmake	Tue Nov 14 11:45:17 2023 +0100
@@ -271,7 +271,7 @@
   else()
     message("Forking the Orthanc source repository using Mercurial")
     execute_process(
-      COMMAND ${ORTHANC_FRAMEWORK_HG} clone "https://hg.orthanc-server.com/orthanc/"
+      COMMAND ${ORTHANC_FRAMEWORK_HG} clone "https://orthanc.uclouvain.be/hg/orthanc/"
       WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
       RESULT_VARIABLE Failure
       )    
--- a/OrthancStone/Resources/SyncOrthancFolder.py	Wed Oct 11 17:10:45 2023 +0200
+++ b/OrthancStone/Resources/SyncOrthancFolder.py	Tue Nov 14 11:45:17 2023 +0100
@@ -38,7 +38,7 @@
 
 
 TARGET = os.path.join(os.path.dirname(__file__), 'Orthanc')
-REPOSITORY = 'https://hg.orthanc-server.com/orthanc/raw-file'
+REPOSITORY = 'https://orthanc.uclouvain.be/hg/orthanc/raw-file'
 
 FILES = [
     ('OrthancFramework/Resources/CMake/AutoGeneratedCode.cmake',        'CMake'),
--- a/OrthancStone/Sources/Loaders/LoadedDicomResources.cpp	Wed Oct 11 17:10:45 2023 +0200
+++ b/OrthancStone/Sources/Loaders/LoadedDicomResources.cpp	Tue Nov 14 11:45:17 2023 +0100
@@ -276,4 +276,22 @@
       return true;
     }
   }
+
+
+  bool LoadedDicomResources::LookupResource(Orthanc::DicomMap& target,
+                                            const std::string& id) const
+  {
+    Resources::const_iterator it = resources_.find(id);
+    
+    if (it == resources_.end())
+    {
+      return false;
+    }
+    else
+    {
+      assert(it->second != NULL);
+      target.Assign(it->second->GetDicom());
+      return true;
+    }
+  }
 }
--- a/OrthancStone/Sources/Loaders/LoadedDicomResources.h	Wed Oct 11 17:10:45 2023 +0200
+++ b/OrthancStone/Sources/Loaders/LoadedDicomResources.h	Tue Nov 14 11:45:17 2023 +0100
@@ -146,5 +146,8 @@
     {
       return GetResourceInternal(index).GetSourceJson();
     }
+
+    bool LookupResource(Orthanc::DicomMap& target,
+                        const std::string& id) const;
   };
 }
--- a/OrthancStone/Sources/StoneEnumerations.cpp	Wed Oct 11 17:10:45 2023 +0200
+++ b/OrthancStone/Sources/StoneEnumerations.cpp	Tue Nov 14 11:45:17 2023 +0100
@@ -65,6 +65,10 @@
     {
       return SopClassUid_DicomSeg;
     }
+    else if (s == "1.2.840.10008.5.1.4.1.1.88.33")
+    {
+      return SopClassUid_ComprehensiveSR;
+    }
     else
     {
       //LOG(INFO) << "Other SOP class UID: " << source;
--- a/OrthancStone/Sources/StoneEnumerations.h	Wed Oct 11 17:10:45 2023 +0200
+++ b/OrthancStone/Sources/StoneEnumerations.h	Tue Nov 14 11:45:17 2023 +0100
@@ -116,7 +116,8 @@
     SopClassUid_VideoEndoscopicImageStorage,
     SopClassUid_VideoMicroscopicImageStorage,
     SopClassUid_VideoPhotographicImageStorage,
-    SopClassUid_DicomSeg
+    SopClassUid_DicomSeg,
+    SopClassUid_ComprehensiveSR
   };
 
   enum SeriesThumbnailType
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Toolbox/DicomStructuredReport.cpp	Tue Nov 14 11:45:17 2023 +0100
@@ -0,0 +1,610 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "DicomStructuredReport.h"
+
+#include "../Scene2D/ScenePoint2D.h"
+
+#include <OrthancException.h>
+#include <SerializationToolbox.h>
+
+#include <dcmtk/dcmdata/dcdeftag.h>
+#include <dcmtk/dcmdata/dcsequen.h>
+#include <dcmtk/dcmdata/dcfilefo.h>
+
+
+static std::string FormatTag(const DcmTagKey& key)
+{
+  OFString s = key.toString();
+  return std::string(s.c_str());
+}
+
+
+static std::string GetStringValue(DcmItem& dataset,
+                                  const DcmTagKey& key)
+{
+  const char* value = NULL;
+  if (dataset.findAndGetString(key, value).good() &&
+      value != NULL)
+  {
+    return value;
+  }
+  else
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
+                                    "Missing tag in DICOM-SR: " + FormatTag(key));
+  }
+}
+
+
+static DcmSequenceOfItems& GetSequenceValue(DcmItem& dataset,
+                                            const DcmTagKey& key)
+{
+  DcmSequenceOfItems* sequence = NULL;
+  if (dataset.findAndGetSequence(key, sequence).good() &&
+      sequence != NULL)
+  {
+    return *sequence;
+  }
+  else
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
+                                    "Missing sequence in DICOM-SR: " + FormatTag(key));
+  }
+}
+
+
+static void CheckStringValue(DcmItem& dataset,
+                             const DcmTagKey& key,
+                             const std::string& expected)
+{
+  if (GetStringValue(dataset, key) != expected)
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+  }
+}
+
+
+static bool IsDicomTemplate(DcmItem& dataset,
+                            const std::string& tid)
+{
+  DcmSequenceOfItems& sequence = GetSequenceValue(dataset, DCM_ContentTemplateSequence);
+
+  return (sequence.card() == 1 &&
+          GetStringValue(*sequence.getItem(0), DCM_MappingResource) == "DCMR" &&
+          GetStringValue(*sequence.getItem(0), DCM_TemplateIdentifier) == tid);
+}
+
+
+static bool IsValidConcept(DcmItem& dataset,
+                           const DcmTagKey& key,
+                           const std::string& scheme,
+                           const std::string& concept)
+{
+  DcmSequenceOfItems& sequence = GetSequenceValue(dataset, key);
+
+  return (sequence.card() == 1 &&
+          GetStringValue(*sequence.getItem(0), DCM_CodingSchemeDesignator) == scheme &&
+          GetStringValue(*sequence.getItem(0), DCM_CodeValue) == concept);
+}
+
+
+static bool IsDicomConcept(DcmItem& dataset,
+                           const std::string& concept)
+{
+  return IsValidConcept(dataset, DCM_ConceptNameCodeSequence, "DCM", concept);
+}
+
+
+namespace OrthancStone
+{
+  void DicomStructuredReport::Structure::Copy(const Structure& other)
+  {
+    if (other.HasFrameNumber())
+    {
+      SetFrameNumber(other.GetFrameNumber());
+    }
+
+    if (other.HasProbabilityOfCancer())
+    {
+      SetProbabilityOfCancer(other.GetProbabilityOfCancer());
+    }
+  }
+
+
+  DicomStructuredReport::Structure::Structure(const std::string& sopInstanceUid) :
+    sopInstanceUid_(sopInstanceUid),
+    hasFrameNumber_(false),
+    hasProbabilityOfCancer_(false)
+  {
+  }
+
+
+  void DicomStructuredReport::Structure::SetFrameNumber(unsigned int frame)
+  {
+    hasFrameNumber_ = true;
+    frameNumber_ = frame;
+  }
+
+
+  void DicomStructuredReport::Structure::SetProbabilityOfCancer(float probability)
+  {
+    if (probability < 0 ||
+        probability > 100)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      hasProbabilityOfCancer_ = true;
+      probabilityOfCancer_ = probability;
+    }
+  }
+
+
+  unsigned int DicomStructuredReport::Structure::GetFrameNumber() const
+  {
+    if (hasFrameNumber_)
+    {
+      return frameNumber_;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+  float DicomStructuredReport::Structure::GetProbabilityOfCancer() const
+  {
+    if (hasProbabilityOfCancer_)
+    {
+      return probabilityOfCancer_;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  DicomStructuredReport::Point::Point(const std::string& sopInstanceUid,
+                                      double x,
+                                      double y) :
+    Structure(sopInstanceUid),
+    point_(x, y)
+  {
+  }
+
+
+  DicomStructuredReport::Structure* DicomStructuredReport::Point::Clone() const
+  {
+    std::unique_ptr<Point> cloned(new Point(GetSopInstanceUid(), point_.GetX(), point_.GetY()));
+    cloned->Copy(*this);
+    return cloned.release();
+  }
+
+
+  DicomStructuredReport::Polyline::Polyline(const std::string& sopInstanceUid,
+                                            const float* points,
+                                            unsigned long pointsCount) :
+    Structure(sopInstanceUid)
+  {
+    if (pointsCount % 2 != 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+    }
+
+    points_.reserve(pointsCount / 2);
+
+    for (unsigned long i = 0; i < pointsCount; i += 2)
+    {
+      points_.push_back(ScenePoint2D(points[i], points[i + 1]));
+    }
+  }
+
+
+  DicomStructuredReport::Polyline::Polyline(const std::string& sopInstanceUid,
+                                            const std::vector<ScenePoint2D>& points) :
+    Structure(sopInstanceUid),
+    points_(points)
+  {
+  }
+
+
+  DicomStructuredReport::Structure* DicomStructuredReport::Polyline::Clone() const
+  {
+    std::unique_ptr<Polyline> cloned(new Polyline(GetSopInstanceUid(), points_));
+    cloned->Copy(*this);
+    return cloned.release();
+  }
+
+
+  const ScenePoint2D& DicomStructuredReport::Polyline::GetPoint(size_t i) const
+  {
+    if (i >= points_.size())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return points_[i];
+    }
+  }
+
+
+  void DicomStructuredReport::AddStructure(const std::string& sopInstanceUid,
+                                           DcmItem& group,
+                                           bool hasFrameNumber,
+                                           unsigned int frameNumber,
+                                           bool hasProbabilityOfCancer,
+                                           float probabilityOfCancer)
+  {
+    const std::string graphicType = GetStringValue(group, DCM_GraphicType);
+
+    const Float32* coords = NULL;
+    unsigned long coordsCount = 0;
+    if (!group.findAndGetFloat32Array(DCM_GraphicData, coords, &coordsCount).good() ||
+        (coordsCount != 0 && coords == NULL))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
+                                      "Cannot read coordinates for region in DICOM-SR");
+    }
+
+    std::unique_ptr<Structure> structure;
+
+    if (graphicType == "POINT")
+    {
+      if (coordsCount != 2)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        structure.reset(new Point(sopInstanceUid, coords[0], coords[1]));
+      }
+    }
+    else if (graphicType == "POLYLINE")
+    {
+      structure.reset(new Polyline(sopInstanceUid, coords, coordsCount));
+    }
+    else
+    {
+      return;  // Unsupported graphic type
+    }
+
+    assert(structure.get() != NULL);
+
+    if (hasFrameNumber)
+    {
+      structure->SetFrameNumber(frameNumber);
+    }
+
+    if (hasProbabilityOfCancer)
+    {
+      structure->SetProbabilityOfCancer(probabilityOfCancer);
+    }
+
+    structures_.push_back(structure.release());
+  }
+
+
+  DicomStructuredReport::DicomStructuredReport(Orthanc::ParsedDicomFile& dicom)
+  {
+    DcmDataset& dataset = *dicom.GetDcmtkObject().getDataset();
+
+    studyInstanceUid_ = GetStringValue(dataset, DCM_StudyInstanceUID);
+    seriesInstanceUid_ = GetStringValue(dataset, DCM_SeriesInstanceUID);
+    sopInstanceUid_ = GetStringValue(dataset, DCM_SOPInstanceUID);
+
+    CheckStringValue(dataset, DCM_Modality, "SR");
+    CheckStringValue(dataset, DCM_SOPClassUID, "1.2.840.10008.5.1.4.1.1.88.33");  // Comprehensive SR IOD
+    CheckStringValue(dataset, DCM_ValueType, "CONTAINER");
+
+    if (!IsDicomConcept(dataset, "126000") /* Imaging measurement report */ ||
+        !IsDicomTemplate(dataset, "1500"))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+    }
+
+    DcmSequenceOfItems& sequence = GetSequenceValue(dataset, DCM_CurrentRequestedProcedureEvidenceSequence);
+
+    std::list<std::string> tmp;
+
+    for (unsigned long i = 0; i < sequence.card(); i++)
+    {
+      std::string studyInstanceUid = GetStringValue(*sequence.getItem(i), DCM_StudyInstanceUID);
+
+      DcmSequenceOfItems* referencedSeries = NULL;
+      if (!sequence.getItem(i)->findAndGetSequence(DCM_ReferencedSeriesSequence, referencedSeries).good() ||
+          referencedSeries == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+
+      for (unsigned long j = 0; j < referencedSeries->card(); j++)
+      {
+        std::string seriesInstanceUid = GetStringValue(*referencedSeries->getItem(j), DCM_SeriesInstanceUID);
+
+        DcmSequenceOfItems* referencedInstances = NULL;
+        if (!referencedSeries->getItem(j)->findAndGetSequence(DCM_ReferencedSOPSequence, referencedInstances).good() ||
+            referencedInstances == NULL)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+        }
+
+        for (unsigned int k = 0; k < referencedInstances->card(); k++)
+        {
+          std::string sopClassUid = GetStringValue(*referencedInstances->getItem(k), DCM_ReferencedSOPClassUID);
+          std::string sopInstanceUid = GetStringValue(*referencedInstances->getItem(k), DCM_ReferencedSOPInstanceUID);
+
+          if (instancesInformation_.find(sopInstanceUid) == instancesInformation_.end())
+          {
+            instancesInformation_[sopInstanceUid] = new ReferencedInstance(studyInstanceUid, seriesInstanceUid, sopClassUid);
+          }
+          else
+          {
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
+                                            "Multiple occurrences of the same instance in DICOM-SR: " + sopInstanceUid);
+          }
+
+          tmp.push_back(sopInstanceUid);
+        }
+      }
+    }
+
+    orderedInstances_.reserve(tmp.size());
+
+    for (std::list<std::string>::const_iterator it = tmp.begin(); it != tmp.end(); ++it)
+    {
+      orderedInstances_.push_back(*it);
+    }
+
+    sequence = GetSequenceValue(dataset, DCM_ContentSequence);
+
+    for (unsigned long i = 0; i < sequence.card(); i++)
+    {
+      DcmItem& item = *sequence.getItem(i);
+
+      if (GetStringValue(item, DCM_RelationshipType) == "CONTAINS" &&
+          GetStringValue(item, DCM_ValueType) == "CONTAINER" &&
+          IsDicomConcept(item, "126010" /* Imaging measurements */))
+      {
+        DcmSequenceOfItems& measurements = GetSequenceValue(item, DCM_ContentSequence);
+
+        for (unsigned long j = 0; j < measurements.card(); j++)
+        {
+          DcmItem& measurement = *measurements.getItem(j);
+
+          if (GetStringValue(measurement, DCM_RelationshipType) == "CONTAINS" &&
+              GetStringValue(measurement, DCM_ValueType) == "CONTAINER" &&
+              IsDicomConcept(measurement, "125007" /* Measurement group */) &&
+              IsDicomTemplate(measurement, "1410"))
+          {
+            DcmSequenceOfItems& groups = GetSequenceValue(measurement, DCM_ContentSequence);
+
+            bool hasProbabilityOfCancer = false;
+            float probabilityOfCancer = 0;
+
+            for (unsigned int k = 0; k < groups.card(); k++)
+            {
+              DcmItem& group = *groups.getItem(k);
+
+              if (GetStringValue(group, DCM_RelationshipType) == "CONTAINS" &&
+                  GetStringValue(group, DCM_ValueType) == "NUM" &&
+                  IsDicomConcept(group, "111047" /* Probability of cancer */))
+              {
+                DcmSequenceOfItems& values = GetSequenceValue(group, DCM_MeasuredValueSequence);
+
+                if (values.card() == 1 &&
+                    IsValidConcept(*values.getItem(0), DCM_MeasurementUnitsCodeSequence, "UCUM", "%"))
+                {
+                  std::string value = GetStringValue(*values.getItem(0), DCM_NumericValue);
+                  if (Orthanc::SerializationToolbox::ParseFloat(probabilityOfCancer, value))
+                  {
+                    hasProbabilityOfCancer = true;
+                  }
+                  else
+                  {
+                    throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
+                                                    "Cannot parse float in DICOM-SR: " + value);
+                  }
+                }
+              }
+            }
+
+            for (unsigned int k = 0; k < groups.card(); k++)
+            {
+              DcmItem& group = *groups.getItem(k);
+
+              if (GetStringValue(group, DCM_RelationshipType) == "CONTAINS" &&
+                  GetStringValue(group, DCM_ValueType) == "SCOORD" &&
+                  IsDicomConcept(group, "111030" /* Image region */))
+              {
+                DcmSequenceOfItems& regions = GetSequenceValue(group, DCM_ContentSequence);
+
+                for (unsigned int l = 0; l < regions.card(); l++)
+                {
+                  DcmItem& region = *regions.getItem(l);
+
+                  if (GetStringValue(region, DCM_RelationshipType) == "SELECTED FROM" &&
+                      GetStringValue(region, DCM_ValueType) == "IMAGE" &&
+                      IsDicomConcept(region, "111040") /* Original source */)
+                  {
+                    DcmSequenceOfItems& instances = GetSequenceValue(region, DCM_ReferencedSOPSequence);
+                    if (instances.card() != 1)
+                    {
+                      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
+                                                      "Region cannot reference multiple instances in DICOM-SR");
+                    }
+
+                    std::string sopInstanceUid = GetStringValue(*instances.getItem(0), DCM_ReferencedSOPInstanceUID);
+                    std::map<std::string, ReferencedInstance*>::iterator instanceInformation = instancesInformation_.find(sopInstanceUid);
+
+                    if (instanceInformation == instancesInformation_.end())
+                    {
+                      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
+                                                      "Referencing unknown instance in DICOM-SR: " + sopInstanceUid);
+                    }
+
+                    assert(instanceInformation->second != NULL);
+
+                    if (instances.getItem(0)->tagExists(DCM_ReferencedFrameNumber))
+                    {
+                      std::string frames = GetStringValue(*instances.getItem(0), DCM_ReferencedFrameNumber);
+                      std::vector<std::string> tokens;
+                      Orthanc::Toolbox::SplitString(tokens, frames, '\\');
+
+                      for (size_t m = 0; m < tokens.size(); m++)
+                      {
+                        uint32_t frame;
+                        if (!Orthanc::SerializationToolbox::ParseUnsignedInteger32(frame, tokens[m]) ||
+                            frame <= 0)
+                        {
+                          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+                        }
+                        else
+                        {
+                          AddStructure(sopInstanceUid, group, true, frame - 1, hasProbabilityOfCancer, probabilityOfCancer);
+                          instanceInformation->second->AddFrame(frame - 1);
+                        }
+                      }
+                    }
+                    else
+                    {
+                      AddStructure(sopInstanceUid, group, false, 0, hasProbabilityOfCancer, probabilityOfCancer);
+                      instanceInformation->second->AddFrame(0);
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+
+
+  DicomStructuredReport::DicomStructuredReport(const DicomStructuredReport& other) :
+    studyInstanceUid_(other.studyInstanceUid_),
+    seriesInstanceUid_(other.seriesInstanceUid_),
+    sopInstanceUid_(other.sopInstanceUid_),
+    orderedInstances_(other.orderedInstances_)
+  {
+    for (std::map<std::string, ReferencedInstance*>::const_iterator
+           it = other.instancesInformation_.begin(); it != other.instancesInformation_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      instancesInformation_[it->first] = new ReferencedInstance(*it->second);
+    }
+
+    for (std::deque<Structure*>::const_iterator it = other.structures_.begin(); it != other.structures_.end(); ++it)
+    {
+      assert(*it != NULL);
+      structures_.push_back((*it)->Clone());
+    }
+  }
+
+
+  DicomStructuredReport::~DicomStructuredReport()
+  {
+    for (std::deque<Structure*>::iterator it = structures_.begin(); it != structures_.end(); ++it)
+    {
+      assert(*it != NULL);
+      delete *it;
+    }
+
+    for (std::map<std::string, ReferencedInstance*>::iterator
+           it = instancesInformation_.begin(); it != instancesInformation_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      delete it->second;
+    }
+  }
+
+
+  void DicomStructuredReport::GetReferencedInstance(std::string& studyInstanceUid,
+                                                    std::string& seriesInstanceUid,
+                                                    std::string& sopInstanceUid,
+                                                    std::string& sopClassUid,
+                                                    size_t i) const
+  {
+    if (i >= orderedInstances_.size())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    sopInstanceUid = orderedInstances_[i];
+
+    std::map<std::string, ReferencedInstance*>::const_iterator found = instancesInformation_.find(sopInstanceUid);
+    if (found == instancesInformation_.end())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    assert(found->second != NULL);
+    studyInstanceUid = found->second->GetStudyInstanceUid();
+    seriesInstanceUid = found->second->GetSeriesInstanceUid();
+    sopClassUid = found->second->GetSopClassUid();
+  }
+
+
+  void DicomStructuredReport::ExportReferencedFrames(std::list<ReferencedFrame>& frames) const
+  {
+    frames.clear();
+
+    for (size_t i = 0; i < orderedInstances_.size(); i++)
+    {
+      std::map<std::string, ReferencedInstance*>::const_iterator found = instancesInformation_.find(orderedInstances_[i]);
+      if (found == instancesInformation_.end())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+
+      assert(found->second != NULL);
+
+      for (std::set<unsigned int>::const_iterator frame = found->second->GetFrames().begin();
+           frame != found->second->GetFrames().end(); ++frame)
+      {
+        frames.push_back(ReferencedFrame(found->second->GetStudyInstanceUid(),
+                                         found->second->GetSeriesInstanceUid(),
+                                         orderedInstances_[i],
+                                         found->second->GetSopClassUid(), *frame));
+      }
+    }
+  }
+
+
+  const DicomStructuredReport::Structure& DicomStructuredReport::GetStructure(size_t index) const
+  {
+    if (index >= structures_.size())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      assert(structures_[index] != NULL);
+      return *structures_[index];
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Toolbox/DicomStructuredReport.h	Tue Nov 14 11:45:17 2023 +0100
@@ -0,0 +1,306 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#if !defined(ORTHANC_ENABLE_DCMTK)
+#  error The macro ORTHANC_ENABLE_DCMTK must be defined
+#endif
+
+#if ORTHANC_ENABLE_DCMTK != 1
+#  error Support for DCMTK must be enabled
+#endif
+
+#include "../Scene2D/ScenePoint2D.h"
+
+#include <DicomParsing/ParsedDicomFile.h>
+
+#include <dcmtk/dcmdata/dcitem.h>
+#include <deque>
+#include <list>
+#include <set>
+
+namespace OrthancStone
+{
+  class DicomStructuredReport : public boost::noncopyable
+  {
+  public:
+    enum StructureType
+    {
+      StructureType_Point,
+      StructureType_Polyline
+    };
+
+    class Structure : public boost::noncopyable
+    {
+    private:
+      std::string   sopInstanceUid_;
+      bool          hasFrameNumber_;
+      unsigned int  frameNumber_;
+      bool          hasProbabilityOfCancer_;
+      float         probabilityOfCancer_;
+
+    protected:
+      void Copy(const Structure& other);
+
+    public:
+      Structure(const std::string& sopInstanceUid);
+
+      virtual ~Structure()
+      {
+      }
+
+      virtual Structure* Clone() const = 0;
+
+      virtual StructureType GetType() const = 0;
+
+      const std::string& GetSopInstanceUid() const
+      {
+        return sopInstanceUid_;
+      }
+
+      void SetFrameNumber(unsigned int frame);
+
+      void SetProbabilityOfCancer(float probability);
+
+      bool HasFrameNumber() const
+      {
+        return hasFrameNumber_;
+      }
+
+      bool HasProbabilityOfCancer() const
+      {
+        return hasProbabilityOfCancer_;
+      }
+
+      unsigned int GetFrameNumber() const;
+
+      float GetProbabilityOfCancer() const;
+    };
+
+
+    class Point : public Structure
+    {
+    private:
+      ScenePoint2D  point_;
+
+    public:
+      Point(const std::string& sopInstanceUid,
+            double x,
+            double y);
+
+      virtual Structure* Clone() const ORTHANC_OVERRIDE;
+
+      virtual StructureType GetType() const ORTHANC_OVERRIDE
+      {
+        return StructureType_Point;
+      }
+
+      const ScenePoint2D& GetPoint() const
+      {
+        return point_;
+      }
+    };
+
+
+    class Polyline : public Structure
+    {
+    private:
+      std::vector<ScenePoint2D>  points_;
+
+    public:
+      Polyline(const std::string& sopInstanceUid,
+               const float* points,
+               unsigned long pointsCount);
+
+      Polyline(const std::string& sopInstanceUid,
+               const std::vector<ScenePoint2D>& points);
+
+      virtual Structure* Clone() const ORTHANC_OVERRIDE;
+
+      virtual StructureType GetType() const ORTHANC_OVERRIDE
+      {
+        return StructureType_Polyline;
+      }
+
+      size_t GetSize() const
+      {
+        return points_.size();
+      }
+
+      const ScenePoint2D& GetPoint(size_t i) const;
+    };
+
+
+  private:
+    class ReferencedInstance
+    {
+    private:
+      std::string  studyInstanceUid_;
+      std::string  seriesInstanceUid_;
+      std::string  sopClassUid_;
+      std::set<unsigned int>  frames_;
+
+    public:
+      ReferencedInstance(const std::string& studyInstanceUid,
+                         const std::string& seriesInstanceUid,
+                         const std::string& sopClassUid) :
+        studyInstanceUid_(studyInstanceUid),
+        seriesInstanceUid_(seriesInstanceUid),
+        sopClassUid_(sopClassUid)
+      {
+      }
+
+      const std::string& GetStudyInstanceUid() const
+      {
+        return studyInstanceUid_;
+      }
+
+      const std::string& GetSeriesInstanceUid() const
+      {
+        return seriesInstanceUid_;
+      }
+
+      const std::string& GetSopClassUid() const
+      {
+        return sopClassUid_;
+      }
+
+      void AddFrame(unsigned int frame)
+      {
+        frames_.insert(frame);
+      }
+
+      const std::set<unsigned int>& GetFrames() const
+      {
+        return frames_;
+      }
+    };
+
+
+    void AddStructure(const std::string& sopInstanceUid,
+                      DcmItem& group,
+                      bool hasFrameNumber,
+                      unsigned int frameNumber,
+                      bool hasProbabilityOfCancer,
+                      float probabilityOfCancer);
+
+    std::string                                 studyInstanceUid_;
+    std::string                                 seriesInstanceUid_;
+    std::string                                 sopInstanceUid_;
+    std::map<std::string, ReferencedInstance*>  instancesInformation_;
+    std::vector<std::string>                    orderedInstances_;
+    std::deque<Structure*>                      structures_;
+
+  public:
+    class ReferencedFrame
+    {
+    private:
+      std::string  studyInstanceUid_;
+      std::string  seriesInstanceUid_;
+      std::string  sopInstanceUid_;
+      std::string  sopClassUid_;
+      unsigned int frameNumber_;
+
+    public:
+      ReferencedFrame(const std::string& studyInstanceUid,
+                      const std::string& seriesInstanceUid,
+                      const std::string& sopInstanceUid,
+                      const std::string& sopClassUid,
+                      unsigned int frameNumber) :
+        studyInstanceUid_(studyInstanceUid),
+        seriesInstanceUid_(seriesInstanceUid),
+        sopInstanceUid_(sopInstanceUid),
+        sopClassUid_(sopClassUid),
+        frameNumber_(frameNumber)
+      {
+      }
+
+      const std::string& GetStudyInstanceUid() const
+      {
+        return studyInstanceUid_;
+      }
+
+      const std::string& GetSeriesInstanceUid() const
+      {
+        return seriesInstanceUid_;
+      }
+
+      const std::string& GetSopInstanceUid() const
+      {
+        return sopInstanceUid_;
+      }
+
+      const std::string& GetSopClassUid() const
+      {
+        return sopClassUid_;
+      }
+
+      unsigned int GetFrameNumber() const
+      {
+        return frameNumber_;
+      }
+    };
+
+    DicomStructuredReport(Orthanc::ParsedDicomFile& dicom);
+
+    DicomStructuredReport(const DicomStructuredReport& other);  // Copy constructor
+
+    ~DicomStructuredReport();
+
+    const std::string& GetStudyInstanceUid() const
+    {
+      return studyInstanceUid_;
+    }
+
+    const std::string& GetSeriesInstanceUid() const
+    {
+      return seriesInstanceUid_;
+    }
+    
+    const std::string& GetSopInstanceUid() const
+    {
+      return sopInstanceUid_;
+    }
+
+    size_t GetReferencedInstancesCount() const
+    {
+      return orderedInstances_.size();
+    }
+
+    void GetReferencedInstance(std::string& studyInstanceUid,
+                               std::string& seriesInstanceUid,
+                               std::string& sopInstanceUid,
+                               std::string& sopClassUid,
+                               size_t i) const;
+
+    void ExportReferencedFrames(std::list<ReferencedFrame>& frames) const;
+
+    size_t GetStructuresCount() const
+    {
+      return structures_.size();
+    }
+
+    const Structure& GetStructure(size_t index) const;
+  };
+}
--- a/OrthancStone/UnitTestsSources/UnitTestsMain.cpp	Wed Oct 11 17:10:45 2023 +0200
+++ b/OrthancStone/UnitTestsSources/UnitTestsMain.cpp	Tue Nov 14 11:45:17 2023 +0100
@@ -44,6 +44,7 @@
   ASSERT_EQ(SopClassUid_VideoEndoscopicImageStorage, StringToSopClassUid("1.2.840.10008.5.1.4.1.1.77.1.1.1"));
   ASSERT_EQ(SopClassUid_VideoMicroscopicImageStorage, StringToSopClassUid("1.2.840.10008.5.1.4.1.1.77.1.2.1"));
   ASSERT_EQ(SopClassUid_VideoPhotographicImageStorage, StringToSopClassUid("1.2.840.10008.5.1.4.1.1.77.1.4.1"));
+  ASSERT_EQ(SopClassUid_ComprehensiveSR, StringToSopClassUid("1.2.840.10008.5.1.4.1.1.88.33"));
   ASSERT_EQ(SopClassUid_Other, StringToSopClassUid("nope"));
 
   ASSERT_EQ(SeriesThumbnailType_Pdf, GetSeriesThumbnailType(SopClassUid_EncapsulatedPdf));
--- a/RenderingPlugin/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp	Wed Oct 11 17:10:45 2023 +0200
+++ b/RenderingPlugin/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp	Tue Nov 14 11:45:17 2023 +0100
@@ -79,6 +79,10 @@
     }
   }
 
+  void ResetGlobalContext()
+  {
+    globalContext_ = NULL;
+  }
 
   bool HasGlobalContext()
   {
--- a/RenderingPlugin/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h	Wed Oct 11 17:10:45 2023 +0200
+++ b/RenderingPlugin/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h	Tue Nov 14 11:45:17 2023 +0100
@@ -137,6 +137,8 @@
 
   void SetGlobalContext(OrthancPluginContext* context);
 
+  void ResetGlobalContext();
+
   bool HasGlobalContext();
 
   OrthancPluginContext* GetGlobalContext();
--- a/RenderingPlugin/Resources/SyncOrthancFolder.py	Wed Oct 11 17:10:45 2023 +0200
+++ b/RenderingPlugin/Resources/SyncOrthancFolder.py	Tue Nov 14 11:45:17 2023 +0100
@@ -38,7 +38,7 @@
 
 TARGET = os.path.join(os.path.dirname(__file__), 'Orthanc')
 PLUGIN_SDK_VERSION = '1.0.0'
-REPOSITORY = 'https://hg.orthanc-server.com/orthanc/raw-file'
+REPOSITORY = 'https://orthanc.uclouvain.be/hg/orthanc/raw-file'
 
 FILES = [
     ('OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h',   'Plugins'),