Mercurial > hg > orthanc-wsi
changeset 340:82a922ecd03c
skeleton to serve on-the-fly tiles
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Sat, 07 Dec 2024 14:36:41 +0100 |
parents | f1e026052d76 |
children | f14e81852a6c |
files | Framework/ImageToolbox.cpp Framework/ImageToolbox.h Framework/Inputs/OnTheFlyPyramid.cpp Framework/Inputs/OnTheFlyPyramidsCache.cpp Framework/Inputs/OnTheFlyPyramidsCache.h ViewerPlugin/Plugin.cpp ViewerPlugin/RawTile.cpp |
diffstat | 7 files changed, 271 insertions(+), 35 deletions(-) [+] |
line wrap: on
line diff
--- a/Framework/ImageToolbox.cpp Sat Dec 07 14:36:26 2024 +0100 +++ b/Framework/ImageToolbox.cpp Sat Dec 07 14:36:41 2024 +0100 @@ -329,5 +329,24 @@ } #endif } + + + ImageCompression Convert(Orthanc::MimeType type) + { + switch (type) + { + case Orthanc::MimeType_Png: + return ImageCompression_Png; + + case Orthanc::MimeType_Jpeg: + return ImageCompression_Jpeg; + + case Orthanc::MimeType_Jpeg2000: + return ImageCompression_Jpeg2000; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } } }
--- a/Framework/ImageToolbox.h Sat Dec 07 14:36:26 2024 +0100 +++ b/Framework/ImageToolbox.h Sat Dec 07 14:36:41 2024 +0100 @@ -72,5 +72,7 @@ void CheckConstantTileSize(const ITiledPyramid& source); void ConvertJpegYCbCrToRgb(Orthanc::ImageAccessor& image /* inplace */); + + ImageCompression Convert(Orthanc::MimeType type); } }
--- a/Framework/Inputs/OnTheFlyPyramid.cpp Sat Dec 07 14:36:26 2024 +0100 +++ b/Framework/Inputs/OnTheFlyPyramid.cpp Sat Dec 07 14:36:41 2024 +0100 @@ -40,7 +40,45 @@ unsigned y) { isEmpty = false; - GetLevel(level).GetRegion(target, x, y, tileWidth_, tileHeight_); + + const Orthanc::ImageAccessor& source = GetLevel(level); + + unsigned int fromWidth; + if (x + tileWidth_ <= source.GetWidth()) + { + fromWidth = tileWidth_; + } + else + { + fromWidth = source.GetWidth() - x; + } + + unsigned int fromHeight; + if (y + tileHeight_ <= source.GetHeight()) + { + fromHeight = tileHeight_; + } + else + { + fromHeight = source.GetHeight() - y; + } + + if (fromWidth == tileWidth_ && + fromHeight == tileHeight_) + { + source.GetRegion(target, x, y, tileWidth_, tileHeight_); + } + else + { + uint8_t red, green, blue; + GetBackgroundColor(red, green, blue); + Orthanc::ImageProcessing::Set(target, 255, green, blue, 255); + + Orthanc::ImageAccessor from, to; + source.GetRegion(from, x, y, fromWidth, fromHeight); + target.GetRegion(to, x, y, fromWidth, fromHeight); + Orthanc::ImageProcessing::Copy(to, from); + } }
--- a/Framework/Inputs/OnTheFlyPyramidsCache.cpp Sat Dec 07 14:36:26 2024 +0100 +++ b/Framework/Inputs/OnTheFlyPyramidsCache.cpp Sat Dec 07 14:36:41 2024 +0100 @@ -49,7 +49,7 @@ } } - const DecodedTiledPyramid& GetPyramid() const + DecodedTiledPyramid& GetPyramid() const { assert(pyramid_ != NULL); return *pyramid_; @@ -106,10 +106,11 @@ MakeRoom(payload->GetMemoryUsage()); + memoryUsage_ += payload->GetMemoryUsage(); + // Add a new element to the cache and make it the most // recently used entry cache_.Add(identifier, payload.release()); - memoryUsage_ += payload->GetMemoryUsage(); assert(SanityCheck()); return result; @@ -179,7 +180,7 @@ } - OnTheFlyPyramidsCache::Accessor::Accessor(OnTheFlyPyramidsCache that, + OnTheFlyPyramidsCache::Accessor::Accessor(OnTheFlyPyramidsCache& that, const std::string &instanceId, unsigned int frameNumber): lock_(that.mutex_), @@ -210,7 +211,7 @@ } - const DecodedTiledPyramid & OnTheFlyPyramidsCache::Accessor::GetPyramid() const + DecodedTiledPyramid & OnTheFlyPyramidsCache::Accessor::GetPyramid() const { if (IsValid()) {
--- a/Framework/Inputs/OnTheFlyPyramidsCache.h Sat Dec 07 14:36:26 2024 +0100 +++ b/Framework/Inputs/OnTheFlyPyramidsCache.h Sat Dec 07 14:36:41 2024 +0100 @@ -89,7 +89,7 @@ CachedPyramid* pyramid_; public: - Accessor(OnTheFlyPyramidsCache that, + Accessor(OnTheFlyPyramidsCache& that, const std::string& instanceId, unsigned int frameNumber); @@ -108,7 +108,7 @@ return identifier_.second; } - const DecodedTiledPyramid& GetPyramid() const; + DecodedTiledPyramid& GetPyramid() const; }; }; }
--- a/ViewerPlugin/Plugin.cpp Sat Dec 07 14:36:26 2024 +0100 +++ b/ViewerPlugin/Plugin.cpp Sat Dec 07 14:36:41 2024 +0100 @@ -29,6 +29,7 @@ #include "../Framework/Inputs/DecodedTiledPyramid.h" #include "../Framework/Inputs/OnTheFlyPyramid.h" #include "../Framework/Inputs/OnTheFlyPyramidsCache.h" +#include "../Framework/ImageToolbox.h" #include <Compatibility.h> // For std::unique_ptr #include <Images/Image.h> @@ -44,6 +45,8 @@ #include <cassert> #include <Images/PngReader.h> +#include "OrthancPluginConnection.h" + #define ORTHANC_PLUGIN_NAME "wsi" @@ -71,13 +74,31 @@ DecodedTiledPyramid * Fetch(const std::string &instanceId, unsigned frameNumber) ORTHANC_OVERRIDE { - std::string png; - orthanc_->RestApiGet(png, "/instances/" + instanceId + "/frames/" + boost::lexical_cast<std::string>(frameNumber) + "/preview"); + OrthancPlugins::MemoryBuffer buffer; + buffer.GetDicomInstance(instanceId.c_str()); + + OrthancPlugins::DicomInstance dicom(buffer.GetData(), buffer.GetSize()); + + std::unique_ptr<OrthancPlugins::OrthancImage> frame(dicom.GetDecodedFrame(frameNumber)); - std::unique_ptr<Orthanc::PngReader> reader(new Orthanc::PngReader()); - reader->ReadFromMemory(png); + Orthanc::PixelFormat format; + switch (frame->GetPixelFormat()) + { + case OrthancPluginPixelFormat_RGB24: + format = Orthanc::PixelFormat_RGB24; + break; - return new OnTheFlyPyramid(reader.release(), 512, 512, smooth_); + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + Orthanc::ImageAccessor source; + source.AssignReadOnly(format, frame->GetWidth(), frame->GetHeight(), frame->GetPitch(), frame->GetBuffer()); + + std::unique_ptr<Orthanc::ImageAccessor> copy(new Orthanc::Image(Orthanc::PixelFormat_RGB24, source.GetWidth(), source.GetHeight(), false)); + Orthanc::ImageProcessing::Convert(*copy, source); + + return new OnTheFlyPyramid(copy.release(), 512, 512, smooth_); } }; } @@ -268,7 +289,172 @@ } -OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType, +void ServeFramePyramid(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + std::string instanceId(request->groups[0]); + int frameNumber = boost::lexical_cast<int>(request->groups[1]); + + LOG(INFO) << "Accessing pyramid of frame " << frameNumber << " in instance " << instanceId; + + if (frameNumber < 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + OrthancWSI::OnTheFlyPyramidsCache::Accessor accessor(OrthancWSI::OnTheFlyPyramidsCache::GetInstance(), instanceId, frameNumber); + + unsigned int totalWidth = accessor.GetPyramid().GetLevelWidth(0); + unsigned int totalHeight = accessor.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 < accessor.GetPyramid().GetLevelCount(); i++) + { + const unsigned int levelWidth = accessor.GetPyramid().GetLevelWidth(i); + const unsigned int levelHeight = accessor.GetPyramid().GetLevelHeight(i); + const unsigned int tileWidth = accessor.GetPyramid().GetTileWidth(i); + const unsigned int tileHeight = accessor.GetPyramid().GetTileHeight(i); + + resolutions.append(static_cast<float>(totalWidth) / static_cast<float>(levelWidth)); + + Json::Value s = Json::arrayValue; + s.append(levelWidth); + s.append(levelHeight); + sizes.append(s); + + s = Json::arrayValue; + 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"] = instanceId; + result["FrameNumber"] = frameNumber; + result["Resolutions"] = resolutions; + result["Sizes"] = sizes; + result["TilesCount"] = tilesCount; + result["TilesSizes"] = tilesSizes; + result["TotalHeight"] = totalHeight; + result["TotalWidth"] = totalWidth; + + { + uint8_t red, green, blue; + accessor.GetPyramid().GetBackgroundColor(red, green, blue); // TODO + + char tmp[64]; + sprintf(tmp, "#%02x%02x%02x", red, green, blue); + result["BackgroundColor"] = tmp; + } + + std::string s = result.toStyledString(); + OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, s.c_str(), s.size(), "application/json"); +} + + +void ServeFrameTile(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + std::string instanceId(request->groups[0]); + int frameNumber = boost::lexical_cast<int>(request->groups[1]); + int level = boost::lexical_cast<int>(request->groups[2]); + int tileY = boost::lexical_cast<int>(request->groups[4]); + int tileX = boost::lexical_cast<int>(request->groups[3]); + + char tmp[1024]; + sprintf(tmp, "Accessing on-the-fly tile in frame %d of instance %s: (%d,%d) at level %d", frameNumber, instanceId.c_str(), tileX, tileY, level); + OrthancPluginLogInfo(OrthancPlugins::GetGlobalContext(), tmp); + + if (frameNumber < 0 || + level < 0 || + tileX < 0 || + tileY < 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + std::unique_ptr<Orthanc::ImageAccessor> tile; + + { + OrthancWSI::OnTheFlyPyramidsCache::Accessor accessor(OrthancWSI::OnTheFlyPyramidsCache::GetInstance(), instanceId, frameNumber); + if (!accessor.IsValid()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource); + } + + bool isEmpty; // Ignored + tile.reset(accessor.GetPyramid().DecodeTile(isEmpty, level, tileX, tileY)); + } + + Orthanc::MimeType mime = Orthanc::MimeType_Png; // By default, use lossless compression + + // Lookup whether a "Accept" HTTP header is present, to overwrite + // the default MIME type + for (uint32_t i = 0; i < request->headersCount; i++) + { + std::string key(request->headersKeys[i]); + Orthanc::Toolbox::ToLowerCase(key); + + if (key == "accept") + { + std::vector<std::string> tokens; + Orthanc::Toolbox::TokenizeString(tokens, request->headersValues[i], ','); + + bool found = false; + + for (size_t j = 0; j < tokens.size(); j++) + { + std::string s = Orthanc::Toolbox::StripSpaces(tokens[j]); + + if (s == Orthanc::EnumerationToString(Orthanc::MimeType_Png)) + { + mime = Orthanc::MimeType_Png; + found = true; + } + else if (s == Orthanc::EnumerationToString(Orthanc::MimeType_Jpeg)) + { + mime = Orthanc::MimeType_Jpeg; + found = true; + } + else if (s == Orthanc::EnumerationToString(Orthanc::MimeType_Jpeg2000)) + { + mime = Orthanc::MimeType_Jpeg2000; + found = true; + } + else if (s == "*/*" || + s == "image/*") + { + found = true; + } + } + + if (!found) + { + OrthancPluginSendHttpStatusCode(OrthancPlugins::GetGlobalContext(), output, 406 /* Not acceptable */); + return; + } + } + } + + std::string encoded; + OrthancWSI::ImageToolbox::EncodeTile(encoded, *tile, OrthancWSI::ImageToolbox::Convert(mime), 90 /* only used for JPEG */); + + OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, encoded.c_str(), + encoded.size(), Orthanc::EnumerationToString(mime)); +} + + +OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType, OrthancPluginResourceType resourceType, const char *resourceId) { @@ -342,7 +528,7 @@ { ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context) { - OrthancPlugins::SetGlobalContext(context); + OrthancPlugins::SetGlobalContext(context, ORTHANC_PLUGIN_NAME); assert(DisplayPerformanceWarning()); /* Check the version of the Orthanc core */ @@ -364,7 +550,9 @@ return -1; } -#if ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 7, 2) +#if ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 12, 4) + Orthanc::Logging::InitializePluginContext(context, ORTHANC_PLUGIN_NAME); +#elif ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 7, 2) Orthanc::Logging::InitializePluginContext(context); #else Orthanc::Logging::Initialize(context); @@ -383,6 +571,10 @@ OrthancPlugins::SetDescription(ORTHANC_PLUGIN_NAME, "Provides a Web viewer of whole-slide microscopic images within Orthanc."); OrthancWSI::DicomPyramidCache::InitializeInstance(10 /* Number of pyramids to be cached - TODO parameter */); + OrthancWSI::OnTheFlyPyramidsCache::InitializeInstance( + new OrthancWSI::OrthancPyramidFrameFetcher(new OrthancWSI::OrthancPluginConnection(), false /* TODO PARAMETER */), + 10 /* TODO - PARAMETER */, + 256 * 1024 * 1024 /* TODO - PARAMETER */); OrthancPluginRegisterOnChangeCallback(OrthancPlugins::GetGlobalContext(), OnChangeCallback); @@ -392,6 +584,8 @@ OrthancPlugins::RegisterRestCallback<ServeFile>("/wsi/app/(viewer.js)", true); OrthancPlugins::RegisterRestCallback<ServePyramid>("/wsi/pyramids/([0-9a-f-]+)", true); OrthancPlugins::RegisterRestCallback<ServeTile>("/wsi/tiles/([0-9a-f-]+)/([0-9-]+)/([0-9-]+)/([0-9-]+)", true); + OrthancPlugins::RegisterRestCallback<ServeFramePyramid>("/wsi/frames-pyramids/([0-9a-f-]+)/([0-9-]+)", true); + OrthancPlugins::RegisterRestCallback<ServeFrameTile>("/wsi/frames-tiles/([0-9a-f-]+)/([0-9-]+)/([0-9-]+)/([0-9-]+)/([0-9-]+)", true); OrthancPlugins::OrthancConfiguration mainConfiguration; @@ -475,6 +669,7 @@ ORTHANC_PLUGINS_API void OrthancPluginFinalize() { + OrthancWSI::OnTheFlyPyramidsCache::FinalizeInstance(); OrthancWSI::DicomPyramidCache::FinalizeInstance(); OrthancWSI::RawTile::FinalizeTranscoderSemaphore(); }
--- a/ViewerPlugin/RawTile.cpp Sat Dec 07 14:36:26 2024 +0100 +++ b/ViewerPlugin/RawTile.cpp Sat Dec 07 14:36:41 2024 +0100 @@ -40,25 +40,6 @@ namespace OrthancWSI { - static ImageCompression Convert(Orthanc::MimeType type) - { - switch (type) - { - case Orthanc::MimeType_Png: - return ImageCompression_Png; - - case Orthanc::MimeType_Jpeg: - return ImageCompression_Jpeg; - - case Orthanc::MimeType_Jpeg2000: - return ImageCompression_Jpeg2000; - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); - } - } - - Orthanc::ImageAccessor* RawTile::DecodeInternal() { switch (compression_) @@ -106,7 +87,7 @@ const Orthanc::ImageAccessor& decoded, Orthanc::MimeType encoding) { - ImageToolbox::EncodeTile(encoded, decoded, Convert(encoding), 90 /* only used for JPEG */); + ImageToolbox::EncodeTile(encoded, decoded, ImageToolbox::Convert(encoding), 90 /* only used for JPEG */); }