changeset 2255:ee2b76f07bad

support of US series with varying SequenceOfUltrasoundRegions
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 03 Dec 2025 15:30:50 +0100
parents 14cd7e87def4
children 0633b2841e44
files Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp OrthancStone/Sources/Scene2D/Scene2D.cpp OrthancStone/Sources/Scene2D/Scene2D.h OrthancStone/Sources/Toolbox/DicomInstanceParameters.h OrthancStone/Sources/Toolbox/ParsedDicomDataset.cpp OrthancStone/Sources/Toolbox/SortedFrames.cpp OrthancStone/Sources/Toolbox/SortedFrames.h
diffstat 7 files changed, 123 insertions(+), 22 deletions(-) [+]
line wrap: on
line diff
--- a/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp	Wed Dec 03 11:17:37 2025 +0100
+++ b/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp	Wed Dec 03 15:30:50 2025 +0100
@@ -56,6 +56,7 @@
 #include "../../../OrthancStone/Sources/Toolbox/DicomStructuredReport.h"
 #include "../../../OrthancStone/Sources/Toolbox/GeometryToolbox.h"
 #include "../../../OrthancStone/Sources/Toolbox/OsiriX/CollectionOfAnnotations.h"
+#include "../../../OrthancStone/Sources/Toolbox/ParsedDicomDataset.h"
 #include "../../../OrthancStone/Sources/Toolbox/SortedFrames.h"
 #include "../../../OrthancStone/Sources/Viewport/DefaultViewportInteractor.h"
 
@@ -275,6 +276,9 @@
                                 const OrthancStone::Vector& point,
                                 double maximumDistance) const = 0;
 
+  virtual void EnrichInstance(const std::string& sopInstanceUid,
+                              Orthanc::ParsedDicomFile& dicom) = 0;
+
   static OrthancStone::CoordinateSystem3D GetFrameGeometry(const IFramesCollection& frames,
                                                            size_t frameIndex)
   {
@@ -334,6 +338,13 @@
   {
     return frames_->FindClosestFrame(frameIndex, point, maximumDistance);
   }
+
+  virtual void EnrichInstance(const std::string& sopInstanceUid,
+                              Orthanc::ParsedDicomFile& dicom) ORTHANC_OVERRIDE
+  {
+    OrthancStone::ParsedDicomDataset dataset(dicom);
+    frames_->EnrichInstance(sopInstanceUid, dataset);
+  }
 };
 
 
@@ -605,6 +616,12 @@
   {
     GetColorInternal() = color;
   }
+
+  virtual void EnrichInstance(const std::string& sopInstanceUid,
+                              Orthanc::ParsedDicomFile& dicom) ORTHANC_OVERRIDE
+  {
+    // Not useful in this case
+  }
 };
 
 
@@ -2559,6 +2576,8 @@
     
     virtual void Handle(const OrthancStone::ParseDicomSuccessMessage& message) const ORTHANC_OVERRIDE
     {
+      GetViewport().frames_->EnrichInstance(sopInstanceUid_, message.GetDicom());
+
       std::unique_ptr<Orthanc::ImageAccessor> frame;
       
       try
@@ -2956,7 +2975,9 @@
       pixelSpacingY = 1000;
     }
 
-    if (FIX_LSD_479)
+    layer->SetPixelSpacing(pixelSpacingX, pixelSpacingY);
+
+    if (0 /* FIX_LSD_479 */)
     {
       /**
        * Some series contain a first instance (secondary capture) that
@@ -2968,13 +2989,9 @@
       double physicalWidth = pixelSpacingX * static_cast<double>(frame.GetWidth()); 
       double physicalHeight = pixelSpacingY * static_cast<double>(frame.GetHeight());
 
-      if (OrthancStone::LinearAlgebra::IsCloseToZero(physicalWidth) ||
-          OrthancStone::LinearAlgebra::IsCloseToZero(physicalHeight))
-      {
-        // Numerical instability, don't try further processing
-        layer->SetPixelSpacing(pixelSpacingX, pixelSpacingY);
-      }
-      else
+      // On numerical instability, don't try further processing
+      if (!OrthancStone::LinearAlgebra::IsCloseToZero(physicalWidth) &&
+          !OrthancStone::LinearAlgebra::IsCloseToZero(physicalHeight))
       {
         double scale = std::max(centralPhysicalWidth_ / physicalWidth,
                                 centralPhysicalHeight_ / physicalHeight);
@@ -2983,10 +3000,6 @@
                          (centralPhysicalHeight_ - physicalHeight * scale) / 2.0);
       }
     }
-    else
-    {
-      layer->SetPixelSpacing(pixelSpacingX, pixelSpacingY);
-    }
 
     StoneAnnotationsRegistry::GetInstance().Load(*stoneAnnotations_, instance.GetSopInstanceUid(), frameIndex);
 
@@ -3014,6 +3027,8 @@
       holder.AddLayer(LAYER_STRUCTURED_REPORT, NULL);
     }
 
+    const unsigned int currentWidth = layer->GetTexture().GetWidth();
+    const unsigned int currentHeight = layer->GetTexture().GetHeight();
     holder.AddLayer(LAYER_TEXTURE, layer.release());
 
     {
@@ -3021,6 +3036,25 @@
 
       OrthancStone::Scene2D& scene = lock->GetController().GetScene();
 
+      bool hasPreviousSize = false;
+      unsigned int previousWidth, previousHeight;
+      OrthancStone::Extent2D previousExtent;
+      if (scene.HasLayer(LAYER_TEXTURE))
+      {
+        const OrthancStone::ISceneLayer& previousLayer = scene.GetLayer(LAYER_TEXTURE);
+        previousLayer.GetBoundingBox(previousExtent);
+        if (!previousExtent.IsEmpty() &&
+            (previousLayer.GetType() == OrthancStone::ISceneLayer::Type_ColorTexture ||
+             previousLayer.GetType() == OrthancStone::ISceneLayer::Type_FloatTexture ||
+             previousLayer.GetType() == OrthancStone::ISceneLayer::Type_LookupTableTexture))
+        {
+          const OrthancStone::TextureBaseSceneLayer& texture = dynamic_cast<const OrthancStone::TextureBaseSceneLayer&>(previousLayer);
+          hasPreviousSize = true;
+          previousWidth = texture.GetTexture().GetWidth();
+          previousHeight = texture.GetTexture().GetHeight();
+        }
+      }
+
       holder.Commit(scene);
 
       stoneAnnotations_->Render(scene);  // Necessary for "FitContent()" to work
@@ -3039,9 +3073,24 @@
           lock->GetCompositor().FitContent(scene);
         }
 
-        stoneAnnotations_->Render(scene);
+        //stoneAnnotations_->Render(scene);
         fitNextContent_ = false;
       }
+      else if (hasPreviousSize)
+      {
+        if (currentWidth == previousWidth &&
+            currentHeight == previousHeight)
+        {
+          // This is notably useful for US images, where the width/height is constant
+          // across the series, while the zoom level varies (cf. Michael Vitale)
+          scene.PreserveExtent(LAYER_TEXTURE, previousExtent);
+        }
+        else
+        {
+          // This supersedes the (incorrect) FIX_LSD_479 that broke pixel spacing in annotations
+          lock->GetCompositor().FitContent(scene);
+        }
+      }
         
       //lock->GetCompositor().Refresh(scene);
       lock->Invalidate();
--- a/OrthancStone/Sources/Scene2D/Scene2D.cpp	Wed Dec 03 11:17:37 2025 +0100
+++ b/OrthancStone/Sources/Scene2D/Scene2D.cpp	Wed Dec 03 15:30:50 2025 +0100
@@ -25,6 +25,8 @@
 
 #include <OrthancException.h>
 
+#include "ScenePoint2D.h"
+
 
 namespace OrthancStone
 {
@@ -354,4 +356,27 @@
       GetSceneToCanvasTransform());
     FitContent(transform, canvasWidth, canvasHeight);
   }
+
+
+  void Scene2D::PreserveExtent(int depth,
+                               const Extent2D& previousExtent)
+  {
+    if (!previousExtent.IsEmpty() &&
+        HasLayer(depth))
+    {
+      Extent2D currentExtent;
+      GetLayer(depth).GetBoundingBox(currentExtent);
+
+      if (!currentExtent.IsEmpty())
+      {
+        AffineTransform2D t1 = GetSceneToCanvasTransform();
+        AffineTransform2D t2 = AffineTransform2D::CreateOffset(-previousExtent.GetCenterX(), -previousExtent.GetCenterY());
+        AffineTransform2D t3 = AffineTransform2D::CreateScaling(previousExtent.GetWidth() / currentExtent.GetWidth(),
+                                                                previousExtent.GetHeight() / currentExtent.GetHeight());
+        AffineTransform2D t4 = AffineTransform2D::CreateOffset(currentExtent.GetCenterX(), currentExtent.GetCenterY());
+
+        SetSceneToCanvasTransform(AffineTransform2D::Combine(t1, t2, t3, t4));
+      }
+    }
+  }
 }
--- a/OrthancStone/Sources/Scene2D/Scene2D.h	Wed Dec 03 11:17:37 2025 +0100
+++ b/OrthancStone/Sources/Scene2D/Scene2D.h	Wed Dec 03 15:30:50 2025 +0100
@@ -136,5 +136,8 @@
 
     void FlipViewportY(unsigned int canvasWidth,
                        unsigned int canvasHeight);
+
+    void PreserveExtent(int depth,
+                        const Extent2D& previousExtent);
   };
 }
--- a/OrthancStone/Sources/Toolbox/DicomInstanceParameters.h	Wed Dec 03 11:17:37 2025 +0100
+++ b/OrthancStone/Sources/Toolbox/DicomInstanceParameters.h	Wed Dec 03 15:30:50 2025 +0100
@@ -77,8 +77,6 @@
     std::unique_ptr<Orthanc::DicomMap>               tags_;
     std::unique_ptr<Orthanc::DicomImageInformation>  imageInformation_;  // Lazy evaluation
 
-    void InjectSequenceTags(const IDicomDataset& dataset);
-
   public:
     explicit DicomInstanceParameters(const DicomInstanceParameters& other);
 
@@ -264,5 +262,7 @@
 
     bool LookupPerFrameWindowing(Windowing& windowing,
                                  unsigned int frame) const;
+
+    void InjectSequenceTags(const IDicomDataset& dataset);
   };
 }
--- a/OrthancStone/Sources/Toolbox/ParsedDicomDataset.cpp	Wed Dec 03 11:17:37 2025 +0100
+++ b/OrthancStone/Sources/Toolbox/ParsedDicomDataset.cpp	Wed Dec 03 15:30:50 2025 +0100
@@ -23,7 +23,9 @@
 
 #include "ParsedDicomDataset.h"
 
-#include <dcmtk/dcmdata/dcfilefo.h>
+#include <Logging.h>
+#include <DicomParsing/FromDcmtkBridge.h>
+
 
 namespace OrthancStone
 {
@@ -31,7 +33,7 @@
                              const Orthanc::DicomPath& path)
   {
     DcmItem* node = dicom.GetDcmtkObject().getDataset();
-      
+
     for (size_t i = 0; i < path.GetPrefixLength(); i++)
     {
       const Orthanc::DicomTag& tmp = path.GetPrefixTag(i);
@@ -70,12 +72,17 @@
     {
       DcmTagKey tag(path.GetFinalTag().GetGroup(), path.GetFinalTag().GetElement());
 
-      const char* s = NULL;
-      if (node->findAndGetString(tag, s).good() &&
-          s != NULL)
+      DcmElement* element = NULL;
+      if (node->findAndGetElement(tag, element).good() &&
+          element != NULL)
       {
-        result.assign(s);
-        return true;
+        // Leverage the Orthanc framework to convert any VR as a string
+        const Orthanc::ValueRepresentation vr = Orthanc::FromDcmtkBridge::LookupValueRepresentation(path.GetFinalTag());
+        const std::set<Orthanc::DicomTag> ignoreTagLength;
+        std::unique_ptr<Orthanc::DicomValue> value(
+          Orthanc::FromDcmtkBridge::ConvertLeafElement(
+            *element, Orthanc::DicomToJsonFlags_None, 0, Orthanc::Encoding_Ascii, false, ignoreTagLength, vr));
+        return value->CopyToString(result, false /* no binary */);
       }
     }
 
--- a/OrthancStone/Sources/Toolbox/SortedFrames.cpp	Wed Dec 03 11:17:37 2025 +0100
+++ b/OrthancStone/Sources/Toolbox/SortedFrames.cpp	Wed Dec 03 15:30:50 2025 +0100
@@ -443,4 +443,18 @@
                                       "Sort() has not been called");
     }
   }
+
+
+  void SortedFrames::EnrichInstance(const std::string& sopInstanceUid,
+                                    const IDicomDataset& dicom)
+  {
+    size_t index;
+    if (LookupSopInstanceUid(index, sopInstanceUid))
+    {
+      DicomInstanceParameters* instance = instances_[index];
+      assert(instance != NULL);
+
+      instance->InjectSequenceTags(dicom);
+    }
+  }
 }
--- a/OrthancStone/Sources/Toolbox/SortedFrames.h	Wed Dec 03 11:17:37 2025 +0100
+++ b/OrthancStone/Sources/Toolbox/SortedFrames.h	Wed Dec 03 15:30:50 2025 +0100
@@ -154,5 +154,8 @@
     bool FindClosestFrame(size_t& frameIndex,
                           const Vector& point,
                           double maximumDistance) const;
+
+    void EnrichInstance(const std::string& sopInstanceUid,
+                        const IDicomDataset& dicom);
   };
 }