# HG changeset patch # User Sebastien Jodogne # Date 1610457858 -3600 # Node ID 20bc074ec19a9df50a5510823b5a416f2eb4570f # Parent c35a3a0627b9445f42e9e8342444cbe4d6e4c337 Viewer can display DICOM pyramids whose tile sizes vary across levels diff -r c35a3a0627b9 -r 20bc074ec19a Applications/DicomToTiff.cpp --- 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(), 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 { diff -r c35a3a0627b9 -r 20bc074ec19a Applications/Dicomizer.cpp --- 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"; diff -r c35a3a0627b9 -r 20bc074ec19a Framework/Algorithms/PyramidReader.cpp --- 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) diff -r c35a3a0627b9 -r 20bc074ec19a Framework/Algorithms/TranscodeTileCommand.cpp --- 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; diff -r c35a3a0627b9 -r 20bc074ec19a Framework/DicomizerParameters.cpp --- 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 diff -r c35a3a0627b9 -r 20bc074ec19a Framework/DicomizerParameters.h --- 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); diff -r c35a3a0627b9 -r 20bc074ec19a Framework/Enumerations.h --- 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 diff -r c35a3a0627b9 -r 20bc074ec19a Framework/ImageToolbox.cpp --- 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 tile(pyramid.DecodeTile(level, - x / pyramid.GetTileWidth(), - y / pyramid.GetTileHeight())); + std::unique_ptr 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(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(result[i]); + } + } + } + } +#endif + } } } diff -r c35a3a0627b9 -r 20bc074ec19a Framework/ImageToolbox.h --- 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 */); } } diff -r c35a3a0627b9 -r 20bc074ec19a Framework/Inputs/DecodedTiledPyramid.cpp --- 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 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 { diff -r c35a3a0627b9 -r 20bc074ec19a Framework/Inputs/DicomPyramid.cpp --- 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(); } diff -r c35a3a0627b9 -r 20bc074ec19a Framework/Inputs/DicomPyramid.h --- 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, diff -r c35a3a0627b9 -r 20bc074ec19a Framework/Inputs/DicomPyramidInstance.cpp --- 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 #include +#include #include #include -#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(compression_); + content[PIXEL_FORMAT] = static_cast(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(Orthanc::SerializationToolbox::ReadInteger(content, IMAGE_COMPRESSION)); + format_ = static_cast(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(content["TileHeight"].asInt()); - tileWidth_ = static_cast(content["TileWidth"].asInt()); - totalHeight_ = static_cast(content["TotalHeight"].asInt()); - totalWidth_ = static_cast(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++) diff -r c35a3a0627b9 -r 20bc074ec19a Framework/Inputs/HierarchicalTiff.h --- 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_; } diff -r c35a3a0627b9 -r 20bc074ec19a Framework/Inputs/ITiledPyramid.h --- 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, diff -r c35a3a0627b9 -r 20bc074ec19a Framework/Inputs/OpenSlidePyramid.h --- 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_; } diff -r c35a3a0627b9 -r 20bc074ec19a Framework/Inputs/PyramidWithRawTiles.cpp --- 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 { diff -r c35a3a0627b9 -r 20bc074ec19a Framework/Inputs/SingleLevelDecodedPyramid.h --- 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_; } diff -r c35a3a0627b9 -r 20bc074ec19a Framework/Inputs/TiledPyramidStatistics.h --- 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 diff -r c35a3a0627b9 -r 20bc074ec19a Framework/Jpeg2000Reader.cpp --- 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 // For std::unique_ptr +#include #include #include @@ -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 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(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(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(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 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(resized.GetConstRow(y)); + uint8_t *p = reinterpret_cast(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(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()); } diff -r c35a3a0627b9 -r 20bc074ec19a Framework/Outputs/InMemoryTiledImage.h --- 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, diff -r c35a3a0627b9 -r 20bc074ec19a NEWS --- 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 diff -r c35a3a0627b9 -r 20bc074ec19a ViewerPlugin/Plugin.cpp --- 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(totalWidth) / static_cast(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(level), @@ -193,6 +201,12 @@ case OrthancWSI::ImageCompression_Jpeg2000: decoded.reset(new OrthancWSI::Jpeg2000Reader); dynamic_cast(*decoded).ReadFromMemory(tile); + + if (photometric == Orthanc::PhotometricInterpretation_YBR_ICT) + { + OrthancWSI::ImageToolbox::ConvertJpegYCbCrToRgb(*decoded); + } + break; case OrthancWSI::ImageCompression_None: diff -r c35a3a0627b9 -r 20bc074ec19a ViewerPlugin/viewer.js --- 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,