changeset 369:3adb57efc32f

the viewer now displays the scale
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 17 Mar 2025 13:27:36 +0100 (2 months ago)
parents 60100f5effee
children 5fb0dfa5c155
files Framework/Inputs/DicomPyramid.cpp Framework/Inputs/DicomPyramid.h Framework/Inputs/DicomPyramidInstance.cpp Framework/Inputs/DicomPyramidInstance.h NEWS ViewerPlugin/DicomPyramidCache.cpp ViewerPlugin/DicomPyramidCache.h ViewerPlugin/Plugin.cpp ViewerPlugin/viewer.js
diffstat 9 files changed, 163 insertions(+), 17 deletions(-) [+]
line wrap: on
line diff
--- a/Framework/Inputs/DicomPyramid.cpp	Mon Mar 17 11:34:33 2025 +0100
+++ b/Framework/Inputs/DicomPyramid.cpp	Mon Mar 17 13:27:36 2025 +0100
@@ -271,4 +271,34 @@
     assert(!instances_.empty() && instances_[0] != NULL);
     return instances_[0]->GetPhotometricInterpretation();
   }
+
+
+  bool DicomPyramid::LookupImagedVolumeSize(double& width,
+                                            double& height) const
+  {
+    bool found = false;
+
+    for (size_t i = 0; i < instances_.size(); i++)
+    {
+      assert(instances_[i] != NULL);
+
+      if (instances_[i]->HasImagedVolumeSize())
+      {
+        if (!found)
+        {
+          found = true;
+          width = instances_[i]->GetImagedVolumeWidth();
+          height = instances_[i]->GetImagedVolumeHeight();
+        }
+        else if (std::abs(width - instances_[i]->GetImagedVolumeWidth()) > 100.0 * std::numeric_limits<double>::epsilon() ||
+                 std::abs(height - instances_[i]->GetImagedVolumeHeight()) > 100.0 * std::numeric_limits<double>::epsilon())
+        {
+          LOG(WARNING) << "Inconsistency of imaged volume width/height in series: " << seriesId_;
+          return false;
+        }
+      }
+    }
+
+    return found;
+  }
 }
--- a/Framework/Inputs/DicomPyramid.h	Mon Mar 17 11:34:33 2025 +0100
+++ b/Framework/Inputs/DicomPyramid.h	Mon Mar 17 13:27:36 2025 +0100
@@ -103,5 +103,8 @@
     {
       return backgroundBlue_;
     }
+
+    bool LookupImagedVolumeSize(double& width,
+                                double& height) const;
   };
 }
--- a/Framework/Inputs/DicomPyramidInstance.cpp	Mon Mar 17 11:34:33 2025 +0100
+++ b/Framework/Inputs/DicomPyramidInstance.cpp	Mon Mar 17 13:27:36 2025 +0100
@@ -41,6 +41,7 @@
 #endif
 
 #define SERIALIZED_METADATA  "4201"   // Was "4200" if versions <= 0.7 of this plugin
+#define SERIALIZED_VERSION   "2"      // Introduced in WSI 3.1
 
 
 namespace OrthancWSI
@@ -53,6 +54,8 @@
   static const Orthanc::DicomTag DICOM_TAG_TOTAL_PIXEL_MATRIX_ROWS(0x0048, 0x0007);
   static const Orthanc::DicomTag DICOM_TAG_IMAGE_TYPE(0x0008, 0x0008);
   static const Orthanc::DicomTag DICOM_TAG_RECOMMENDED_ABSENT_PIXEL_CIELAB(0x0048, 0x0015);
+  static const Orthanc::DicomTag DICOM_TAG_IMAGED_VOLUME_WIDTH(0x0048, 0x0001);
+  static const Orthanc::DicomTag DICOM_TAG_IMAGED_VOLUME_HEIGHT(0x0048, 0x0002);
 
   static ImageCompression DetectImageCompression(OrthancStone::IOrthancConnection& orthanc,
                                                  const std::string& instanceId)
@@ -282,6 +285,11 @@
         backgroundBlue_ = rgb.GetB();
       }
     }
+
+    // New in WSI 3.1
+    hasImagedVolumeSize_ = (
+      reader.GetDoubleValue(imagedVolumeWidth_, Orthanc::DicomPath(DICOM_TAG_IMAGED_VOLUME_WIDTH)) &&
+      reader.GetDoubleValue(imagedVolumeHeight_, Orthanc::DicomPath(DICOM_TAG_IMAGED_VOLUME_HEIGHT)));
   }
 
 
@@ -294,7 +302,10 @@
     hasBackgroundColor_(false),
     backgroundRed_(0),
     backgroundGreen_(0),
-    backgroundBlue_(0)
+    backgroundBlue_(0),
+    hasImagedVolumeSize_(false),
+    imagedVolumeWidth_(0),
+    imagedVolumeHeight_(0)
   {
     if (useCache)
     {
@@ -303,8 +314,10 @@
         // Try and deserialized the cached information about this instance
         std::string serialized;
         orthanc.RestApiGet(serialized, "/instances/" + instanceId + "/metadata/" + SERIALIZED_METADATA);
-        Deserialize(serialized);
-        return;  // Success
+        if (Deserialize(serialized))
+        {
+          return;  // Success
+        }
       }
       catch (Orthanc::OrthancException&)
       {
@@ -351,6 +364,8 @@
   static const char* const PHOTOMETRIC_INTERPRETATION = "PhotometricInterpretation";
   static const char* const IMAGE_TYPE = "ImageType";
   static const char* const BACKGROUND_COLOR = "BackgroundColor";
+  static const char* const VERSION = "Version";
+  static const char* const IMAGED_VOLUME_SIZE = "ImagedVolumeSize";
   
   
   void DicomPyramidInstance::Serialize(std::string& result) const
@@ -378,6 +393,7 @@
     content[TOTAL_HEIGHT] = totalHeight_;
     content[PHOTOMETRIC_INTERPRETATION] = Orthanc::EnumerationToString(photometric_);
     content[IMAGE_TYPE] = imageType_;
+    content[VERSION] = SERIALIZED_VERSION;
 
     if (hasBackgroundColor_)
     {
@@ -388,6 +404,14 @@
       content[BACKGROUND_COLOR] = color;
     }
 
+    if (hasImagedVolumeSize_)
+    {
+      Json::Value size = Json::arrayValue;
+      size.append(imagedVolumeWidth_);
+      size.append(imagedVolumeHeight_);
+      content[IMAGED_VOLUME_SIZE] = size;
+    }
+
 #if ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 9, 0)
     Orthanc::Toolbox::WriteFastJson(result, content);
 #else
@@ -397,7 +421,7 @@
   }
 
 
-  void DicomPyramidInstance::Deserialize(const std::string& s)
+  bool DicomPyramidInstance::Deserialize(const std::string& s)
   {
     Json::Value content;
     OrthancStone::IOrthancConnection::ParseJson(content, s);
@@ -409,6 +433,12 @@
       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
     }
 
+    std::string version = Orthanc::SerializationToolbox::ReadString(content, VERSION, "1");
+    if (version != SERIALIZED_VERSION)
+    {
+      return false;  // Serialized using a different version of the plugin, must be reconstructed
+    }
+
     hasCompression_ = Orthanc::SerializationToolbox::ReadBoolean(content, HAS_COMPRESSION);
     compression_ = static_cast<ImageCompression>(Orthanc::SerializationToolbox::ReadInteger(content, IMAGE_COMPRESSION));
     format_ = static_cast<Orthanc::PixelFormat>(Orthanc::SerializationToolbox::ReadInteger(content, PIXEL_FORMAT));
@@ -457,6 +487,23 @@
         backgroundBlue_ = color[2].asUInt();
       }
     }
+
+    hasImagedVolumeSize_ = false;
+    if (content.isMember(IMAGED_VOLUME_SIZE))
+    {
+      const Json::Value& size = content[IMAGED_VOLUME_SIZE];
+      if (size.type() == Json::arrayValue &&
+          size.size() == 2u &&
+          size[0].isDouble() &&
+          size[1].isDouble())
+      {
+        hasImagedVolumeSize_ = true;
+        imagedVolumeWidth_ = size[0].asDouble();
+        imagedVolumeHeight_ = size[1].asDouble();
+      }
+    }
+
+    return true;  // Success
   }
 
 
@@ -497,4 +544,30 @@
       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
     }
   }
+
+
+  double DicomPyramidInstance::GetImagedVolumeWidth() const
+  {
+    if (hasImagedVolumeSize_)
+    {
+      return imagedVolumeWidth_;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  double DicomPyramidInstance::GetImagedVolumeHeight() const
+  {
+    if (hasImagedVolumeSize_)
+    {
+      return imagedVolumeHeight_;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
 }
--- a/Framework/Inputs/DicomPyramidInstance.h	Mon Mar 17 11:34:33 2025 +0100
+++ b/Framework/Inputs/DicomPyramidInstance.h	Mon Mar 17 13:27:36 2025 +0100
@@ -51,11 +51,14 @@
     uint8_t                             backgroundRed_;
     uint8_t                             backgroundGreen_;
     uint8_t                             backgroundBlue_;
+    bool                                hasImagedVolumeSize_;
+    double                              imagedVolumeWidth_;
+    double                              imagedVolumeHeight_;
 
     void Load(OrthancStone::IOrthancConnection&  orthanc,
               const std::string& instanceId);
 
-    void Deserialize(const std::string& content);
+    bool Deserialize(const std::string& content);
 
   public:
     DicomPyramidInstance(OrthancStone::IOrthancConnection&  orthanc,
@@ -125,5 +128,14 @@
     uint8_t GetBackgroundGreen() const;
 
     uint8_t GetBackgroundBlue() const;
+
+    bool HasImagedVolumeSize() const
+    {
+      return hasImagedVolumeSize_;
+    }
+
+    double GetImagedVolumeWidth() const;
+
+    double GetImagedVolumeHeight() const;
   };
 }
--- a/NEWS	Mon Mar 17 11:34:33 2025 +0100
+++ b/NEWS	Mon Mar 17 13:27:36 2025 +0100
@@ -2,6 +2,7 @@
 ===============================
 
 * Upgraded to OpenLayers 10.4.0 (was previously 3.19.0)
+* The viewer now displays the scale if the imaged volume size is available
 * Fix handling of "Image Type" in the viewer for compatibility with other vendors
 
 Compatibility notes about the viewer
--- a/ViewerPlugin/DicomPyramidCache.cpp	Mon Mar 17 11:34:33 2025 +0100
+++ b/ViewerPlugin/DicomPyramidCache.cpp	Mon Mar 17 13:27:36 2025 +0100
@@ -57,7 +57,8 @@
 
 
   DicomPyramid& DicomPyramidCache::GetPyramid(const std::string& seriesId,
-                                              boost::mutex::scoped_lock& lock)
+                                              boost::mutex::scoped_lock& lock,
+                                              bool useMetadataCache)
   {
     // Mutex is assumed to be locked
 
@@ -75,7 +76,7 @@
 
     assert(orthanc_.get() != NULL);
     std::unique_ptr<DicomPyramid> pyramid
-      (new DicomPyramid(*orthanc_, seriesId, true /* use metadata cache */));
+      (new DicomPyramid(*orthanc_, seriesId, useMetadataCache));
 
     {
       // The pyramid is constructed: Store it into the cache
@@ -120,9 +121,11 @@
 
 
   DicomPyramidCache::DicomPyramidCache(OrthancStone::IOrthancConnection* orthanc /* takes ownership */,
-                                       size_t maxSize) :
+                                       size_t maxSize,
+                                       bool useMetadataCache) :
     orthanc_(orthanc),
-    maxSize_(maxSize)
+    maxSize_(maxSize),
+    useMetadataCache_(useMetadataCache)
   {
     if (orthanc == NULL)
     {
@@ -146,11 +149,12 @@
   }
 
 
-  void DicomPyramidCache::InitializeInstance(size_t maxSize)
+  void DicomPyramidCache::InitializeInstance(size_t maxSize,
+                                             bool useMetadataCache)
   {
     if (singleton_.get() == NULL)
     {
-      singleton_.reset(new DicomPyramidCache(new OrthancWSI::OrthancPluginConnection, maxSize));
+      singleton_.reset(new DicomPyramidCache(new OrthancWSI::OrthancPluginConnection, maxSize, useMetadataCache));
     }
     else
     {
@@ -204,7 +208,7 @@
   DicomPyramidCache::Locker::Locker(const std::string& seriesId) :
     cache_(DicomPyramidCache::GetInstance()),
     lock_(cache_.mutex_),
-    pyramid_(cache_.GetPyramid(seriesId, lock_))
+    pyramid_(cache_.GetPyramid(seriesId, lock_, cache_.useMetadataCache_))
   {
   }
 }
--- a/ViewerPlugin/DicomPyramidCache.h	Mon Mar 17 11:34:33 2025 +0100
+++ b/ViewerPlugin/DicomPyramidCache.h	Mon Mar 17 13:27:36 2025 +0100
@@ -43,19 +43,23 @@
     boost::mutex  mutex_;
     size_t        maxSize_;
     Cache         cache_;
+    bool          useMetadataCache_;
 
     DicomPyramidCache(OrthancStone::IOrthancConnection* orthanc /* takes ownership */,
-                      size_t maxSize);
+                      size_t maxSize,
+                      bool useMetadataCache);
 
     DicomPyramid* GetCachedPyramid(const std::string& seriesId);
 
     DicomPyramid& GetPyramid(const std::string& seriesId,
-                             boost::mutex::scoped_lock& lock);
+                             boost::mutex::scoped_lock& lock,
+                             bool useMetadataCache);
 
   public:
     ~DicomPyramidCache();
 
-    static void InitializeInstance(size_t maxSize);
+    static void InitializeInstance(size_t maxSize,
+                                   bool useMetadataCache);
 
     static void FinalizeInstance();
 
--- a/ViewerPlugin/Plugin.cpp	Mon Mar 17 11:34:33 2025 +0100
+++ b/ViewerPlugin/Plugin.cpp	Mon Mar 17 13:27:36 2025 +0100
@@ -129,6 +129,14 @@
               locker.GetPyramid().GetBackgroundBlue());
       answer["BackgroundColor"] = tmp;
     }
+
+    // New in WSI 3.1
+    double imagedVolumeWidth, imagedVolumeHeight;
+    if (locker.GetPyramid().LookupImagedVolumeSize(imagedVolumeWidth, imagedVolumeHeight))
+    {
+      answer["ImagedVolumeWidth"] = imagedVolumeWidth;
+      answer["ImagedVolumeHeight"] = imagedVolumeHeight;
+    }
   }
 
   std::string s = answer.toStyledString();
@@ -501,7 +509,8 @@
 
     OrthancPlugins::SetDescription(ORTHANC_PLUGIN_NAME, "Provides a Web viewer of whole-slide microscopic images within Orthanc.");
 
-    OrthancWSI::DicomPyramidCache::InitializeInstance(10 /* Number of pyramids to be cached - TODO parameter */);
+    OrthancWSI::DicomPyramidCache::InitializeInstance(10 /* Number of pyramids to be cached - TODO parameter */,
+                                                      true /* Use the metadata cache - Should be "false" only during development */);
 
     {
       std::unique_ptr<OrthancWSI::OrthancPyramidFrameFetcher> fetcher(
--- a/ViewerPlugin/viewer.js	Mon Mar 17 11:34:33 2025 +0100
+++ b/ViewerPlugin/viewer.js	Mon Mar 17 13:27:36 2025 +0100
@@ -56,12 +56,22 @@
   var height = pyramid['TotalHeight'];
   var countLevels = pyramid['Resolutions'].length;
 
+  var metersPerUnit = null;
+  var imagedVolumeWidth = pyramid['ImagedVolumeWidth'];  // In millimeters
+  var imagedVolumeHeight = pyramid['ImagedVolumeHeight'];
+  if (imagedVolumeWidth !== undefined &&
+      imagedVolumeHeight !== undefined) {
+    metersPerUnit = parseFloat(imagedVolumeWidth) / (1000.0 * parseFloat(height));
+    //metersPerUnit = parseFloat(imagedVolumeHeight) / (1000.0 * parseFloat(width));
+  }
+
   // Maps always need a projection, but Zoomify layers are not geo-referenced, and
   // are only measured in pixels.  So, we create a fake projection that the map
   // can use to properly display the layer.
   var proj = new ol.proj.Projection({
     code: 'pixel',
-    units: 'pixels',
+    units: 'pixel',
+    metersPerUnit: metersPerUnit,
     extent: [0, 0, width, height]
   });