Mercurial > hg > orthanc-wsi
changeset 369:3adb57efc32f
the viewer now displays the scale
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Mon, 17 Mar 2025 13:27:36 +0100 (2 months ago) |
parents | 60100f5effee |
children | 5fb0dfa5c155 |
files | Framework/Inputs/DicomPyramid.cpp Framework/Inputs/DicomPyramid.h Framework/Inputs/DicomPyramidInstance.cpp Framework/Inputs/DicomPyramidInstance.h NEWS ViewerPlugin/DicomPyramidCache.cpp ViewerPlugin/DicomPyramidCache.h ViewerPlugin/Plugin.cpp ViewerPlugin/viewer.js |
diffstat | 9 files changed, 163 insertions(+), 17 deletions(-) [+] |
line wrap: on
line diff
--- a/Framework/Inputs/DicomPyramid.cpp Mon Mar 17 11:34:33 2025 +0100 +++ b/Framework/Inputs/DicomPyramid.cpp Mon Mar 17 13:27:36 2025 +0100 @@ -271,4 +271,34 @@ assert(!instances_.empty() && instances_[0] != NULL); return instances_[0]->GetPhotometricInterpretation(); } + + + bool DicomPyramid::LookupImagedVolumeSize(double& width, + double& height) const + { + bool found = false; + + for (size_t i = 0; i < instances_.size(); i++) + { + assert(instances_[i] != NULL); + + if (instances_[i]->HasImagedVolumeSize()) + { + if (!found) + { + found = true; + width = instances_[i]->GetImagedVolumeWidth(); + height = instances_[i]->GetImagedVolumeHeight(); + } + else if (std::abs(width - instances_[i]->GetImagedVolumeWidth()) > 100.0 * std::numeric_limits<double>::epsilon() || + std::abs(height - instances_[i]->GetImagedVolumeHeight()) > 100.0 * std::numeric_limits<double>::epsilon()) + { + LOG(WARNING) << "Inconsistency of imaged volume width/height in series: " << seriesId_; + return false; + } + } + } + + return found; + } }
--- a/Framework/Inputs/DicomPyramid.h Mon Mar 17 11:34:33 2025 +0100 +++ b/Framework/Inputs/DicomPyramid.h Mon Mar 17 13:27:36 2025 +0100 @@ -103,5 +103,8 @@ { return backgroundBlue_; } + + bool LookupImagedVolumeSize(double& width, + double& height) const; }; }
--- a/Framework/Inputs/DicomPyramidInstance.cpp Mon Mar 17 11:34:33 2025 +0100 +++ b/Framework/Inputs/DicomPyramidInstance.cpp Mon Mar 17 13:27:36 2025 +0100 @@ -41,6 +41,7 @@ #endif #define SERIALIZED_METADATA "4201" // Was "4200" if versions <= 0.7 of this plugin +#define SERIALIZED_VERSION "2" // Introduced in WSI 3.1 namespace OrthancWSI @@ -53,6 +54,8 @@ static const Orthanc::DicomTag DICOM_TAG_TOTAL_PIXEL_MATRIX_ROWS(0x0048, 0x0007); static const Orthanc::DicomTag DICOM_TAG_IMAGE_TYPE(0x0008, 0x0008); static const Orthanc::DicomTag DICOM_TAG_RECOMMENDED_ABSENT_PIXEL_CIELAB(0x0048, 0x0015); + static const Orthanc::DicomTag DICOM_TAG_IMAGED_VOLUME_WIDTH(0x0048, 0x0001); + static const Orthanc::DicomTag DICOM_TAG_IMAGED_VOLUME_HEIGHT(0x0048, 0x0002); static ImageCompression DetectImageCompression(OrthancStone::IOrthancConnection& orthanc, const std::string& instanceId) @@ -282,6 +285,11 @@ backgroundBlue_ = rgb.GetB(); } } + + // New in WSI 3.1 + hasImagedVolumeSize_ = ( + reader.GetDoubleValue(imagedVolumeWidth_, Orthanc::DicomPath(DICOM_TAG_IMAGED_VOLUME_WIDTH)) && + reader.GetDoubleValue(imagedVolumeHeight_, Orthanc::DicomPath(DICOM_TAG_IMAGED_VOLUME_HEIGHT))); } @@ -294,7 +302,10 @@ hasBackgroundColor_(false), backgroundRed_(0), backgroundGreen_(0), - backgroundBlue_(0) + backgroundBlue_(0), + hasImagedVolumeSize_(false), + imagedVolumeWidth_(0), + imagedVolumeHeight_(0) { if (useCache) { @@ -303,8 +314,10 @@ // Try and deserialized the cached information about this instance std::string serialized; orthanc.RestApiGet(serialized, "/instances/" + instanceId + "/metadata/" + SERIALIZED_METADATA); - Deserialize(serialized); - return; // Success + if (Deserialize(serialized)) + { + return; // Success + } } catch (Orthanc::OrthancException&) { @@ -351,6 +364,8 @@ static const char* const PHOTOMETRIC_INTERPRETATION = "PhotometricInterpretation"; static const char* const IMAGE_TYPE = "ImageType"; static const char* const BACKGROUND_COLOR = "BackgroundColor"; + static const char* const VERSION = "Version"; + static const char* const IMAGED_VOLUME_SIZE = "ImagedVolumeSize"; void DicomPyramidInstance::Serialize(std::string& result) const @@ -378,6 +393,7 @@ content[TOTAL_HEIGHT] = totalHeight_; content[PHOTOMETRIC_INTERPRETATION] = Orthanc::EnumerationToString(photometric_); content[IMAGE_TYPE] = imageType_; + content[VERSION] = SERIALIZED_VERSION; if (hasBackgroundColor_) { @@ -388,6 +404,14 @@ content[BACKGROUND_COLOR] = color; } + if (hasImagedVolumeSize_) + { + Json::Value size = Json::arrayValue; + size.append(imagedVolumeWidth_); + size.append(imagedVolumeHeight_); + content[IMAGED_VOLUME_SIZE] = size; + } + #if ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 9, 0) Orthanc::Toolbox::WriteFastJson(result, content); #else @@ -397,7 +421,7 @@ } - void DicomPyramidInstance::Deserialize(const std::string& s) + bool DicomPyramidInstance::Deserialize(const std::string& s) { Json::Value content; OrthancStone::IOrthancConnection::ParseJson(content, s); @@ -409,6 +433,12 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); } + std::string version = Orthanc::SerializationToolbox::ReadString(content, VERSION, "1"); + if (version != SERIALIZED_VERSION) + { + return false; // Serialized using a different version of the plugin, must be reconstructed + } + 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)); @@ -457,6 +487,23 @@ backgroundBlue_ = color[2].asUInt(); } } + + hasImagedVolumeSize_ = false; + if (content.isMember(IMAGED_VOLUME_SIZE)) + { + const Json::Value& size = content[IMAGED_VOLUME_SIZE]; + if (size.type() == Json::arrayValue && + size.size() == 2u && + size[0].isDouble() && + size[1].isDouble()) + { + hasImagedVolumeSize_ = true; + imagedVolumeWidth_ = size[0].asDouble(); + imagedVolumeHeight_ = size[1].asDouble(); + } + } + + return true; // Success } @@ -497,4 +544,30 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } } + + + double DicomPyramidInstance::GetImagedVolumeWidth() const + { + if (hasImagedVolumeSize_) + { + return imagedVolumeWidth_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + double DicomPyramidInstance::GetImagedVolumeHeight() const + { + if (hasImagedVolumeSize_) + { + return imagedVolumeHeight_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } }
--- a/Framework/Inputs/DicomPyramidInstance.h Mon Mar 17 11:34:33 2025 +0100 +++ b/Framework/Inputs/DicomPyramidInstance.h Mon Mar 17 13:27:36 2025 +0100 @@ -51,11 +51,14 @@ uint8_t backgroundRed_; uint8_t backgroundGreen_; uint8_t backgroundBlue_; + bool hasImagedVolumeSize_; + double imagedVolumeWidth_; + double imagedVolumeHeight_; void Load(OrthancStone::IOrthancConnection& orthanc, const std::string& instanceId); - void Deserialize(const std::string& content); + bool Deserialize(const std::string& content); public: DicomPyramidInstance(OrthancStone::IOrthancConnection& orthanc, @@ -125,5 +128,14 @@ uint8_t GetBackgroundGreen() const; uint8_t GetBackgroundBlue() const; + + bool HasImagedVolumeSize() const + { + return hasImagedVolumeSize_; + } + + double GetImagedVolumeWidth() const; + + double GetImagedVolumeHeight() const; }; }
--- a/NEWS Mon Mar 17 11:34:33 2025 +0100 +++ b/NEWS Mon Mar 17 13:27:36 2025 +0100 @@ -2,6 +2,7 @@ =============================== * Upgraded to OpenLayers 10.4.0 (was previously 3.19.0) +* The viewer now displays the scale if the imaged volume size is available * Fix handling of "Image Type" in the viewer for compatibility with other vendors Compatibility notes about the viewer
--- a/ViewerPlugin/DicomPyramidCache.cpp Mon Mar 17 11:34:33 2025 +0100 +++ b/ViewerPlugin/DicomPyramidCache.cpp Mon Mar 17 13:27:36 2025 +0100 @@ -57,7 +57,8 @@ DicomPyramid& DicomPyramidCache::GetPyramid(const std::string& seriesId, - boost::mutex::scoped_lock& lock) + boost::mutex::scoped_lock& lock, + bool useMetadataCache) { // Mutex is assumed to be locked @@ -75,7 +76,7 @@ assert(orthanc_.get() != NULL); std::unique_ptr<DicomPyramid> pyramid - (new DicomPyramid(*orthanc_, seriesId, true /* use metadata cache */)); + (new DicomPyramid(*orthanc_, seriesId, useMetadataCache)); { // The pyramid is constructed: Store it into the cache @@ -120,9 +121,11 @@ DicomPyramidCache::DicomPyramidCache(OrthancStone::IOrthancConnection* orthanc /* takes ownership */, - size_t maxSize) : + size_t maxSize, + bool useMetadataCache) : orthanc_(orthanc), - maxSize_(maxSize) + maxSize_(maxSize), + useMetadataCache_(useMetadataCache) { if (orthanc == NULL) { @@ -146,11 +149,12 @@ } - void DicomPyramidCache::InitializeInstance(size_t maxSize) + void DicomPyramidCache::InitializeInstance(size_t maxSize, + bool useMetadataCache) { if (singleton_.get() == NULL) { - singleton_.reset(new DicomPyramidCache(new OrthancWSI::OrthancPluginConnection, maxSize)); + singleton_.reset(new DicomPyramidCache(new OrthancWSI::OrthancPluginConnection, maxSize, useMetadataCache)); } else { @@ -204,7 +208,7 @@ DicomPyramidCache::Locker::Locker(const std::string& seriesId) : cache_(DicomPyramidCache::GetInstance()), lock_(cache_.mutex_), - pyramid_(cache_.GetPyramid(seriesId, lock_)) + pyramid_(cache_.GetPyramid(seriesId, lock_, cache_.useMetadataCache_)) { } }
--- a/ViewerPlugin/DicomPyramidCache.h Mon Mar 17 11:34:33 2025 +0100 +++ b/ViewerPlugin/DicomPyramidCache.h Mon Mar 17 13:27:36 2025 +0100 @@ -43,19 +43,23 @@ boost::mutex mutex_; size_t maxSize_; Cache cache_; + bool useMetadataCache_; DicomPyramidCache(OrthancStone::IOrthancConnection* orthanc /* takes ownership */, - size_t maxSize); + size_t maxSize, + bool useMetadataCache); DicomPyramid* GetCachedPyramid(const std::string& seriesId); DicomPyramid& GetPyramid(const std::string& seriesId, - boost::mutex::scoped_lock& lock); + boost::mutex::scoped_lock& lock, + bool useMetadataCache); public: ~DicomPyramidCache(); - static void InitializeInstance(size_t maxSize); + static void InitializeInstance(size_t maxSize, + bool useMetadataCache); static void FinalizeInstance();
--- a/ViewerPlugin/Plugin.cpp Mon Mar 17 11:34:33 2025 +0100 +++ b/ViewerPlugin/Plugin.cpp Mon Mar 17 13:27:36 2025 +0100 @@ -129,6 +129,14 @@ locker.GetPyramid().GetBackgroundBlue()); answer["BackgroundColor"] = tmp; } + + // New in WSI 3.1 + double imagedVolumeWidth, imagedVolumeHeight; + if (locker.GetPyramid().LookupImagedVolumeSize(imagedVolumeWidth, imagedVolumeHeight)) + { + answer["ImagedVolumeWidth"] = imagedVolumeWidth; + answer["ImagedVolumeHeight"] = imagedVolumeHeight; + } } std::string s = answer.toStyledString(); @@ -501,7 +509,8 @@ 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::DicomPyramidCache::InitializeInstance(10 /* Number of pyramids to be cached - TODO parameter */, + true /* Use the metadata cache - Should be "false" only during development */); { std::unique_ptr<OrthancWSI::OrthancPyramidFrameFetcher> fetcher(
--- a/ViewerPlugin/viewer.js Mon Mar 17 11:34:33 2025 +0100 +++ b/ViewerPlugin/viewer.js Mon Mar 17 13:27:36 2025 +0100 @@ -56,12 +56,22 @@ var height = pyramid['TotalHeight']; var countLevels = pyramid['Resolutions'].length; + var metersPerUnit = null; + var imagedVolumeWidth = pyramid['ImagedVolumeWidth']; // In millimeters + var imagedVolumeHeight = pyramid['ImagedVolumeHeight']; + if (imagedVolumeWidth !== undefined && + imagedVolumeHeight !== undefined) { + metersPerUnit = parseFloat(imagedVolumeWidth) / (1000.0 * parseFloat(height)); + //metersPerUnit = parseFloat(imagedVolumeHeight) / (1000.0 * parseFloat(width)); + } + // Maps always need a projection, but Zoomify layers are not geo-referenced, and // are only measured in pixels. So, we create a fake projection that the map // can use to properly display the layer. var proj = new ol.proj.Projection({ code: 'pixel', - units: 'pixels', + units: 'pixel', + metersPerUnit: metersPerUnit, extent: [0, 0, width, height] });