changeset 337:82d976848b34

created OnTheFlyPyramidsCache class
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 06 Dec 2024 11:15:46 +0100
parents dfd9ecf38091
children a76e0cf77264
files Applications/CMakeLists.txt Framework/Inputs/CytomineImage.h Framework/Inputs/DecodedTiledPyramid.h Framework/Inputs/OnTheFlyPyramid.cpp Framework/Inputs/OnTheFlyPyramid.h Framework/Inputs/OnTheFlyPyramidsCache.cpp Framework/Inputs/OnTheFlyPyramidsCache.h Framework/Inputs/OpenSlidePyramid.cpp Framework/Inputs/OpenSlidePyramid.h Framework/Inputs/SingleLevelDecodedPyramid.h ViewerPlugin/CMakeLists.txt ViewerPlugin/Plugin.cpp
diffstat 12 files changed, 424 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/Applications/CMakeLists.txt	Wed Dec 04 22:41:47 2024 +0100
+++ b/Applications/CMakeLists.txt	Fri Dec 06 11:15:46 2024 +0100
@@ -120,6 +120,7 @@
   ${ORTHANC_WSI_DIR}/Framework/Inputs/DicomPyramidLevel.cpp
   ${ORTHANC_WSI_DIR}/Framework/Inputs/HierarchicalTiff.cpp
   ${ORTHANC_WSI_DIR}/Framework/Inputs/OnTheFlyPyramid.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Inputs/OnTheFlyPyramidsCache.cpp
   ${ORTHANC_WSI_DIR}/Framework/Inputs/OpenSlideLibrary.cpp
   ${ORTHANC_WSI_DIR}/Framework/Inputs/OpenSlidePyramid.cpp
   ${ORTHANC_WSI_DIR}/Framework/Inputs/PlainTiff.cpp
--- a/Framework/Inputs/CytomineImage.h	Wed Dec 04 22:41:47 2024 +0100
+++ b/Framework/Inputs/CytomineImage.h	Fri Dec 06 11:15:46 2024 +0100
@@ -91,5 +91,10 @@
     }
 
     void SetImageCompression(ImageCompression compression);
+
+    virtual size_t GetMemoryUsage() const ORTHANC_OVERRIDE
+    {
+      return 0;  // Image is stored on the remote Cytomine server
+    }
   };
 }
--- a/Framework/Inputs/DecodedTiledPyramid.h	Wed Dec 04 22:41:47 2024 +0100
+++ b/Framework/Inputs/DecodedTiledPyramid.h	Fri Dec 06 11:15:46 2024 +0100
@@ -72,5 +72,7 @@
     {
       return false;   // No access to the raw tiles
     }
+
+    virtual size_t GetMemoryUsage() const = 0;
   };
 }
--- a/Framework/Inputs/OnTheFlyPyramid.cpp	Wed Dec 04 22:41:47 2024 +0100
+++ b/Framework/Inputs/OnTheFlyPyramid.cpp	Fri Dec 06 11:15:46 2024 +0100
@@ -118,4 +118,18 @@
       return *higherLevels_[level - 1];
     }
   }
+
+
+  size_t OnTheFlyPyramid::GetMemoryUsage() const
+  {
+    size_t memory = baseLevel_->GetSize();
+
+    for (size_t i= 0; i < higherLevels_.size(); i++)
+    {
+      assert(higherLevels_[i] != NULL);
+      memory += higherLevels_[i]->GetSize();
+    }
+
+    return memory;
+  }
 }
--- a/Framework/Inputs/OnTheFlyPyramid.h	Wed Dec 04 22:41:47 2024 +0100
+++ b/Framework/Inputs/OnTheFlyPyramid.h	Fri Dec 06 11:15:46 2024 +0100
@@ -91,5 +91,7 @@
     {
       return Orthanc::PhotometricInterpretation_RGB;
     }
+
+    size_t GetMemoryUsage() const ORTHANC_OVERRIDE;
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Inputs/OnTheFlyPyramidsCache.cpp	Fri Dec 06 11:15:46 2024 +0100
@@ -0,0 +1,224 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero 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
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeadersWSI.h"
+#include "OnTheFlyPyramidsCache.h"
+
+
+static std::unique_ptr<OrthancWSI::OnTheFlyPyramidsCache>  singleton_;
+
+namespace OrthancWSI
+{
+  class OnTheFlyPyramidsCache::CachedPyramid : public boost::noncopyable
+  {
+  private:
+    std::unique_ptr<DecodedTiledPyramid>  pyramid_;
+    size_t                                memory_;
+
+  public:
+    explicit CachedPyramid(DecodedTiledPyramid* pyramid) :
+      pyramid_(pyramid)
+    {
+      if (pyramid == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      }
+      else
+      {
+        memory_ = pyramid->GetMemoryUsage();
+      }
+    }
+
+    const DecodedTiledPyramid& GetPyramid() const
+    {
+      assert(pyramid_ != NULL);
+      return *pyramid_;
+    }
+
+    size_t GetMemoryUsage() const
+    {
+      return memory_;
+    }
+  };
+
+
+  bool OnTheFlyPyramidsCache::SanityCheck()
+  {
+    return (cache_.GetSize() < maxCount_ &&
+            (cache_.IsEmpty() || maxMemory_ == 0 || memoryUsage_ <= maxMemory_));
+  }
+
+
+  void OnTheFlyPyramidsCache::MakeRoom(size_t memory)
+  {
+    // Mutex must be locked
+
+    while (cache_.GetSize() >= maxCount_ ||
+           (!cache_.IsEmpty() &&
+            maxMemory_ != 0 &&
+            memoryUsage_ + memory > maxMemory_))
+    {
+      CachedPyramid* oldest = NULL;
+      cache_.RemoveOldest(oldest);
+
+      if (oldest == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+      else
+      {
+        memoryUsage_ -= oldest->GetMemoryUsage();
+        delete oldest;
+      }
+    }
+
+    assert(SanityCheck());
+  }
+
+
+  OnTheFlyPyramidsCache::CachedPyramid * OnTheFlyPyramidsCache::Store(FrameIdentifier identifier,
+                                                                      DecodedTiledPyramid *pyramid)
+  {
+    // Mutex must be locked
+
+    std::unique_ptr<CachedPyramid> payload(new CachedPyramid(pyramid));
+    CachedPyramid* result = payload.get();
+
+    MakeRoom(payload->GetMemoryUsage());
+
+    // Add a new element to the cache and make it the most
+    // recently used entry
+    cache_.Add(identifier, payload.release());
+    memoryUsage_ += payload->GetMemoryUsage();
+
+    assert(SanityCheck());
+    return result;
+  }
+
+
+  OnTheFlyPyramidsCache::OnTheFlyPyramidsCache(IPyramidFetcher *fetcher,
+                                               size_t maxCount,
+                                               size_t maxMemory):
+    fetcher_(fetcher),
+    maxCount_(maxCount),
+    maxMemory_(maxMemory),  // 256 MB
+    memoryUsage_(0)
+  {
+    if (fetcher == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+
+    if (maxCount == 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    assert(SanityCheck());
+  }
+
+
+  void OnTheFlyPyramidsCache::InitializeInstance(IPyramidFetcher *fetcher,
+                                                 size_t maxSize,
+                                                 size_t maxMemory)
+  {
+    if (singleton_.get() == NULL)
+    {
+      singleton_.reset(new OnTheFlyPyramidsCache(fetcher, maxSize, maxMemory));
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void OnTheFlyPyramidsCache::FinalizeInstance()
+  {
+    if (singleton_.get() == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      singleton_.reset(NULL);
+    }
+  }
+
+
+  OnTheFlyPyramidsCache & OnTheFlyPyramidsCache::GetInstance()
+  {
+    if (singleton_.get() == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return *singleton_;
+    }
+  }
+
+
+  OnTheFlyPyramidsCache::Accessor::Accessor(OnTheFlyPyramidsCache that,
+                                            const std::string &instanceId,
+                                            unsigned int frameNumber):
+    lock_(that.mutex_),
+    identifier_(instanceId, frameNumber),
+    pyramid_(NULL)
+  {
+    if (that.cache_.Contains(identifier_, pyramid_))
+    {
+      if (pyramid_ == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+
+      // Tag the series as the most recently used
+      that.cache_.MakeMostRecent(identifier_);
+    }
+    else
+    {
+      // Unlock the mutex as creating the pyramid is a time-consuming operation
+      lock_.unlock();
+
+      std::unique_ptr<DecodedTiledPyramid> payload(that.fetcher_->Fetch(instanceId, frameNumber));
+
+      // Re-lock, as we now modify the cache
+      lock_.lock();
+      pyramid_ = that.Store(identifier_, payload.release());
+    }
+  }
+
+
+  const DecodedTiledPyramid & OnTheFlyPyramidsCache::Accessor::GetPyramid() const
+  {
+    if (IsValid())
+    {
+      return pyramid_->GetPyramid();
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Inputs/OnTheFlyPyramidsCache.h	Fri Dec 06 11:15:46 2024 +0100
@@ -0,0 +1,114 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero 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
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "DecodedTiledPyramid.h"
+
+#include <Cache/LeastRecentlyUsedIndex.h>
+
+#include <boost/thread/mutex.hpp>
+
+
+namespace OrthancWSI
+{
+  class OnTheFlyPyramidsCache : public boost::noncopyable
+  {
+  public:
+    class IPyramidFetcher : public boost::noncopyable
+    {
+    public:
+      virtual ~IPyramidFetcher()
+      {
+      }
+
+      virtual DecodedTiledPyramid* Fetch(const std::string& instanceId,
+                                         unsigned int frameNumber) = 0;
+    };
+
+  private:
+    class CachedPyramid;
+
+    typedef std::pair<std::string, unsigned int> FrameIdentifier;  // Associates an instance ID with a frame number
+
+    typedef Orthanc::LeastRecentlyUsedIndex<FrameIdentifier, CachedPyramid*>  Cache;
+
+    std::unique_ptr<IPyramidFetcher> fetcher_;
+
+    boost::mutex  mutex_;
+    size_t        maxCount_;
+    size_t        maxMemory_;
+    size_t        memoryUsage_;
+    Cache         cache_;
+
+    bool SanityCheck();
+
+    void MakeRoom(size_t memory);
+
+    CachedPyramid* Store(FrameIdentifier identifier,
+                         DecodedTiledPyramid* pyramid);
+
+    OnTheFlyPyramidsCache(IPyramidFetcher* fetcher /* takes ownership */,
+                          size_t maxCount,
+                          size_t maxMemory);
+
+  public:
+    static void InitializeInstance(IPyramidFetcher* fetcher,
+                                   size_t maxSize,
+                                   size_t maxMemory);
+
+    static void FinalizeInstance();
+
+    static OnTheFlyPyramidsCache& GetInstance();
+
+    class Accessor : public boost::noncopyable
+    {
+    private:
+      boost::mutex::scoped_lock lock_;
+      FrameIdentifier           identifier_;
+      CachedPyramid*            pyramid_;
+
+    public:
+      Accessor(OnTheFlyPyramidsCache that,
+               const std::string& instanceId,
+               unsigned int frameNumber);
+
+      bool IsValid() const
+      {
+        return pyramid_ != NULL;
+      }
+
+      const std::string& GetInstanceId() const
+      {
+        return identifier_.first;
+      }
+
+      unsigned int GetFrameNumber() const
+      {
+        return identifier_.second;
+      }
+
+      const DecodedTiledPyramid& GetPyramid() const;
+    };
+  };
+}
--- a/Framework/Inputs/OpenSlidePyramid.cpp	Wed Dec 04 22:41:47 2024 +0100
+++ b/Framework/Inputs/OpenSlidePyramid.cpp	Fri Dec 06 11:15:46 2024 +0100
@@ -160,4 +160,17 @@
       return false;
     }
   }
+
+
+  size_t OpenSlidePyramid::GetMemoryUsage() const
+  {
+    size_t countPixels = 0;
+
+    for (unsigned int i = 0; i < image_.GetLevelCount(); i++)
+    {
+      countPixels += image_.GetLevelWidth(i) * image_.GetLevelHeight(i);
+    }
+
+    return countPixels * Orthanc::GetBytesPerPixel(Orthanc::PixelFormat_RGBA32);
+  }
 }
--- a/Framework/Inputs/OpenSlidePyramid.h	Wed Dec 04 22:41:47 2024 +0100
+++ b/Framework/Inputs/OpenSlidePyramid.h	Fri Dec 06 11:15:46 2024 +0100
@@ -84,5 +84,7 @@
 
     bool LookupImagedVolumeSize(float& width,
                                 float& height) const;
+
+    size_t GetMemoryUsage() const ORTHANC_OVERRIDE;
   };
 }
--- a/Framework/Inputs/SingleLevelDecodedPyramid.h	Wed Dec 04 22:41:47 2024 +0100
+++ b/Framework/Inputs/SingleLevelDecodedPyramid.h	Fri Dec 06 11:15:46 2024 +0100
@@ -84,5 +84,10 @@
                     uint8_t backgroundRed,
                     uint8_t backgroundGreen,
                     uint8_t backgroundBlue);
+
+    size_t GetMemoryUsage() const ORTHANC_OVERRIDE
+    {
+      return image_.GetSize();
+    }
   };
 }
--- a/ViewerPlugin/CMakeLists.txt	Wed Dec 04 22:41:47 2024 +0100
+++ b/ViewerPlugin/CMakeLists.txt	Fri Dec 06 11:15:46 2024 +0100
@@ -200,6 +200,7 @@
   ${ORTHANC_WSI_DIR}/Framework/Inputs/DicomPyramidInstance.cpp
   ${ORTHANC_WSI_DIR}/Framework/Inputs/DicomPyramidLevel.cpp
   ${ORTHANC_WSI_DIR}/Framework/Inputs/OnTheFlyPyramid.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Inputs/OnTheFlyPyramidsCache.cpp
   ${ORTHANC_WSI_DIR}/Framework/Inputs/PyramidWithRawTiles.cpp
   ${ORTHANC_WSI_DIR}/Framework/Jpeg2000Reader.cpp
   ${ORTHANC_WSI_DIR}/Framework/Jpeg2000Writer.cpp
--- a/ViewerPlugin/Plugin.cpp	Wed Dec 04 22:41:47 2024 +0100
+++ b/ViewerPlugin/Plugin.cpp	Fri Dec 06 11:15:46 2024 +0100
@@ -26,6 +26,9 @@
 #include "DicomPyramidCache.h"
 #include "IIIF.h"
 #include "RawTile.h"
+#include "../Framework/Inputs/DecodedTiledPyramid.h"
+#include "../Framework/Inputs/OnTheFlyPyramid.h"
+#include "../Framework/Inputs/OnTheFlyPyramidsCache.h"
 
 #include <Compatibility.h>  // For std::unique_ptr
 #include <Images/Image.h>
@@ -39,10 +42,48 @@
 #include <EmbeddedResources.h>
 
 #include <cassert>
+#include <Images/PngReader.h>
+#include <boost/atomic/detail/lock_pool.hpp>
+
 
 #define ORTHANC_PLUGIN_NAME "wsi"
 
 
+namespace OrthancWSI
+{
+  class OrthancPyramidFrameFetcher : public OnTheFlyPyramidsCache::IPyramidFetcher
+  {
+  private:
+    std::unique_ptr<OrthancStone::IOrthancConnection>  orthanc_;
+    bool                                               smooth_;
+
+  public:
+    explicit OrthancPyramidFrameFetcher(OrthancStone::IOrthancConnection* orthanc,
+                                        bool smooth) :
+      orthanc_(orthanc),
+      smooth_(smooth)
+    {
+      if (orthanc == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      }
+    }
+
+    DecodedTiledPyramid * Fetch(const std::string &instanceId,
+                                unsigned frameNumber) ORTHANC_OVERRIDE
+    {
+      std::string png;
+      orthanc_->RestApiGet(png, "/instances/" + instanceId + "/frames/" + boost::lexical_cast<std::string>(frameNumber) + "/preview");
+
+      std::unique_ptr<Orthanc::PngReader> reader(new Orthanc::PngReader());
+      reader->ReadFromMemory(png);
+
+      return new OnTheFlyPyramid(reader.release(), 512, 512, smooth_);
+    }
+  };
+}
+
+
 static bool DisplayPerformanceWarning()
 {
   (void) DisplayPerformanceWarning;   // Disable warning about unused function