changeset 318:8ad12abde290

sparse re-encoding with OpenSlide (notably for MIRAX format)
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 11 Sep 2024 16:11:16 +0200
parents f611fb47d0e8
children 9ce06c06c984
files Framework/Algorithms/PyramidReader.cpp Framework/Algorithms/PyramidReader.h Framework/Algorithms/ReconstructPyramidCommand.cpp Framework/Algorithms/ReconstructPyramidCommand.h Framework/Algorithms/TranscodeTileCommand.cpp Framework/ImageToolbox.cpp Framework/Inputs/CytomineImage.cpp Framework/Inputs/CytomineImage.h Framework/Inputs/DecodedTiledPyramid.cpp Framework/Inputs/DecodedTiledPyramid.h Framework/Inputs/ITiledPyramid.h Framework/Inputs/OpenSlidePyramid.cpp Framework/Inputs/OpenSlidePyramid.h Framework/Inputs/PyramidWithRawTiles.cpp Framework/Inputs/PyramidWithRawTiles.h Framework/Inputs/SingleLevelDecodedPyramid.cpp Framework/Inputs/SingleLevelDecodedPyramid.h Framework/Inputs/TiledPyramidStatistics.cpp Framework/Inputs/TiledPyramidStatistics.h Framework/Outputs/InMemoryTiledImage.cpp Framework/Outputs/InMemoryTiledImage.h NEWS ViewerPlugin/IIIF.cpp ViewerPlugin/Plugin.cpp ViewerPlugin/RawTile.cpp ViewerPlugin/RawTile.h
diffstat 26 files changed, 254 insertions(+), 71 deletions(-) [+]
line wrap: on
line diff
--- a/Framework/Algorithms/PyramidReader.cpp	Wed Sep 11 13:43:39 2024 +0200
+++ b/Framework/Algorithms/PyramidReader.cpp	Wed Sep 11 16:11:16 2024 +0200
@@ -43,6 +43,7 @@
     bool              hasRawTile_;
     std::string       rawTile_;
     ImageCompression  rawTileCompression_;
+    bool              isEmpty_;
 
     std::unique_ptr<Orthanc::ImageAccessor>  decoded_;
 
@@ -107,11 +108,13 @@
           that_.source_.ReadRawTile(rawTile_, rawTileCompression_, that_.level_, tileX, tileY))
       {
         hasRawTile_ = true;
+        isEmpty_ = false;
       }
       else
       {
         hasRawTile_ = false;
-        decoded_.reset(that_.source_.DecodeTile(that_.level_, tileX, tileY));
+
+        decoded_.reset(that_.source_.DecodeTile(isEmpty_, that_.level_, tileX, tileY));
         if (decoded_.get() == NULL)
         {
           throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
@@ -166,6 +169,11 @@
 
       return *decoded_;
     }
+
+    bool IsEmpty() const
+    {
+      return isEmpty_;
+    }
   };
 
 
@@ -290,6 +298,7 @@
 
 
   void PyramidReader::GetDecodedTile(Orthanc::ImageAccessor& target,
+                                     bool& isEmpty,
                                      unsigned int tileX,
                                      unsigned int tileY)
   {
@@ -298,6 +307,7 @@
     {
       // Accessing a tile out of the source image
       GetOutsideTile().GetReadOnlyAccessor(target);
+      isEmpty = true;
     }
     else
     {
@@ -319,7 +329,8 @@
       target.AssignReadOnly(tile.GetFormat(),
                             targetTileWidth_,
                             targetTileHeight_,
-                            tile.GetPitch(), bytes);                                    
+                            tile.GetPitch(), bytes);
+      isEmpty = source.IsEmpty();
     }
   }
 }
--- a/Framework/Algorithms/PyramidReader.h	Wed Sep 11 13:43:39 2024 +0200
+++ b/Framework/Algorithms/PyramidReader.h	Wed Sep 11 16:11:16 2024 +0200
@@ -93,6 +93,7 @@
                                   unsigned int tileY);
 
     void GetDecodedTile(Orthanc::ImageAccessor& target,
+                        bool& isEmpty,
                         unsigned int tileX,
                         unsigned int tileY);  
   };
--- a/Framework/Algorithms/ReconstructPyramidCommand.cpp	Wed Sep 11 13:43:39 2024 +0200
+++ b/Framework/Algorithms/ReconstructPyramidCommand.cpp	Wed Sep 11 16:11:16 2024 +0200
@@ -36,7 +36,8 @@
 
 namespace OrthancWSI
 {
-  Orthanc::ImageAccessor* ReconstructPyramidCommand::Explore(unsigned int level,
+  Orthanc::ImageAccessor* ReconstructPyramidCommand::Explore(bool& isEmpty,
+                                                             unsigned int level,
                                                              unsigned int offsetX,
                                                              unsigned int offsetY)
   {
@@ -56,20 +57,25 @@
     if (level == 0)
     {
       result.reset(new Orthanc::ImageAccessor);
-      source_.GetDecodedTile(*result, x, y);
+      source_.GetDecodedTile(*result, isEmpty, x, y);
 
-      ImageCompression compression;
-      const std::string* rawTile = source_.GetRawTile(compression, x, y);
-
-      if (rawTile != NULL)
+      if ((x == 0 && y == 0) ||  // Make sure to have at least 1 tile at each level
+          !isEmpty ||
+          level == upToLevel_)
       {
-        // Simple transcoding
-        target_.WriteRawTile(*rawTile, compression, level + shiftTargetLevel_, x, y);
-      }
-      else
-      {
-        // Re-encoding the file
-        target_.EncodeTile(*result, level + shiftTargetLevel_, x, y);
+        ImageCompression compression;
+        const std::string* rawTile = source_.GetRawTile(compression, x, y);
+
+        if (rawTile != NULL)
+        {
+          // Simple transcoding
+          target_.WriteRawTile(*rawTile, compression, level + shiftTargetLevel_, x, y);
+        }
+        else
+        {
+          // Re-encoding the file
+          target_.EncodeTile(*result, level + shiftTargetLevel_, x, y);
+        }
       }
     }
     else
@@ -82,35 +88,57 @@
                         source_.GetParameters().GetBackgroundColorGreen(),
                         source_.GetParameters().GetBackgroundColorBlue());
 
+      isEmpty = true;
+
       {
-        std::unique_ptr<Orthanc::ImageAccessor> subTile(Explore(level - 1, 2 * offsetX, 2 * offsetY));
+        bool tmpIsEmpty;
+        std::unique_ptr<Orthanc::ImageAccessor> subTile(Explore(tmpIsEmpty, level - 1, 2 * offsetX, 2 * offsetY));
         if (subTile.get() != NULL)
         {
           ImageToolbox::Embed(*mosaic, *subTile, 0, 0);
+          if (!tmpIsEmpty)
+          {
+            isEmpty = false;
+          }
         }
       }
 
       {
-        std::unique_ptr<Orthanc::ImageAccessor> subTile(Explore(level - 1, 2 * offsetX + 1, 2 * offsetY));
+        bool tmpIsEmpty;
+        std::unique_ptr<Orthanc::ImageAccessor> subTile(Explore(tmpIsEmpty, level - 1, 2 * offsetX + 1, 2 * offsetY));
         if (subTile.get() != NULL)
         {
           ImageToolbox::Embed(*mosaic, *subTile, target_.GetTileWidth(), 0);
+          if (!tmpIsEmpty)
+          {
+            isEmpty = false;
+          }
         }
       }
 
       {
-        std::unique_ptr<Orthanc::ImageAccessor> subTile(Explore(level - 1, 2 * offsetX, 2 * offsetY + 1));
+        bool tmpIsEmpty;
+        std::unique_ptr<Orthanc::ImageAccessor> subTile(Explore(tmpIsEmpty, level - 1, 2 * offsetX, 2 * offsetY + 1));
         if (subTile.get() != NULL)
         {
           ImageToolbox::Embed(*mosaic, *subTile, 0, target_.GetTileHeight());
+          if (!tmpIsEmpty)
+          {
+            isEmpty = false;
+          }
         }
       }
 
       {
-        std::unique_ptr<Orthanc::ImageAccessor> subTile(Explore(level - 1, 2 * offsetX + 1, 2 * offsetY + 1));
+        bool tmpIsEmpty;
+        std::unique_ptr<Orthanc::ImageAccessor> subTile(Explore(tmpIsEmpty, level - 1, 2 * offsetX + 1, 2 * offsetY + 1));
         if (subTile.get() != NULL)
         {
           ImageToolbox::Embed(*mosaic, *subTile, target_.GetTileWidth(), target_.GetTileHeight());
+          if (!tmpIsEmpty)
+          {
+            isEmpty = false;
+          }
         }
       }
 
@@ -121,7 +149,12 @@
 
       result.reset(Orthanc::ImageProcessing::Halve(*mosaic, false /* don't force minimal pitch */));
 
-      target_.EncodeTile(*result, level + shiftTargetLevel_, x, y);
+      if ((x == 0 && y == 0) ||  // Make sure to have at least 1 tile at each level
+          !isEmpty ||
+          level == upToLevel_)
+      {
+        target_.EncodeTile(*result, level + shiftTargetLevel_, x, y);
+      }
     }
 
     return result.release();
@@ -157,7 +190,8 @@
 
   bool ReconstructPyramidCommand::Execute()
   {
-    std::unique_ptr<Orthanc::ImageAccessor> root(Explore(upToLevel_, 0, 0));
+    bool isEmpty;  // Unused
+    std::unique_ptr<Orthanc::ImageAccessor> root(Explore(isEmpty, upToLevel_, 0, 0));
     return true;
   }
 
--- a/Framework/Algorithms/ReconstructPyramidCommand.h	Wed Sep 11 13:43:39 2024 +0200
+++ b/Framework/Algorithms/ReconstructPyramidCommand.h	Wed Sep 11 16:11:16 2024 +0200
@@ -41,7 +41,8 @@
     unsigned int y_;
     unsigned int shiftTargetLevel_;
 
-    Orthanc::ImageAccessor* Explore(unsigned int level,
+    Orthanc::ImageAccessor* Explore(bool& isEmpty,
+                                    unsigned int level,
                                     unsigned int offsetX,
                                     unsigned int offsetY);
 
--- a/Framework/Algorithms/TranscodeTileCommand.cpp	Wed Sep 11 13:43:39 2024 +0200
+++ b/Framework/Algorithms/TranscodeTileCommand.cpp	Wed Sep 11 16:11:16 2024 +0200
@@ -75,11 +75,19 @@
         }
         else
         {
+          bool isEmpty;
           Orthanc::ImageAccessor tile;
-          source_.GetDecodedTile(tile, x, y);
+          source_.GetDecodedTile(tile, isEmpty, x, y);
 
-          // Re-encoding the file
-          target_.EncodeTile(tile, level_, x, y);
+          if (!isEmpty)
+          {
+            // Re-encoding the file
+            target_.EncodeTile(tile, level_, x, y);
+          }
+          else
+          {
+            printf("ICI\n");
+          }
         }
       }
     }
--- a/Framework/ImageToolbox.cpp	Wed Sep 11 13:43:39 2024 +0200
+++ b/Framework/ImageToolbox.cpp	Wed Sep 11 16:11:16 2024 +0200
@@ -240,8 +240,9 @@
       {
         for (unsigned int x = 0; x < width; x += pyramid.GetTileWidth(level))
         {
+          bool isEmpty;  // Unused in this case
           std::unique_ptr<Orthanc::ImageAccessor> tile(
-            pyramid.DecodeTile(level,
+            pyramid.DecodeTile(isEmpty, level,
                                x / pyramid.GetTileWidth(level),
                                y / pyramid.GetTileHeight(level)));
           Embed(*result, *tile, x, y);
--- a/Framework/Inputs/CytomineImage.cpp	Wed Sep 11 13:43:39 2024 +0200
+++ b/Framework/Inputs/CytomineImage.cpp	Wed Sep 11 16:11:16 2024 +0200
@@ -156,10 +156,13 @@
 
 
   void CytomineImage::ReadRegion(Orthanc::ImageAccessor& target,
+                                 bool& isEmpty,
                                  unsigned int level,
                                  unsigned int x,
                                  unsigned int y)
   {
+    isEmpty = false;
+
     if (level != 0 ||
         x >= fullWidth_ ||
         y >= fullHeight_)
--- a/Framework/Inputs/CytomineImage.h	Wed Sep 11 13:43:39 2024 +0200
+++ b/Framework/Inputs/CytomineImage.h	Wed Sep 11 16:11:16 2024 +0200
@@ -48,6 +48,7 @@
 
   protected:
     virtual void ReadRegion(Orthanc::ImageAccessor& target,
+                            bool& isEmpty,
                             unsigned int level,
                             unsigned int x,
                             unsigned int y) ORTHANC_OVERRIDE;
--- a/Framework/Inputs/DecodedTiledPyramid.cpp	Wed Sep 11 13:43:39 2024 +0200
+++ b/Framework/Inputs/DecodedTiledPyramid.cpp	Wed Sep 11 16:11:16 2024 +0200
@@ -59,7 +59,8 @@
   }
 
 
-  Orthanc::ImageAccessor* DecodedTiledPyramid::DecodeTile(unsigned int level,
+  Orthanc::ImageAccessor* DecodedTiledPyramid::DecodeTile(bool& isEmpty,
+                                                          unsigned int level,
                                                           unsigned int tileX,
                                                           unsigned int tileY)
   {
@@ -72,6 +73,7 @@
     if (x >= GetLevelWidth(level) ||
         y >= GetLevelHeight(level))   // (*)
     {
+      isEmpty = true;
       ImageToolbox::Set(*tile, backgroundColor_[0], backgroundColor_[1], backgroundColor_[2]);
       return tile.release();
     }
@@ -104,14 +106,14 @@
     if (fit)
     {
       // The tile entirely lies inside the image
-      ReadRegion(*tile, level, x, y);
+      ReadRegion(*tile, isEmpty, level, x, y);
     }
     else
     {
       // The tile exceeds the size of image, decode it to a temporary buffer
       std::unique_ptr<Orthanc::ImageAccessor> cropped
         (ImageToolbox::Allocate(GetPixelFormat(), regionWidth, regionHeight));
-      ReadRegion(*cropped, level, x, y);
+      ReadRegion(*cropped, isEmpty, level, x, y);
 
       // Create a white tile, and fill it with the cropped content
       ImageToolbox::Set(*tile, backgroundColor_[0], backgroundColor_[1], backgroundColor_[2]);
--- a/Framework/Inputs/DecodedTiledPyramid.h	Wed Sep 11 13:43:39 2024 +0200
+++ b/Framework/Inputs/DecodedTiledPyramid.h	Wed Sep 11 16:11:16 2024 +0200
@@ -43,6 +43,7 @@
     // the image, and that target has the proper size to store the
     // region. Pay attention to implement mutual exclusion in subclasses.
     virtual void ReadRegion(Orthanc::ImageAccessor& target,
+                            bool& isEmpty,
                             unsigned int level,
                             unsigned int x,
                             unsigned int y) = 0;
@@ -58,7 +59,8 @@
                             uint8_t& green,
                             uint8_t& blue) const;
 
-    virtual Orthanc::ImageAccessor* DecodeTile(unsigned int level,
+    virtual Orthanc::ImageAccessor* DecodeTile(bool& isEmpty,
+                                               unsigned int level,
                                                unsigned int tileX,
                                                unsigned int tileY) ORTHANC_OVERRIDE;
 
--- a/Framework/Inputs/ITiledPyramid.h	Wed Sep 11 13:43:39 2024 +0200
+++ b/Framework/Inputs/ITiledPyramid.h	Wed Sep 11 16:11:16 2024 +0200
@@ -61,7 +61,8 @@
                              unsigned int tileX,
                              unsigned int tileY) = 0;
 
-    virtual Orthanc::ImageAccessor* DecodeTile(unsigned int level,
+    virtual Orthanc::ImageAccessor* DecodeTile(bool& isEmpty,
+                                               unsigned int level,
                                                unsigned int tileX,
                                                unsigned int tileY) = 0;
 
--- a/Framework/Inputs/OpenSlidePyramid.cpp	Wed Sep 11 13:43:39 2024 +0200
+++ b/Framework/Inputs/OpenSlidePyramid.cpp	Wed Sep 11 16:11:16 2024 +0200
@@ -36,6 +36,7 @@
 namespace OrthancWSI
 {
   void OpenSlidePyramid::ReadRegion(Orthanc::ImageAccessor& target,
+                                    bool& isEmpty,
                                     unsigned int level,
                                     unsigned int x,
                                     unsigned int y)
@@ -51,6 +52,29 @@
     const unsigned int width = source->GetWidth();
     const unsigned int height = source->GetHeight();
 
+    if (source->GetFormat() == Orthanc::PixelFormat_BGRA32)
+    {
+      isEmpty = true;
+
+      for (unsigned int y = 0; y < height && isEmpty; y++)
+      {
+        const uint8_t* p = reinterpret_cast<const uint8_t*>(source->GetConstRow(y));
+        for (unsigned int x = 0; x < width && isEmpty; x++)
+        {
+          if (p[3] != 0)
+          {
+            isEmpty = false;
+          }
+
+          p += 4;
+        }
+      }
+    }
+    else
+    {
+      isEmpty = false;
+    }
+
     if (target.GetFormat() == Orthanc::PixelFormat_RGB24 &&
         source->GetFormat() == Orthanc::PixelFormat_BGRA32)
     {
--- a/Framework/Inputs/OpenSlidePyramid.h	Wed Sep 11 13:43:39 2024 +0200
+++ b/Framework/Inputs/OpenSlidePyramid.h	Wed Sep 11 16:11:16 2024 +0200
@@ -37,6 +37,7 @@
 
   protected:
     virtual void ReadRegion(Orthanc::ImageAccessor& target,
+                            bool& isEmpty,
                             unsigned int level,
                             unsigned int x,
                             unsigned int y) ORTHANC_OVERRIDE;
--- a/Framework/Inputs/PyramidWithRawTiles.cpp	Wed Sep 11 13:43:39 2024 +0200
+++ b/Framework/Inputs/PyramidWithRawTiles.cpp	Wed Sep 11 16:11:16 2024 +0200
@@ -28,10 +28,13 @@
 
 namespace OrthancWSI
 {
-  Orthanc::ImageAccessor* PyramidWithRawTiles::DecodeTile(unsigned int level,
+  Orthanc::ImageAccessor* PyramidWithRawTiles::DecodeTile(bool& isEmpty,
+                                                          unsigned int level,
                                                           unsigned int tileX,
                                                           unsigned int tileY)
   {
+    isEmpty = false;
+
     std::string tile;
     ImageCompression compression;
 
--- a/Framework/Inputs/PyramidWithRawTiles.h	Wed Sep 11 13:43:39 2024 +0200
+++ b/Framework/Inputs/PyramidWithRawTiles.h	Wed Sep 11 16:11:16 2024 +0200
@@ -30,7 +30,8 @@
   class PyramidWithRawTiles : public ITiledPyramid
   {
   public:
-    virtual Orthanc::ImageAccessor* DecodeTile(unsigned int level,
+    virtual Orthanc::ImageAccessor* DecodeTile(bool& isEmpty,
+                                               unsigned int level,
                                                unsigned int tileX,
                                                unsigned int tileY) ORTHANC_OVERRIDE;
   };
--- a/Framework/Inputs/SingleLevelDecodedPyramid.cpp	Wed Sep 11 13:43:39 2024 +0200
+++ b/Framework/Inputs/SingleLevelDecodedPyramid.cpp	Wed Sep 11 16:11:16 2024 +0200
@@ -31,10 +31,13 @@
 namespace OrthancWSI
 {
   void SingleLevelDecodedPyramid::ReadRegion(Orthanc::ImageAccessor& target,
+                                             bool& isEmpty,
                                              unsigned int level,
                                              unsigned int x,
                                              unsigned int y)
   {
+    isEmpty = false;
+
     Orthanc::ImageAccessor region;
     image_.GetRegion(region, x, y, target.GetWidth(), target.GetHeight());
     Orthanc::ImageProcessing::Copy(target, region);
--- a/Framework/Inputs/SingleLevelDecodedPyramid.h	Wed Sep 11 13:43:39 2024 +0200
+++ b/Framework/Inputs/SingleLevelDecodedPyramid.h	Wed Sep 11 16:11:16 2024 +0200
@@ -41,6 +41,7 @@
     }
 
     virtual void ReadRegion(Orthanc::ImageAccessor& target,
+                            bool& isEmpty,
                             unsigned int level,
                             unsigned int x,
                             unsigned int y) ORTHANC_OVERRIDE;
--- a/Framework/Inputs/TiledPyramidStatistics.cpp	Wed Sep 11 13:43:39 2024 +0200
+++ b/Framework/Inputs/TiledPyramidStatistics.cpp	Wed Sep 11 16:11:16 2024 +0200
@@ -64,7 +64,8 @@
   }
 
 
-  Orthanc::ImageAccessor* TiledPyramidStatistics::DecodeTile(unsigned int level,
+  Orthanc::ImageAccessor* TiledPyramidStatistics::DecodeTile(bool& isEmpty,
+                                                             unsigned int level,
                                                              unsigned int tileX,
                                                              unsigned int tileY)
   {
@@ -73,6 +74,6 @@
       countDecodedTiles_++;
     }
 
-    return source_.DecodeTile(level, tileX, tileY);
+    return source_.DecodeTile(isEmpty, level, tileX, tileY);
   }
 }
--- a/Framework/Inputs/TiledPyramidStatistics.h	Wed Sep 11 13:43:39 2024 +0200
+++ b/Framework/Inputs/TiledPyramidStatistics.h	Wed Sep 11 16:11:16 2024 +0200
@@ -78,7 +78,8 @@
                              unsigned int tileX,
                              unsigned int tileY) ORTHANC_OVERRIDE;
 
-    virtual Orthanc::ImageAccessor* DecodeTile(unsigned int level,
+    virtual Orthanc::ImageAccessor* DecodeTile(bool& isEmpty,
+                                               unsigned int level,
                                                unsigned int tileX,
                                                unsigned int tileY) ORTHANC_OVERRIDE;
 
--- a/Framework/Outputs/InMemoryTiledImage.cpp	Wed Sep 11 13:43:39 2024 +0200
+++ b/Framework/Outputs/InMemoryTiledImage.cpp	Wed Sep 11 16:11:16 2024 +0200
@@ -107,10 +107,13 @@
   }
 
 
-  Orthanc::ImageAccessor* InMemoryTiledImage::DecodeTile(unsigned int level,
+  Orthanc::ImageAccessor* InMemoryTiledImage::DecodeTile(bool& isEmpty,
+                                                         unsigned int level,
                                                          unsigned int tileX,
                                                          unsigned int tileY)
   {
+    isEmpty = false;
+
     CheckLevel(level);
 
     if (tileX >= countTilesX_ ||
--- a/Framework/Outputs/InMemoryTiledImage.h	Wed Sep 11 13:43:39 2024 +0200
+++ b/Framework/Outputs/InMemoryTiledImage.h	Wed Sep 11 16:11:16 2024 +0200
@@ -99,7 +99,8 @@
                              unsigned int tileX,
                              unsigned int tileY) ORTHANC_OVERRIDE;
 
-    virtual Orthanc::ImageAccessor* DecodeTile(unsigned int level,
+    virtual Orthanc::ImageAccessor* DecodeTile(bool& isEmpty,
+                                               unsigned int level,
                                                unsigned int tileX,
                                                unsigned int tileY) ORTHANC_OVERRIDE;
 
--- a/NEWS	Wed Sep 11 13:43:39 2024 +0200
+++ b/NEWS	Wed Sep 11 16:11:16 2024 +0200
@@ -1,7 +1,7 @@
 Pending changes in the mainline
 ===============================
 
-* Support of transparency in OpenSlide
+* Support of sparse encoding of tiles in OpenSlide (notably for MIRAX format)
 * OrthancWSIDicomizer supports plain TIFF, besides hierarchical TIFF
 * New option: "tiff-alignment" to control deep zoom of plain TIFF over IIIF
 * Force version of Mirador to 3.3.0
--- a/ViewerPlugin/IIIF.cpp	Wed Sep 11 13:43:39 2024 +0200
+++ b/ViewerPlugin/IIIF.cpp	Wed Sep 11 16:11:16 2024 +0200
@@ -231,7 +231,9 @@
       for (unsigned int tx = 0; tx < nx; tx++)
       {
         const unsigned int x = tx * pyramid.GetTileWidth(level);
-        std::unique_ptr<Orthanc::ImageAccessor> tile(pyramid.DecodeTile(level, tx, ty));
+
+        bool isEmpty;  // Unused
+        std::unique_ptr<Orthanc::ImageAccessor> tile(pyramid.DecodeTile(isEmpty, level, tx, ty));
 
         const unsigned int width = std::min(pyramid.GetTileWidth(level), full.GetWidth() - x);
 
@@ -328,8 +330,12 @@
                                               regionX / GetPhysicalTileWidth(pyramid, level),
                                               regionY / GetPhysicalTileHeight(pyramid, level)));
 
-        if (static_cast<unsigned int>(cropWidth) < pyramid.GetTileWidth(level) ||
-            static_cast<unsigned int>(cropHeight) < pyramid.GetTileHeight(level))
+        assert(rawTile->GetTileWidth() == pyramid.GetTileWidth(level));
+        assert(rawTile->GetTileHeight() == pyramid.GetTileHeight(level));
+
+        if (!rawTile->IsEmpty() &&
+            (static_cast<unsigned int>(cropWidth) < pyramid.GetTileWidth(level) ||
+             static_cast<unsigned int>(cropHeight) < pyramid.GetTileHeight(level)))
         {
           toCrop.reset(rawTile->Decode());
           rawTile.reset(NULL);
@@ -341,8 +347,23 @@
     {
       assert(toCrop.get() == NULL);
 
-      // Level 0 Compliance of IIIF expects JPEG files
-      rawTile->Answer(output, Orthanc::MimeType_Jpeg);
+      if (rawTile->IsEmpty())
+      {
+        if (static_cast<unsigned int>(cropWidth) < rawTile->GetTileWidth() ||
+            static_cast<unsigned int>(cropHeight) < rawTile->GetTileHeight())
+        {
+          OrthancWSI::RawTile::AnswerBackgroundTile(output, static_cast<unsigned int>(cropWidth), static_cast<unsigned int>(cropHeight));
+        }
+        else
+        {
+          OrthancWSI::RawTile::AnswerBackgroundTile(output, rawTile->GetTileWidth(), rawTile->GetTileHeight());
+        }
+      }
+      else
+      {
+        // Level 0 Compliance of IIIF expects JPEG files
+        rawTile->Answer(output, Orthanc::MimeType_Jpeg);
+      }
     }
     else if (toCrop.get() != NULL)
     {
--- a/ViewerPlugin/Plugin.cpp	Wed Sep 11 13:43:39 2024 +0200
+++ b/ViewerPlugin/Plugin.cpp	Wed Sep 11 16:11:16 2024 +0200
@@ -43,26 +43,6 @@
 #define ORTHANC_PLUGIN_NAME "wsi"
 
 
-static void AnswerSparseTile(OrthancPluginRestOutput* output,
-                             unsigned int tileWidth,
-                             unsigned int tileHeight)
-{
-  Orthanc::Image tile(Orthanc::PixelFormat_RGB24, tileWidth, tileHeight, false);
-
-  // Black (TODO parameter)
-  uint8_t red = 0;
-  uint8_t green = 0;
-  uint8_t blue = 0;
-  Orthanc::ImageProcessing::Set(tile, red, green, blue, 255);
-
-  // TODO Cache the tile
-  OrthancPluginCompressAndAnswerPngImage(OrthancPlugins::GetGlobalContext(),
-                                         output, OrthancPluginPixelFormat_RGB24, 
-                                         tile.GetWidth(), tile.GetHeight(), 
-                                         tile.GetPitch(), tile.GetBuffer());
-}
-
-
 static bool DisplayPerformanceWarning()
 {
   (void) DisplayPerformanceWarning;   // Disable warning about unused function
@@ -165,6 +145,12 @@
                                           static_cast<unsigned int>(tileY)));
   }
 
+  if (rawTile->IsEmpty())
+  {
+    OrthancWSI::RawTile::AnswerBackgroundTile(output, rawTile->GetTileWidth(), rawTile->GetTileHeight());
+    return;
+  }
+
   Orthanc::MimeType mime;
 
   if (rawTile->GetCompression() == OrthancWSI::ImageCompression_Jpeg)
--- a/ViewerPlugin/RawTile.cpp	Wed Sep 11 13:43:39 2024 +0200
+++ b/ViewerPlugin/RawTile.cpp	Wed Sep 11 16:11:16 2024 +0200
@@ -29,8 +29,8 @@
 #include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h"
 
 #include <Compatibility.h>  // For std::unique_ptr
+#include <Images/ImageProcessing.h>
 #include <Images/JpegReader.h>
-#include <Images/JpegWriter.h>
 #include <Images/PngWriter.h>
 #include <MultiThreading/Semaphore.h>
 #include <OrthancException.h>
@@ -119,11 +119,19 @@
     tileHeight_(pyramid.GetTileHeight(level)),
     photometric_(pyramid.GetPhotometricInterpretation())
   {
-    if (!pyramid.ReadRawTile(tile_, compression_, level, tileX, tileY))
+    isEmpty_ = !pyramid.ReadRawTile(tile_, compression_, level, tileX, tileY);
+  }
+
+
+  ImageCompression RawTile::GetCompression() const
+  {
+    if (isEmpty_)
     {
-      // Handling of missing tile (for sparse tiling): TODO parameter?
-      // AnswerSparseTile(output, tileWidth, tileHeight); return;
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return compression_;
     }
   }
 
@@ -131,6 +139,11 @@
   void RawTile::Answer(OrthancPluginRestOutput* output,
                        Orthanc::MimeType encoding)
   {
+    if (isEmpty_)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+
     if ((compression_ == ImageCompression_Jpeg && encoding == Orthanc::MimeType_Jpeg) ||
         (compression_ == ImageCompression_Jpeg2000 && encoding == Orthanc::MimeType_Jpeg2000))
     {
@@ -158,6 +171,11 @@
 
   Orthanc::ImageAccessor* RawTile::Decode()
   {
+    if (isEmpty_)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+
     Orthanc::Semaphore::Locker locker(*transcoderSemaphore_);
     return DecodeInternal();
   }
@@ -182,4 +200,41 @@
   {
     transcoderSemaphore_.reset(NULL);
   }
+
+
+  void RawTile::AnswerBackgroundTile(OrthancPluginRestOutput* output,
+                                     unsigned int tileWidth,
+                                     unsigned int tileHeight)
+  {
+    std::string answer;
+
+    {
+      static boost::mutex mutex;
+      static std::string cachedTile;
+      static unsigned int cachedWidth = 0;
+      static unsigned int cachedHeight = 0;
+
+      boost::mutex::scoped_lock lock(mutex);
+
+      if (cachedTile.empty() ||
+          cachedWidth != tileWidth ||
+          cachedHeight != tileHeight)
+      {
+        Orthanc::Image tile(Orthanc::PixelFormat_RGBA32, tileWidth, tileHeight, false);
+
+        Orthanc::ImageProcessing::Set(tile, 255 /* red */, 255 /* green */,
+                                      255 /* blue */, 0 /* alpha - fully transparent */);
+
+        Orthanc::PngWriter writer;
+        Orthanc::IImageWriter::WriteToMemory(writer, cachedTile, tile);
+        cachedWidth = tileWidth;
+        cachedHeight = tileHeight;
+      }
+
+      answer = cachedTile;  // Make a private copy to avoid mutex on "OrthancPluginAnswerBuffer()"
+    }
+
+    OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, answer.c_str(),
+                              answer.size(), Orthanc::EnumerationToString(Orthanc::MimeType_Png));
+  }
 }
--- a/ViewerPlugin/RawTile.h	Wed Sep 11 13:43:39 2024 +0200
+++ b/ViewerPlugin/RawTile.h	Wed Sep 11 16:11:16 2024 +0200
@@ -34,6 +34,7 @@
   class RawTile : public boost::noncopyable
   {
   private:
+    bool                               isEmpty_;
     Orthanc::PixelFormat               format_;
     unsigned int                       tileWidth_;
     unsigned int                       tileHeight_;
@@ -53,11 +54,23 @@
             unsigned int tileX,
             unsigned int tileY);
 
-    ImageCompression GetCompression() const
+    bool IsEmpty() const
+    {
+      return isEmpty_;
+    }
+
+    unsigned int GetTileWidth() const
     {
-      return compression_;
+      return tileWidth_;
     }
 
+    unsigned int GetTileHeight() const
+    {
+      return tileHeight_;
+    }
+
+    ImageCompression GetCompression() const;
+
     void Answer(OrthancPluginRestOutput* output,
                 Orthanc::MimeType encoding);
 
@@ -72,5 +85,9 @@
     static void InitializeTranscoderSemaphore(unsigned int maxThreads);
 
     static void FinalizeTranscoderSemaphore();
+
+    static void AnswerBackgroundTile(OrthancPluginRestOutput* output,
+                                     unsigned int tileWidth,
+                                     unsigned int tileHeight);
   };
 }