changeset 57:91fc9583b2de

big refactoring to support sparse tiling
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 24 Nov 2016 17:48:24 +0100
parents 83cd735c885d
children 35468714a38e
files Applications/CMakeLists.txt Applications/DicomToTiff.cpp Applications/Dicomizer.cpp Framework/Algorithms/PyramidReader.cpp Framework/Algorithms/PyramidReader.h Framework/Algorithms/ReconstructPyramidCommand.cpp Framework/Algorithms/TranscodeTileCommand.cpp Framework/Inputs/DecodedTiledPyramid.h Framework/Inputs/DicomPyramid.cpp Framework/Inputs/DicomPyramid.h Framework/Inputs/DicomPyramidLevel.cpp Framework/Inputs/DicomPyramidLevel.h Framework/Inputs/HierarchicalTiff.cpp Framework/Inputs/HierarchicalTiff.h Framework/Inputs/ITiledPyramid.h Framework/Inputs/PyramidWithRawTiles.cpp Framework/Inputs/TiledPyramidStatistics.cpp Framework/Inputs/TiledPyramidStatistics.h Framework/Outputs/InMemoryTiledImage.cpp Framework/Outputs/InMemoryTiledImage.h TODO ViewerPlugin/Plugin.cpp
diffstat 22 files changed, 201 insertions(+), 148 deletions(-) [+]
line wrap: on
line diff
--- a/Applications/CMakeLists.txt	Thu Nov 24 15:41:21 2016 +0100
+++ b/Applications/CMakeLists.txt	Thu Nov 24 17:48:24 2016 +0100
@@ -9,6 +9,7 @@
 # Generic parameters
 SET(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)")
 SET(ALLOW_DOWNLOADS OFF CACHE BOOL "Allow CMake to download packages")
+SET(ENABLE_PROFILING OFF CACHE BOOL "Whether to enable the generation of profiling information with gprof")
 
 # Optional components
 SET(ENABLE_SSL OFF CACHE BOOL "Include support for SSL")
@@ -314,3 +315,23 @@
 else()
   message("Doxygen not found. The documentation will not be built.")
 endif()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+add_executable(Hello
+  Hello.cpp
+  ApplicationToolbox.cpp
+  )
+
+target_link_libraries(Hello OrthancWSIFramework ${DCMTK_LIBRARIES})
--- a/Applications/DicomToTiff.cpp	Thu Nov 24 15:41:21 2016 +0100
+++ b/Applications/DicomToTiff.cpp	Thu Nov 24 17:48:24 2016 +0100
@@ -21,6 +21,7 @@
 #include "../Framework/DicomToolbox.h"
 #include "../Framework/ImageToolbox.h"
 #include "../Framework/Inputs/DicomPyramid.h"
+#include "../Framework/Inputs/TiledPyramidStatistics.h"
 #include "../Framework/Messaging/CurlOrthancConnection.h"
 #include "../Framework/Orthanc/Core/Logging.h"
 #include "../Framework/Orthanc/Core/OrthancException.h"
@@ -114,7 +115,8 @@
               << std::endl
               << "Orthanc, lightweight, RESTful DICOM server for healthcare and medical research."
               << std::endl << std::endl
-              << "Convert a DICOM for digital pathology stored in some Orthanc server as a standard hierarchical TIFF."
+              << "Convert a DICOM image for digital pathology stored in some Orthanc server as a" << std::endl
+              << "standard hierarchical TIFF (whose tiles are all encoded using JPEG)."
               << std::endl;
 
     std::cout << allWithoutHidden << "\n";
@@ -167,56 +169,8 @@
 
 
 
-static void RunTranscode(OrthancWSI::ITiledPyramid& source,
-                         const boost::program_options::variables_map& options)
-{
-  OrthancWSI::HierarchicalTiffWriter target(options["output"].as<std::string>(),
-                                            source.GetPixelFormat(), 
-                                            source.GetImageCompression(), 
-                                            source.GetTileWidth(), 
-                                            source.GetTileHeight());
-
-  std::auto_ptr<Orthanc::ImageAccessor> empty(CreateEmptyTile(target, options));
-
-  for (unsigned int level = 0; level < source.GetLevelCount(); level++)
-  {
-    LOG(WARNING) << "Creating level " << level << " of size " 
-                 << source.GetLevelWidth(level) << "x" << source.GetLevelHeight(level);
-    target.AddLevel(source.GetLevelWidth(level), source.GetLevelHeight(level));
-  }
-
-  for (unsigned int level = 0; level < source.GetLevelCount(); level++)
-  {
-    LOG(WARNING) << "Transcoding level " << level;
-
-    unsigned int countX = OrthancWSI::CeilingDivision(source.GetLevelWidth(level), source.GetTileWidth());
-    unsigned int countY = OrthancWSI::CeilingDivision(source.GetLevelHeight(level), source.GetTileHeight());
-
-    for (unsigned int tileY = 0; tileY < countY; tileY++)
-    {
-      for (unsigned int tileX = 0; tileX < countX; tileX++)
-      {
-        LOG(INFO) << "Dealing with tile (" << tileX << "," << tileY << ") at level " << level;
-        std::string tile;
-
-        if (source.ReadRawTile(tile, level, tileX, tileY))
-        {
-          target.WriteRawTile(tile, source.GetImageCompression(), level, tileX, tileY);
-        }
-        else
-        {
-          target.EncodeTile(*empty, level, tileX, tileY);
-        }
-      }        
-    }
-
-    target.Flush();
-  }
-}
-
-
-static void RunReencode(OrthancWSI::ITiledPyramid& source,
-                        const boost::program_options::variables_map& options)
+static void Run(OrthancWSI::ITiledPyramid& source,
+                const boost::program_options::variables_map& options)
 {
   OrthancWSI::HierarchicalTiffWriter target(options["output"].as<std::string>(),
                                             source.GetPixelFormat(), 
@@ -224,6 +178,9 @@
                                             source.GetTileWidth(), 
                                             source.GetTileHeight());
 
+  bool reencode = (options.count("reencode") &&
+                   options["reencode"].as<bool>());
+
   if (options.count("jpeg-quality"))
   {
     target.SetJpegQuality(options["jpeg-quality"].as<int>());
@@ -240,7 +197,7 @@
 
   for (unsigned int level = 0; level < source.GetLevelCount(); level++)
   {
-    LOG(WARNING) << "Reencoding level " << level;
+    LOG(WARNING) << std::string(reencode ? "Reencoding" : "Transcoding") << " level " << level;
 
     unsigned int countX = OrthancWSI::CeilingDivision(source.GetLevelWidth(level), source.GetTileWidth());
     unsigned int countY = OrthancWSI::CeilingDivision(source.GetLevelHeight(level), source.GetTileHeight());
@@ -251,14 +208,58 @@
       {
         LOG(INFO) << "Dealing with tile (" << tileX << "," << tileY << ") at level " << level;
 
-        std::auto_ptr<Orthanc::ImageAccessor> tile(source.DecodeTile(level, tileX, tileY));
-        if (tile.get() == NULL)
+        bool missing = false;
+        bool success = true;
+
+        // Give a first try to get the raw tile
+        std::string tile;
+        OrthancWSI::ImageCompression compression;
+        if (source.ReadRawTile(tile, compression, level, tileX, tileY))
         {
-          target.EncodeTile(*empty, level, tileX, tileY);
+          if (reencode ||
+              compression == OrthancWSI::ImageCompression_Jpeg)
+          {
+            target.WriteRawTile(tile, compression, level, tileX, tileY);
+          }
+          else
+          {
+            success = false;  // Re-encoding is mandatory
+          }
         }
         else
         {
-          target.EncodeTile(*tile, level, tileX, tileY);
+          // Give a second try to get the decoded tile
+          compression = OrthancWSI::ImageCompression_Unknown;
+
+          std::auto_ptr<Orthanc::ImageAccessor> tile(source.DecodeTile(level, tileX, tileY));
+          if (tile.get() == NULL)
+          {
+            // Unable to read the raw tile or to decode it: The tile is missing (sparse tiling)
+            missing = true;
+          }
+          else if (reencode)
+          {
+            target.EncodeTile(*empty, level, tileX, tileY);
+          }
+          else
+          {
+            success = false;  // Re-encoding is mandatory
+          }
+        }
+
+        if (!success)
+        {
+          LOG(WARNING) << "Cannot transcode a DICOM image that is not encoded using JPEG (it is " 
+                       << OrthancWSI::EnumerationToString(compression) 
+                       << "), please use the --reencode=1 option";
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+        }
+        
+        if (missing)
+        {
+          LOG(WARNING) << "Sparse tiling: Using an empty image for missing tile ("
+                       << tileX << "," << tileY << ") at level " << level;
+          target.EncodeTile(*empty, level, tileX, tileY);
         }
       }        
     }
@@ -300,15 +301,8 @@
       OrthancWSI::CurlOrthancConnection orthanc(params);
       OrthancWSI::DicomPyramid source(orthanc, options["input"].as<std::string>());
 
-      if (options.count("reencode") &&
-          options["reencode"].as<bool>())
-      {
-        RunReencode(source, options);
-      }
-      else
-      {
-        RunTranscode(source, options);
-      }
+      OrthancWSI::TiledPyramidStatistics stats(source);
+      Run(stats, options);
     }
   }
   catch (Orthanc::OrthancException& e)
--- a/Applications/Dicomizer.cpp	Thu Nov 24 15:41:21 2016 +0100
+++ b/Applications/Dicomizer.cpp	Thu Nov 24 17:48:24 2016 +0100
@@ -133,7 +133,6 @@
   OrthancWSI::TiledPyramidStatistics stats(source);
 
   LOG(WARNING) << "Size of source tiles: " << stats.GetTileWidth() << "x" << stats.GetTileHeight();
-  LOG(WARNING) << "Source image compression: " << OrthancWSI::EnumerationToString(stats.GetImageCompression());
   LOG(WARNING) << "Pixel format: " << Orthanc::EnumerationToString(stats.GetPixelFormat());
   LOG(WARNING) << "Smoothing is " << (parameters.IsSmoothEnabled() ? "enabled" : "disabled");
 
@@ -338,12 +337,13 @@
 
 static void EnrichDataset(DcmDataset& dataset,
                           const OrthancWSI::ITiledPyramid& source,
+                          OrthancWSI::ImageCompression sourceCompression,
                           const OrthancWSI::DicomizerParameters& parameters,
                           const OrthancWSI::ImagedVolumeParameters& volume)
 {
   Orthanc::Encoding encoding = Orthanc::FromDcmtkBridge::DetectEncoding(dataset, Orthanc::Encoding_Latin1);
 
-  if (source.GetImageCompression() == OrthancWSI::ImageCompression_Jpeg ||
+  if (sourceCompression == OrthancWSI::ImageCompression_Jpeg ||
       parameters.GetTargetCompression() == OrthancWSI::ImageCompression_Jpeg)
   {
     // Takes as estimation a 1:10 compression ratio
@@ -796,7 +796,8 @@
 }
 
 
-OrthancWSI::ITiledPyramid* OpenInputPyramid(const std::string& path,
+OrthancWSI::ITiledPyramid* OpenInputPyramid(OrthancWSI::ImageCompression& sourceCompression,
+                                            const std::string& path,
                                             const OrthancWSI::DicomizerParameters& parameters)
 {
   LOG(WARNING) << "The input image is: " << path;
@@ -807,20 +808,28 @@
   switch (format)
   {
     case OrthancWSI::ImageCompression_Png:
+    {
+      sourceCompression = OrthancWSI::ImageCompression_Unknown;
       return new OrthancWSI::TiledPngImage(path, 
                                            parameters.GetTargetTileWidth(512), 
                                            parameters.GetTargetTileHeight(512));
+    }
 
     case OrthancWSI::ImageCompression_Jpeg:
+    {
+      sourceCompression = OrthancWSI::ImageCompression_Unknown;
       return new OrthancWSI::TiledJpegImage(path, 
                                             parameters.GetTargetTileWidth(512), 
                                             parameters.GetTargetTileHeight(512));
+    }
 
     case OrthancWSI::ImageCompression_Tiff:
     {
       try
       {
-        return new OrthancWSI::HierarchicalTiff(path);
+        std::auto_ptr<OrthancWSI::HierarchicalTiff> tiff(new OrthancWSI::HierarchicalTiff(path));
+        sourceCompression = tiff->GetImageCompression();
+        return tiff.release();
       }
       catch (Orthanc::OrthancException&)
       {
@@ -835,6 +844,7 @@
   try
   {
     LOG(WARNING) << "Trying to open the input pyramid with OpenSlide";
+    sourceCompression = OrthancWSI::ImageCompression_Unknown;
     return new OrthancWSI::OpenSlidePyramid(path, 
                                             parameters.GetTargetTileWidth(512), 
                                             parameters.GetTargetTileHeight(512));
@@ -860,15 +870,20 @@
 
     if (ParseParameters(exitStatus, parameters, volume, argc, argv))
     {
-      std::auto_ptr<OrthancWSI::ITiledPyramid> source(OpenInputPyramid(parameters.GetInputFile(), parameters));
+      OrthancWSI::ImageCompression sourceCompression;
+      std::auto_ptr<OrthancWSI::ITiledPyramid> source;
+
+      source.reset(OpenInputPyramid(sourceCompression, parameters.GetInputFile(), parameters));
       if (source.get() == NULL)
       {
         throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
       }
+
+      LOG(WARNING) << "Compression of the individual source tiles: " << OrthancWSI::EnumerationToString(sourceCompression);
       
       // Create the shared DICOM tags
       std::auto_ptr<DcmDataset> dataset(ParseDataset(parameters.GetDatasetPath()));
-      EnrichDataset(*dataset, *source, parameters, volume);
+      EnrichDataset(*dataset, *source, sourceCompression, parameters, volume);
 
       std::auto_ptr<OrthancWSI::IFileTarget> output(parameters.CreateTarget());
       Recompress(*output, *source, *dataset, parameters, volume);
--- a/Framework/Algorithms/PyramidReader.cpp	Thu Nov 24 15:41:21 2016 +0100
+++ b/Framework/Algorithms/PyramidReader.cpp	Thu Nov 24 17:48:24 2016 +0100
@@ -32,11 +32,12 @@
   class PyramidReader::SourceTile : public boost::noncopyable
   {
   private: 
-    PyramidReader&  that_;
-    unsigned int    tileX_;
-    unsigned int    tileY_;
-    bool            hasRawTile_;
-    std::string     rawTile_;
+    PyramidReader&    that_;
+    unsigned int      tileX_;
+    unsigned int      tileY_;
+    bool              hasRawTile_;
+    std::string       rawTile_;
+    ImageCompression  rawTileCompression_;
 
     std::auto_ptr<Orthanc::ImageAccessor>  decoded_;
 
@@ -96,7 +97,7 @@
     {
       if (!that_.parameters_.IsForceReencode() &&
           !IsRepaintNeeded() &&
-          that_.source_.ReadRawTile(rawTile_, that_.level_, tileX, tileY))
+          that_.source_.ReadRawTile(rawTile_, rawTileCompression_, that_.level_, tileX, tileY))
       {
         hasRawTile_ = true;
       }
@@ -113,9 +114,17 @@
       }
     }
 
-    bool HasRawTile() const
+    bool HasRawTile(ImageCompression& compression) const
     {
-      return hasRawTile_;
+      if (hasRawTile_)
+      {
+        compression = rawTileCompression_;
+        return true;
+      }
+      else
+      {
+        return false;
+      }
     }
 
     const std::string& GetRawTile() const
@@ -139,7 +148,7 @@
           throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
         }
 
-        decoded_.reset(ImageToolbox::DecodeTile(rawTile_, that_.source_.GetImageCompression()));
+        decoded_.reset(ImageToolbox::DecodeTile(rawTile_, rawTileCompression_));
         if (decoded_.get() == NULL)
         {
           throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
@@ -180,11 +189,12 @@
   }
 
 
-  void PyramidReader::CheckTileSize(const std::string& tile) const
+  void PyramidReader::CheckTileSize(const std::string& tile,
+                                    ImageCompression compression) const
   {
     if (parameters_.IsSafetyCheck())
     {
-      std::auto_ptr<Orthanc::ImageAccessor> decoded(ImageToolbox::DecodeTile(tile, source_.GetImageCompression()));
+      std::auto_ptr<Orthanc::ImageAccessor> decoded(ImageToolbox::DecodeTile(tile, compression));
       CheckTileSize(*decoded);
     }
   }
@@ -225,9 +235,9 @@
     levelHeight_(source.GetLevelHeight(level)),
     sourceTileWidth_(source.GetTileWidth()),
     sourceTileHeight_(source.GetTileHeight()),
-                                                                         targetTileWidth_(targetTileWidth),
-                                                                         targetTileHeight_(targetTileHeight),
-                                                                         parameters_(parameters)
+    targetTileWidth_(targetTileWidth),
+    targetTileHeight_(targetTileHeight),
+    parameters_(parameters)
   {
     if (sourceTileWidth_ % targetTileWidth_ != 0 ||
         sourceTileHeight_ % targetTileHeight_ != 0)
@@ -248,7 +258,8 @@
   }
 
 
-  const std::string* PyramidReader::GetRawTile(unsigned int tileX,
+  const std::string* PyramidReader::GetRawTile(ImageCompression& compression,
+                                               unsigned int tileX,
                                                unsigned int tileY)
   {
     if (sourceTileWidth_ != targetTileWidth_ ||
@@ -258,9 +269,10 @@
     }
 
     SourceTile& source = AccessSourceTile(MapTargetToSourceLocation(tileX, tileY));
-    if (source.HasRawTile())
+
+    if (source.HasRawTile(compression))
     {
-      CheckTileSize(source.GetRawTile());
+      CheckTileSize(source.GetRawTile(), compression);
       return &source.GetRawTile();
     }
     else
--- a/Framework/Algorithms/PyramidReader.h	Thu Nov 24 15:41:21 2016 +0100
+++ b/Framework/Algorithms/PyramidReader.h	Thu Nov 24 17:48:24 2016 +0100
@@ -56,7 +56,8 @@
 
     void CheckTileSize(const Orthanc::ImageAccessor& tile) const;
 
-    void CheckTileSize(const std::string& tile) const;
+    void CheckTileSize(const std::string& tile,
+                       ImageCompression compression) const;
 
     SourceTile& AccessSourceTile(const Location& location);
 
@@ -77,17 +78,13 @@
       return parameters_;
     }
 
-    ImageCompression GetImageCompression() const
-    {
-      return source_.GetImageCompression();
-    }
-
     Orthanc::PixelFormat GetPixelFormat() const
     {
       return source_.GetPixelFormat();
     }
 
-    const std::string* GetRawTile(unsigned int tileX,
+    const std::string* GetRawTile(ImageCompression& compression,
+                                  unsigned int tileX,
                                   unsigned int tileY);
 
     Orthanc::ImageAccessor GetDecodedTile(unsigned int tileX,
--- a/Framework/Algorithms/ReconstructPyramidCommand.cpp	Thu Nov 24 15:41:21 2016 +0100
+++ b/Framework/Algorithms/ReconstructPyramidCommand.cpp	Thu Nov 24 17:48:24 2016 +0100
@@ -51,12 +51,13 @@
     {
       result.reset(new Orthanc::ImageAccessor(source_.GetDecodedTile(x, y)));
 
-      const std::string* rawTile = source_.GetRawTile(x, y);
+      ImageCompression compression;
+      const std::string* rawTile = source_.GetRawTile(compression, x, y);
 
       if (rawTile != NULL)
       {
         // Simple transcoding
-        target_.WriteRawTile(*rawTile, source_.GetImageCompression(), level + shiftTargetLevel_, x, y);
+        target_.WriteRawTile(*rawTile, compression, level + shiftTargetLevel_, x, y);
       }
       else
       {
--- a/Framework/Algorithms/TranscodeTileCommand.cpp	Thu Nov 24 15:41:21 2016 +0100
+++ b/Framework/Algorithms/TranscodeTileCommand.cpp	Thu Nov 24 17:48:24 2016 +0100
@@ -61,12 +61,14 @@
       for (unsigned int y = y_; y < y_ + countTilesY_; y++)
       {
         LOG(INFO) << "Adding tile (" << x << "," << y << ") at level " << level_;
-        const std::string* rawTile = source_.GetRawTile(x, y);
+
+        ImageCompression compression;
+        const std::string* rawTile = source_.GetRawTile(compression, x, y);
 
         if (rawTile != NULL)
         {
           // Simple transcoding
-          target_.WriteRawTile(*rawTile, source_.GetImageCompression(), level_, x, y);
+          target_.WriteRawTile(*rawTile, compression, level_, x, y);
         }
         else
         {
--- a/Framework/Inputs/DecodedTiledPyramid.h	Thu Nov 24 15:41:21 2016 +0100
+++ b/Framework/Inputs/DecodedTiledPyramid.h	Thu Nov 24 17:48:24 2016 +0100
@@ -59,12 +59,8 @@
                                                unsigned int tileX,
                                                unsigned int tileY);
 
-    virtual ImageCompression GetImageCompression() const
-    {
-      return ImageCompression_None;
-    }
-
     virtual bool ReadRawTile(std::string& tile,
+                             ImageCompression& compression,
                              unsigned int level,
                              unsigned int tileX,
                              unsigned int tileY)
--- a/Framework/Inputs/DicomPyramid.cpp	Thu Nov 24 15:41:21 2016 +0100
+++ b/Framework/Inputs/DicomPyramid.cpp	Thu Nov 24 17:48:24 2016 +0100
@@ -200,19 +200,22 @@
 
 
   bool DicomPyramid::ReadRawTile(std::string& tile,
+                                 ImageCompression& compression,
                                  unsigned int level,
                                  unsigned int tileX,
                                  unsigned int tileY)
   {
     CheckLevel(level);
       
-    ImageCompression compression;
     Orthanc::PixelFormat format;
       
-    if (levels_[level]->DownloadRawTile(compression, format, tile, orthanc_, tileX, tileY))
+    if (levels_[level]->DownloadRawTile(tile, format, compression, orthanc_, tileX, tileY))
     {
-      assert(compression == GetImageCompression() &&
-             format == GetPixelFormat());
+      if (format != GetPixelFormat())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+
       return true;
     }
     else
@@ -222,13 +225,6 @@
   }
 
 
-  ImageCompression DicomPyramid::GetImageCompression() const
-  {
-    assert(!instances_.empty() && instances_[0] != NULL);
-    return instances_[0]->GetImageCompression(orthanc_);
-  }
-
-
   Orthanc::PixelFormat DicomPyramid::GetPixelFormat() const
   {
     assert(!instances_.empty() && instances_[0] != NULL);
--- a/Framework/Inputs/DicomPyramid.h	Thu Nov 24 15:41:21 2016 +0100
+++ b/Framework/Inputs/DicomPyramid.h	Thu Nov 24 17:48:24 2016 +0100
@@ -72,12 +72,11 @@
     virtual unsigned int GetTileHeight() const;
 
     virtual bool ReadRawTile(std::string& tile,
+                             ImageCompression& compression,
                              unsigned int level,
                              unsigned int tileX,
                              unsigned int tileY);
 
-    virtual ImageCompression GetImageCompression() const;
-
     virtual Orthanc::PixelFormat GetPixelFormat() const;
   };
 }
--- a/Framework/Inputs/DicomPyramidLevel.cpp	Thu Nov 24 15:41:21 2016 +0100
+++ b/Framework/Inputs/DicomPyramidLevel.cpp	Thu Nov 24 17:48:24 2016 +0100
@@ -116,9 +116,9 @@
   }
 
 
-  bool DicomPyramidLevel::DownloadRawTile(ImageCompression& compression /* out */,
+  bool DicomPyramidLevel::DownloadRawTile(std::string& raw /* out */,
                                           Orthanc::PixelFormat& format /* out */,
-                                          std::string& raw /* out */,
+                                          ImageCompression& compression /* out */,
                                           IOrthancConnection& orthanc,
                                           unsigned int tileX,
                                           unsigned int tileY) const
--- a/Framework/Inputs/DicomPyramidLevel.h	Thu Nov 24 15:41:21 2016 +0100
+++ b/Framework/Inputs/DicomPyramidLevel.h	Thu Nov 24 17:48:24 2016 +0100
@@ -84,9 +84,9 @@
       return tileHeight_;
     }
 
-    bool DownloadRawTile(ImageCompression& compression /* out */,
+    bool DownloadRawTile(std::string& raw /* out */,
                          Orthanc::PixelFormat& format /* out */,
-                         std::string& raw /* out */,
+                         ImageCompression& compression /* out */,
                          IOrthancConnection& orthanc,
                          unsigned int tileX,
                          unsigned int tileY) const;
--- a/Framework/Inputs/HierarchicalTiff.cpp	Thu Nov 24 15:41:21 2016 +0100
+++ b/Framework/Inputs/HierarchicalTiff.cpp	Thu Nov 24 17:48:24 2016 +0100
@@ -245,6 +245,7 @@
 
 
   bool HierarchicalTiff::ReadRawTile(std::string& tile,
+                                     ImageCompression& compression,
                                      unsigned int level,
                                      unsigned int tileX,
                                      unsigned int tileY)
@@ -253,6 +254,8 @@
 
     CheckLevel(level);
 
+    compression = compression_;
+
     // Make the TIFF context point to the level of interest
     if (!TIFFSetDirectory(tiff_, levels_[level].directory_))
     {
--- a/Framework/Inputs/HierarchicalTiff.h	Thu Nov 24 15:41:21 2016 +0100
+++ b/Framework/Inputs/HierarchicalTiff.h	Thu Nov 24 17:48:24 2016 +0100
@@ -94,18 +94,19 @@
     }
 
     virtual bool ReadRawTile(std::string& tile,
+                             ImageCompression& compression,
                              unsigned int level,
                              unsigned int tileX,
                              unsigned int tileY);
 
-    virtual ImageCompression GetImageCompression() const
-    {
-      return compression_;
-    }
-
     virtual Orthanc::PixelFormat GetPixelFormat() const
     {
       return pixelFormat_;
     }
+
+    ImageCompression GetImageCompression()
+    {
+      return compression_;
+    }
   };
 }
--- a/Framework/Inputs/ITiledPyramid.h	Thu Nov 24 15:41:21 2016 +0100
+++ b/Framework/Inputs/ITiledPyramid.h	Thu Nov 24 17:48:24 2016 +0100
@@ -52,6 +52,7 @@
     virtual unsigned int GetTileHeight() const = 0;
 
     virtual bool ReadRawTile(std::string& tile,
+                             ImageCompression& compression,
                              unsigned int level,
                              unsigned int tileX,
                              unsigned int tileY) = 0;
@@ -60,9 +61,6 @@
                                                unsigned int tileX,
                                                unsigned int tileY) = 0;
 
-    // Only makes sense for images with raw access to tiles
-    virtual ImageCompression GetImageCompression() const = 0;
-
     virtual Orthanc::PixelFormat GetPixelFormat() const = 0;
   };
 }
--- a/Framework/Inputs/PyramidWithRawTiles.cpp	Thu Nov 24 15:41:21 2016 +0100
+++ b/Framework/Inputs/PyramidWithRawTiles.cpp	Thu Nov 24 17:48:24 2016 +0100
@@ -33,14 +33,16 @@
                                                           unsigned int tileY)
   {
     std::string tile;
-    if (!ReadRawTile(tile, level, tileX, tileY))
+    ImageCompression compression;
+
+    if (!ReadRawTile(tile, compression, level, tileX, tileY))
     {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      return NULL;
     }
 
     std::auto_ptr<Orthanc::ImageAccessor> result;
 
-    switch (GetImageCompression())
+    switch (compression)
     {
       case ImageCompression_None:
         result.reset(new Orthanc::ImageAccessor);
--- a/Framework/Inputs/TiledPyramidStatistics.cpp	Thu Nov 24 15:41:21 2016 +0100
+++ b/Framework/Inputs/TiledPyramidStatistics.cpp	Thu Nov 24 17:48:24 2016 +0100
@@ -43,11 +43,12 @@
 
 
   bool TiledPyramidStatistics::ReadRawTile(std::string& tile,
+                                           ImageCompression& compression,
                                            unsigned int level,
                                            unsigned int tileX,
                                            unsigned int tileY)
   {
-    if (source_.ReadRawTile(tile, level, tileX, tileY))
+    if (source_.ReadRawTile(tile, compression, level, tileX, tileY))
     {
       boost::mutex::scoped_lock lock(mutex_);
       countRawAccesses_++;
--- a/Framework/Inputs/TiledPyramidStatistics.h	Thu Nov 24 15:41:21 2016 +0100
+++ b/Framework/Inputs/TiledPyramidStatistics.h	Thu Nov 24 17:48:24 2016 +0100
@@ -30,7 +30,7 @@
   {
   private:
     boost::mutex   mutex_;
-    ITiledPyramid& source_;
+    ITiledPyramid& source_;  // This is a facade design pattern
     unsigned int   countRawAccesses_;
     unsigned int   countDecodedTiles_;
 
@@ -64,17 +64,13 @@
       return source_.GetTileHeight();
     }
 
-    virtual ImageCompression GetImageCompression() const
-    {
-      return source_.GetImageCompression();
-    }
-
     virtual Orthanc::PixelFormat GetPixelFormat() const
     {
       return source_.GetPixelFormat();
     }
 
     virtual bool ReadRawTile(std::string& tile,
+                             ImageCompression& compression,
                              unsigned int level,
                              unsigned int tileX,
                              unsigned int tileY);
--- a/Framework/Outputs/InMemoryTiledImage.cpp	Thu Nov 24 15:41:21 2016 +0100
+++ b/Framework/Outputs/InMemoryTiledImage.cpp	Thu Nov 24 17:48:24 2016 +0100
@@ -88,6 +88,7 @@
 
 
   bool InMemoryTiledImage::ReadRawTile(std::string& tile,
+                                       ImageCompression& compression,
                                        unsigned int level,
                                        unsigned int tileX,
                                        unsigned int tileY)
--- a/Framework/Outputs/InMemoryTiledImage.h	Thu Nov 24 15:41:21 2016 +0100
+++ b/Framework/Outputs/InMemoryTiledImage.h	Thu Nov 24 17:48:24 2016 +0100
@@ -77,6 +77,7 @@
     }
 
     virtual bool ReadRawTile(std::string& tile,
+                             ImageCompression& compression,
                              unsigned int level,
                              unsigned int tileX,
                              unsigned int tileY);
@@ -85,11 +86,6 @@
                                                unsigned int tileX,
                                                unsigned int tileY);
 
-    virtual ImageCompression GetImageCompression() const
-    {
-      return ImageCompression_None;
-    }
-
     virtual Orthanc::PixelFormat GetPixelFormat() const
     {
       return format_;
--- a/TODO	Thu Nov 24 15:41:21 2016 +0100
+++ b/TODO	Thu Nov 24 17:48:24 2016 +0100
@@ -6,8 +6,9 @@
 General
 -------
 
-* Support sparse tiling (both in encoder and decoder)
+* Thoroughly test sparse tiling (both in encoder and decoder)
 * Display physical scale in Web viewer
+* Viewer: Configure how missing tiles are displayed for sparse tiling
 
 
 -----------
@@ -30,4 +31,3 @@
 ---------
 
 * Move "Framework/Orthanc/" as "Resources/Orthanc/"
-* Publish history of code size on Web site
--- a/ViewerPlugin/Plugin.cpp	Thu Nov 24 15:41:21 2016 +0100
+++ b/ViewerPlugin/Plugin.cpp	Thu Nov 24 17:48:24 2016 +0100
@@ -22,6 +22,7 @@
 #include "../Framework/Inputs/DicomPyramid.h"
 #include "../Framework/Jpeg2000Reader.h"
 #include "../Framework/Messaging/PluginOrthancConnection.h"
+#include "../Framework/Orthanc/Core/Images/ImageProcessing.h"
 #include "../Framework/Orthanc/Core/Images/PngWriter.h"
 #include "../Framework/Orthanc/Core/MultiThreading/Semaphore.h"
 #include "../Framework/Orthanc/Core/OrthancException.h"
@@ -106,6 +107,26 @@
 std::auto_ptr<OrthancWSI::PluginOrthancConnection>  orthanc_;
 std::auto_ptr<OrthancWSI::DicomPyramidCache>        cache_;
 std::auto_ptr<Orthanc::Semaphore>                   transcoderSemaphore_;
+std::string                                         sparseTile_;
+
+
+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(context_, output, OrthancPluginPixelFormat_RGB24, 
+                                         tile.GetWidth(), tile.GetHeight(), 
+                                         tile.GetPitch(), tile.GetBuffer());
+}
 
 
 static bool DisplayPerformanceWarning()
@@ -182,16 +203,17 @@
   {
     OrthancWSI::DicomPyramidCache::Locker locker(*cache_, seriesId);
 
-    compression = locker.GetPyramid().GetImageCompression();
     format = locker.GetPyramid().GetPixelFormat();
     tileWidth = locker.GetPyramid().GetTileWidth();
     tileHeight = locker.GetPyramid().GetTileHeight();
 
-    if (!locker.GetPyramid().ReadRawTile(tile, 
+    if (!locker.GetPyramid().ReadRawTile(tile, compression, 
                                          static_cast<unsigned int>(level),
                                          static_cast<unsigned int>(tileX),
                                          static_cast<unsigned int>(tileY)))
     {
+      // Handling of missing tile (for sparse tiling): TODO parameter?
+      // AnswerSparseTile(output, tileWidth, tileHeight); return;
       throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
     }
   }