# HG changeset patch # User Sebastien Jodogne # Date 1729604440 -7200 # Node ID 8e3c403cc6430c5969b288fd57d5a6f6aa6fdcd7 # Parent 7e8b918b0482e990dc5b7e6bda2cc0114fc1a609 Improved support of the (0028,9132) tag for Philips multiframe images diff -r 7e8b918b0482 -r 8e3c403cc643 Applications/StoneWebViewer/NEWS --- a/Applications/StoneWebViewer/NEWS Mon Oct 21 16:02:28 2024 +0200 +++ b/Applications/StoneWebViewer/NEWS Tue Oct 22 15:40:40 2024 +0200 @@ -1,6 +1,7 @@ Pending changes in the mainline =============================== +* Improved support of the (0028,9132) tag for Philips multiframe images * Remember the previous layout when re-opening the viewer. * Added a Print button in the PDF viewer toolbar. * Added a Download button in the PDF viewer toolbar. diff -r 7e8b918b0482 -r 8e3c403cc643 Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp --- a/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp Mon Oct 21 16:02:28 2024 +0200 +++ b/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp Tue Oct 22 15:40:40 2024 +0200 @@ -120,7 +120,8 @@ ThumbnailType_Pdf, ThumbnailType_Video, ThumbnailType_Loading, - ThumbnailType_Unknown + ThumbnailType_Unknown, + ThumbnailType_Unavailable }; @@ -204,6 +205,76 @@ +enum WindowingState +{ + WindowingState_None = 1, + WindowingState_Fallback = 2, + WindowingState_GlobalPreset = 3, + WindowingState_FramePreset = 4, + WindowingState_User = 5 +}; + + +class WindowingTracker +{ +private: + WindowingState state_; + OrthancStone::Windowing windowing_; + +public: + WindowingTracker() : + state_(WindowingState_None) + { + } + + WindowingState GetState() const + { + return state_; + } + + const OrthancStone::Windowing& GetWindowing() const + { + return windowing_; + } + + void Reset() + { + state_ = WindowingState_None; + windowing_ = OrthancStone::Windowing(); + } + + // Returns "true" iif. the windowing needed an update + bool Update(WindowingState newState, + const OrthancStone::Windowing& newWindowing) + { + if (newState == WindowingState_None) + { + // "Reset()" should have been called + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + if (newState >= state_) + { + state_ = newState; + + if (windowing_.IsNear(newWindowing)) + { + return false; + } + else + { + windowing_ = newWindowing; + return true; + } + } + else + { + return false; + } + } +}; + + class IFramesCollection : public boost::noncopyable { public: @@ -1122,6 +1193,79 @@ +class InstancesCache : public boost::noncopyable +{ +private: + // Maps "SOP Instance UID" to DICOM parameters + typedef std::map Content; + + Content content_; + + void Clear() + { + for (Content::iterator it = content_.begin(); it != content_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + + content_.clear(); + } + +public: + ~InstancesCache() + { + Clear(); + } + + void Store(const std::string& sopInstanceUid, + const OrthancStone::DicomInstanceParameters& parameters) + { + Content::iterator found = content_.find(sopInstanceUid); + if (found == content_.end()) + { + content_[sopInstanceUid] = parameters.Clone(); + } + } + + class Accessor : public boost::noncopyable + { + private: + std::unique_ptr parameters_; + + public: + Accessor(InstancesCache& that, + const std::string& sopInstanceUid) + { + Content::iterator found = that.content_.find(sopInstanceUid); + if (found != that.content_.end()) + { + assert(found->second != NULL); + parameters_.reset(found->second->Clone()); + } + } + + bool IsValid() const + { + return parameters_.get() != NULL; + } + + const OrthancStone::DicomInstanceParameters& GetParameters() const + { + if (IsValid()) + { + return *parameters_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + }; +}; + + + class SeriesCursor : public boost::noncopyable { public: @@ -1785,14 +1929,7 @@ GetViewport().windowingPresets_[i] = params.GetWindowingPreset(i); } - if (params.GetWindowingPresetsCount() == 0) - { - LOG(INFO) << "No preset windowing"; - } - - GetViewport().fallbackWindowing_ = params.GetFallbackWindowing(); - - GetViewport().SetWindowingPreset(); + GetViewport().SetDefaultWindowing(params); } uint32_t cineRate; @@ -1976,16 +2113,6 @@ } else { - if (GetViewport().windowingPresets_.empty()) - { - // New in Stone Web viewer 2.2: Deal with Philips multiframe - // (cf. mail from Tomas Kenda on 2021-08-17) - double windowingCenter, windowingWidth; - message.GetDicom().GetDefaultWindowing(windowingCenter, windowingWidth, frameNumber_); - GetViewport().windowingPresets_.push_back(OrthancStone::Windowing(windowingCenter, windowingWidth)); - GetViewport().SetWindowingPreset(); - } - Apply(GetViewport(), message.GetDicom(), frame.release(), sopInstanceUid_, frameNumber_); if (isPrefetch_) @@ -2007,6 +2134,7 @@ dicom.ExtractDicomSummary(tags, ORTHANC_STONE_MAX_TAG_LENGTH); OrthancStone::DicomInstanceParameters parameters(tags); + viewport.instancesCache_->Store(sopInstanceUid, parameters); std::unique_ptr converted; @@ -2076,12 +2204,12 @@ boost::shared_ptr viewport_; boost::shared_ptr loader_; OrthancStone::DicomSource source_; - boost::shared_ptr framesCache_; + boost::shared_ptr framesCache_; + boost::shared_ptr instancesCache_; std::unique_ptr frames_; std::unique_ptr cursor_; - OrthancStone::Windowing currentWindowing_; + WindowingTracker windowingTracker_; std::vector windowingPresets_; - OrthancStone::Windowing fallbackWindowing_; unsigned int cineRate_; bool inverted_; bool fitNextContent_; @@ -2109,6 +2237,37 @@ bool linearInterpolation_; + void UpdateWindowing(WindowingState state, + const OrthancStone::Windowing& windowing) + { + if (windowingTracker_.Update(state, windowing)) + { + UpdateCurrentTextureParameters(); + + if (observer_.get() != NULL) + { + observer_->SignalWindowingUpdated(*this, windowingTracker_.GetWindowing()); + } + } + } + + + void SetDefaultWindowing(const OrthancStone::DicomInstanceParameters& instance) + { + windowingTracker_.Reset(); + + if (instance.GetWindowingPresetsCount() == 0) + { + LOG(INFO) << "No preset windowing"; + UpdateWindowing(WindowingState_Fallback, instance.GetFallbackWindowing()); + } + else + { + UpdateWindowing(WindowingState_GlobalPreset, instance.GetWindowingPreset(0)); + } + } + + void ScheduleNextPrefetch() { while (!prefetchQueue_.empty()) @@ -2231,9 +2390,22 @@ case Orthanc::PixelFormat_Float32: { + { + // New in Stone Web viewer 2.2: Deal with Philips multiframe + // (cf. mail from Tomas Kenda on 2021-08-17) + InstancesCache::Accessor accessor(*instancesCache_, instance.GetSopInstanceUid()); + OrthancStone::Windowing windowing; + if (accessor.IsValid() && + accessor.GetParameters().LookupPerFrameWindowing(windowing, frameIndex)) + { + UpdateWindowing(WindowingState_FramePreset, windowing); + } + } + std::unique_ptr tmp( new OrthancStone::FloatTextureSceneLayer(frame)); - tmp->SetCustomWindowing(currentWindowing_.GetCenter(), currentWindowing_.GetWidth()); + + tmp->SetCustomWindowing(windowingTracker_.GetWindowing().GetCenter(), windowingTracker_.GetWindowing().GetWidth()); tmp->SetInverted(inverted_ ^ isMonochrome1); layer.reset(tmp.release()); break; @@ -2559,14 +2731,14 @@ std::map headers, arguments; // arguments["quality"] = "10"; // Low-level quality for test purpose arguments["window"] = ( - boost::lexical_cast(currentWindowing_.GetCenter()) + "," + - boost::lexical_cast(currentWindowing_.GetWidth()) + ",linear"); + boost::lexical_cast(windowingTracker_.GetWindowing().GetCenter()) + "," + + boost::lexical_cast(windowingTracker_.GetWindowing().GetWidth()) + ",linear"); std::unique_ptr command( source_.CreateDicomWebCommand( uri, arguments, headers, new SetLowQualityFrame( GetSharedObserver(), instance.GetSopInstanceUid(), frameNumber, - currentWindowing_, isMonochrome1, isPrefetch))); + windowingTracker_.GetWindowing(), isMonochrome1, isPrefetch))); { std::unique_ptr lock(context_.Lock()); @@ -2586,7 +2758,7 @@ { dynamic_cast( lock->GetController().GetScene().GetLayer(LAYER_TEXTURE)). - SetCustomWindowing(currentWindowing_.GetCenter(), currentWindowing_.GetWidth()); + SetCustomWindowing(windowingTracker_.GetWindowing().GetCenter(), windowingTracker_.GetWindowing().GetWidth()); } lock->Invalidate(); @@ -2597,11 +2769,13 @@ const OrthancStone::DicomSource& source, const std::string& canvas, boost::shared_ptr cache, + boost::shared_ptr instancesCache, bool softwareRendering, bool linearInterpolation) : context_(context), source_(source), framesCache_(cache), + instancesCache_(instancesCache), fitNextContent_(true), hasFocusOnInstance_(false), focusFrameNumber_(0), @@ -2638,8 +2812,6 @@ emscripten_set_wheel_callback(viewport_->GetCanvasCssSelector().c_str(), this, true, OnWheel); - SetWindowingPreset(); - stoneAnnotations_.reset(new OrthancStone::AnnotationsSceneLayer(LAYER_ANNOTATIONS_STONE)); stoneAnnotations_->SetProbedLayer(LAYER_TEXTURE); } @@ -2648,12 +2820,7 @@ void Handle(const OrthancStone::ViewportController::GrayscaleWindowingChanged& message) { // This event is triggered by the windowing mouse action, from class "GrayscaleWindowingSceneTracker" - currentWindowing_ = message.GetWindowing(); - - if (observer_.get() != NULL) - { - observer_->SignalWindowingUpdated(*this, currentWindowing_); - } + UpdateWindowing(WindowingState_User, message.GetWindowing()); } @@ -2796,12 +2963,13 @@ static boost::shared_ptr Create(OrthancStone::WebAssemblyLoadersContext& context, const OrthancStone::DicomSource& source, const std::string& canvas, - boost::shared_ptr cache, + boost::shared_ptr framesCache, + boost::shared_ptr instancesCache, bool softwareRendering, bool linearInterpolation) { boost::shared_ptr viewport( - new ViewerViewport(context, source, canvas, cache, softwareRendering, linearInterpolation)); + new ViewerViewport(context, source, canvas, framesCache, instancesCache, softwareRendering, linearInterpolation)); { std::unique_ptr lock(context.Lock()); @@ -2853,7 +3021,7 @@ frames_.reset(frames); cursor_.reset(new SeriesCursor(frames_->GetFramesCount(), false)); - + if (frames_->GetFramesCount() != 0) { const OrthancStone::DicomInstanceParameters& firstInstance = frames_->GetInstanceOfFrame(0); @@ -2873,11 +3041,16 @@ cursor_.reset(new SeriesCursor(frames_->GetFramesCount(), true)); } } + + SetDefaultWindowing(firstInstance); + } + else + { + windowingTracker_.Reset(); } LOG(INFO) << "Number of frames in series: " << frames_->GetFramesCount(); - SetWindowingPreset(); ClearViewport(); prefetchQueue_.clear(); @@ -3147,29 +3320,6 @@ } - void SetWindowingPreset() - { - if (windowingPresets_.empty()) - { - SetWindowing(fallbackWindowing_); - } - else - { - SetWindowing(windowingPresets_[0]); - } - } - - void SetWindowing(const OrthancStone::Windowing& windowing) - { - currentWindowing_ = windowing; - UpdateCurrentTextureParameters(); - - if (observer_.get() != NULL) - { - observer_->SignalWindowingUpdated(*this, currentWindowing_); - } - } - void StretchWindowing() { float minValue, maxValue; @@ -3197,7 +3347,7 @@ const float center = (minValue + maxValue) / 2.0f; const float width = maxValue - minValue; - SetWindowing(OrthancStone::Windowing(center, width)); + UpdateWindowing(WindowingState_User, OrthancStone::Windowing(center, width)); } void FlipX() @@ -3578,6 +3728,12 @@ *this, current.GetOrigin() + synchronizationOffset_, current.GetNormal()); } } + + + void SetUserWindowing(const OrthancStone::Windowing& windowing) + { + UpdateWindowing(WindowingState_User, windowing); + } }; @@ -3864,6 +4020,7 @@ static OrthancStone::DicomSource source_; static boost::shared_ptr framesCache_; +static boost::shared_ptr instancesCache_; static boost::shared_ptr context_; static std::string stringBuffer_; static bool softwareRendering_ = false; @@ -3914,7 +4071,7 @@ if (found == allViewports_.end()) { boost::shared_ptr viewport( - ViewerViewport::Create(*context_, source_, canvas, framesCache_, softwareRendering_, linearInterpolation_)); + ViewerViewport::Create(*context_, source_, canvas, framesCache_, instancesCache_, softwareRendering_, linearInterpolation_)); viewport->SetMouseButtonActions(leftButtonAction_, middleButtonAction_, rightButtonAction_); viewport->AcquireObserver(new WebAssemblyObserver); viewport->SetOsiriXAnnotations(osiriXAnnotations_); @@ -3943,6 +4100,7 @@ context_->SetDicomCacheSize(128 * 1024 * 1024); // 128MB framesCache_.reset(new FramesCache); + instancesCache_.reset(new InstancesCache); osiriXAnnotations_.reset(new OrthancStone::OsiriX::CollectionOfAnnotations); DISPATCH_JAVASCRIPT_EVENT("StoneInitialized"); @@ -4366,7 +4524,7 @@ { try { - GetViewport(canvas)->SetWindowing(OrthancStone::Windowing(center, width)); + GetViewport(canvas)->SetUserWindowing(OrthancStone::Windowing(center, width)); } EXTERN_CATCH_EXCEPTIONS; } diff -r 7e8b918b0482 -r 8e3c403cc643 OrthancStone/Sources/Toolbox/DicomInstanceParameters.cpp --- a/OrthancStone/Sources/Toolbox/DicomInstanceParameters.cpp Mon Oct 21 16:02:28 2024 +0200 +++ b/OrthancStone/Sources/Toolbox/DicomInstanceParameters.cpp Tue Oct 22 15:40:40 2024 +0200 @@ -32,6 +32,7 @@ #include #include #include +#include #include @@ -230,6 +231,65 @@ { instanceNumber_ = 0; } + + + static const Orthanc::DicomTag DICOM_TAG_PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE(0x5200, 0x9230); + static const Orthanc::DicomTag DICOM_TAG_FRAME_VOI_LUT_SEQUENCE_ATTRIBUTE(0x0028, 0x9132); + + const Orthanc::DicomValue* frames = dicom.TestAndGetValue(DICOM_TAG_PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE); + if (frames != NULL && + hasNumberOfFrames_ && + frames->IsSequence()) + { + /** + * New in Stone Web viewer 2.2: Deal with Philips multiframe + * (cf. mail from Tomas Kenda on 2021-08-17). This cannot be done + * in LoadSeriesDetailsFromInstance, as the "Per Frame Functional Groups Sequence" + * is not available at that point. + **/ + + const Json::Value& sequence = frames->GetSequenceContent(); + + perFrameWindowing_.resize(numberOfFrames_); + + // This corresponds to "ParsedDicomFile::GetDefaultWindowing()" + for (Json::ArrayIndex i = 0; i < sequence.size(); i++) + { + if (i < numberOfFrames_ && + sequence[i].isMember(DICOM_TAG_FRAME_VOI_LUT_SEQUENCE_ATTRIBUTE.Format())) + { + const Json::Value& v = sequence[i][DICOM_TAG_FRAME_VOI_LUT_SEQUENCE_ATTRIBUTE.Format()]; + + static const char* KEY_VALUE = "Value"; + + if (v.isMember(KEY_VALUE) && + v[KEY_VALUE].type() == Json::arrayValue && + v[KEY_VALUE].size() >= 1 && + v[KEY_VALUE][0].isMember(Orthanc::DICOM_TAG_WINDOW_CENTER.Format()) && + v[KEY_VALUE][0].isMember(Orthanc::DICOM_TAG_WINDOW_WIDTH.Format()) && + v[KEY_VALUE][0][Orthanc::DICOM_TAG_WINDOW_CENTER.Format()].isMember(KEY_VALUE) && + v[KEY_VALUE][0][Orthanc::DICOM_TAG_WINDOW_WIDTH.Format()].isMember(KEY_VALUE)) + { + const Json::Value& scenter = v[KEY_VALUE][0][Orthanc::DICOM_TAG_WINDOW_CENTER.Format()][KEY_VALUE]; + const Json::Value& swidth = v[KEY_VALUE][0][Orthanc::DICOM_TAG_WINDOW_WIDTH.Format()][KEY_VALUE]; + + double center, width; + if (scenter.isString() && + swidth.isString() && + Orthanc::SerializationToolbox::ParseDouble(center, scenter.asString()) && + Orthanc::SerializationToolbox::ParseDouble(width, swidth.asString())) + { + perFrameWindowing_[i] = Windowing(center, width); + } + else if (scenter.isNumeric() && + swidth.isNumeric()) + { + perFrameWindowing_[i] = Windowing(scenter.asDouble(), swidth.asDouble()); + } + } + } + } + } } @@ -824,4 +884,19 @@ return (data_.frameOffsets_[0] > data_.frameOffsets_[1]); } } + + + bool DicomInstanceParameters::LookupPerFrameWindowing(Windowing& windowing, + unsigned int frame) const + { + if (frame < data_.perFrameWindowing_.size()) + { + windowing = data_.perFrameWindowing_[frame]; + return true; + } + else + { + return false; + } + } } diff -r 7e8b918b0482 -r 8e3c403cc643 OrthancStone/Sources/Toolbox/DicomInstanceParameters.h --- a/OrthancStone/Sources/Toolbox/DicomInstanceParameters.h Mon Oct 21 16:02:28 2024 +0200 +++ b/OrthancStone/Sources/Toolbox/DicomInstanceParameters.h Tue Oct 22 15:40:40 2024 +0200 @@ -67,6 +67,7 @@ bool hasPixelSpacing_; bool hasNumberOfFrames_; int32_t instanceNumber_; + std::vector perFrameWindowing_; explicit Data(const Orthanc::DicomMap& dicom); }; @@ -266,5 +267,8 @@ CoordinateSystem3D GetMultiFrameGeometry() const; bool IsReversedFrameOffsets() const; + + bool LookupPerFrameWindowing(Windowing& windowing, + unsigned int frame) const; }; }