changeset 217:20bc074ec19a

Viewer can display DICOM pyramids whose tile sizes vary across levels
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 12 Jan 2021 14:24:18 +0100
parents c35a3a0627b9
children c5a8b46c4cba
files Applications/DicomToTiff.cpp Applications/Dicomizer.cpp Framework/Algorithms/PyramidReader.cpp Framework/Algorithms/TranscodeTileCommand.cpp Framework/DicomizerParameters.cpp Framework/DicomizerParameters.h Framework/Enumerations.h Framework/ImageToolbox.cpp Framework/ImageToolbox.h Framework/Inputs/DecodedTiledPyramid.cpp Framework/Inputs/DicomPyramid.cpp Framework/Inputs/DicomPyramid.h Framework/Inputs/DicomPyramidInstance.cpp Framework/Inputs/HierarchicalTiff.h Framework/Inputs/ITiledPyramid.h Framework/Inputs/OpenSlidePyramid.h Framework/Inputs/PyramidWithRawTiles.cpp Framework/Inputs/SingleLevelDecodedPyramid.h Framework/Inputs/TiledPyramidStatistics.h Framework/Jpeg2000Reader.cpp Framework/Outputs/InMemoryTiledImage.h NEWS ViewerPlugin/Plugin.cpp ViewerPlugin/viewer.js
diffstat 24 files changed, 319 insertions(+), 161 deletions(-) [+]
line wrap: on
line diff
--- a/Applications/DicomToTiff.cpp	Tue Jan 12 10:21:36 2021 +0100
+++ b/Applications/DicomToTiff.cpp	Tue Jan 12 14:24:18 2021 +0100
@@ -199,11 +199,12 @@
     targetPhotometric = source.GetPhotometricInterpretation();
   }
 
+  OrthancWSI::ImageToolbox::CheckConstantTileSize(source); // (**)
   OrthancWSI::HierarchicalTiffWriter target(options[OPTION_OUTPUT].as<std::string>(),
                                             source.GetPixelFormat(), 
                                             OrthancWSI::ImageCompression_Jpeg,  // (*)
-                                            source.GetTileWidth(), 
-                                            source.GetTileHeight(),
+                                            source.GetTileWidth(0),   // (**) 
+                                            source.GetTileHeight(0),  // (**)
                                             targetPhotometric);
 
   if (options.count(OPTION_JPEG_QUALITY))
@@ -229,10 +230,10 @@
                  << " level " << level;
 
     unsigned int countX = OrthancWSI::CeilingDivision
-      (source.GetLevelWidth(level), source.GetTileWidth());
+      (source.GetLevelWidth(level), source.GetTileWidth(level));
 
     unsigned int countY = OrthancWSI::CeilingDivision
-      (source.GetLevelHeight(level), source.GetTileHeight());
+      (source.GetLevelHeight(level), source.GetTileHeight(level));
 
     for (unsigned int tileY = 0; tileY < countY; tileY++)
     {
@@ -259,8 +260,9 @@
 
             if (compression == OrthancWSI::ImageCompression_None)
             {
-              decoded.reset(OrthancWSI::ImageToolbox::DecodeRawTile(tile, source.GetPixelFormat(),
-                                                                    source.GetTileWidth(), source.GetTileHeight()));
+              decoded.reset(OrthancWSI::ImageToolbox::DecodeRawTile(
+                              tile, source.GetPixelFormat(),
+                              source.GetTileWidth(level), source.GetTileHeight(level)));
             }
             else
             {
--- a/Applications/Dicomizer.cpp	Tue Jan 12 10:21:36 2021 +0100
+++ b/Applications/Dicomizer.cpp	Tue Jan 12 14:24:18 2021 +0100
@@ -23,6 +23,7 @@
 #include "../Framework/Algorithms/TranscodeTileCommand.h"
 #include "../Framework/DicomToolbox.h"
 #include "../Framework/DicomizerParameters.h"
+#include "../Framework/ImageToolbox.h"
 #include "../Framework/ImagedVolumeParameters.h"
 #include "../Framework/Inputs/HierarchicalTiff.h"
 #include "../Framework/Inputs/OpenSlidePyramid.h"
@@ -169,7 +170,8 @@
 {
   OrthancWSI::TiledPyramidStatistics stats(source);
 
-  LOG(WARNING) << "Size of source tiles: " << stats.GetTileWidth() << "x" << stats.GetTileHeight();
+  OrthancWSI::ImageToolbox::CheckConstantTileSize(stats);  // Sanity check
+  LOG(WARNING) << "Size of source tiles: " << stats.GetTileWidth(0) << "x" << stats.GetTileHeight(0);
   LOG(WARNING) << "Pixel format: " << Orthanc::EnumerationToString(source.GetPixelFormat());
   LOG(WARNING) << "Source photometric interpretation: " << Orthanc::EnumerationToString(source.GetPhotometricInterpretation());
   LOG(WARNING) << "Source compression: " << EnumerationToString(sourceCompression);
@@ -251,8 +253,9 @@
                  << OrthancWSI::EnumerationToString(target.GetImageCompression());
   }
 
-  if (stats.GetTileWidth() % target.GetTileWidth() != 0 ||
-      stats.GetTileHeight() % target.GetTileHeight() != 0)
+  OrthancWSI::ImageToolbox::CheckConstantTileSize(stats);
+  if (stats.GetTileWidth(0) % target.GetTileWidth() != 0 ||
+      stats.GetTileHeight(0) % target.GetTileHeight() != 0)
   {
     LOG(ERROR) << "When resampling the tile size, "
                << "it must be a integer divisor of the original tile size";
--- a/Framework/Algorithms/PyramidReader.cpp	Tue Jan 12 10:21:36 2021 +0100
+++ b/Framework/Algorithms/PyramidReader.cpp	Tue Jan 12 14:24:18 2021 +0100
@@ -188,7 +188,7 @@
         tile.GetHeight() != sourceTileHeight_)
     {
       LOG(ERROR) << "One tile in the input image has size " << tile.GetWidth() << "x" << tile.GetHeight() 
-                 << " instead of required " << source_.GetTileWidth() << "x" << source_.GetTileHeight();
+                 << " instead of required " << sourceTileWidth_ << "x" << sourceTileHeight_;
       throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize);
     }
   }
@@ -238,8 +238,8 @@
     level_(level),
     levelWidth_(source.GetLevelWidth(level)),
     levelHeight_(source.GetLevelHeight(level)),
-    sourceTileWidth_(source.GetTileWidth()),
-    sourceTileHeight_(source.GetTileHeight()),
+    sourceTileWidth_(source.GetTileWidth(level)),
+    sourceTileHeight_(source.GetTileHeight(level)),
     targetTileWidth_(targetTileWidth),
     targetTileHeight_(targetTileHeight),
     parameters_(parameters)
--- a/Framework/Algorithms/TranscodeTileCommand.cpp	Tue Jan 12 10:21:36 2021 +0100
+++ b/Framework/Algorithms/TranscodeTileCommand.cpp	Tue Jan 12 14:24:18 2021 +0100
@@ -91,15 +91,15 @@
                                                ITiledPyramid& source,
                                                const DicomizerParameters& parameters)
   {
-    const unsigned int stepX = source.GetTileWidth() / target.GetTileWidth();
-    const unsigned int stepY = source.GetTileHeight() / target.GetTileHeight();
-    assert(stepX >= 1 && stepY >= 1);
-
     for (unsigned int level = 0; level < source.GetLevelCount(); level++)
     {
       const unsigned int targetCountTilesX = target.GetCountTilesX(level);
       const unsigned int targetCountTilesY = target.GetCountTilesY(level);
 
+      const unsigned int stepX = source.GetTileWidth(level) / target.GetTileWidth();
+      const unsigned int stepY = source.GetTileHeight(level) / target.GetTileHeight();
+      assert(stepX >= 1 && stepY >= 1);
+
       for (unsigned int y = 0; y < targetCountTilesY; y += stepY)
       {
         unsigned int cy = stepY;
--- a/Framework/DicomizerParameters.cpp	Tue Jan 12 10:21:36 2021 +0100
+++ b/Framework/DicomizerParameters.cpp	Tue Jan 12 14:24:18 2021 +0100
@@ -22,6 +22,7 @@
 #include "PrecompiledHeadersWSI.h"
 #include "DicomizerParameters.h"
 
+#include "ImageToolbox.h"
 #include "Targets/FolderTarget.h"
 #include "Targets/OrthancTarget.h"
 
@@ -111,6 +112,13 @@
   }
 
 
+  unsigned int DicomizerParameters::GetTargetTileWidth(const ITiledPyramid& source) const
+  {
+    ImageToolbox::CheckConstantTileSize(source);
+    return GetTargetTileWidth(source.GetTileWidth(0));
+  }
+  
+
   unsigned int DicomizerParameters::GetTargetTileHeight(unsigned int defaultHeight) const
   {
     if (hasTargetTileSize_ &&
@@ -125,6 +133,13 @@
   }
 
 
+  unsigned int DicomizerParameters::GetTargetTileHeight(const ITiledPyramid& source) const
+  {
+    ImageToolbox::CheckConstantTileSize(source);
+    return GetTargetTileHeight(source.GetTileHeight(0));
+  }
+
+
   void DicomizerParameters::SetThreadsCount(unsigned int threads)
   {
     if (threads == 0)
@@ -161,6 +176,8 @@
       return pyramidLevelsCount_;
     }
 
+    ImageToolbox::CheckConstantTileSize(source);
+
     // By default, the number of levels for the pyramid is taken so that
     // the upper level is reduced either to 1 column of tiles, or to 1
     // row of tiles.
@@ -205,8 +222,8 @@
     }
 
     unsigned int fullNumberOfTiles = (
-      CeilingDivision(source.GetLevelWidth(0), source.GetTileWidth()) * 
-      CeilingDivision(source.GetLevelHeight(0), source.GetTileHeight()));
+      CeilingDivision(source.GetLevelWidth(0), source.GetTileWidth(0)) * 
+      CeilingDivision(source.GetLevelHeight(0), source.GetTileHeight(0)));
 
     // By default, the number of lower levels in the pyramid is chosen
     // as a compromise between the number of tasks (there should not be
--- a/Framework/DicomizerParameters.h	Tue Jan 12 10:21:36 2021 +0100
+++ b/Framework/DicomizerParameters.h	Tue Jan 12 14:24:18 2021 +0100
@@ -115,17 +115,11 @@
 
     unsigned int GetTargetTileWidth(unsigned int defaultWidth) const;
 
-    unsigned int GetTargetTileWidth(const ITiledPyramid& source) const
-    {
-      return GetTargetTileWidth(source.GetTileWidth());
-    }
+    unsigned int GetTargetTileWidth(const ITiledPyramid& source) const;
 
     unsigned int GetTargetTileHeight(unsigned int defaultHeight) const;
 
-    unsigned int GetTargetTileHeight(const ITiledPyramid& source) const
-    {
-      return GetTargetTileHeight(source.GetTileHeight());
-    }
+    unsigned int GetTargetTileHeight(const ITiledPyramid& source) const;
 
     void SetThreadsCount(unsigned int threads);
 
--- a/Framework/Enumerations.h	Tue Jan 12 10:21:36 2021 +0100
+++ b/Framework/Enumerations.h	Tue Jan 12 14:24:18 2021 +0100
@@ -28,15 +28,17 @@
 
 namespace OrthancWSI
 {
+  // WARNING - Don't change the enum values below, as this would break
+  // serialization of "DicomPyramidInstance"
   enum ImageCompression
   {
-    ImageCompression_Unknown,
-    ImageCompression_None,
-    ImageCompression_Dicom,
-    ImageCompression_Png,
-    ImageCompression_Jpeg,
-    ImageCompression_Jpeg2000,
-    ImageCompression_Tiff
+    ImageCompression_Unknown = 1,
+    ImageCompression_None = 2,
+    ImageCompression_Dicom = 3,
+    ImageCompression_Png = 4,
+    ImageCompression_Jpeg = 5,
+    ImageCompression_Jpeg2000 = 6,
+    ImageCompression_Tiff = 7
   };
 
   enum OpticalPath
--- a/Framework/ImageToolbox.cpp	Tue Jan 12 10:21:36 2021 +0100
+++ b/Framework/ImageToolbox.cpp	Tue Jan 12 14:24:18 2021 +0100
@@ -388,18 +388,96 @@
       const unsigned int width = result->GetWidth();
       const unsigned int height = result->GetHeight();
       
-      for (unsigned int y = 0; y < height; y += pyramid.GetTileHeight())
+      for (unsigned int y = 0; y < height; y += pyramid.GetTileHeight(level))
       {
-        for (unsigned int x = 0; x < width; x += pyramid.GetTileWidth())
+        for (unsigned int x = 0; x < width; x += pyramid.GetTileWidth(level))
         {
-          std::unique_ptr<Orthanc::ImageAccessor> tile(pyramid.DecodeTile(level,
-                                                                        x / pyramid.GetTileWidth(),
-                                                                        y / pyramid.GetTileHeight()));
+          std::unique_ptr<Orthanc::ImageAccessor> tile(
+            pyramid.DecodeTile(level,
+                               x / pyramid.GetTileWidth(level),
+                               y / pyramid.GetTileHeight(level)));
           Embed(*result, *tile, x, y);
         }
       }
 
       return result.release();
     }
+
+
+    void CheckConstantTileSize(const ITiledPyramid& source)
+    {
+      if (source.GetLevelCount() == 0)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize,
+                                        "Input pyramid has no level");
+      }
+      else
+      {
+        for (unsigned int level = 0; level < source.GetLevelCount(); level++)
+        {
+          if (source.GetTileWidth(level) != source.GetTileWidth(0) ||
+              source.GetTileHeight(level) != source.GetTileHeight(0))
+          {
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize,
+                                            "The DICOMizer requires that the input pyramid has constant "
+                                            "tile sizes across all its levels, which is not the case");
+          } 
+        }
+      }
+    }
+
+
+    void ConvertJpegYCbCrToRgb(Orthanc::ImageAccessor& image)
+    {
+#if defined(ORTHANC_FRAMEWORK_VERSION_IS_ABOVE) && ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 9, 0)
+      Orthanc::ImageProcessing::ConvertJpegYCbCrToRgb(image);
+#else
+#  warning You are using an old version of the Orthanc framework
+      const unsigned int width = image.GetWidth();
+      const unsigned int height = image.GetHeight();
+      const unsigned int pitch = image.GetPitch();
+      uint8_t* buffer = reinterpret_cast<uint8_t*>(image.GetBuffer());
+        
+      if (image.GetFormat() != Orthanc::PixelFormat_RGB24 ||
+          pitch < 3 * width)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
+      }
+
+      for (unsigned int y = 0; y < height; y++)
+      {
+        uint8_t* p = buffer + y * pitch;
+          
+        for (unsigned int x = 0; x < width; x++, p += 3)
+        {
+          const float Y  = p[0];
+          const float Cb = p[1];
+          const float Cr = p[2];
+
+          const float result[3] = {
+            Y                             + 1.402f    * (Cr - 128.0f),
+            Y - 0.344136f * (Cb - 128.0f) - 0.714136f * (Cr - 128.0f),
+            Y + 1.772f    * (Cb - 128.0f)
+          };
+
+          for (uint8_t i = 0; i < 3 ; i++)
+          {
+            if (result[i] < 0)
+            {
+              p[i] = 0;
+            }
+            else if (result[i] > 255)
+            {
+              p[i] = 255;
+            }
+            else
+            {
+              p[i] = static_cast<uint8_t>(result[i]);
+            }
+          }    
+        }
+      }
+#endif
+    }
   }
 }
--- a/Framework/ImageToolbox.h	Tue Jan 12 10:21:36 2021 +0100
+++ b/Framework/ImageToolbox.h	Tue Jan 12 14:24:18 2021 +0100
@@ -77,5 +77,9 @@
 
     Orthanc::ImageAccessor* Render(ITiledPyramid& pyramid,
                                    unsigned int level);
+
+    void CheckConstantTileSize(const ITiledPyramid& source);
+
+    void ConvertJpegYCbCrToRgb(Orthanc::ImageAccessor& image /* inplace */);
   }
 }
--- a/Framework/Inputs/DecodedTiledPyramid.cpp	Tue Jan 12 10:21:36 2021 +0100
+++ b/Framework/Inputs/DecodedTiledPyramid.cpp	Tue Jan 12 14:24:18 2021 +0100
@@ -61,11 +61,11 @@
                                                           unsigned int tileX,
                                                           unsigned int tileY)
   {
-    unsigned int x = tileX * GetTileWidth();
-    unsigned int y = tileY * GetTileHeight();
+    unsigned int x = tileX * GetTileWidth(level);
+    unsigned int y = tileY * GetTileHeight(level);
 
     std::unique_ptr<Orthanc::ImageAccessor> tile
-      (ImageToolbox::Allocate(GetPixelFormat(), GetTileWidth(), GetTileHeight()));
+      (ImageToolbox::Allocate(GetPixelFormat(), GetTileWidth(level), GetTileHeight(level)));
 
     if (x >= GetLevelWidth(level) ||
         y >= GetLevelHeight(level))   // (*)
@@ -76,9 +76,9 @@
 
     bool fit = true;
     unsigned int regionWidth;
-    if (x + GetTileWidth() <= GetLevelWidth(level))
+    if (x + GetTileWidth(level) <= GetLevelWidth(level))
     {
-      regionWidth = GetTileWidth();
+      regionWidth = GetTileWidth(level);
     }
     else
     {
@@ -88,9 +88,9 @@
     }
     
     unsigned int regionHeight;
-    if (y + GetTileHeight() <= GetLevelHeight(level))
+    if (y + GetTileHeight(level) <= GetLevelHeight(level))
     {
-      regionHeight = GetTileHeight();
+      regionHeight = GetTileHeight(level);
     }
     else
     {
--- a/Framework/Inputs/DicomPyramid.cpp	Tue Jan 12 10:21:36 2021 +0100
+++ b/Framework/Inputs/DicomPyramid.cpp	Tue Jan 12 14:24:18 2021 +0100
@@ -113,8 +113,6 @@
       const DicomPyramidInstance& b = *instances_[i];
 
       if (a.GetPixelFormat() != b.GetPixelFormat() ||
-          a.GetTileWidth() != b.GetTileWidth() ||
-          a.GetTileHeight() != b.GetTileHeight() ||
           a.GetTotalWidth() < b.GetTotalWidth() ||
           a.GetTotalHeight() < b.GetTotalHeight())            
       {
@@ -190,17 +188,17 @@
   }
 
 
-  unsigned int DicomPyramid::GetTileWidth() const
+  unsigned int DicomPyramid::GetTileWidth(unsigned int level) const
   {
-    assert(!levels_.empty() && levels_[0] != NULL);
-    return levels_[0]->GetTileWidth();
+    CheckLevel(level);
+    return levels_[level]->GetTileWidth();
   }
 
 
-  unsigned int DicomPyramid::GetTileHeight() const
+  unsigned int DicomPyramid::GetTileHeight(unsigned int level) const
   {
-    assert(!levels_.empty() && levels_[0] != NULL);
-    return levels_[0]->GetTileHeight();
+    CheckLevel(level);
+    return levels_[level]->GetTileHeight();
   }
 
 
--- a/Framework/Inputs/DicomPyramid.h	Tue Jan 12 10:21:36 2021 +0100
+++ b/Framework/Inputs/DicomPyramid.h	Tue Jan 12 14:24:18 2021 +0100
@@ -70,9 +70,9 @@
 
     virtual unsigned int GetLevelHeight(unsigned int level) const ORTHANC_OVERRIDE;
 
-    virtual unsigned int GetTileWidth() const ORTHANC_OVERRIDE;
+    virtual unsigned int GetTileWidth(unsigned int level) const ORTHANC_OVERRIDE;
 
-    virtual unsigned int GetTileHeight() const ORTHANC_OVERRIDE;
+    virtual unsigned int GetTileHeight(unsigned int level) const ORTHANC_OVERRIDE;
 
     virtual bool ReadRawTile(std::string& tile,
                              ImageCompression& compression,
--- a/Framework/Inputs/DicomPyramidInstance.cpp	Tue Jan 12 10:21:36 2021 +0100
+++ b/Framework/Inputs/DicomPyramidInstance.cpp	Tue Jan 12 14:24:18 2021 +0100
@@ -28,11 +28,12 @@
 
 #include <Logging.h>
 #include <OrthancException.h>
+#include <SerializationToolbox.h>
 #include <Toolbox.h>
 
 #include <cassert>
 
-#define SERIALIZED_METADATA  "4200"
+#define SERIALIZED_METADATA  "4201"   // Was "4200" if versions <= 0.7 of this plugin
 
 
 namespace OrthancWSI
@@ -85,7 +86,7 @@
 
     std::string p = Orthanc::Toolbox::StripSpaces
       (reader.GetMandatoryStringValue(OrthancStone::DicomPath(Orthanc::DICOM_TAG_PHOTOMETRIC_INTERPRETATION)));
-    
+
     photometric = Orthanc::StringToPhotometricInterpretation(p.c_str());
 
     if (photometric == Orthanc::PhotometricInterpretation_Palette)
@@ -260,7 +261,8 @@
                                              const std::string& instanceId,
                                              bool useCache) :
     instanceId_(instanceId),
-    hasCompression_(false)
+    hasCompression_(false),
+    compression_(ImageCompression_None)  // Dummy initialization for serialization
   {
     if (useCache)
     {
@@ -269,6 +271,7 @@
         // Try and deserialized the cached information about this instance
         std::string serialized;
         orthanc.RestApiGet(serialized, "/instances/" + instanceId + "/metadata/" + SERIALIZED_METADATA);
+        std::cout << serialized;
         Deserialize(serialized);
         return;  // Success
       }
@@ -304,6 +307,18 @@
     return frames_[frame].second;
   }
 
+
+
+  static const char* const HAS_COMPRESSION = "HasCompression";
+  static const char* const IMAGE_COMPRESSION = "ImageCompression";
+  static const char* const PIXEL_FORMAT = "PixelFormat";
+  static const char* const FRAMES = "Frames";
+  static const char* const TILE_WIDTH = "TileWidth";
+  static const char* const TILE_HEIGHT = "TileHeight";
+  static const char* const TOTAL_WIDTH = "TotalWidth";
+  static const char* const TOTAL_HEIGHT = "TotalHeight";
+  static const char* const PHOTOMETRIC_INTERPRETATION = "PhotometricInterpretation";
+  
   
   void DicomPyramidInstance::Serialize(std::string& result) const
   {
@@ -313,30 +328,22 @@
       Json::Value frame = Json::arrayValue;
       frame.append(frames_[i].first);
       frame.append(frames_[i].second);
-
       frames.append(frame);
     }
 
-    Json::Value content;
-    content["Frames"] = frames;
-    content["TileHeight"] = tileHeight_;
-    content["TileWidth"] = tileWidth_;
-    content["TotalHeight"] = totalHeight_;
-    content["TotalWidth"] = totalWidth_;    
+    Json::Value content = Json::objectValue;
+    content[FRAMES] = frames;
 
-    switch (format_)
-    {
-      case Orthanc::PixelFormat_RGB24:
-        content["PixelFormat"] = 0;
-        break;
-
-      case Orthanc::PixelFormat_Grayscale8:
-        content["PixelFormat"] = 1;
-        break;
-
-      default:
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-    }
+    // "instanceId_" is set by the constructor
+    
+    content[HAS_COMPRESSION] = hasCompression_;
+    content[IMAGE_COMPRESSION] = static_cast<int>(compression_);
+    content[PIXEL_FORMAT] = static_cast<int>(format_);
+    content[TILE_WIDTH] = tileWidth_;
+    content[TILE_HEIGHT] = tileHeight_;
+    content[TOTAL_WIDTH] = totalWidth_;
+    content[TOTAL_HEIGHT] = totalHeight_;
+    content[PHOTOMETRIC_INTERPRETATION] = Orthanc::EnumerationToString(photometric_);
 
     Orthanc::Toolbox::WriteFastJson(result, content);
   }
@@ -344,53 +351,28 @@
 
   void DicomPyramidInstance::Deserialize(const std::string& s)
   {
-    hasCompression_ = false;
-
     Json::Value content;
-    OrthancStone::IOrthancConnection::ParseJson(content, s);
+    Orthanc::Toolbox::ReadJson(content, s);
 
     if (content.type() != Json::objectValue ||
-        !content.isMember("Frames") ||
-        !content.isMember("PixelFormat") ||
-        !content.isMember("TileHeight") ||
-        !content.isMember("TileWidth") ||
-        !content.isMember("TotalHeight") ||
-        !content.isMember("TotalWidth") ||
-        content["Frames"].type() != Json::arrayValue ||
-        content["PixelFormat"].type() != Json::intValue ||
-        content["TileHeight"].type() != Json::intValue ||
-        content["TileWidth"].type() != Json::intValue ||
-        content["TotalHeight"].type() != Json::intValue ||
-        content["TotalWidth"].type() != Json::intValue ||
-        content["TileHeight"].asInt() < 0 ||
-        content["TileWidth"].asInt() < 0 ||
-        content["TotalHeight"].asInt() < 0 ||
-        content["TotalWidth"].asInt() < 0)
+        !content.isMember(FRAMES) ||
+        content[FRAMES].type() != Json::arrayValue)
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
     }
 
-    switch (content["PixelFormat"].asInt())
-    {
-      case 0:
-        format_ = Orthanc::PixelFormat_RGB24;
-        break;
-
-      case 1:
-        format_ = Orthanc::PixelFormat_Grayscale8;
-        break;
+    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));
+    tileWidth_ = Orthanc::SerializationToolbox::ReadUnsignedInteger(content, TILE_WIDTH);
+    tileHeight_ = Orthanc::SerializationToolbox::ReadUnsignedInteger(content, TILE_HEIGHT);
+    totalWidth_ = Orthanc::SerializationToolbox::ReadUnsignedInteger(content, TOTAL_WIDTH);
+    totalHeight_ = Orthanc::SerializationToolbox::ReadUnsignedInteger(content, TOTAL_HEIGHT);
 
-      default:
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-    }
-
-    hasCompression_ = false;
-    tileHeight_ = static_cast<unsigned int>(content["TileHeight"].asInt());
-    tileWidth_ = static_cast<unsigned int>(content["TileWidth"].asInt());
-    totalHeight_ = static_cast<unsigned int>(content["TotalHeight"].asInt());
-    totalWidth_ = static_cast<unsigned int>(content["TotalWidth"].asInt());
-
-    const Json::Value f = content["Frames"];
+    std::string p = Orthanc::SerializationToolbox::ReadString(content, PHOTOMETRIC_INTERPRETATION);
+    photometric_ = Orthanc::StringToPhotometricInterpretation(p.c_str());
+    
+    const Json::Value f = content[FRAMES];
     frames_.resize(f.size());
 
     for (Json::Value::ArrayIndex i = 0; i < f.size(); i++)
--- a/Framework/Inputs/HierarchicalTiff.h	Tue Jan 12 10:21:36 2021 +0100
+++ b/Framework/Inputs/HierarchicalTiff.h	Tue Jan 12 14:24:18 2021 +0100
@@ -86,12 +86,12 @@
 
     virtual unsigned int GetLevelHeight(unsigned int level) const ORTHANC_OVERRIDE;
 
-    virtual unsigned int GetTileWidth() const ORTHANC_OVERRIDE
+    virtual unsigned int GetTileWidth(unsigned int level) const ORTHANC_OVERRIDE
     {
       return tileWidth_;
     }
 
-    virtual unsigned int GetTileHeight() const ORTHANC_OVERRIDE
+    virtual unsigned int GetTileHeight(unsigned int level) const ORTHANC_OVERRIDE
     {
       return tileHeight_;
     }
--- a/Framework/Inputs/ITiledPyramid.h	Tue Jan 12 10:21:36 2021 +0100
+++ b/Framework/Inputs/ITiledPyramid.h	Tue Jan 12 14:24:18 2021 +0100
@@ -49,9 +49,9 @@
 
     virtual unsigned int GetLevelHeight(unsigned int level) const = 0;
 
-    virtual unsigned int GetTileWidth() const = 0;
+    virtual unsigned int GetTileWidth(unsigned int level) const = 0;
 
-    virtual unsigned int GetTileHeight() const = 0;
+    virtual unsigned int GetTileHeight(unsigned int level) const = 0;
 
     virtual bool ReadRawTile(std::string& tile,
                              ImageCompression& compression,
--- a/Framework/Inputs/OpenSlidePyramid.h	Tue Jan 12 10:21:36 2021 +0100
+++ b/Framework/Inputs/OpenSlidePyramid.h	Tue Jan 12 14:24:18 2021 +0100
@@ -44,12 +44,12 @@
                      unsigned int tileWidth,
                      unsigned int tileHeight);
 
-    virtual unsigned int GetTileWidth() const ORTHANC_OVERRIDE
+    virtual unsigned int GetTileWidth(unsigned int level) const ORTHANC_OVERRIDE
     {
       return tileWidth_;
     }
 
-    virtual unsigned int GetTileHeight() const ORTHANC_OVERRIDE
+    virtual unsigned int GetTileHeight(unsigned int level) const ORTHANC_OVERRIDE
     {
       return tileHeight_;
     }
--- a/Framework/Inputs/PyramidWithRawTiles.cpp	Tue Jan 12 10:21:36 2021 +0100
+++ b/Framework/Inputs/PyramidWithRawTiles.cpp	Tue Jan 12 14:24:18 2021 +0100
@@ -39,7 +39,7 @@
     }
     else if (compression == ImageCompression_None)
     {
-      return ImageToolbox::DecodeRawTile(tile, GetPixelFormat(), GetTileWidth(), GetTileHeight());
+      return ImageToolbox::DecodeRawTile(tile, GetPixelFormat(), GetTileWidth(level), GetTileHeight(level));
     }
     else
     {
--- a/Framework/Inputs/SingleLevelDecodedPyramid.h	Tue Jan 12 10:21:36 2021 +0100
+++ b/Framework/Inputs/SingleLevelDecodedPyramid.h	Tue Jan 12 14:24:18 2021 +0100
@@ -51,12 +51,12 @@
     {
     }
 
-    virtual unsigned int GetTileWidth() const ORTHANC_OVERRIDE
+    virtual unsigned int GetTileWidth(unsigned int level) const ORTHANC_OVERRIDE
     {
       return tileWidth_;
     }
 
-    virtual unsigned int GetTileHeight() const ORTHANC_OVERRIDE
+    virtual unsigned int GetTileHeight(unsigned int level) const ORTHANC_OVERRIDE
     {
       return tileHeight_;
     }
--- a/Framework/Inputs/TiledPyramidStatistics.h	Tue Jan 12 10:21:36 2021 +0100
+++ b/Framework/Inputs/TiledPyramidStatistics.h	Tue Jan 12 14:24:18 2021 +0100
@@ -55,14 +55,14 @@
       return source_.GetLevelHeight(level);
     }
 
-    virtual unsigned int GetTileWidth() const ORTHANC_OVERRIDE
+    virtual unsigned int GetTileWidth(unsigned int level) const ORTHANC_OVERRIDE
     {
-      return source_.GetTileWidth();
+      return source_.GetTileWidth(level);
     }
     
-    virtual unsigned int GetTileHeight() const ORTHANC_OVERRIDE
+    virtual unsigned int GetTileHeight(unsigned int level) const ORTHANC_OVERRIDE
     {
-      return source_.GetTileHeight();
+      return source_.GetTileHeight(level);
     }
 
     virtual Orthanc::PixelFormat GetPixelFormat() const ORTHANC_OVERRIDE
--- a/Framework/Jpeg2000Reader.cpp	Tue Jan 12 10:21:36 2021 +0100
+++ b/Framework/Jpeg2000Reader.cpp	Tue Jan 12 14:24:18 2021 +0100
@@ -25,6 +25,7 @@
 #include "ImageToolbox.h"
 
 #include <Compatibility.h>  // For std::unique_ptr
+#include <Images/ImageProcessing.h>
 #include <OrthancException.h>
 #include <SystemToolbox.h>
 
@@ -40,8 +41,9 @@
 typedef opj_dinfo_t opj_codec_t;
 typedef opj_cio_t opj_stream_t;
 #elif ORTHANC_OPENJPEG_MAJOR_VERSION == 2
+// OK, no need for compatibility macros
 #else
-#error Unsupported version of OpenJpeg
+#  error Unsupported version of OpenJpeg
 #endif
 
 
@@ -295,27 +297,80 @@
     {
     private:
       opj_image_t* image_;
+      
+      Orthanc::ImageAccessor* ExtractChannel(unsigned int channel)
+      {
+        const unsigned int width = image_->comps[channel].w;
+        const unsigned int height = image_->comps[channel].h;
+        
+        std::unique_ptr<Orthanc::ImageAccessor> target(
+          new Orthanc::Image(Orthanc::PixelFormat_Grayscale8, width, height, false));
 
+        const int32_t* q = image_->comps[channel].data;
+        assert(q != NULL);
+
+        for (unsigned int y = 0; y < height; y++)
+        {
+          uint8_t *p = reinterpret_cast<uint8_t*>(target->GetRow(y));
+
+          for (unsigned int x = 0; x < width; x++, p++, q++)
+          {
+            *p = *q;
+          }
+        }        
+
+        return target.release();
+      }
+      
       void CopyChannel(Orthanc::ImageAccessor& target,
                        unsigned int channel,
                        unsigned int targetIncrement)
       {
-        int32_t* q = image_->comps[channel].data;
-        assert(q != NULL);
-
         const unsigned int width = target.GetWidth();
         const unsigned int height = target.GetHeight();
 
-        for (unsigned int y = 0; y < height; y++)
+        if (0 &&   // TODO
+            width == image_->comps[channel].w &&
+            height == image_->comps[channel].h)
         {
-          uint8_t *p = reinterpret_cast<uint8_t*>(target.GetRow(y)) + channel;
+          const int32_t* q = image_->comps[channel].data;
+          assert(q != NULL);
+
+          for (unsigned int y = 0; y < height; y++)
+          {
+            uint8_t *p = reinterpret_cast<uint8_t*>(target.GetRow(y)) + channel;
 
-          for (unsigned int x = 0; x < width; x++, p += targetIncrement)
+            for (unsigned int x = 0; x < width; x++, p += targetIncrement)
+            {
+              *p = *q;
+              q++;
+            }
+          }
+        }
+        else
+        {
+          // This component is subsampled
+          std::unique_ptr<Orthanc::ImageAccessor> source(ExtractChannel(channel));
+          Orthanc::Image resized(Orthanc::PixelFormat_Grayscale8, width, height, false);
+          Orthanc::ImageProcessing::Resize(resized, *source);
+
+          assert(resized.GetWidth() == target.GetWidth() &&
+                 resized.GetHeight() == target.GetHeight() &&
+                 resized.GetFormat() == Orthanc::PixelFormat_Grayscale8 &&
+                 source->GetFormat() == Orthanc::PixelFormat_Grayscale8);            
+          
+          for (unsigned int y = 0; y < height; y++)
           {
-            *p = *q;
-            q++;
-          }
-        }        
+            const uint8_t *q = reinterpret_cast<const uint8_t*>(resized.GetConstRow(y));
+            uint8_t *p = reinterpret_cast<uint8_t*>(target.GetRow(y)) + channel;
+
+            for (unsigned int x = 0; x < width; x++, p += targetIncrement)
+            {
+              *p = *q;
+              q++;
+            }
+          }          
+        }
       }
 
     public:
@@ -375,12 +430,10 @@
 
         for (unsigned int c = 0; c < static_cast<unsigned int>(image_->numcomps); c++)
         {
-          if (image_->comps[c].dx != 1 ||
-              image_->comps[c].dy != 1 ||
-              image_->comps[c].x0 != 0 ||
+          if (image_->comps[c].x0 != 0 ||
               image_->comps[c].y0 != 0 ||
-              image_->comps[c].w != image_->x1 ||
-              image_->comps[c].h != image_->y1 ||
+              image_->comps[c].dx * image_->comps[c].w != image_->x1 ||
+              image_->comps[c].dy * image_->comps[c].h != image_->y1 ||
               image_->comps[c].prec != 8 ||
               image_->comps[c].sgnd != 0)
           {
@@ -429,7 +482,6 @@
 
         return image.release();
       }
-
     };
   }
 
@@ -442,11 +494,11 @@
     OpenJpegImage image(decoder, input);
     
     image_.reset(image.ProvideImage());
-    AssignReadOnly(image_->GetFormat(), 
+    AssignWritable(image_->GetFormat(), 
                    image_->GetWidth(),
                    image_->GetHeight(), 
                    image_->GetPitch(), 
-                   image_->GetConstBuffer());
+                   image_->GetBuffer());
   }
 
 
--- a/Framework/Outputs/InMemoryTiledImage.h	Tue Jan 12 10:21:36 2021 +0100
+++ b/Framework/Outputs/InMemoryTiledImage.h	Tue Jan 12 14:24:18 2021 +0100
@@ -69,6 +69,7 @@
 
     virtual unsigned int GetLevelHeight(unsigned int level) const ORTHANC_OVERRIDE;
 
+    // From IPyramidWriter (if used as an output)
     virtual unsigned int GetTileWidth() const ORTHANC_OVERRIDE
     {
       return tileWidth_;
@@ -79,6 +80,17 @@
       return tileHeight_;
     }
 
+    // From ITiledPyramid (if used as an input)
+    virtual unsigned int GetTileWidth(unsigned int level) const ORTHANC_OVERRIDE
+    {
+      return tileWidth_;
+    }
+
+    virtual unsigned int GetTileHeight(unsigned int level) const ORTHANC_OVERRIDE
+    {
+      return tileHeight_;
+    }
+
     virtual bool ReadRawTile(std::string& tile,
                              ImageCompression& compression,
                              unsigned int level,
--- a/NEWS	Tue Jan 12 10:21:36 2021 +0100
+++ b/NEWS	Tue Jan 12 14:24:18 2021 +0100
@@ -1,7 +1,9 @@
 Pending changes in the mainline
 ===============================
 
+* Viewer can display DICOM pyramids whose tile sizes vary across levels
 * Allow images without "Per frame functional groups sequence" tag (0x5200,0x9230)
+* Better handling of PhotometricInterpretation in viewer
 * Support of dynamic linking against the system-wide Orthanc framework library
 
 
--- a/ViewerPlugin/Plugin.cpp	Tue Jan 12 10:21:36 2021 +0100
+++ b/ViewerPlugin/Plugin.cpp	Tue Jan 12 14:24:18 2021 +0100
@@ -21,6 +21,7 @@
 
 #include "../Framework/PrecompiledHeadersWSI.h"
 
+#include "../Framework/ImageToolbox.h"
 #include "../Framework/Jpeg2000Reader.h"
 #include "DicomPyramidCache.h"
 #include "OrthancPluginConnection.h"
@@ -86,19 +87,20 @@
 
   OrthancWSI::DicomPyramidCache::Locker locker(*cache_, seriesId);
 
-  unsigned int tileWidth = locker.GetPyramid().GetTileWidth();
-  unsigned int tileHeight = locker.GetPyramid().GetTileHeight();
   unsigned int totalWidth = locker.GetPyramid().GetLevelWidth(0);
   unsigned int totalHeight = locker.GetPyramid().GetLevelHeight(0);
 
   Json::Value sizes = Json::arrayValue;
   Json::Value resolutions = Json::arrayValue;
   Json::Value tilesCount = Json::arrayValue;
+  Json::Value tilesSizes = Json::arrayValue;
   for (unsigned int i = 0; i < locker.GetPyramid().GetLevelCount(); i++)
   {
-    unsigned int levelWidth = locker.GetPyramid().GetLevelWidth(i);
-    unsigned int levelHeight = locker.GetPyramid().GetLevelHeight(i);
-
+    const unsigned int levelWidth = locker.GetPyramid().GetLevelWidth(i);
+    const unsigned int levelHeight = locker.GetPyramid().GetLevelHeight(i);
+    const unsigned int tileWidth = locker.GetPyramid().GetTileWidth(i);
+    const unsigned int tileHeight = locker.GetPyramid().GetTileHeight(i);
+    
     resolutions.append(static_cast<float>(totalWidth) / static_cast<float>(levelWidth));
     
     Json::Value s = Json::arrayValue;
@@ -110,15 +112,19 @@
     s.append(OrthancWSI::CeilingDivision(levelWidth, tileWidth));
     s.append(OrthancWSI::CeilingDivision(levelHeight, tileHeight));
     tilesCount.append(s);
+
+    s = Json::arrayValue;
+    s.append(tileWidth);
+    s.append(tileHeight);
+    tilesSizes.append(s);
   }
 
   Json::Value result;
   result["ID"] = seriesId;
   result["Resolutions"] = resolutions;
   result["Sizes"] = sizes;
-  result["TileHeight"] = tileHeight;
-  result["TileWidth"] = tileWidth;
   result["TilesCount"] = tilesCount;
+  result["TilesSizes"] = tilesSizes;
   result["TotalHeight"] = totalHeight;
   result["TotalWidth"] = totalWidth;
 
@@ -149,6 +155,7 @@
 
   // Retrieve the raw tile from the WSI pyramid
   OrthancWSI::ImageCompression compression;
+  Orthanc::PhotometricInterpretation photometric;
   Orthanc::PixelFormat format;
   std::string tile;
   unsigned int tileWidth, tileHeight;
@@ -157,8 +164,9 @@
     OrthancWSI::DicomPyramidCache::Locker locker(*cache_, seriesId);
 
     format = locker.GetPyramid().GetPixelFormat();
-    tileWidth = locker.GetPyramid().GetTileWidth();
-    tileHeight = locker.GetPyramid().GetTileHeight();
+    tileWidth = locker.GetPyramid().GetTileWidth(level);
+    tileHeight = locker.GetPyramid().GetTileHeight(level);
+    photometric = locker.GetPyramid().GetPhotometricInterpretation();
 
     if (!locker.GetPyramid().ReadRawTile(tile, compression, 
                                          static_cast<unsigned int>(level),
@@ -193,6 +201,12 @@
     case OrthancWSI::ImageCompression_Jpeg2000:
       decoded.reset(new OrthancWSI::Jpeg2000Reader);
       dynamic_cast<OrthancWSI::Jpeg2000Reader&>(*decoded).ReadFromMemory(tile);
+
+      if (photometric == Orthanc::PhotometricInterpretation_YBR_ICT)
+      {
+        OrthancWSI::ImageToolbox::ConvertJpegYCbCrToRgb(*decoded);
+      }
+      
       break;
 
     case OrthancWSI::ImageCompression_None:
--- a/ViewerPlugin/viewer.js	Tue Jan 12 10:21:36 2021 +0100
+++ b/ViewerPlugin/viewer.js	Tue Jan 12 14:24:18 2021 +0100
@@ -63,8 +63,6 @@
       success : function(series) {
         var width = series['TotalWidth'];
         var height = series['TotalHeight'];
-        var tileWidth = series['TileWidth'];
-        var tileHeight = series['TileHeight'];
         var countLevels = series['Resolutions'].length;
 
         // Maps always need a projection, but Zoomify layers are not geo-referenced, and
@@ -99,7 +97,7 @@
             tileGrid: new ol.tilegrid.TileGrid({
               extent: extent,
               resolutions: series['Resolutions'].reverse(),
-              tileSize: [tileWidth, tileHeight]
+              tileSizes: series['TilesSizes'].reverse()
             })
           }),
           wrapX: false,