# HG changeset patch # User Benjamin Golinvaux # Date 1585923186 -7200 # Node ID b1396be5aa277fc36a4b3ca974b2183df27c93be # Parent 04055b6b9e2cdc3fd4384a676a5724b36197e272 Moved the fixed loaders back from the dead diff -r 04055b6b9e2c -r b1396be5aa27 Framework/Deprecated/Loaders/DicomStructureSetLoader.cpp --- a/Framework/Deprecated/Loaders/DicomStructureSetLoader.cpp Tue Mar 31 11:01:34 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,428 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - - -#include "DicomStructureSetLoader.h" - -#include "../../Scene2D/PolylineSceneLayer.h" -#include "../../StoneException.h" -#include "../../Toolbox/GeometryToolbox.h" - -#include - -#include - -#if 0 -bool logbgo233 = false; -bool logbgo115 = false; -#endif - -namespace Deprecated -{ - -#if 0 - void DumpDicomMap(std::ostream& o, const Orthanc::DicomMap& dicomMap) - { - using namespace std; - //ios_base::fmtflags state = o.flags(); - //o.flags(ios::right | ios::hex); - //o << "(" << setfill('0') << setw(4) << tag.GetGroup() - // << "," << setw(4) << tag.GetElement() << ")"; - //o.flags(state); - Json::Value val; - dicomMap.Serialize(val); - o << val; - //return o; - } -#endif - - - class DicomStructureSetLoader::AddReferencedInstance : public LoaderStateMachine::State - { - private: - std::string instanceId_; - - public: - AddReferencedInstance(DicomStructureSetLoader& that, - const std::string& instanceId) : - State(that), - instanceId_(instanceId) - { - } - - virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) - { - Json::Value tags; - message.ParseJsonBody(tags); - - Orthanc::DicomMap dicom; - dicom.FromDicomAsJson(tags); - - DicomStructureSetLoader& loader = GetLoader(); - - loader.content_->AddReferencedSlice(dicom); - - loader.countProcessedInstances_ ++; - assert(loader.countProcessedInstances_ <= loader.countReferencedInstances_); - - if (loader.countProcessedInstances_ == loader.countReferencedInstances_) - { - // All the referenced instances have been loaded, finalize the RT-STRUCT - loader.content_->CheckReferencedSlices(); - loader.revision_++; - loader.SetStructuresReady(); - } - } - }; - - - // State that converts a "SOP Instance UID" to an Orthanc identifier - class DicomStructureSetLoader::LookupInstance : public LoaderStateMachine::State - { - private: - std::string sopInstanceUid_; - - public: - LookupInstance(DicomStructureSetLoader& that, - const std::string& sopInstanceUid) : - State(that), - sopInstanceUid_(sopInstanceUid) - { - } - - virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) - { -#if 0 - LOG(TRACE) << "DicomStructureSetLoader::LookupInstance::Handle() (SUCCESS)"; -#endif - DicomStructureSetLoader& loader = GetLoader(); - - Json::Value lookup; - message.ParseJsonBody(lookup); - - if (lookup.type() != Json::arrayValue || - lookup.size() != 1 || - !lookup[0].isMember("Type") || - !lookup[0].isMember("Path") || - lookup[0]["Type"].type() != Json::stringValue || - lookup[0]["ID"].type() != Json::stringValue || - lookup[0]["Type"].asString() != "Instance") - { - std::stringstream msg; - msg << "Unknown resource! message.GetAnswer() = " << message.GetAnswer() << " message.GetAnswerHeaders() = "; - for (OrthancStone::OrthancRestApiCommand::HttpHeaders::const_iterator it = message.GetAnswerHeaders().begin(); - it != message.GetAnswerHeaders().end(); ++it) - { - msg << "\nkey: \"" << it->first << "\" value: \"" << it->second << "\"\n"; - } - const std::string msgStr = msg.str(); - LOG(ERROR) << msgStr; - throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource); - } - - const std::string instanceId = lookup[0]["ID"].asString(); - - { - std::unique_ptr command(new OrthancStone::OrthancRestApiCommand); - command->SetHttpHeader("Accept-Encoding", "gzip"); - std::string uri = "/instances/" + instanceId + "/tags"; - command->SetUri(uri); - command->AcquirePayload(new AddReferencedInstance(loader, instanceId)); - Schedule(command.release()); - } - } - }; - - - class DicomStructureSetLoader::LoadStructure : public LoaderStateMachine::State - { - public: - LoadStructure(DicomStructureSetLoader& that) : - State(that) - { - } - - virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) - { -#if 0 - if (logbgo115) - LOG(TRACE) << "DicomStructureSetLoader::LoadStructure::Handle() (SUCCESS)"; -#endif - DicomStructureSetLoader& loader = GetLoader(); - - { - OrthancPlugins::FullOrthancDataset dicom(message.GetAnswer()); - loader.content_.reset(new OrthancStone::DicomStructureSet(dicom)); - size_t structureCount = loader.content_->GetStructuresCount(); - loader.structureVisibility_.resize(structureCount); - bool everythingVisible = false; - if ((loader.initiallyVisibleStructures_.size() == 1) - && (loader.initiallyVisibleStructures_[0].size() == 1) - && (loader.initiallyVisibleStructures_[0][0] == '*')) - { - everythingVisible = true; - } - - for (size_t i = 0; i < structureCount; ++i) - { - // if a single "*" string is supplied, this means we want everything - // to be visible... - if(everythingVisible) - { - loader.structureVisibility_.at(i) = true; - } - else - { - // otherwise, we only enable visibility for those structures whose - // names are mentioned in the initiallyVisibleStructures_ array - const std::string& structureName = loader.content_->GetStructureName(i); - - std::vector::iterator foundIt = - std::find( - loader.initiallyVisibleStructures_.begin(), - loader.initiallyVisibleStructures_.end(), - structureName); - std::vector::iterator endIt = loader.initiallyVisibleStructures_.end(); - if (foundIt != endIt) - loader.structureVisibility_.at(i) = true; - else - loader.structureVisibility_.at(i) = false; - } - } - } - - // Some (admittedly invalid) Dicom files have empty values in the - // 0008,1155 tag. We try our best to cope with this. - std::set instances; - std::set nonEmptyInstances; - loader.content_->GetReferencedInstances(instances); - for (std::set::const_iterator - it = instances.begin(); it != instances.end(); ++it) - { - std::string instance = Orthanc::Toolbox::StripSpaces(*it); - if(instance != "") - nonEmptyInstances.insert(instance); - } - - loader.countReferencedInstances_ = - static_cast(nonEmptyInstances.size()); - - for (std::set::const_iterator - it = nonEmptyInstances.begin(); it != nonEmptyInstances.end(); ++it) - { - std::unique_ptr command(new OrthancStone::OrthancRestApiCommand); - command->SetUri("/tools/lookup"); - command->SetMethod(Orthanc::HttpMethod_Post); - command->SetBody(*it); - command->AcquirePayload(new LookupInstance(loader, *it)); - Schedule(command.release()); - } - } - }; - - - class DicomStructureSetLoader::Slice : public IExtractedSlice - { - private: - const OrthancStone::DicomStructureSet& content_; - uint64_t revision_; - bool isValid_; - std::vector visibility_; - - public: - /** - The visibility vector must either: - - be empty - or - - contain the same number of items as the number of structures in the - structure set. - In the first case (empty vector), all the structures are displayed. - In the second case, the visibility of each structure is defined by the - content of the vector at the corresponding index. - */ - Slice(const OrthancStone::DicomStructureSet& content, - uint64_t revision, - const OrthancStone::CoordinateSystem3D& cuttingPlane, - std::vector visibility = std::vector()) - : content_(content) - , revision_(revision) - , visibility_(visibility) - { - ORTHANC_ASSERT((visibility_.size() == content_.GetStructuresCount()) - || (visibility_.size() == 0u)); - - bool opposite; - - const OrthancStone::Vector normal = content.GetNormal(); - isValid_ = ( - OrthancStone::GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetNormal()) || - OrthancStone::GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetAxisX()) || - OrthancStone::GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetAxisY())); - } - - virtual bool IsValid() - { - return isValid_; - } - - virtual uint64_t GetRevision() - { - return revision_; - } - - virtual OrthancStone::ISceneLayer* CreateSceneLayer( - const OrthancStone::ILayerStyleConfigurator* configurator, - const OrthancStone::CoordinateSystem3D& cuttingPlane) - { - assert(isValid_); - - std::unique_ptr layer(new OrthancStone::PolylineSceneLayer); - layer->SetThickness(2); - - for (size_t i = 0; i < content_.GetStructuresCount(); i++) - { - if ((visibility_.size() == 0) || visibility_.at(i)) - { - const OrthancStone::Color& color = content_.GetStructureColor(i); - -#ifdef USE_BOOST_UNION_FOR_POLYGONS - std::vector< std::vector > polygons; - - if (content_.ProjectStructure(polygons, i, cuttingPlane)) - { - for (size_t j = 0; j < polygons.size(); j++) - { - PolylineSceneLayer::Chain chain; - chain.resize(polygons[j].size()); - - for (size_t k = 0; k < polygons[j].size(); k++) - { - chain[k] = ScenePoint2D(polygons[j][k].x, polygons[j][k].y); - } - - layer->AddChain(chain, true /* closed */, color); - } - } -#else - std::vector< std::pair > segments; - - if (content_.ProjectStructure(segments, i, cuttingPlane)) - { - for (size_t j = 0; j < segments.size(); j++) - { - OrthancStone::PolylineSceneLayer::Chain chain; - chain.resize(2); - - chain[0] = OrthancStone::ScenePoint2D(segments[j].first.x, segments[j].first.y); - chain[1] = OrthancStone::ScenePoint2D(segments[j].second.x, segments[j].second.y); - - layer->AddChain(chain, false /* NOT closed */, color); - } - } -#endif - } - } - - return layer.release(); - } - }; - - - DicomStructureSetLoader::DicomStructureSetLoader( - OrthancStone::ILoadersContext& loadersContext) - : LoaderStateMachine(loadersContext) - , loadersContext_(loadersContext) - , revision_(0) - , countProcessedInstances_(0) - , countReferencedInstances_(0) - , structuresReady_(false) - { - } - - - boost::shared_ptr DicomStructureSetLoader::Create(OrthancStone::ILoadersContext& loadersContext) - { - boost::shared_ptr obj( - new DicomStructureSetLoader( - loadersContext)); - obj->LoaderStateMachine::PostConstructor(); - return obj; - - } - - void DicomStructureSetLoader::SetStructureDisplayState(size_t structureIndex, bool display) - { - structureVisibility_.at(structureIndex) = display; - revision_++; - } - - DicomStructureSetLoader::~DicomStructureSetLoader() - { - LOG(TRACE) << "DicomStructureSetLoader::~DicomStructureSetLoader()"; - } - - void DicomStructureSetLoader::LoadInstance( - const std::string& instanceId, - const std::vector& initiallyVisibleStructures) - { - Start(); - - instanceId_ = instanceId; - initiallyVisibleStructures_ = initiallyVisibleStructures; - - { - std::unique_ptr command(new OrthancStone::OrthancRestApiCommand); - command->SetHttpHeader("Accept-Encoding", "gzip"); - - std::string uri = "/instances/" + instanceId + "/tags?ignore-length=3006-0050"; - - command->SetUri(uri); - command->AcquirePayload(new LoadStructure(*this)); - Schedule(command.release()); - } - } - - - OrthancStone::IVolumeSlicer::IExtractedSlice* DicomStructureSetLoader::ExtractSlice(const OrthancStone::CoordinateSystem3D& cuttingPlane) - { - if (content_.get() == NULL) - { - // Geometry is not available yet - return new OrthancStone::IVolumeSlicer::InvalidSlice; - } - else - { - return new Slice(*content_, revision_, cuttingPlane, structureVisibility_); - } - } - - void DicomStructureSetLoader::SetStructuresReady() - { - ORTHANC_ASSERT(!structuresReady_); - structuresReady_ = true; - BroadcastMessage(DicomStructureSetLoader::StructuresReady(*this)); - } - - bool DicomStructureSetLoader::AreStructuresReady() const - { - return structuresReady_; - } - -} diff -r 04055b6b9e2c -r b1396be5aa27 Framework/Deprecated/Loaders/DicomStructureSetLoader.h --- a/Framework/Deprecated/Loaders/DicomStructureSetLoader.h Tue Mar 31 11:01:34 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,104 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "../../Toolbox/DicomStructureSet.h" -#include "../../Volumes/IVolumeSlicer.h" -#include "LoaderStateMachine.h" - -#include - -namespace Deprecated -{ - class DicomStructureSetLoader : - public LoaderStateMachine, - public OrthancStone::IVolumeSlicer, - public OrthancStone::IObservable - { - private: - class Slice; - - // States of LoaderStateMachine - class AddReferencedInstance; // 3rd state - class LookupInstance; // 2nd state - class LoadStructure; // 1st state - - OrthancStone::ILoadersContext& loadersContext_; - std::unique_ptr content_; - uint64_t revision_; - std::string instanceId_; - unsigned int countProcessedInstances_; - unsigned int countReferencedInstances_; - - // will be set to true once the loading is finished - bool structuresReady_; - - /** - At load time, these strings are used to initialize the structureVisibility_ - vector. - - As a special case, if initiallyVisibleStructures_ contains a single string - that is '*', ALL structures will be made visible. - */ - std::vector initiallyVisibleStructures_; - - /** - Contains the "Should this structure be displayed?" flag for all structures. - Only filled when structures are loaded. - - Changing this value directly affects the rendering - */ - std::vector structureVisibility_; - - protected: - DicomStructureSetLoader(OrthancStone::ILoadersContext& loadersContext); - - public: - ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, StructuresReady, DicomStructureSetLoader); - - static boost::shared_ptr Create( - OrthancStone::ILoadersContext& loadersContext); - - OrthancStone::DicomStructureSet* GetContent() - { - return content_.get(); - } - - void SetStructureDisplayState(size_t structureIndex, bool display); - - bool GetStructureDisplayState(size_t structureIndex) const - { - return structureVisibility_.at(structureIndex); - } - - ~DicomStructureSetLoader(); - - void LoadInstance(const std::string& instanceId, - const std::vector& initiallyVisibleStructures = std::vector()); - - virtual IExtractedSlice* ExtractSlice(const OrthancStone::CoordinateSystem3D& cuttingPlane) ORTHANC_OVERRIDE; - - void SetStructuresReady(); - - bool AreStructuresReady() const; - }; -} diff -r 04055b6b9e2c -r b1396be5aa27 Framework/Deprecated/Loaders/LoaderCache.cpp --- a/Framework/Deprecated/Loaders/LoaderCache.cpp Tue Mar 31 11:01:34 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,384 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - -#include "LoaderCache.h" - -#include "../../StoneException.h" -#include "OrthancSeriesVolumeProgressiveLoader.h" -#include "OrthancMultiframeVolumeLoader.h" -#include "DicomStructureSetLoader.h" - -#include "../../Loaders/ILoadersContext.h" - - -#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 -#include "DicomStructureSetLoader2.h" -#endif - //BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - - -#if ORTHANC_ENABLE_WASM == 1 -# include -# include "../../Oracle/WebAssemblyOracle.h" -#else -# include "../../Oracle/ThreadedOracle.h" -#endif - -#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 -#include "../../Toolbox/DicomStructureSet2.h" -#endif -//BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - -#include "../../Volumes/DicomVolumeImage.h" -#include "../../Volumes/DicomVolumeImageMPRSlicer.h" - -#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 -#include "../../Volumes/DicomStructureSetSlicer2.h" -#endif -//BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - -#include -#include - -namespace Deprecated -{ - LoaderCache::LoaderCache(OrthancStone::ILoadersContext& loadersContext) - : loadersContext_(loadersContext) - { - - } - - boost::shared_ptr - LoaderCache::GetSeriesVolumeProgressiveLoader(std::string seriesUuid) - { - try - { - - // normalize keys a little - seriesUuid = Orthanc::Toolbox::StripSpaces(seriesUuid); - Orthanc::Toolbox::ToLowerCase(seriesUuid); - - // find in cache - if (seriesVolumeProgressiveLoaders_.find(seriesUuid) == seriesVolumeProgressiveLoaders_.end()) - { - std::unique_ptr lock(loadersContext_.Lock()); - - boost::shared_ptr volumeImage(new OrthancStone::DicomVolumeImage); - boost::shared_ptr loader; - - // true means "use progressive quality" - // false means "load high quality slices only" - loader = OrthancSeriesVolumeProgressiveLoader::Create(loadersContext_, volumeImage, false); - loader->LoadSeries(seriesUuid); - seriesVolumeProgressiveLoaders_[seriesUuid] = loader; - } - else - { -// LOG(TRACE) << "LoaderCache::GetSeriesVolumeProgressiveLoader : returning cached loader for seriesUUid = " << seriesUuid; - } - return seriesVolumeProgressiveLoaders_[seriesUuid]; - } - catch (const Orthanc::OrthancException& e) - { - if (e.HasDetails()) - { - LOG(ERROR) << "OrthancException in LoaderCache: " << e.What() << " Details: " << e.GetDetails(); - } - else - { - LOG(ERROR) << "OrthancException in LoaderCache: " << e.What(); - } - throw; - } - catch (const std::exception& e) - { - LOG(ERROR) << "std::exception in LoaderCache: " << e.what(); - throw; - } - catch (...) - { - LOG(ERROR) << "Unknown exception in LoaderCache"; - throw; - } - } - - boost::shared_ptr LoaderCache::GetMultiframeVolumeLoader(std::string instanceUuid) - { - // if the loader is not available, let's trigger its creation - if(multiframeVolumeLoaders_.find(instanceUuid) == multiframeVolumeLoaders_.end()) - { - GetMultiframeDicomVolumeImageMPRSlicer(instanceUuid); - } - ORTHANC_ASSERT(multiframeVolumeLoaders_.find(instanceUuid) != multiframeVolumeLoaders_.end()); - - return multiframeVolumeLoaders_[instanceUuid]; - } - - boost::shared_ptr LoaderCache::GetMultiframeDicomVolumeImageMPRSlicer(std::string instanceUuid) - { - try - { - // normalize keys a little - instanceUuid = Orthanc::Toolbox::StripSpaces(instanceUuid); - Orthanc::Toolbox::ToLowerCase(instanceUuid); - - // find in cache - if (dicomVolumeImageMPRSlicers_.find(instanceUuid) == dicomVolumeImageMPRSlicers_.end()) - { - std::unique_ptr lock(loadersContext_.Lock()); - boost::shared_ptr volumeImage(new OrthancStone::DicomVolumeImage); - boost::shared_ptr loader; - { - loader = OrthancMultiframeVolumeLoader::Create(loadersContext_, volumeImage); - loader->LoadInstance(instanceUuid); - } - multiframeVolumeLoaders_[instanceUuid] = loader; - boost::shared_ptr mprSlicer(new OrthancStone::DicomVolumeImageMPRSlicer(volumeImage)); - dicomVolumeImageMPRSlicers_[instanceUuid] = mprSlicer; - } - return dicomVolumeImageMPRSlicers_[instanceUuid]; - } - catch (const Orthanc::OrthancException& e) - { - if (e.HasDetails()) - { - LOG(ERROR) << "OrthancException in LoaderCache: " << e.What() << " Details: " << e.GetDetails(); - } - else - { - LOG(ERROR) << "OrthancException in LoaderCache: " << e.What(); - } - throw; - } - catch (const std::exception& e) - { - LOG(ERROR) << "std::exception in LoaderCache: " << e.what(); - throw; - } - catch (...) - { - LOG(ERROR) << "Unknown exception in LoaderCache"; - throw; - } - } - -#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - - boost::shared_ptr LoaderCache::GetDicomStructureSetSlicer2(std::string instanceUuid) - { - // if the loader is not available, let's trigger its creation - if (dicomStructureSetSlicers2_.find(instanceUuid) == dicomStructureSetSlicers2_.end()) - { - GetDicomStructureSetLoader2(instanceUuid); - } - ORTHANC_ASSERT(dicomStructureSetSlicers2_.find(instanceUuid) != dicomStructureSetSlicers2_.end()); - - return dicomStructureSetSlicers2_[instanceUuid]; - } -#endif -//BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - - - /** - This method allows to convert a list of string into a string by - sorting the strings then joining them - */ - static std::string SortAndJoin(const std::vector& stringList) - { - if (stringList.size() == 0) - { - return ""; - } - else - { - std::vector sortedStringList = stringList; - std::sort(sortedStringList.begin(), sortedStringList.end()); - std::stringstream s; - s << sortedStringList[0]; - for (size_t i = 1; i < sortedStringList.size(); ++i) - { - s << "-" << sortedStringList[i]; - } - return s.str(); - } - } - - boost::shared_ptr - LoaderCache::GetDicomStructureSetLoader( - std::string inInstanceUuid, - const std::vector& initiallyVisibleStructures) - { - try - { - // normalize keys a little - inInstanceUuid = Orthanc::Toolbox::StripSpaces(inInstanceUuid); - Orthanc::Toolbox::ToLowerCase(inInstanceUuid); - - std::string initiallyVisibleStructuresKey = - SortAndJoin(initiallyVisibleStructures); - - std::string entryKey = inInstanceUuid + "_" + initiallyVisibleStructuresKey; - - // find in cache - if (dicomStructureSetLoaders_.find(entryKey) == dicomStructureSetLoaders_.end()) - { - std::unique_ptr lock(loadersContext_.Lock()); - - boost::shared_ptr loader; - { - loader = DicomStructureSetLoader::Create(loadersContext_); - loader->LoadInstance(inInstanceUuid, initiallyVisibleStructures); - } - dicomStructureSetLoaders_[entryKey] = loader; - } - return dicomStructureSetLoaders_[entryKey]; - } - catch (const Orthanc::OrthancException& e) - { - if (e.HasDetails()) - { - LOG(ERROR) << "OrthancException in LoaderCache: " << e.What() << " Details: " << e.GetDetails(); - } - else - { - LOG(ERROR) << "OrthancException in LoaderCache: " << e.What(); - } - throw; - } - catch (const std::exception& e) - { - LOG(ERROR) << "std::exception in LoaderCache: " << e.what(); - throw; - } - catch (...) - { - LOG(ERROR) << "Unknown exception in LoaderCache"; - throw; - } - } - -#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - - boost::shared_ptr LoaderCache::GetDicomStructureSetLoader2(std::string instanceUuid) - { - try - { - // normalize keys a little - instanceUuid = Orthanc::Toolbox::StripSpaces(instanceUuid); - Orthanc::Toolbox::ToLowerCase(instanceUuid); - - // find in cache - if (dicomStructureSetLoaders2_.find(instanceUuid) == dicomStructureSetLoaders2_.end()) - { - boost::shared_ptr loader; - boost::shared_ptr structureSet(new DicomStructureSet2()); - boost::shared_ptr rtSlicer(new DicomStructureSetSlicer2(structureSet)); - dicomStructureSetSlicers2_[instanceUuid] = rtSlicer; - dicomStructureSets2_[instanceUuid] = structureSet; // to prevent it from being deleted - { -#if ORTHANC_ENABLE_WASM == 1 - loader.reset(new DicomStructureSetLoader2(*(structureSet.get()), oracle_, oracle_)); -#else - LockingEmitter::WriterLock lock(lockingEmitter_); - // TODO: clarify lifetimes... this is DANGEROUS! - loader.reset(new DicomStructureSetLoader2(*(structureSet.get()), oracle_, lock.GetOracleObservable())); -#endif - loader->LoadInstance(instanceUuid); - } - dicomStructureSetLoaders2_[instanceUuid] = loader; - } - return dicomStructureSetLoaders2_[instanceUuid]; - } - catch (const Orthanc::OrthancException& e) - { - if (e.HasDetails()) - { - LOG(ERROR) << "OrthancException in GetDicomStructureSetLoader2: " << e.What() << " Details: " << e.GetDetails(); - } - else - { - LOG(ERROR) << "OrthancException in GetDicomStructureSetLoader2: " << e.What(); - } - throw; - } - catch (const std::exception& e) - { - LOG(ERROR) << "std::exception in GetDicomStructureSetLoader2: " << e.what(); - throw; - } - catch (...) - { - LOG(ERROR) << "Unknown exception in GetDicomStructureSetLoader2"; - throw; - } - } - -#endif -// BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - - - void LoaderCache::ClearCache() - { - std::unique_ptr lock(loadersContext_.Lock()); - -#ifndef NDEBUG - // ISO way of checking for debug builds - DebugDisplayObjRefCounts(); -#endif - seriesVolumeProgressiveLoaders_.clear(); - multiframeVolumeLoaders_.clear(); - dicomVolumeImageMPRSlicers_.clear(); - dicomStructureSetLoaders_.clear(); - -#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - // order is important! - dicomStructureSetLoaders2_.clear(); - dicomStructureSetSlicers2_.clear(); - dicomStructureSets2_.clear(); -#endif -// BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - } - - template void DebugDisplayObjRefCountsInMap( - const std::string& name, const std::map >& myMap) - { - LOG(TRACE) << "Map \"" << name << "\" ref counts:"; - size_t i = 0; - for (typename std::map >::const_iterator - it = myMap.begin(); it != myMap.end(); ++it) - { - LOG(TRACE) << " element #" << i << ": ref count = " << it->second.use_count(); - i++; - } - } - - void LoaderCache::DebugDisplayObjRefCounts() - { - DebugDisplayObjRefCountsInMap("seriesVolumeProgressiveLoaders_", seriesVolumeProgressiveLoaders_); - DebugDisplayObjRefCountsInMap("multiframeVolumeLoaders_", multiframeVolumeLoaders_); - DebugDisplayObjRefCountsInMap("dicomVolumeImageMPRSlicers_", dicomVolumeImageMPRSlicers_); - DebugDisplayObjRefCountsInMap("dicomStructureSetLoaders_", dicomStructureSetLoaders_); -#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - DebugDisplayObjRefCountsInMap("dicomStructureSetLoaders2_", dicomStructureSetLoaders2_); - DebugDisplayObjRefCountsInMap("dicomStructureSetSlicers2_", dicomStructureSetSlicers2_); -#endif -//BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - } -} diff -r 04055b6b9e2c -r b1396be5aa27 Framework/Deprecated/Loaders/LoaderCache.h --- a/Framework/Deprecated/Loaders/LoaderCache.h Tue Mar 31 11:01:34 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,97 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - -#pragma once - -#include "../Messages/LockingEmitter.h" -#include "../../Volumes/DicomVolumeImageMPRSlicer.h" -#include "OrthancSeriesVolumeProgressiveLoader.h" -#include "OrthancMultiframeVolumeLoader.h" -#include "DicomStructureSetLoader.h" - -#include - -#include -#include -#include - -namespace OrthancStone -{ - class ILoadersContext; -} - -namespace Deprecated -{ - class LoaderCache - { - public: - LoaderCache(OrthancStone::ILoadersContext& loadersContext); - - boost::shared_ptr - GetSeriesVolumeProgressiveLoader (std::string seriesUuid); - - boost::shared_ptr - GetMultiframeDicomVolumeImageMPRSlicer(std::string instanceUuid); - - boost::shared_ptr - GetMultiframeVolumeLoader(std::string instanceUuid); - - boost::shared_ptr - GetDicomStructureSetLoader( - std::string instanceUuid, - const std::vector& initiallyVisibleStructures); - -#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - boost::shared_ptr - GetDicomStructureSetLoader2(std::string instanceUuid); - - boost::shared_ptr - GetDicomStructureSetSlicer2(std::string instanceUuid); -#endif - //BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - - void ClearCache(); - - private: - - void DebugDisplayObjRefCounts(); - - OrthancStone::ILoadersContext& loadersContext_; - - std::map > - seriesVolumeProgressiveLoaders_; - std::map > - multiframeVolumeLoaders_; - std::map > - dicomVolumeImageMPRSlicers_; - std::map > - dicomStructureSetLoaders_; -#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - std::map > - dicomStructureSetLoaders2_; - std::map > - dicomStructureSets2_; - std::map > - dicomStructureSetSlicers2_; -#endif - //BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - }; -} - diff -r 04055b6b9e2c -r b1396be5aa27 Framework/Deprecated/Loaders/LoaderStateMachine.cpp --- a/Framework/Deprecated/Loaders/LoaderStateMachine.cpp Tue Mar 31 11:01:34 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,219 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - - -#include "LoaderStateMachine.h" - -#include "../../Loaders/ILoadersContext.h" - -#include - -namespace Deprecated -{ - void LoaderStateMachine::State::Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - - - void LoaderStateMachine::State::Handle(const OrthancStone::GetOrthancImageCommand::SuccessMessage& message) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - - - void LoaderStateMachine::State::Handle(const OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage& message) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - - - void LoaderStateMachine::Schedule(OrthancStone::OracleCommandBase* command) - { - LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::Schedule()"; - - std::unique_ptr protection(command); - - if (command == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - - if (!command->HasPayload()) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, - "The payload must contain the next state"); - } - pendingCommands_.push_back(protection.release()); - - Step(); - } - - - void LoaderStateMachine::Start() - { - LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::Start()"; - - if (active_) - { - LOG(TRACE) << "LoaderStateMachine::Start() called while active_ is true"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - - active_ = true; - - for (size_t i = 0; i < simultaneousDownloads_; i++) - { - Step(); - } - } - - - void LoaderStateMachine::Step() - { - if (!pendingCommands_.empty() && - activeCommands_ < simultaneousDownloads_) - { - - OrthancStone::IOracleCommand* nextCommand = pendingCommands_.front(); - - LOG(TRACE) << " LoaderStateMachine(" << std::hex << this << std::dec << - ")::Step(): activeCommands_ (" << activeCommands_ << - ") < simultaneousDownloads_ (" << simultaneousDownloads_ << - ") --> will Schedule command addr " << std::hex << nextCommand << std::dec; - - { - std::unique_ptr lock(loadersContext_.Lock()); - boost::shared_ptr observer(GetSharedObserver()); - lock->Schedule(observer, 0, nextCommand); // TODO: priority! - } - pendingCommands_.pop_front(); - - activeCommands_++; - } - else - { - LOG(TRACE) << " LoaderStateMachine(" << std::hex << this << std::dec << - ")::Step(): activeCommands_ (" << activeCommands_ << - ") >= simultaneousDownloads_ (" << simultaneousDownloads_ << - ") --> will NOT Schedule command"; - } - } - - - void LoaderStateMachine::Clear() - { - LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::Clear()"; - for (PendingCommands::iterator it = pendingCommands_.begin(); - it != pendingCommands_.end(); ++it) - { - delete *it; - } - - pendingCommands_.clear(); - } - - - void LoaderStateMachine::HandleExceptionMessage(const OrthancStone::OracleCommandExceptionMessage& message) - { - LOG(ERROR) << "LoaderStateMachine::HandleExceptionMessage: error in the state machine, stopping all processing"; - LOG(ERROR) << "Error: " << message.GetException().What() << " Details: " << - message.GetException().GetDetails(); - Clear(); - } - - template - void LoaderStateMachine::HandleSuccessMessage(const T& message) - { - if (activeCommands_ <= 0) { - LOG(ERROR) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::HandleSuccessMessage : activeCommands_ should be > 0 but is: " << activeCommands_; - } - else { - activeCommands_--; - try - { - dynamic_cast(message.GetOrigin().GetPayload()).Handle(message); - Step(); - } - catch (Orthanc::OrthancException& e) - { - LOG(ERROR) << "Error in the state machine, stopping all processing: " << - e.What() << " Details: " << e.GetDetails(); - Clear(); - } - } - } - - - LoaderStateMachine::LoaderStateMachine( - OrthancStone::ILoadersContext& loadersContext) - : loadersContext_(loadersContext) - , active_(false) - , simultaneousDownloads_(4) - , activeCommands_(0) - { - using OrthancStone::ILoadersContext; - - LOG(TRACE) - << "LoaderStateMachine(" << std::hex << this - << std::dec << ")::LoaderStateMachine()"; - } - - void LoaderStateMachine::PostConstructor() - { - std::unique_ptr - lock(loadersContext_.Lock()); - - OrthancStone::IObservable& observable = lock->GetOracleObservable(); - - // TODO => Move this out of constructor - Register( - observable, &LoaderStateMachine::HandleSuccessMessage); - Register( - observable, &LoaderStateMachine::HandleSuccessMessage); - Register( - observable, &LoaderStateMachine::HandleSuccessMessage); - Register( - observable, &LoaderStateMachine::HandleExceptionMessage); - } - - LoaderStateMachine::~LoaderStateMachine() - { - LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::~LoaderStateMachine()"; - Clear(); - } - - void LoaderStateMachine::SetSimultaneousDownloads(unsigned int count) - { - if (active_) - { - LOG(ERROR) << "LoaderStateMachine::SetSimultaneousDownloads called while active_ is true"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - else if (count == 0) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); - } - else - { - simultaneousDownloads_ = count; - } - } -} diff -r 04055b6b9e2c -r b1396be5aa27 Framework/Deprecated/Loaders/LoaderStateMachine.h --- a/Framework/Deprecated/Loaders/LoaderStateMachine.h Tue Mar 31 11:01:34 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,129 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "../../Messages/IObservable.h" -#include "../../Messages/ObserverBase.h" -#include "../../Oracle/GetOrthancImageCommand.h" -#include "../../Oracle/GetOrthancWebViewerJpegCommand.h" -#include "../../Oracle/IOracle.h" -#include "../../Oracle/OracleCommandExceptionMessage.h" -#include "../../Oracle/OrthancRestApiCommand.h" - -#include - -#include - -namespace OrthancStone -{ - class ILoadersContext; -} - -namespace Deprecated -{ - /** - This class is supplied with Oracle commands and will schedule up to - simultaneousDownloads_ of them at the same time, then will schedule the - rest once slots become available. It is used, a.o., by the - OrtancMultiframeVolumeLoader class. - - To use it, you need to create commands that derive from State. - - You need to initialize them with the object that must be called when - an answer is received. - */ - - class LoaderStateMachine : public OrthancStone::ObserverBase - { - public: - class State : public Orthanc::IDynamicObject - { - private: - LoaderStateMachine& that_; - - public: - State(LoaderStateMachine& that) : - that_(that) - { - } - - State(const State& currentState) : - that_(currentState.that_) - { - } - - void Schedule(OrthancStone::OracleCommandBase* command) const - { - that_.Schedule(command); - } - - template - T& GetLoader() const - { - return dynamic_cast(that_); - } - - virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message); - - virtual void Handle(const OrthancStone::GetOrthancImageCommand::SuccessMessage& message); - - virtual void Handle(const OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage& message); - }; - - void Schedule(OrthancStone::OracleCommandBase* command); - - void Start(); - - private: - void Step(); - - void Clear(); - - void HandleExceptionMessage(const OrthancStone::OracleCommandExceptionMessage& message); - - template - void HandleSuccessMessage(const T& message); - - typedef std::list PendingCommands; - - OrthancStone::ILoadersContext& loadersContext_; - bool active_; - unsigned int simultaneousDownloads_; - PendingCommands pendingCommands_; - unsigned int activeCommands_; - - - public: - LoaderStateMachine(OrthancStone::ILoadersContext& loadersContext); - - void PostConstructor(); - - virtual ~LoaderStateMachine(); - - bool IsActive() const - { - return active_; - } - - void SetSimultaneousDownloads(unsigned int count); - }; -} diff -r 04055b6b9e2c -r b1396be5aa27 Framework/Deprecated/Loaders/OrthancMultiframeVolumeLoader.cpp --- a/Framework/Deprecated/Loaders/OrthancMultiframeVolumeLoader.cpp Tue Mar 31 11:01:34 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,593 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - - -#include "OrthancMultiframeVolumeLoader.h" - -#include -#include - -namespace Deprecated -{ - class OrthancMultiframeVolumeLoader::LoadRTDoseGeometry : public LoaderStateMachine::State - { - private: - std::unique_ptr dicom_; - - public: - LoadRTDoseGeometry(OrthancMultiframeVolumeLoader& that, - Orthanc::DicomMap* dicom) : - State(that), - dicom_(dicom) - { - if (dicom == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - - } - - virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) - { - // Complete the DICOM tags with just-received "Grid Frame Offset Vector" - std::string s = Orthanc::Toolbox::StripSpaces(message.GetAnswer()); - dicom_->SetValue(Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR, s, false); - - GetLoader().SetGeometry(*dicom_); - } - }; - - - static std::string GetSopClassUid(const Orthanc::DicomMap& dicom) - { - std::string s; - if (!dicom.LookupStringValue(s, Orthanc::DICOM_TAG_SOP_CLASS_UID, false)) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, - "DICOM file without SOP class UID"); - } - else - { - return s; - } - } - - - class OrthancMultiframeVolumeLoader::LoadGeometry : public State - { - public: - LoadGeometry(OrthancMultiframeVolumeLoader& that) : - State(that) - { - } - - virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) - { - OrthancMultiframeVolumeLoader& loader = GetLoader(); - - Json::Value body; - message.ParseJsonBody(body); - - if (body.type() != Json::objectValue) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); - } - - std::unique_ptr dicom(new Orthanc::DicomMap); - dicom->FromDicomAsJson(body); - - if (OrthancStone::StringToSopClassUid(GetSopClassUid(*dicom)) == OrthancStone::SopClassUid_RTDose) - { - // Download the "Grid Frame Offset Vector" DICOM tag, that is - // mandatory for RT-DOSE, but is too long to be returned by default - - std::unique_ptr command(new OrthancStone::OrthancRestApiCommand); - command->SetUri("/instances/" + loader.GetInstanceId() + "/content/" + - Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR.Format()); - command->AcquirePayload(new LoadRTDoseGeometry(loader, dicom.release())); - - Schedule(command.release()); - } - else - { - loader.SetGeometry(*dicom); - } - } - }; - - class OrthancMultiframeVolumeLoader::LoadTransferSyntax : public State - { - public: - LoadTransferSyntax(OrthancMultiframeVolumeLoader& that) : - State(that) - { - } - - virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) - { - GetLoader().SetTransferSyntax(message.GetAnswer()); - } - }; - - class OrthancMultiframeVolumeLoader::LoadUncompressedPixelData : public State - { - public: - LoadUncompressedPixelData(OrthancMultiframeVolumeLoader& that) : - State(that) - { - } - - virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) - { - GetLoader().SetUncompressedPixelData(message.GetAnswer()); - } - }; - - const std::string& OrthancMultiframeVolumeLoader::GetInstanceId() const - { - if (IsActive()) - { - return instanceId_; - } - else - { - LOG(ERROR) << "OrthancMultiframeVolumeLoader::GetInstanceId(): (!IsActive())"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - } - - void OrthancMultiframeVolumeLoader::ScheduleFrameDownloads() - { - if (transferSyntaxUid_.empty() || - !volume_->HasGeometry()) - { - return; - } - /* - 1.2.840.10008.1.2 Implicit VR Endian: Default Transfer Syntax for DICOM - 1.2.840.10008.1.2.1 Explicit VR Little Endian - 1.2.840.10008.1.2.2 Explicit VR Big Endian - - See https://www.dicomlibrary.com/dicom/transfer-syntax/ - */ - if (transferSyntaxUid_ == "1.2.840.10008.1.2" || - transferSyntaxUid_ == "1.2.840.10008.1.2.1" || - transferSyntaxUid_ == "1.2.840.10008.1.2.2") - { - std::unique_ptr command(new OrthancStone::OrthancRestApiCommand); - command->SetHttpHeader("Accept-Encoding", "gzip"); - command->SetUri("/instances/" + instanceId_ + "/content/" + - Orthanc::DICOM_TAG_PIXEL_DATA.Format() + "/0"); - command->AcquirePayload(new LoadUncompressedPixelData(*this)); - Schedule(command.release()); - } - else - { - throw Orthanc::OrthancException( - Orthanc::ErrorCode_NotImplemented, - "No support for multiframe instances with transfer syntax: " + transferSyntaxUid_); - } - } - - void OrthancMultiframeVolumeLoader::SetTransferSyntax(const std::string& transferSyntax) - { - transferSyntaxUid_ = Orthanc::Toolbox::StripSpaces(transferSyntax); - ScheduleFrameDownloads(); - } - - void OrthancMultiframeVolumeLoader::SetGeometry(const Orthanc::DicomMap& dicom) - { - OrthancStone::DicomInstanceParameters parameters(dicom); - volume_->SetDicomParameters(parameters); - - Orthanc::PixelFormat format; - if (!parameters.GetImageInformation().ExtractPixelFormat(format, true)) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - - double spacingZ; - switch (parameters.GetSopClassUid()) - { - case OrthancStone::SopClassUid_RTDose: - spacingZ = parameters.GetThickness(); - break; - - default: - throw Orthanc::OrthancException( - Orthanc::ErrorCode_NotImplemented, - "No support for multiframe instances with SOP class UID: " + GetSopClassUid(dicom)); - } - - const unsigned int width = parameters.GetImageInformation().GetWidth(); - const unsigned int height = parameters.GetImageInformation().GetHeight(); - const unsigned int depth = parameters.GetImageInformation().GetNumberOfFrames(); - - { - OrthancStone::VolumeImageGeometry geometry; - geometry.SetSizeInVoxels(width, height, depth); - geometry.SetAxialGeometry(parameters.GetGeometry()); - geometry.SetVoxelDimensions(parameters.GetPixelSpacingX(), - parameters.GetPixelSpacingY(), spacingZ); - volume_->Initialize(geometry, format, true /* Do compute range */); - } - - volume_->GetPixelData().Clear(); - - ScheduleFrameDownloads(); - - - - BroadcastMessage(OrthancStone::DicomVolumeImage::GeometryReadyMessage(*volume_)); - } - - - ORTHANC_FORCE_INLINE - static void CopyPixel(uint32_t& target, const void* source) - { - // TODO - check alignement? - target = le32toh(*reinterpret_cast(source)); - } - - ORTHANC_FORCE_INLINE - static void CopyPixel(uint16_t& target, const void* source) - { - // TODO - check alignement? - target = le16toh(*reinterpret_cast(source)); - } - - ORTHANC_FORCE_INLINE - static void CopyPixel(int16_t& target, const void* source) - { - // byte swapping is the same for unsigned and signed integers - // (the sign bit is always stored with the MSByte) - uint16_t* targetUp = reinterpret_cast(&target); - CopyPixel(*targetUp, source); - } - - template - void OrthancMultiframeVolumeLoader::CopyPixelDataAndComputeDistribution( - const std::string& pixelData, std::map& distribution) - { - OrthancStone::ImageBuffer3D& target = volume_->GetPixelData(); - - const unsigned int bpp = target.GetBytesPerPixel(); - const unsigned int width = target.GetWidth(); - const unsigned int height = target.GetHeight(); - const unsigned int depth = target.GetDepth(); - - if (pixelData.size() != bpp * width * height * depth) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, - "The pixel data has not the proper size"); - } - - if (pixelData.empty()) - { - return; - } - - // first pass to initialize map - { - const uint8_t* source = reinterpret_cast(pixelData.c_str()); - - for (unsigned int z = 0; z < depth; z++) - { - for (unsigned int y = 0; y < height; y++) - { - for (unsigned int x = 0; x < width; x++) - { - T value; - CopyPixel(value, source); - distribution[value] = 0; - source += bpp; - } - } - } - } - - { - const uint8_t* source = reinterpret_cast(pixelData.c_str()); - - for (unsigned int z = 0; z < depth; z++) - { - OrthancStone::ImageBuffer3D::SliceWriter writer(target, OrthancStone::VolumeProjection_Axial, z); - - assert(writer.GetAccessor().GetWidth() == width && - writer.GetAccessor().GetHeight() == height); - - for (unsigned int y = 0; y < height; y++) - { - assert(sizeof(T) == Orthanc::GetBytesPerPixel(target.GetFormat())); - - T* target = reinterpret_cast(writer.GetAccessor().GetRow(y)); - - for (unsigned int x = 0; x < width; x++) - { - CopyPixel(*target, source); - - distribution[*target] += 1; - - target++; - source += bpp; - } - } - } - } - } - - template - void OrthancMultiframeVolumeLoader::ComputeMinMaxWithOutlierRejection( - const std::map& distribution) - { - if (distribution.size() == 0) - { - LOG(ERROR) << "ComputeMinMaxWithOutlierRejection -- Volume image empty."; - } - else - { - OrthancStone::ImageBuffer3D& target = volume_->GetPixelData(); - - const uint64_t width = target.GetWidth(); - const uint64_t height = target.GetHeight(); - const uint64_t depth = target.GetDepth(); - const uint64_t voxelCount = width * height * depth; - - // now that we have distribution[pixelValue] == numberOfPixelsWithValue - // compute number of values and check (assertion) that it is equal to - // width * height * depth - { - typename std::map::const_iterator it = distribution.begin(); - uint64_t totalCount = 0; - distributionRawMin_ = static_cast(it->first); - - while (it != distribution.end()) - { - T pixelValue = it->first; - uint64_t count = it->second; - totalCount += count; - it++; - if (it == distribution.end()) - distributionRawMax_ = static_cast(pixelValue); - } - LOG(INFO) << "Volume image. First distribution value = " - << static_cast(distributionRawMin_) - << " | Last distribution value = " - << static_cast(distributionRawMax_); - - if (totalCount != voxelCount) - { - LOG(ERROR) << "Internal error in dose distribution computation. TC (" - << totalCount << ") != VoxC (" << voxelCount; - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } - } - - // compute the number of voxels to reject at each end of the distribution - uint64_t endRejectionCount = static_cast( - outliersHalfRejectionRate_ * voxelCount); - - if (endRejectionCount > voxelCount) - { - LOG(ERROR) << "Internal error in dose distribution computation." - << " endRejectionCount = " << endRejectionCount - << " | voxelCount = " << voxelCount; - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } - - // this will contain the actual distribution minimum after outlier - // rejection - T resultMin = 0; - - // then start from start and remove pixel values up to - // endRejectionCount voxels rejected - { - typename std::map::const_iterator it = distribution.begin(); - - uint64_t currentCount = 0; - - while (it != distribution.end()) - { - T pixelValue = it->first; - uint64_t count = it->second; - - // if this pixelValue crosses the rejection threshold, let's set it - // and exit the loop - if ((currentCount <= endRejectionCount) && - (currentCount + count > endRejectionCount)) - { - resultMin = pixelValue; - break; - } - else - { - currentCount += count; - } - // and continue walking along the distribution - it++; - } - } - - // this will contain the actual distribution maximum after outlier - // rejection - T resultMax = 0; - // now start from END and remove pixel values up to - // endRejectionCount voxels rejected - { - typename std::map::const_reverse_iterator it = distribution.rbegin(); - - uint64_t currentCount = 0; - - while (it != distribution.rend()) - { - T pixelValue = it->first; - uint64_t count = it->second; - - if ((currentCount <= endRejectionCount) && - (currentCount + count > endRejectionCount)) - { - resultMax = pixelValue; - break; - } - else - { - currentCount += count; - } - // and continue walking along the distribution - it++; - } - } - if (resultMin > resultMax) - { - LOG(ERROR) << "Internal error in dose distribution computation! " << - "resultMin (" << resultMin << ") > resultMax (" << resultMax << ")"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } - computedDistributionMin_ = static_cast(resultMin); - computedDistributionMax_ = static_cast(resultMax); - } - } - - template - void OrthancMultiframeVolumeLoader::CopyPixelDataAndComputeMinMax( - const std::string& pixelData) - { - std::map distribution; - CopyPixelDataAndComputeDistribution(pixelData, distribution); - ComputeMinMaxWithOutlierRejection(distribution); - } - - void OrthancMultiframeVolumeLoader::SetUncompressedPixelData(const std::string& pixelData) - { - switch (volume_->GetPixelData().GetFormat()) - { - case Orthanc::PixelFormat_Grayscale32: - CopyPixelDataAndComputeMinMax(pixelData); - break; - case Orthanc::PixelFormat_Grayscale16: - CopyPixelDataAndComputeMinMax(pixelData); - break; - case Orthanc::PixelFormat_SignedGrayscale16: - CopyPixelDataAndComputeMinMax(pixelData); - break; - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - - volume_->IncrementRevision(); - - pixelDataLoaded_ = true; - BroadcastMessage(OrthancStone::DicomVolumeImage::ContentUpdatedMessage(*volume_)); - } - - bool OrthancMultiframeVolumeLoader::HasGeometry() const - { - return volume_->HasGeometry(); - } - - const OrthancStone::VolumeImageGeometry& OrthancMultiframeVolumeLoader::GetImageGeometry() const - { - return volume_->GetGeometry(); - } - - OrthancMultiframeVolumeLoader::OrthancMultiframeVolumeLoader( - OrthancStone::ILoadersContext& loadersContext, - boost::shared_ptr volume, - float outliersHalfRejectionRate) - : LoaderStateMachine(loadersContext) - , volume_(volume) - , pixelDataLoaded_(false) - , outliersHalfRejectionRate_(outliersHalfRejectionRate) - , distributionRawMin_(0) - , distributionRawMax_(0) - , computedDistributionMin_(0) - , computedDistributionMax_(0) - { - if (volume.get() == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - } - - - boost::shared_ptr - OrthancMultiframeVolumeLoader::Create( - OrthancStone::ILoadersContext& loadersContext, - boost::shared_ptr volume, - float outliersHalfRejectionRate /*= 0.0005*/) - { - boost::shared_ptr obj( - new OrthancMultiframeVolumeLoader( - loadersContext, - volume, - outliersHalfRejectionRate)); - obj->LoaderStateMachine::PostConstructor(); - return obj; - } - - OrthancMultiframeVolumeLoader::~OrthancMultiframeVolumeLoader() - { - LOG(TRACE) << "OrthancMultiframeVolumeLoader::~OrthancMultiframeVolumeLoader()"; - } - - void OrthancMultiframeVolumeLoader::GetDistributionMinMax - (float& minValue, float& maxValue) const - { - if (distributionRawMin_ == 0 && distributionRawMax_ == 0) - { - LOG(WARNING) << "GetDistributionMinMaxWithOutliersRejection called before computation!"; - } - minValue = distributionRawMin_; - maxValue = distributionRawMax_; - } - - void OrthancMultiframeVolumeLoader::GetDistributionMinMaxWithOutliersRejection - (float& minValue, float& maxValue) const - { - if (computedDistributionMin_ == 0 && computedDistributionMax_ == 0) - { - LOG(WARNING) << "GetDistributionMinMaxWithOutliersRejection called before computation!"; - } - minValue = computedDistributionMin_; - maxValue = computedDistributionMax_; - } - - void OrthancMultiframeVolumeLoader::LoadInstance(const std::string& instanceId) - { - Start(); - - instanceId_ = instanceId; - - { - std::unique_ptr command(new OrthancStone::OrthancRestApiCommand); - command->SetHttpHeader("Accept-Encoding", "gzip"); - command->SetUri("/instances/" + instanceId + "/tags"); - command->AcquirePayload(new LoadGeometry(*this)); - Schedule(command.release()); - } - - { - std::unique_ptr command(new OrthancStone::OrthancRestApiCommand); - command->SetUri("/instances/" + instanceId + "/metadata/TransferSyntax"); - command->AcquirePayload(new LoadTransferSyntax(*this)); - Schedule(command.release()); - } - } -} diff -r 04055b6b9e2c -r b1396be5aa27 Framework/Deprecated/Loaders/OrthancMultiframeVolumeLoader.h --- a/Framework/Deprecated/Loaders/OrthancMultiframeVolumeLoader.h Tue Mar 31 11:01:34 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,123 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "LoaderStateMachine.h" -#include "../../Volumes/DicomVolumeImage.h" -#include "../Volumes/IGeometryProvider.h" - -#include - -namespace Deprecated -{ - class OrthancMultiframeVolumeLoader : - public LoaderStateMachine, - public OrthancStone::IObservable, - public IGeometryProvider - { - private: - class LoadRTDoseGeometry; - class LoadGeometry; - class LoadTransferSyntax; - class LoadUncompressedPixelData; - - boost::shared_ptr volume_; - std::string instanceId_; - std::string transferSyntaxUid_; - bool pixelDataLoaded_; - float outliersHalfRejectionRate_; - float distributionRawMin_; - float distributionRawMax_; - float computedDistributionMin_; - float computedDistributionMax_; - - const std::string& GetInstanceId() const; - - void ScheduleFrameDownloads(); - - void SetTransferSyntax(const std::string& transferSyntax); - - void SetGeometry(const Orthanc::DicomMap& dicom); - - - /** - This method will : - - - copy the pixel values from the response to the volume image - - compute the maximum and minimum value while discarding the - outliersHalfRejectionRate_ fraction of the outliers from both the start - and the end of the distribution. - - In English, this means that, if the volume dataset contains a few extreme - values very different from the rest (outliers) that we want to get rid of, - this method allows to do so. - - If you supply 0.005, for instance, it means 1% of the extreme values will - be rejected (0.5% on each side of the distribution) - */ - template - void CopyPixelDataAndComputeMinMax(const std::string& pixelData); - - /** Service method for CopyPixelDataAndComputeMinMax*/ - template - void CopyPixelDataAndComputeDistribution( - const std::string& pixelData, - std::map& distribution); - - /** Service method for CopyPixelDataAndComputeMinMax*/ - template - void ComputeMinMaxWithOutlierRejection( - const std::map& distribution); - - void SetUncompressedPixelData(const std::string& pixelData); - - bool HasGeometry() const; - const OrthancStone::VolumeImageGeometry& GetImageGeometry() const; - - protected: - OrthancMultiframeVolumeLoader( - OrthancStone::ILoadersContext& loadersContext, - boost::shared_ptr volume, - float outliersHalfRejectionRate); - public: - - static boost::shared_ptr Create( - OrthancStone::ILoadersContext& loadersContext, - boost::shared_ptr volume, - float outliersHalfRejectionRate = 0.0005); - - virtual ~OrthancMultiframeVolumeLoader(); - - bool IsPixelDataLoaded() const - { - return pixelDataLoaded_; - } - - void GetDistributionMinMax - (float& minValue, float& maxValue) const; - - void GetDistributionMinMaxWithOutliersRejection - (float& minValue, float& maxValue) const; - - void LoadInstance(const std::string& instanceId); - }; -} diff -r 04055b6b9e2c -r b1396be5aa27 Framework/Deprecated/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp --- a/Framework/Deprecated/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp Tue Mar 31 11:01:34 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,577 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - - -#include "OrthancSeriesVolumeProgressiveLoader.h" - -#include "../../StoneException.h" -#include "../../Loaders/ILoadersContext.h" -#include "../../Loaders/BasicFetchingItemsSorter.h" -#include "../../Loaders/BasicFetchingStrategy.h" -#include "../../Toolbox/GeometryToolbox.h" -#include "../../Volumes/DicomVolumeImageMPRSlicer.h" - -#include -#include - -namespace Deprecated -{ - using OrthancStone::ILoadersContext; - - class OrthancSeriesVolumeProgressiveLoader::ExtractedSlice : public OrthancStone::DicomVolumeImageMPRSlicer::Slice - { - private: - const OrthancSeriesVolumeProgressiveLoader& that_; - - public: - ExtractedSlice(const OrthancSeriesVolumeProgressiveLoader& that, - const OrthancStone::CoordinateSystem3D& plane) : - OrthancStone::DicomVolumeImageMPRSlicer::Slice(*that.volume_, plane), - that_(that) - { - if (IsValid()) - { - if (GetProjection() == OrthancStone::VolumeProjection_Axial) - { - // For coronal and sagittal projections, we take the global - // revision of the volume because even if a single slice changes, - // this means the projection will yield a different result --> - // we must increase the revision as soon as any slice changes - SetRevision(that_.seriesGeometry_.GetSliceRevision(GetSliceIndex())); - } - - if (that_.strategy_.get() != NULL && - GetProjection() == OrthancStone::VolumeProjection_Axial) - { - that_.strategy_->SetCurrent(GetSliceIndex()); - } - } - } - }; - - void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckSlice( - size_t index, const OrthancStone::DicomInstanceParameters& reference) const - { - const OrthancStone::DicomInstanceParameters& slice = *slices_[index]; - - if (!OrthancStone::GeometryToolbox::IsParallel( - reference.GetGeometry().GetNormal(), - slice.GetGeometry().GetNormal())) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, - "A slice in the volume image is not parallel to the others"); - } - - if (reference.GetExpectedPixelFormat() != slice.GetExpectedPixelFormat()) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat, - "The pixel format changes across the slices of the volume image"); - } - - if (reference.GetImageInformation().GetWidth() != slice.GetImageInformation().GetWidth() || - reference.GetImageInformation().GetHeight() != slice.GetImageInformation().GetHeight()) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize, - "The width/height of slices are not constant in the volume image"); - } - - if (!OrthancStone::LinearAlgebra::IsNear(reference.GetPixelSpacingX(), slice.GetPixelSpacingX()) || - !OrthancStone::LinearAlgebra::IsNear(reference.GetPixelSpacingY(), slice.GetPixelSpacingY())) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, - "The pixel spacing of the slices change across the volume image"); - } - } - - - void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckVolume() const - { - for (size_t i = 0; i < slices_.size(); i++) - { - assert(slices_[i] != NULL); - if (slices_[i]->GetImageInformation().GetNumberOfFrames() != 1) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, - "This class does not support multi-frame images"); - } - } - - if (slices_.size() != 0) - { - const OrthancStone::DicomInstanceParameters& reference = *slices_[0]; - - for (size_t i = 1; i < slices_.size(); i++) - { - CheckSlice(i, reference); - } - } - } - - - void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::Clear() - { - for (size_t i = 0; i < slices_.size(); i++) - { - assert(slices_[i] != NULL); - delete slices_[i]; - } - - slices_.clear(); - slicesRevision_.clear(); - } - - - void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckSliceIndex(size_t index) const - { - if (!HasGeometry()) - { - LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckSliceIndex(size_t index): (!HasGeometry())"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - else if (index >= slices_.size()) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); - } - else - { - assert(slices_.size() == GetImageGeometry().GetDepth() && - slices_.size() == slicesRevision_.size()); - } - } - - - // WARNING: The payload of "slices" must be of class "DicomInstanceParameters" - // (called with the slices created in LoadGeometry) - void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::ComputeGeometry(OrthancStone::SlicesSorter& slices) - { - Clear(); - - if (!slices.Sort()) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, - "Cannot sort the 3D slices of a DICOM series"); - } - - if (slices.GetSlicesCount() == 0) - { - geometry_.reset(new OrthancStone::VolumeImageGeometry); - } - else - { - slices_.reserve(slices.GetSlicesCount()); - slicesRevision_.resize(slices.GetSlicesCount(), 0); - - for (size_t i = 0; i < slices.GetSlicesCount(); i++) - { - const OrthancStone::DicomInstanceParameters& slice = - dynamic_cast(slices.GetSlicePayload(i)); - slices_.push_back(new OrthancStone::DicomInstanceParameters(slice)); - } - - CheckVolume(); - - double spacingZ; - - if (slices.ComputeSpacingBetweenSlices(spacingZ)) - { - LOG(TRACE) << "Computed spacing between slices: " << spacingZ << "mm"; - - const OrthancStone::DicomInstanceParameters& parameters = *slices_[0]; - - geometry_.reset(new OrthancStone::VolumeImageGeometry); - geometry_->SetSizeInVoxels(parameters.GetImageInformation().GetWidth(), - parameters.GetImageInformation().GetHeight(), - static_cast(slices.GetSlicesCount())); - geometry_->SetAxialGeometry(slices.GetSliceGeometry(0)); - geometry_->SetVoxelDimensions(parameters.GetPixelSpacingX(), - parameters.GetPixelSpacingY(), spacingZ); - } - else - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, - "The origins of the slices of a volume image are not regularly spaced"); - } - } - } - - - const OrthancStone::VolumeImageGeometry& OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetImageGeometry() const - { - if (!HasGeometry()) - { - LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetImageGeometry(): (!HasGeometry())"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - else - { - assert(slices_.size() == geometry_->GetDepth()); - return *geometry_; - } - } - - - const OrthancStone::DicomInstanceParameters& OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetSliceParameters(size_t index) const - { - CheckSliceIndex(index); - return *slices_[index]; - } - - - uint64_t OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetSliceRevision(size_t index) const - { - CheckSliceIndex(index); - return slicesRevision_[index]; - } - - - void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::IncrementSliceRevision(size_t index) - { - CheckSliceIndex(index); - slicesRevision_[index] ++; - } - - - static unsigned int GetSliceIndexPayload(const OrthancStone::OracleCommandBase& command) - { - assert(command.HasPayload()); - return dynamic_cast< const Orthanc::SingleValueObject& >(command.GetPayload()).GetValue(); - } - - - void OrthancSeriesVolumeProgressiveLoader::ScheduleNextSliceDownload() - { - assert(strategy_.get() != NULL); - - unsigned int sliceIndex = 0, quality = 0; - - if (strategy_->GetNext(sliceIndex, quality)) - { - if (!progressiveQuality_) - { - ORTHANC_ASSERT(quality == QUALITY_00, "INTERNAL ERROR. quality != QUALITY_00 in " - << "OrthancSeriesVolumeProgressiveLoader::ScheduleNextSliceDownload"); - } - - const OrthancStone::DicomInstanceParameters& slice = seriesGeometry_.GetSliceParameters(sliceIndex); - - const std::string& instance = slice.GetOrthancInstanceIdentifier(); - if (instance.empty()) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } - - std::unique_ptr command; - - if (!progressiveQuality_ || quality == QUALITY_02) - { - std::unique_ptr tmp(new OrthancStone::GetOrthancImageCommand); - // TODO: review the following comment. - // - Commented out by bgo on 2019-07-19 | reason: Alain has seen cases - // where gzipping the uint16 image took 11 sec to produce 5mb. - // The unzipped request was much much faster. - // - Re-enabled on 2019-07-30. Reason: in Web Assembly, the browser - // does not use the Accept-Encoding header and always requests - // compression. Furthermore, NOT - tmp->SetHttpHeader("Accept-Encoding", "gzip"); - tmp->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Pam))); - tmp->SetInstanceUri(instance, slice.GetExpectedPixelFormat()); - tmp->SetExpectedPixelFormat(slice.GetExpectedPixelFormat()); - //LOG(INFO) - // << "OrthancSeriesVolumeProgressiveLoader.ScheduleNextSliceDownload()" - // << " sliceIndex = " << sliceIndex << " slice quality = " << quality - // << " URI = " << tmp->GetUri(); - command.reset(tmp.release()); - } - else // progressive mode is true AND quality is not final (different from QUALITY_02 - { - std::unique_ptr tmp( - new OrthancStone::GetOrthancWebViewerJpegCommand); - - // TODO: review the following comment. Commented out by bgo on 2019-07-19 - // (gzip for jpeg seems overkill) - //tmp->SetHttpHeader("Accept-Encoding", "gzip"); - tmp->SetInstance(instance); - tmp->SetQuality((quality == 0 ? 50 : 90)); // QUALITY_00 is Jpeg50 while QUALITY_01 is Jpeg90 - tmp->SetExpectedPixelFormat(slice.GetExpectedPixelFormat()); - LOG(TRACE) - << "OrthancSeriesVolumeProgressiveLoader.ScheduleNextSliceDownload()" - << " sliceIndex = " << sliceIndex << " slice quality = " << quality; - command.reset(tmp.release()); - } - - command->AcquirePayload(new Orthanc::SingleValueObject(sliceIndex)); - - { - std::unique_ptr lock(loadersContext_.Lock()); - boost::shared_ptr observer(GetSharedObserver()); - lock->Schedule(observer, 0, command.release()); // TODO: priority! - } - } - else - { - // loading is finished! - volumeImageReadyInHighQuality_ = true; - BroadcastMessage(OrthancSeriesVolumeProgressiveLoader::VolumeImageReadyInHighQuality(*this)); - } - } - -/** - This is called in response to GET "/series/XXXXXXXXXXXXX/instances-tags" -*/ - void OrthancSeriesVolumeProgressiveLoader::LoadGeometry(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) - { - Json::Value body; - message.ParseJsonBody(body); - - if (body.type() != Json::objectValue) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); - } - - { - Json::Value::Members instances = body.getMemberNames(); - - OrthancStone::SlicesSorter slices; - - for (size_t i = 0; i < instances.size(); i++) - { - Orthanc::DicomMap dicom; - dicom.FromDicomAsJson(body[instances[i]]); - - std::unique_ptr instance(new OrthancStone::DicomInstanceParameters(dicom)); - instance->SetOrthancInstanceIdentifier(instances[i]); - - // the 3D plane corresponding to the slice - OrthancStone::CoordinateSystem3D geometry = instance->GetGeometry(); - slices.AddSlice(geometry, instance.release()); - } - - seriesGeometry_.ComputeGeometry(slices); - } - - size_t slicesCount = seriesGeometry_.GetImageGeometry().GetDepth(); - - if (slicesCount == 0) - { - volume_->Initialize(seriesGeometry_.GetImageGeometry(), Orthanc::PixelFormat_Grayscale8); - } - else - { - const OrthancStone::DicomInstanceParameters& parameters = seriesGeometry_.GetSliceParameters(0); - - volume_->Initialize(seriesGeometry_.GetImageGeometry(), parameters.GetExpectedPixelFormat()); - volume_->SetDicomParameters(parameters); - volume_->GetPixelData().Clear(); - - // If we are in progressive mode, the Fetching strategy will first request QUALITY_00, then QUALITY_01, then - // QUALITY_02... Otherwise, it's only QUALITY_00 - unsigned int maxQuality = QUALITY_00; - if (progressiveQuality_) - maxQuality = QUALITY_02; - - strategy_.reset(new OrthancStone::BasicFetchingStrategy( - sorter_->CreateSorter(static_cast(slicesCount)), - maxQuality)); - - assert(simultaneousDownloads_ != 0); - for (unsigned int i = 0; i < simultaneousDownloads_; i++) - { - ScheduleNextSliceDownload(); - } - } - - slicesQuality_.resize(slicesCount, 0); - - BroadcastMessage(OrthancStone::DicomVolumeImage::GeometryReadyMessage(*volume_)); - } - - - void OrthancSeriesVolumeProgressiveLoader::SetSliceContent(unsigned int sliceIndex, - const Orthanc::ImageAccessor& image, - unsigned int quality) - { - ORTHANC_ASSERT(sliceIndex < slicesQuality_.size() && - slicesQuality_.size() == volume_->GetPixelData().GetDepth()); - - if (!progressiveQuality_) - { - ORTHANC_ASSERT(quality == QUALITY_00); - ORTHANC_ASSERT(slicesQuality_[sliceIndex] == QUALITY_00); - } - - if (quality >= slicesQuality_[sliceIndex]) - { - { - OrthancStone::ImageBuffer3D::SliceWriter writer(volume_->GetPixelData(), - OrthancStone::VolumeProjection_Axial, - sliceIndex); - - Orthanc::ImageProcessing::Copy(writer.GetAccessor(), image); - } - - volume_->IncrementRevision(); - seriesGeometry_.IncrementSliceRevision(sliceIndex); - slicesQuality_[sliceIndex] = quality; - - BroadcastMessage(OrthancStone::DicomVolumeImage::ContentUpdatedMessage(*volume_)); - } - LOG(TRACE) << "SetSliceContent sliceIndex = " << sliceIndex << " -- will " - << " now call ScheduleNextSliceDownload()"; - ScheduleNextSliceDownload(); - } - - void OrthancSeriesVolumeProgressiveLoader::LoadBestQualitySliceContent( - const OrthancStone::GetOrthancImageCommand::SuccessMessage& message) - { - unsigned int quality = QUALITY_00; - if (progressiveQuality_) - quality = QUALITY_02; - - SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), - message.GetImage(), - quality); - } - - void OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent( - const OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage& message) - { - ORTHANC_ASSERT(progressiveQuality_, "INTERNAL ERROR: OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent" - << " called while progressiveQuality_ is false!"); - - LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent"; - unsigned int quality; - - switch (dynamic_cast(message.GetOrigin()).GetQuality()) - { - case 50: - quality = QUALITY_00; - break; - - case 90: - quality = QUALITY_01; - break; - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } - - SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), message.GetImage(), quality); - } - - OrthancSeriesVolumeProgressiveLoader::OrthancSeriesVolumeProgressiveLoader( - OrthancStone::ILoadersContext& loadersContext, - boost::shared_ptr volume, - bool progressiveQuality) - : loadersContext_(loadersContext) - , active_(false) - , progressiveQuality_(progressiveQuality) - , simultaneousDownloads_(4) - , volume_(volume) - , sorter_(new OrthancStone::BasicFetchingItemsSorter::Factory) - , volumeImageReadyInHighQuality_(false) - { - } - - boost::shared_ptr - OrthancSeriesVolumeProgressiveLoader::Create( - OrthancStone::ILoadersContext& loadersContext, - boost::shared_ptr volume, - bool progressiveQuality) - { - std::auto_ptr lock(loadersContext.Lock()); - - boost::shared_ptr obj( - new OrthancSeriesVolumeProgressiveLoader( - loadersContext, volume, progressiveQuality)); - - obj->Register( - lock->GetOracleObservable(), - &OrthancSeriesVolumeProgressiveLoader::LoadGeometry); - - obj->Register( - lock->GetOracleObservable(), - &OrthancSeriesVolumeProgressiveLoader::LoadBestQualitySliceContent); - - obj->Register( - lock->GetOracleObservable(), - &OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent); - - return obj; - } - - - OrthancSeriesVolumeProgressiveLoader::~OrthancSeriesVolumeProgressiveLoader() - { - LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::~OrthancSeriesVolumeProgressiveLoader()"; - } - - void OrthancSeriesVolumeProgressiveLoader::SetSimultaneousDownloads(unsigned int count) - { - if (active_) - { - LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::SetSimultaneousDownloads(): (active_)"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - else if (count == 0) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); - } - else - { - simultaneousDownloads_ = count; - } - } - - - void OrthancSeriesVolumeProgressiveLoader::LoadSeries(const std::string& seriesId) - { - if (active_) - { - LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::LoadSeries(const std::string& seriesId): (active_)"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - else - { - active_ = true; - - std::unique_ptr command(new OrthancStone::OrthancRestApiCommand); - command->SetUri("/series/" + seriesId + "/instances-tags"); - { - std::unique_ptr lock(loadersContext_.Lock()); - boost::shared_ptr observer(GetSharedObserver()); - lock->Schedule(observer, 0, command.release()); //TODO: priority! - } - } - } - - - OrthancStone::IVolumeSlicer::IExtractedSlice* - OrthancSeriesVolumeProgressiveLoader::ExtractSlice(const OrthancStone::CoordinateSystem3D& cuttingPlane) - { - if (volume_->HasGeometry()) - { - return new ExtractedSlice(*this, cuttingPlane); - } - else - { - return new IVolumeSlicer::InvalidSlice; - } - } -} diff -r 04055b6b9e2c -r b1396be5aa27 Framework/Deprecated/Loaders/OrthancSeriesVolumeProgressiveLoader.h --- a/Framework/Deprecated/Loaders/OrthancSeriesVolumeProgressiveLoader.h Tue Mar 31 11:01:34 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,180 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "../../Loaders/IFetchingItemsSorter.h" -#include "../../Loaders/IFetchingStrategy.h" -#include "../../Messages/IObservable.h" -#include "../../Messages/ObserverBase.h" -#include "../../Oracle/GetOrthancImageCommand.h" -#include "../../Oracle/GetOrthancWebViewerJpegCommand.h" -#include "../../Oracle/IOracle.h" -#include "../../Oracle/OrthancRestApiCommand.h" -#include "../../Toolbox/SlicesSorter.h" -#include "../../Volumes/DicomVolumeImage.h" -#include "../../Volumes/IVolumeSlicer.h" - -#include "../Volumes/IGeometryProvider.h" - - -#include - -namespace OrthancStone -{ - class ILoadersContext; -} - -namespace Deprecated -{ - /** - This class is used to manage the progressive loading of a volume that - is stored in a Dicom series. - */ - class OrthancSeriesVolumeProgressiveLoader : - public OrthancStone::ObserverBase, - public OrthancStone::IObservable, - public OrthancStone::IVolumeSlicer, - public IGeometryProvider - { - private: - static const unsigned int QUALITY_00 = 0; - static const unsigned int QUALITY_01 = 1; - static const unsigned int QUALITY_02 = 2; - - class ExtractedSlice; - - /** Helper class internal to OrthancSeriesVolumeProgressiveLoader */ - class SeriesGeometry : public boost::noncopyable - { - private: - void CheckSlice(size_t index, - const OrthancStone::DicomInstanceParameters& reference) const; - - void CheckVolume() const; - - void Clear(); - - void CheckSliceIndex(size_t index) const; - - std::unique_ptr geometry_; - std::vector slices_; - std::vector slicesRevision_; - - public: - ~SeriesGeometry() - { - Clear(); - } - - void ComputeGeometry(OrthancStone::SlicesSorter& slices); - - virtual bool HasGeometry() const - { - return geometry_.get() != NULL; - } - - virtual const OrthancStone::VolumeImageGeometry& GetImageGeometry() const; - - const OrthancStone::DicomInstanceParameters& GetSliceParameters(size_t index) const; - - uint64_t GetSliceRevision(size_t index) const; - - void IncrementSliceRevision(size_t index); - }; - - void ScheduleNextSliceDownload(); - - void LoadGeometry(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message); - - void SetSliceContent(unsigned int sliceIndex, - const Orthanc::ImageAccessor& image, - unsigned int quality); - - void LoadBestQualitySliceContent(const OrthancStone::GetOrthancImageCommand::SuccessMessage& message); - - void LoadJpegSliceContent(const OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage& message); - - OrthancStone::ILoadersContext& loadersContext_; - bool active_; - bool progressiveQuality_; - unsigned int simultaneousDownloads_; - SeriesGeometry seriesGeometry_; - boost::shared_ptr volume_; - std::unique_ptr sorter_; - std::unique_ptr strategy_; - std::vector slicesQuality_; - bool volumeImageReadyInHighQuality_; - - OrthancSeriesVolumeProgressiveLoader( - OrthancStone::ILoadersContext& loadersContext, - boost::shared_ptr volume, - bool progressiveQuality); - - public: - ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, VolumeImageReadyInHighQuality, OrthancSeriesVolumeProgressiveLoader); - - /** - See doc for the progressiveQuality_ field - */ - static boost::shared_ptr Create( - OrthancStone::ILoadersContext& context, - boost::shared_ptr volume, - bool progressiveQuality = false); - - virtual ~OrthancSeriesVolumeProgressiveLoader(); - - void SetSimultaneousDownloads(unsigned int count); - - bool IsVolumeImageReadyInHighQuality() const - { - return volumeImageReadyInHighQuality_; - } - - void LoadSeries(const std::string& seriesId); - - /** - This getter is used by clients that do not receive the geometry through - subscribing, for instance if they are created or listening only AFTER the - "geometry loaded" message is broadcast - */ - bool HasGeometry() const ORTHANC_OVERRIDE - { - return seriesGeometry_.HasGeometry(); - } - - /** - Same remark as HasGeometry - */ - const OrthancStone::VolumeImageGeometry& GetImageGeometry() const ORTHANC_OVERRIDE - { - return seriesGeometry_.GetImageGeometry(); - } - - /** - When a slice is requested, the strategy algorithm (that defines the - sequence of resources to be loaded from the server) is modified to - take into account this request (this is done in the ExtractedSlice ctor) - */ - virtual IExtractedSlice* - ExtractSlice(const OrthancStone::CoordinateSystem3D& cuttingPlane) ORTHANC_OVERRIDE; - }; -} diff -r 04055b6b9e2c -r b1396be5aa27 Framework/Loaders/DicomStructureSetLoader.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/DicomStructureSetLoader.cpp Fri Apr 03 16:13:06 2020 +0200 @@ -0,0 +1,428 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "DicomStructureSetLoader.h" + +#include "../../Scene2D/PolylineSceneLayer.h" +#include "../../StoneException.h" +#include "../../Toolbox/GeometryToolbox.h" + +#include + +#include + +#if 0 +bool logbgo233 = false; +bool logbgo115 = false; +#endif + +namespace OrthancStone +{ + +#if 0 + void DumpDicomMap(std::ostream& o, const Orthanc::DicomMap& dicomMap) + { + using namespace std; + //ios_base::fmtflags state = o.flags(); + //o.flags(ios::right | ios::hex); + //o << "(" << setfill('0') << setw(4) << tag.GetGroup() + // << "," << setw(4) << tag.GetElement() << ")"; + //o.flags(state); + Json::Value val; + dicomMap.Serialize(val); + o << val; + //return o; + } +#endif + + + class DicomStructureSetLoader::AddReferencedInstance : public LoaderStateMachine::State + { + private: + std::string instanceId_; + + public: + AddReferencedInstance(DicomStructureSetLoader& that, + const std::string& instanceId) : + State(that), + instanceId_(instanceId) + { + } + + virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) + { + Json::Value tags; + message.ParseJsonBody(tags); + + Orthanc::DicomMap dicom; + dicom.FromDicomAsJson(tags); + + DicomStructureSetLoader& loader = GetLoader(); + + loader.content_->AddReferencedSlice(dicom); + + loader.countProcessedInstances_ ++; + assert(loader.countProcessedInstances_ <= loader.countReferencedInstances_); + + if (loader.countProcessedInstances_ == loader.countReferencedInstances_) + { + // All the referenced instances have been loaded, finalize the RT-STRUCT + loader.content_->CheckReferencedSlices(); + loader.revision_++; + loader.SetStructuresReady(); + } + } + }; + + + // State that converts a "SOP Instance UID" to an Orthanc identifier + class DicomStructureSetLoader::LookupInstance : public LoaderStateMachine::State + { + private: + std::string sopInstanceUid_; + + public: + LookupInstance(DicomStructureSetLoader& that, + const std::string& sopInstanceUid) : + State(that), + sopInstanceUid_(sopInstanceUid) + { + } + + virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) + { +#if 0 + LOG(TRACE) << "DicomStructureSetLoader::LookupInstance::Handle() (SUCCESS)"; +#endif + DicomStructureSetLoader& loader = GetLoader(); + + Json::Value lookup; + message.ParseJsonBody(lookup); + + if (lookup.type() != Json::arrayValue || + lookup.size() != 1 || + !lookup[0].isMember("Type") || + !lookup[0].isMember("Path") || + lookup[0]["Type"].type() != Json::stringValue || + lookup[0]["ID"].type() != Json::stringValue || + lookup[0]["Type"].asString() != "Instance") + { + std::stringstream msg; + msg << "Unknown resource! message.GetAnswer() = " << message.GetAnswer() << " message.GetAnswerHeaders() = "; + for (OrthancStone::OrthancRestApiCommand::HttpHeaders::const_iterator it = message.GetAnswerHeaders().begin(); + it != message.GetAnswerHeaders().end(); ++it) + { + msg << "\nkey: \"" << it->first << "\" value: \"" << it->second << "\"\n"; + } + const std::string msgStr = msg.str(); + LOG(ERROR) << msgStr; + throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource); + } + + const std::string instanceId = lookup[0]["ID"].asString(); + + { + std::unique_ptr command(new OrthancStone::OrthancRestApiCommand); + command->SetHttpHeader("Accept-Encoding", "gzip"); + std::string uri = "/instances/" + instanceId + "/tags"; + command->SetUri(uri); + command->AcquirePayload(new AddReferencedInstance(loader, instanceId)); + Schedule(command.release()); + } + } + }; + + + class DicomStructureSetLoader::LoadStructure : public LoaderStateMachine::State + { + public: + LoadStructure(DicomStructureSetLoader& that) : + State(that) + { + } + + virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) + { +#if 0 + if (logbgo115) + LOG(TRACE) << "DicomStructureSetLoader::LoadStructure::Handle() (SUCCESS)"; +#endif + DicomStructureSetLoader& loader = GetLoader(); + + { + OrthancPlugins::FullOrthancDataset dicom(message.GetAnswer()); + loader.content_.reset(new OrthancStone::DicomStructureSet(dicom)); + size_t structureCount = loader.content_->GetStructuresCount(); + loader.structureVisibility_.resize(structureCount); + bool everythingVisible = false; + if ((loader.initiallyVisibleStructures_.size() == 1) + && (loader.initiallyVisibleStructures_[0].size() == 1) + && (loader.initiallyVisibleStructures_[0][0] == '*')) + { + everythingVisible = true; + } + + for (size_t i = 0; i < structureCount; ++i) + { + // if a single "*" string is supplied, this means we want everything + // to be visible... + if(everythingVisible) + { + loader.structureVisibility_.at(i) = true; + } + else + { + // otherwise, we only enable visibility for those structures whose + // names are mentioned in the initiallyVisibleStructures_ array + const std::string& structureName = loader.content_->GetStructureName(i); + + std::vector::iterator foundIt = + std::find( + loader.initiallyVisibleStructures_.begin(), + loader.initiallyVisibleStructures_.end(), + structureName); + std::vector::iterator endIt = loader.initiallyVisibleStructures_.end(); + if (foundIt != endIt) + loader.structureVisibility_.at(i) = true; + else + loader.structureVisibility_.at(i) = false; + } + } + } + + // Some (admittedly invalid) Dicom files have empty values in the + // 0008,1155 tag. We try our best to cope with this. + std::set instances; + std::set nonEmptyInstances; + loader.content_->GetReferencedInstances(instances); + for (std::set::const_iterator + it = instances.begin(); it != instances.end(); ++it) + { + std::string instance = Orthanc::Toolbox::StripSpaces(*it); + if(instance != "") + nonEmptyInstances.insert(instance); + } + + loader.countReferencedInstances_ = + static_cast(nonEmptyInstances.size()); + + for (std::set::const_iterator + it = nonEmptyInstances.begin(); it != nonEmptyInstances.end(); ++it) + { + std::unique_ptr command(new OrthancStone::OrthancRestApiCommand); + command->SetUri("/tools/lookup"); + command->SetMethod(Orthanc::HttpMethod_Post); + command->SetBody(*it); + command->AcquirePayload(new LookupInstance(loader, *it)); + Schedule(command.release()); + } + } + }; + + + class DicomStructureSetLoader::Slice : public IExtractedSlice + { + private: + const OrthancStone::DicomStructureSet& content_; + uint64_t revision_; + bool isValid_; + std::vector visibility_; + + public: + /** + The visibility vector must either: + - be empty + or + - contain the same number of items as the number of structures in the + structure set. + In the first case (empty vector), all the structures are displayed. + In the second case, the visibility of each structure is defined by the + content of the vector at the corresponding index. + */ + Slice(const OrthancStone::DicomStructureSet& content, + uint64_t revision, + const OrthancStone::CoordinateSystem3D& cuttingPlane, + std::vector visibility = std::vector()) + : content_(content) + , revision_(revision) + , visibility_(visibility) + { + ORTHANC_ASSERT((visibility_.size() == content_.GetStructuresCount()) + || (visibility_.size() == 0u)); + + bool opposite; + + const OrthancStone::Vector normal = content.GetNormal(); + isValid_ = ( + OrthancStone::GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetNormal()) || + OrthancStone::GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetAxisX()) || + OrthancStone::GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetAxisY())); + } + + virtual bool IsValid() + { + return isValid_; + } + + virtual uint64_t GetRevision() + { + return revision_; + } + + virtual OrthancStone::ISceneLayer* CreateSceneLayer( + const OrthancStone::ILayerStyleConfigurator* configurator, + const OrthancStone::CoordinateSystem3D& cuttingPlane) + { + assert(isValid_); + + std::unique_ptr layer(new OrthancStone::PolylineSceneLayer); + layer->SetThickness(2); + + for (size_t i = 0; i < content_.GetStructuresCount(); i++) + { + if ((visibility_.size() == 0) || visibility_.at(i)) + { + const OrthancStone::Color& color = content_.GetStructureColor(i); + +#ifdef USE_BOOST_UNION_FOR_POLYGONS + std::vector< std::vector > polygons; + + if (content_.ProjectStructure(polygons, i, cuttingPlane)) + { + for (size_t j = 0; j < polygons.size(); j++) + { + PolylineSceneLayer::Chain chain; + chain.resize(polygons[j].size()); + + for (size_t k = 0; k < polygons[j].size(); k++) + { + chain[k] = ScenePoint2D(polygons[j][k].x, polygons[j][k].y); + } + + layer->AddChain(chain, true /* closed */, color); + } + } +#else + std::vector< std::pair > segments; + + if (content_.ProjectStructure(segments, i, cuttingPlane)) + { + for (size_t j = 0; j < segments.size(); j++) + { + OrthancStone::PolylineSceneLayer::Chain chain; + chain.resize(2); + + chain[0] = OrthancStone::ScenePoint2D(segments[j].first.x, segments[j].first.y); + chain[1] = OrthancStone::ScenePoint2D(segments[j].second.x, segments[j].second.y); + + layer->AddChain(chain, false /* NOT closed */, color); + } + } +#endif + } + } + + return layer.release(); + } + }; + + + DicomStructureSetLoader::DicomStructureSetLoader( + OrthancStone::ILoadersContext& loadersContext) + : LoaderStateMachine(loadersContext) + , loadersContext_(loadersContext) + , revision_(0) + , countProcessedInstances_(0) + , countReferencedInstances_(0) + , structuresReady_(false) + { + } + + + boost::shared_ptr DicomStructureSetLoader::Create(OrthancStone::ILoadersContext& loadersContext) + { + boost::shared_ptr obj( + new DicomStructureSetLoader( + loadersContext)); + obj->LoaderStateMachine::PostConstructor(); + return obj; + + } + + void DicomStructureSetLoader::SetStructureDisplayState(size_t structureIndex, bool display) + { + structureVisibility_.at(structureIndex) = display; + revision_++; + } + + DicomStructureSetLoader::~DicomStructureSetLoader() + { + LOG(TRACE) << "DicomStructureSetLoader::~DicomStructureSetLoader()"; + } + + void DicomStructureSetLoader::LoadInstance( + const std::string& instanceId, + const std::vector& initiallyVisibleStructures) + { + Start(); + + instanceId_ = instanceId; + initiallyVisibleStructures_ = initiallyVisibleStructures; + + { + std::unique_ptr command(new OrthancStone::OrthancRestApiCommand); + command->SetHttpHeader("Accept-Encoding", "gzip"); + + std::string uri = "/instances/" + instanceId + "/tags?ignore-length=3006-0050"; + + command->SetUri(uri); + command->AcquirePayload(new LoadStructure(*this)); + Schedule(command.release()); + } + } + + + OrthancStone::IVolumeSlicer::IExtractedSlice* DicomStructureSetLoader::ExtractSlice(const OrthancStone::CoordinateSystem3D& cuttingPlane) + { + if (content_.get() == NULL) + { + // Geometry is not available yet + return new OrthancStone::IVolumeSlicer::InvalidSlice; + } + else + { + return new Slice(*content_, revision_, cuttingPlane, structureVisibility_); + } + } + + void DicomStructureSetLoader::SetStructuresReady() + { + ORTHANC_ASSERT(!structuresReady_); + structuresReady_ = true; + BroadcastMessage(DicomStructureSetLoader::StructuresReady(*this)); + } + + bool DicomStructureSetLoader::AreStructuresReady() const + { + return structuresReady_; + } + +} diff -r 04055b6b9e2c -r b1396be5aa27 Framework/Loaders/DicomStructureSetLoader.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/DicomStructureSetLoader.h Fri Apr 03 16:13:06 2020 +0200 @@ -0,0 +1,104 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../../Toolbox/DicomStructureSet.h" +#include "../../Volumes/IVolumeSlicer.h" +#include "LoaderStateMachine.h" + +#include + +namespace OrthancStone +{ + class DicomStructureSetLoader : + public LoaderStateMachine, + public OrthancStone::IVolumeSlicer, + public OrthancStone::IObservable + { + private: + class Slice; + + // States of LoaderStateMachine + class AddReferencedInstance; // 3rd state + class LookupInstance; // 2nd state + class LoadStructure; // 1st state + + OrthancStone::ILoadersContext& loadersContext_; + std::unique_ptr content_; + uint64_t revision_; + std::string instanceId_; + unsigned int countProcessedInstances_; + unsigned int countReferencedInstances_; + + // will be set to true once the loading is finished + bool structuresReady_; + + /** + At load time, these strings are used to initialize the structureVisibility_ + vector. + + As a special case, if initiallyVisibleStructures_ contains a single string + that is '*', ALL structures will be made visible. + */ + std::vector initiallyVisibleStructures_; + + /** + Contains the "Should this structure be displayed?" flag for all structures. + Only filled when structures are loaded. + + Changing this value directly affects the rendering + */ + std::vector structureVisibility_; + + protected: + DicomStructureSetLoader(OrthancStone::ILoadersContext& loadersContext); + + public: + ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, StructuresReady, DicomStructureSetLoader); + + static boost::shared_ptr Create( + OrthancStone::ILoadersContext& loadersContext); + + OrthancStone::DicomStructureSet* GetContent() + { + return content_.get(); + } + + void SetStructureDisplayState(size_t structureIndex, bool display); + + bool GetStructureDisplayState(size_t structureIndex) const + { + return structureVisibility_.at(structureIndex); + } + + ~DicomStructureSetLoader(); + + void LoadInstance(const std::string& instanceId, + const std::vector& initiallyVisibleStructures = std::vector()); + + virtual IExtractedSlice* ExtractSlice(const OrthancStone::CoordinateSystem3D& cuttingPlane) ORTHANC_OVERRIDE; + + void SetStructuresReady(); + + bool AreStructuresReady() const; + }; +} diff -r 04055b6b9e2c -r b1396be5aa27 Framework/Loaders/LoaderCache.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/LoaderCache.cpp Fri Apr 03 16:13:06 2020 +0200 @@ -0,0 +1,384 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + +#include "LoaderCache.h" + +#include "../../StoneException.h" +#include "OrthancSeriesVolumeProgressiveLoader.h" +#include "OrthancMultiframeVolumeLoader.h" +#include "DicomStructureSetLoader.h" + +#include "../../Loaders/ILoadersContext.h" + + +#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 +#include "DicomStructureSetLoader2.h" +#endif + //BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + + +#if ORTHANC_ENABLE_WASM == 1 +# include +# include "../../Oracle/WebAssemblyOracle.h" +#else +# include "../../Oracle/ThreadedOracle.h" +#endif + +#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 +#include "../../Toolbox/DicomStructureSet2.h" +#endif +//BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + +#include "../../Volumes/DicomVolumeImage.h" +#include "../../Volumes/DicomVolumeImageMPRSlicer.h" + +#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 +#include "../../Volumes/DicomStructureSetSlicer2.h" +#endif +//BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + +#include +#include + +namespace OrthancStone +{ + LoaderCache::LoaderCache(OrthancStone::ILoadersContext& loadersContext) + : loadersContext_(loadersContext) + { + + } + + boost::shared_ptr + LoaderCache::GetSeriesVolumeProgressiveLoader(std::string seriesUuid) + { + try + { + + // normalize keys a little + seriesUuid = Orthanc::Toolbox::StripSpaces(seriesUuid); + Orthanc::Toolbox::ToLowerCase(seriesUuid); + + // find in cache + if (seriesVolumeProgressiveLoaders_.find(seriesUuid) == seriesVolumeProgressiveLoaders_.end()) + { + std::unique_ptr lock(loadersContext_.Lock()); + + boost::shared_ptr volumeImage(new OrthancStone::DicomVolumeImage); + boost::shared_ptr loader; + + // true means "use progressive quality" + // false means "load high quality slices only" + loader = OrthancSeriesVolumeProgressiveLoader::Create(loadersContext_, volumeImage, false); + loader->LoadSeries(seriesUuid); + seriesVolumeProgressiveLoaders_[seriesUuid] = loader; + } + else + { +// LOG(TRACE) << "LoaderCache::GetSeriesVolumeProgressiveLoader : returning cached loader for seriesUUid = " << seriesUuid; + } + return seriesVolumeProgressiveLoaders_[seriesUuid]; + } + catch (const Orthanc::OrthancException& e) + { + if (e.HasDetails()) + { + LOG(ERROR) << "OrthancException in LoaderCache: " << e.What() << " Details: " << e.GetDetails(); + } + else + { + LOG(ERROR) << "OrthancException in LoaderCache: " << e.What(); + } + throw; + } + catch (const std::exception& e) + { + LOG(ERROR) << "std::exception in LoaderCache: " << e.what(); + throw; + } + catch (...) + { + LOG(ERROR) << "Unknown exception in LoaderCache"; + throw; + } + } + + boost::shared_ptr LoaderCache::GetMultiframeVolumeLoader(std::string instanceUuid) + { + // if the loader is not available, let's trigger its creation + if(multiframeVolumeLoaders_.find(instanceUuid) == multiframeVolumeLoaders_.end()) + { + GetMultiframeDicomVolumeImageMPRSlicer(instanceUuid); + } + ORTHANC_ASSERT(multiframeVolumeLoaders_.find(instanceUuid) != multiframeVolumeLoaders_.end()); + + return multiframeVolumeLoaders_[instanceUuid]; + } + + boost::shared_ptr LoaderCache::GetMultiframeDicomVolumeImageMPRSlicer(std::string instanceUuid) + { + try + { + // normalize keys a little + instanceUuid = Orthanc::Toolbox::StripSpaces(instanceUuid); + Orthanc::Toolbox::ToLowerCase(instanceUuid); + + // find in cache + if (dicomVolumeImageMPRSlicers_.find(instanceUuid) == dicomVolumeImageMPRSlicers_.end()) + { + std::unique_ptr lock(loadersContext_.Lock()); + boost::shared_ptr volumeImage(new OrthancStone::DicomVolumeImage); + boost::shared_ptr loader; + { + loader = OrthancMultiframeVolumeLoader::Create(loadersContext_, volumeImage); + loader->LoadInstance(instanceUuid); + } + multiframeVolumeLoaders_[instanceUuid] = loader; + boost::shared_ptr mprSlicer(new OrthancStone::DicomVolumeImageMPRSlicer(volumeImage)); + dicomVolumeImageMPRSlicers_[instanceUuid] = mprSlicer; + } + return dicomVolumeImageMPRSlicers_[instanceUuid]; + } + catch (const Orthanc::OrthancException& e) + { + if (e.HasDetails()) + { + LOG(ERROR) << "OrthancException in LoaderCache: " << e.What() << " Details: " << e.GetDetails(); + } + else + { + LOG(ERROR) << "OrthancException in LoaderCache: " << e.What(); + } + throw; + } + catch (const std::exception& e) + { + LOG(ERROR) << "std::exception in LoaderCache: " << e.what(); + throw; + } + catch (...) + { + LOG(ERROR) << "Unknown exception in LoaderCache"; + throw; + } + } + +#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + + boost::shared_ptr LoaderCache::GetDicomStructureSetSlicer2(std::string instanceUuid) + { + // if the loader is not available, let's trigger its creation + if (dicomStructureSetSlicers2_.find(instanceUuid) == dicomStructureSetSlicers2_.end()) + { + GetDicomStructureSetLoader2(instanceUuid); + } + ORTHANC_ASSERT(dicomStructureSetSlicers2_.find(instanceUuid) != dicomStructureSetSlicers2_.end()); + + return dicomStructureSetSlicers2_[instanceUuid]; + } +#endif +//BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + + + /** + This method allows to convert a list of string into a string by + sorting the strings then joining them + */ + static std::string SortAndJoin(const std::vector& stringList) + { + if (stringList.size() == 0) + { + return ""; + } + else + { + std::vector sortedStringList = stringList; + std::sort(sortedStringList.begin(), sortedStringList.end()); + std::stringstream s; + s << sortedStringList[0]; + for (size_t i = 1; i < sortedStringList.size(); ++i) + { + s << "-" << sortedStringList[i]; + } + return s.str(); + } + } + + boost::shared_ptr + LoaderCache::GetDicomStructureSetLoader( + std::string inInstanceUuid, + const std::vector& initiallyVisibleStructures) + { + try + { + // normalize keys a little + inInstanceUuid = Orthanc::Toolbox::StripSpaces(inInstanceUuid); + Orthanc::Toolbox::ToLowerCase(inInstanceUuid); + + std::string initiallyVisibleStructuresKey = + SortAndJoin(initiallyVisibleStructures); + + std::string entryKey = inInstanceUuid + "_" + initiallyVisibleStructuresKey; + + // find in cache + if (dicomStructureSetLoaders_.find(entryKey) == dicomStructureSetLoaders_.end()) + { + std::unique_ptr lock(loadersContext_.Lock()); + + boost::shared_ptr loader; + { + loader = DicomStructureSetLoader::Create(loadersContext_); + loader->LoadInstance(inInstanceUuid, initiallyVisibleStructures); + } + dicomStructureSetLoaders_[entryKey] = loader; + } + return dicomStructureSetLoaders_[entryKey]; + } + catch (const Orthanc::OrthancException& e) + { + if (e.HasDetails()) + { + LOG(ERROR) << "OrthancException in LoaderCache: " << e.What() << " Details: " << e.GetDetails(); + } + else + { + LOG(ERROR) << "OrthancException in LoaderCache: " << e.What(); + } + throw; + } + catch (const std::exception& e) + { + LOG(ERROR) << "std::exception in LoaderCache: " << e.what(); + throw; + } + catch (...) + { + LOG(ERROR) << "Unknown exception in LoaderCache"; + throw; + } + } + +#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + + boost::shared_ptr LoaderCache::GetDicomStructureSetLoader2(std::string instanceUuid) + { + try + { + // normalize keys a little + instanceUuid = Orthanc::Toolbox::StripSpaces(instanceUuid); + Orthanc::Toolbox::ToLowerCase(instanceUuid); + + // find in cache + if (dicomStructureSetLoaders2_.find(instanceUuid) == dicomStructureSetLoaders2_.end()) + { + boost::shared_ptr loader; + boost::shared_ptr structureSet(new DicomStructureSet2()); + boost::shared_ptr rtSlicer(new DicomStructureSetSlicer2(structureSet)); + dicomStructureSetSlicers2_[instanceUuid] = rtSlicer; + dicomStructureSets2_[instanceUuid] = structureSet; // to prevent it from being deleted + { +#if ORTHANC_ENABLE_WASM == 1 + loader.reset(new DicomStructureSetLoader2(*(structureSet.get()), oracle_, oracle_)); +#else + LockingEmitter::WriterLock lock(lockingEmitter_); + // TODO: clarify lifetimes... this is DANGEROUS! + loader.reset(new DicomStructureSetLoader2(*(structureSet.get()), oracle_, lock.GetOracleObservable())); +#endif + loader->LoadInstance(instanceUuid); + } + dicomStructureSetLoaders2_[instanceUuid] = loader; + } + return dicomStructureSetLoaders2_[instanceUuid]; + } + catch (const Orthanc::OrthancException& e) + { + if (e.HasDetails()) + { + LOG(ERROR) << "OrthancException in GetDicomStructureSetLoader2: " << e.What() << " Details: " << e.GetDetails(); + } + else + { + LOG(ERROR) << "OrthancException in GetDicomStructureSetLoader2: " << e.What(); + } + throw; + } + catch (const std::exception& e) + { + LOG(ERROR) << "std::exception in GetDicomStructureSetLoader2: " << e.what(); + throw; + } + catch (...) + { + LOG(ERROR) << "Unknown exception in GetDicomStructureSetLoader2"; + throw; + } + } + +#endif +// BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + + + void LoaderCache::ClearCache() + { + std::unique_ptr lock(loadersContext_.Lock()); + +#ifndef NDEBUG + // ISO way of checking for debug builds + DebugDisplayObjRefCounts(); +#endif + seriesVolumeProgressiveLoaders_.clear(); + multiframeVolumeLoaders_.clear(); + dicomVolumeImageMPRSlicers_.clear(); + dicomStructureSetLoaders_.clear(); + +#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + // order is important! + dicomStructureSetLoaders2_.clear(); + dicomStructureSetSlicers2_.clear(); + dicomStructureSets2_.clear(); +#endif +// BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + } + + template void DebugDisplayObjRefCountsInMap( + const std::string& name, const std::map >& myMap) + { + LOG(TRACE) << "Map \"" << name << "\" ref counts:"; + size_t i = 0; + for (typename std::map >::const_iterator + it = myMap.begin(); it != myMap.end(); ++it) + { + LOG(TRACE) << " element #" << i << ": ref count = " << it->second.use_count(); + i++; + } + } + + void LoaderCache::DebugDisplayObjRefCounts() + { + DebugDisplayObjRefCountsInMap("seriesVolumeProgressiveLoaders_", seriesVolumeProgressiveLoaders_); + DebugDisplayObjRefCountsInMap("multiframeVolumeLoaders_", multiframeVolumeLoaders_); + DebugDisplayObjRefCountsInMap("dicomVolumeImageMPRSlicers_", dicomVolumeImageMPRSlicers_); + DebugDisplayObjRefCountsInMap("dicomStructureSetLoaders_", dicomStructureSetLoaders_); +#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + DebugDisplayObjRefCountsInMap("dicomStructureSetLoaders2_", dicomStructureSetLoaders2_); + DebugDisplayObjRefCountsInMap("dicomStructureSetSlicers2_", dicomStructureSetSlicers2_); +#endif +//BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + } +} diff -r 04055b6b9e2c -r b1396be5aa27 Framework/Loaders/LoaderCache.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/LoaderCache.h Fri Apr 03 16:13:06 2020 +0200 @@ -0,0 +1,97 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + +#pragma once + +#include "../Messages/LockingEmitter.h" +#include "../../Volumes/DicomVolumeImageMPRSlicer.h" +#include "OrthancSeriesVolumeProgressiveLoader.h" +#include "OrthancMultiframeVolumeLoader.h" +#include "DicomStructureSetLoader.h" + +#include + +#include +#include +#include + +namespace OrthancStone +{ + class ILoadersContext; +} + +namespace OrthancStone +{ + class LoaderCache + { + public: + LoaderCache(OrthancStone::ILoadersContext& loadersContext); + + boost::shared_ptr + GetSeriesVolumeProgressiveLoader (std::string seriesUuid); + + boost::shared_ptr + GetMultiframeDicomVolumeImageMPRSlicer(std::string instanceUuid); + + boost::shared_ptr + GetMultiframeVolumeLoader(std::string instanceUuid); + + boost::shared_ptr + GetDicomStructureSetLoader( + std::string instanceUuid, + const std::vector& initiallyVisibleStructures); + +#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + boost::shared_ptr + GetDicomStructureSetLoader2(std::string instanceUuid); + + boost::shared_ptr + GetDicomStructureSetSlicer2(std::string instanceUuid); +#endif + //BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + + void ClearCache(); + + private: + + void DebugDisplayObjRefCounts(); + + OrthancStone::ILoadersContext& loadersContext_; + + std::map > + seriesVolumeProgressiveLoaders_; + std::map > + multiframeVolumeLoaders_; + std::map > + dicomVolumeImageMPRSlicers_; + std::map > + dicomStructureSetLoaders_; +#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + std::map > + dicomStructureSetLoaders2_; + std::map > + dicomStructureSets2_; + std::map > + dicomStructureSetSlicers2_; +#endif + //BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + }; +} + diff -r 04055b6b9e2c -r b1396be5aa27 Framework/Loaders/LoaderStateMachine.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/LoaderStateMachine.cpp Fri Apr 03 16:13:06 2020 +0200 @@ -0,0 +1,219 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "LoaderStateMachine.h" + +#include "../../Loaders/ILoadersContext.h" + +#include + +namespace OrthancStone +{ + void LoaderStateMachine::State::Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + + void LoaderStateMachine::State::Handle(const OrthancStone::GetOrthancImageCommand::SuccessMessage& message) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + + void LoaderStateMachine::State::Handle(const OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage& message) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + + void LoaderStateMachine::Schedule(OrthancStone::OracleCommandBase* command) + { + LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::Schedule()"; + + std::unique_ptr protection(command); + + if (command == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + if (!command->HasPayload()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, + "The payload must contain the next state"); + } + pendingCommands_.push_back(protection.release()); + + Step(); + } + + + void LoaderStateMachine::Start() + { + LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::Start()"; + + if (active_) + { + LOG(TRACE) << "LoaderStateMachine::Start() called while active_ is true"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + active_ = true; + + for (size_t i = 0; i < simultaneousDownloads_; i++) + { + Step(); + } + } + + + void LoaderStateMachine::Step() + { + if (!pendingCommands_.empty() && + activeCommands_ < simultaneousDownloads_) + { + + OrthancStone::IOracleCommand* nextCommand = pendingCommands_.front(); + + LOG(TRACE) << " LoaderStateMachine(" << std::hex << this << std::dec << + ")::Step(): activeCommands_ (" << activeCommands_ << + ") < simultaneousDownloads_ (" << simultaneousDownloads_ << + ") --> will Schedule command addr " << std::hex << nextCommand << std::dec; + + { + std::unique_ptr lock(loadersContext_.Lock()); + boost::shared_ptr observer(GetSharedObserver()); + lock->Schedule(observer, 0, nextCommand); // TODO: priority! + } + pendingCommands_.pop_front(); + + activeCommands_++; + } + else + { + LOG(TRACE) << " LoaderStateMachine(" << std::hex << this << std::dec << + ")::Step(): activeCommands_ (" << activeCommands_ << + ") >= simultaneousDownloads_ (" << simultaneousDownloads_ << + ") --> will NOT Schedule command"; + } + } + + + void LoaderStateMachine::Clear() + { + LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::Clear()"; + for (PendingCommands::iterator it = pendingCommands_.begin(); + it != pendingCommands_.end(); ++it) + { + delete *it; + } + + pendingCommands_.clear(); + } + + + void LoaderStateMachine::HandleExceptionMessage(const OrthancStone::OracleCommandExceptionMessage& message) + { + LOG(ERROR) << "LoaderStateMachine::HandleExceptionMessage: error in the state machine, stopping all processing"; + LOG(ERROR) << "Error: " << message.GetException().What() << " Details: " << + message.GetException().GetDetails(); + Clear(); + } + + template + void LoaderStateMachine::HandleSuccessMessage(const T& message) + { + if (activeCommands_ <= 0) { + LOG(ERROR) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::HandleSuccessMessage : activeCommands_ should be > 0 but is: " << activeCommands_; + } + else { + activeCommands_--; + try + { + dynamic_cast(message.GetOrigin().GetPayload()).Handle(message); + Step(); + } + catch (Orthanc::OrthancException& e) + { + LOG(ERROR) << "Error in the state machine, stopping all processing: " << + e.What() << " Details: " << e.GetDetails(); + Clear(); + } + } + } + + + LoaderStateMachine::LoaderStateMachine( + OrthancStone::ILoadersContext& loadersContext) + : loadersContext_(loadersContext) + , active_(false) + , simultaneousDownloads_(4) + , activeCommands_(0) + { + using OrthancStone::ILoadersContext; + + LOG(TRACE) + << "LoaderStateMachine(" << std::hex << this + << std::dec << ")::LoaderStateMachine()"; + } + + void LoaderStateMachine::PostConstructor() + { + std::unique_ptr + lock(loadersContext_.Lock()); + + OrthancStone::IObservable& observable = lock->GetOracleObservable(); + + // TODO => Move this out of constructor + Register( + observable, &LoaderStateMachine::HandleSuccessMessage); + Register( + observable, &LoaderStateMachine::HandleSuccessMessage); + Register( + observable, &LoaderStateMachine::HandleSuccessMessage); + Register( + observable, &LoaderStateMachine::HandleExceptionMessage); + } + + LoaderStateMachine::~LoaderStateMachine() + { + LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::~LoaderStateMachine()"; + Clear(); + } + + void LoaderStateMachine::SetSimultaneousDownloads(unsigned int count) + { + if (active_) + { + LOG(ERROR) << "LoaderStateMachine::SetSimultaneousDownloads called while active_ is true"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else if (count == 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + else + { + simultaneousDownloads_ = count; + } + } +} diff -r 04055b6b9e2c -r b1396be5aa27 Framework/Loaders/LoaderStateMachine.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/LoaderStateMachine.h Fri Apr 03 16:13:06 2020 +0200 @@ -0,0 +1,124 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../../Messages/IObservable.h" +#include "../../Messages/ObserverBase.h" +#include "../../Oracle/GetOrthancImageCommand.h" +#include "../../Oracle/GetOrthancWebViewerJpegCommand.h" +#include "../../Oracle/IOracle.h" +#include "../../Oracle/OracleCommandExceptionMessage.h" +#include "../../Oracle/OrthancRestApiCommand.h" + +#include + +#include + +namespace OrthancStone +{ + /** + This class is supplied with Oracle commands and will schedule up to + simultaneousDownloads_ of them at the same time, then will schedule the + rest once slots become available. It is used, a.o., by the + OrtancMultiframeVolumeLoader class. + + To use it, you need to create commands that derive from State. + + You need to initialize them with the object that must be called when + an answer is received. + */ + + class LoaderStateMachine : public OrthancStone::ObserverBase + { + public: + class State : public Orthanc::IDynamicObject + { + private: + LoaderStateMachine& that_; + + public: + State(LoaderStateMachine& that) : + that_(that) + { + } + + State(const State& currentState) : + that_(currentState.that_) + { + } + + void Schedule(OrthancStone::OracleCommandBase* command) const + { + that_.Schedule(command); + } + + template + T& GetLoader() const + { + return dynamic_cast(that_); + } + + virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message); + + virtual void Handle(const OrthancStone::GetOrthancImageCommand::SuccessMessage& message); + + virtual void Handle(const OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage& message); + }; + + void Schedule(OrthancStone::OracleCommandBase* command); + + void Start(); + + private: + void Step(); + + void Clear(); + + void HandleExceptionMessage(const OrthancStone::OracleCommandExceptionMessage& message); + + template + void HandleSuccessMessage(const T& message); + + typedef std::list PendingCommands; + + OrthancStone::ILoadersContext& loadersContext_; + bool active_; + unsigned int simultaneousDownloads_; + PendingCommands pendingCommands_; + unsigned int activeCommands_; + + + public: + LoaderStateMachine(OrthancStone::ILoadersContext& loadersContext); + + void PostConstructor(); + + virtual ~LoaderStateMachine(); + + bool IsActive() const + { + return active_; + } + + void SetSimultaneousDownloads(unsigned int count); + }; +} diff -r 04055b6b9e2c -r b1396be5aa27 Framework/Loaders/OrthancMultiframeVolumeLoader.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/OrthancMultiframeVolumeLoader.cpp Fri Apr 03 16:13:06 2020 +0200 @@ -0,0 +1,593 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "OrthancMultiframeVolumeLoader.h" + +#include +#include + +namespace OrthancStone +{ + class OrthancMultiframeVolumeLoader::LoadRTDoseGeometry : public LoaderStateMachine::State + { + private: + std::unique_ptr dicom_; + + public: + LoadRTDoseGeometry(OrthancMultiframeVolumeLoader& that, + Orthanc::DicomMap* dicom) : + State(that), + dicom_(dicom) + { + if (dicom == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + } + + virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) + { + // Complete the DICOM tags with just-received "Grid Frame Offset Vector" + std::string s = Orthanc::Toolbox::StripSpaces(message.GetAnswer()); + dicom_->SetValue(Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR, s, false); + + GetLoader().SetGeometry(*dicom_); + } + }; + + + static std::string GetSopClassUid(const Orthanc::DicomMap& dicom) + { + std::string s; + if (!dicom.LookupStringValue(s, Orthanc::DICOM_TAG_SOP_CLASS_UID, false)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, + "DICOM file without SOP class UID"); + } + else + { + return s; + } + } + + + class OrthancMultiframeVolumeLoader::LoadGeometry : public State + { + public: + LoadGeometry(OrthancMultiframeVolumeLoader& that) : + State(that) + { + } + + virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) + { + OrthancMultiframeVolumeLoader& loader = GetLoader(); + + Json::Value body; + message.ParseJsonBody(body); + + if (body.type() != Json::objectValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + + std::unique_ptr dicom(new Orthanc::DicomMap); + dicom->FromDicomAsJson(body); + + if (OrthancStone::StringToSopClassUid(GetSopClassUid(*dicom)) == OrthancStone::SopClassUid_RTDose) + { + // Download the "Grid Frame Offset Vector" DICOM tag, that is + // mandatory for RT-DOSE, but is too long to be returned by default + + std::unique_ptr command(new OrthancStone::OrthancRestApiCommand); + command->SetUri("/instances/" + loader.GetInstanceId() + "/content/" + + Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR.Format()); + command->AcquirePayload(new LoadRTDoseGeometry(loader, dicom.release())); + + Schedule(command.release()); + } + else + { + loader.SetGeometry(*dicom); + } + } + }; + + class OrthancMultiframeVolumeLoader::LoadTransferSyntax : public State + { + public: + LoadTransferSyntax(OrthancMultiframeVolumeLoader& that) : + State(that) + { + } + + virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) + { + GetLoader().SetTransferSyntax(message.GetAnswer()); + } + }; + + class OrthancMultiframeVolumeLoader::LoadUncompressedPixelData : public State + { + public: + LoadUncompressedPixelData(OrthancMultiframeVolumeLoader& that) : + State(that) + { + } + + virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) + { + GetLoader().SetUncompressedPixelData(message.GetAnswer()); + } + }; + + const std::string& OrthancMultiframeVolumeLoader::GetInstanceId() const + { + if (IsActive()) + { + return instanceId_; + } + else + { + LOG(ERROR) << "OrthancMultiframeVolumeLoader::GetInstanceId(): (!IsActive())"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + void OrthancMultiframeVolumeLoader::ScheduleFrameDownloads() + { + if (transferSyntaxUid_.empty() || + !volume_->HasGeometry()) + { + return; + } + /* + 1.2.840.10008.1.2 Implicit VR Endian: Default Transfer Syntax for DICOM + 1.2.840.10008.1.2.1 Explicit VR Little Endian + 1.2.840.10008.1.2.2 Explicit VR Big Endian + + See https://www.dicomlibrary.com/dicom/transfer-syntax/ + */ + if (transferSyntaxUid_ == "1.2.840.10008.1.2" || + transferSyntaxUid_ == "1.2.840.10008.1.2.1" || + transferSyntaxUid_ == "1.2.840.10008.1.2.2") + { + std::unique_ptr command(new OrthancStone::OrthancRestApiCommand); + command->SetHttpHeader("Accept-Encoding", "gzip"); + command->SetUri("/instances/" + instanceId_ + "/content/" + + Orthanc::DICOM_TAG_PIXEL_DATA.Format() + "/0"); + command->AcquirePayload(new LoadUncompressedPixelData(*this)); + Schedule(command.release()); + } + else + { + throw Orthanc::OrthancException( + Orthanc::ErrorCode_NotImplemented, + "No support for multiframe instances with transfer syntax: " + transferSyntaxUid_); + } + } + + void OrthancMultiframeVolumeLoader::SetTransferSyntax(const std::string& transferSyntax) + { + transferSyntaxUid_ = Orthanc::Toolbox::StripSpaces(transferSyntax); + ScheduleFrameDownloads(); + } + + void OrthancMultiframeVolumeLoader::SetGeometry(const Orthanc::DicomMap& dicom) + { + OrthancStone::DicomInstanceParameters parameters(dicom); + volume_->SetDicomParameters(parameters); + + Orthanc::PixelFormat format; + if (!parameters.GetImageInformation().ExtractPixelFormat(format, true)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + double spacingZ; + switch (parameters.GetSopClassUid()) + { + case OrthancStone::SopClassUid_RTDose: + spacingZ = parameters.GetThickness(); + break; + + default: + throw Orthanc::OrthancException( + Orthanc::ErrorCode_NotImplemented, + "No support for multiframe instances with SOP class UID: " + GetSopClassUid(dicom)); + } + + const unsigned int width = parameters.GetImageInformation().GetWidth(); + const unsigned int height = parameters.GetImageInformation().GetHeight(); + const unsigned int depth = parameters.GetImageInformation().GetNumberOfFrames(); + + { + OrthancStone::VolumeImageGeometry geometry; + geometry.SetSizeInVoxels(width, height, depth); + geometry.SetAxialGeometry(parameters.GetGeometry()); + geometry.SetVoxelDimensions(parameters.GetPixelSpacingX(), + parameters.GetPixelSpacingY(), spacingZ); + volume_->Initialize(geometry, format, true /* Do compute range */); + } + + volume_->GetPixelData().Clear(); + + ScheduleFrameDownloads(); + + + + BroadcastMessage(OrthancStone::DicomVolumeImage::GeometryReadyMessage(*volume_)); + } + + + ORTHANC_FORCE_INLINE + static void CopyPixel(uint32_t& target, const void* source) + { + // TODO - check alignement? + target = le32toh(*reinterpret_cast(source)); + } + + ORTHANC_FORCE_INLINE + static void CopyPixel(uint16_t& target, const void* source) + { + // TODO - check alignement? + target = le16toh(*reinterpret_cast(source)); + } + + ORTHANC_FORCE_INLINE + static void CopyPixel(int16_t& target, const void* source) + { + // byte swapping is the same for unsigned and signed integers + // (the sign bit is always stored with the MSByte) + uint16_t* targetUp = reinterpret_cast(&target); + CopyPixel(*targetUp, source); + } + + template + void OrthancMultiframeVolumeLoader::CopyPixelDataAndComputeDistribution( + const std::string& pixelData, std::map& distribution) + { + OrthancStone::ImageBuffer3D& target = volume_->GetPixelData(); + + const unsigned int bpp = target.GetBytesPerPixel(); + const unsigned int width = target.GetWidth(); + const unsigned int height = target.GetHeight(); + const unsigned int depth = target.GetDepth(); + + if (pixelData.size() != bpp * width * height * depth) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, + "The pixel data has not the proper size"); + } + + if (pixelData.empty()) + { + return; + } + + // first pass to initialize map + { + const uint8_t* source = reinterpret_cast(pixelData.c_str()); + + for (unsigned int z = 0; z < depth; z++) + { + for (unsigned int y = 0; y < height; y++) + { + for (unsigned int x = 0; x < width; x++) + { + T value; + CopyPixel(value, source); + distribution[value] = 0; + source += bpp; + } + } + } + } + + { + const uint8_t* source = reinterpret_cast(pixelData.c_str()); + + for (unsigned int z = 0; z < depth; z++) + { + OrthancStone::ImageBuffer3D::SliceWriter writer(target, OrthancStone::VolumeProjection_Axial, z); + + assert(writer.GetAccessor().GetWidth() == width && + writer.GetAccessor().GetHeight() == height); + + for (unsigned int y = 0; y < height; y++) + { + assert(sizeof(T) == Orthanc::GetBytesPerPixel(target.GetFormat())); + + T* target = reinterpret_cast(writer.GetAccessor().GetRow(y)); + + for (unsigned int x = 0; x < width; x++) + { + CopyPixel(*target, source); + + distribution[*target] += 1; + + target++; + source += bpp; + } + } + } + } + } + + template + void OrthancMultiframeVolumeLoader::ComputeMinMaxWithOutlierRejection( + const std::map& distribution) + { + if (distribution.size() == 0) + { + LOG(ERROR) << "ComputeMinMaxWithOutlierRejection -- Volume image empty."; + } + else + { + OrthancStone::ImageBuffer3D& target = volume_->GetPixelData(); + + const uint64_t width = target.GetWidth(); + const uint64_t height = target.GetHeight(); + const uint64_t depth = target.GetDepth(); + const uint64_t voxelCount = width * height * depth; + + // now that we have distribution[pixelValue] == numberOfPixelsWithValue + // compute number of values and check (assertion) that it is equal to + // width * height * depth + { + typename std::map::const_iterator it = distribution.begin(); + uint64_t totalCount = 0; + distributionRawMin_ = static_cast(it->first); + + while (it != distribution.end()) + { + T pixelValue = it->first; + uint64_t count = it->second; + totalCount += count; + it++; + if (it == distribution.end()) + distributionRawMax_ = static_cast(pixelValue); + } + LOG(INFO) << "Volume image. First distribution value = " + << static_cast(distributionRawMin_) + << " | Last distribution value = " + << static_cast(distributionRawMax_); + + if (totalCount != voxelCount) + { + LOG(ERROR) << "Internal error in dose distribution computation. TC (" + << totalCount << ") != VoxC (" << voxelCount; + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + + // compute the number of voxels to reject at each end of the distribution + uint64_t endRejectionCount = static_cast( + outliersHalfRejectionRate_ * voxelCount); + + if (endRejectionCount > voxelCount) + { + LOG(ERROR) << "Internal error in dose distribution computation." + << " endRejectionCount = " << endRejectionCount + << " | voxelCount = " << voxelCount; + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + // this will contain the actual distribution minimum after outlier + // rejection + T resultMin = 0; + + // then start from start and remove pixel values up to + // endRejectionCount voxels rejected + { + typename std::map::const_iterator it = distribution.begin(); + + uint64_t currentCount = 0; + + while (it != distribution.end()) + { + T pixelValue = it->first; + uint64_t count = it->second; + + // if this pixelValue crosses the rejection threshold, let's set it + // and exit the loop + if ((currentCount <= endRejectionCount) && + (currentCount + count > endRejectionCount)) + { + resultMin = pixelValue; + break; + } + else + { + currentCount += count; + } + // and continue walking along the distribution + it++; + } + } + + // this will contain the actual distribution maximum after outlier + // rejection + T resultMax = 0; + // now start from END and remove pixel values up to + // endRejectionCount voxels rejected + { + typename std::map::const_reverse_iterator it = distribution.rbegin(); + + uint64_t currentCount = 0; + + while (it != distribution.rend()) + { + T pixelValue = it->first; + uint64_t count = it->second; + + if ((currentCount <= endRejectionCount) && + (currentCount + count > endRejectionCount)) + { + resultMax = pixelValue; + break; + } + else + { + currentCount += count; + } + // and continue walking along the distribution + it++; + } + } + if (resultMin > resultMax) + { + LOG(ERROR) << "Internal error in dose distribution computation! " << + "resultMin (" << resultMin << ") > resultMax (" << resultMax << ")"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + computedDistributionMin_ = static_cast(resultMin); + computedDistributionMax_ = static_cast(resultMax); + } + } + + template + void OrthancMultiframeVolumeLoader::CopyPixelDataAndComputeMinMax( + const std::string& pixelData) + { + std::map distribution; + CopyPixelDataAndComputeDistribution(pixelData, distribution); + ComputeMinMaxWithOutlierRejection(distribution); + } + + void OrthancMultiframeVolumeLoader::SetUncompressedPixelData(const std::string& pixelData) + { + switch (volume_->GetPixelData().GetFormat()) + { + case Orthanc::PixelFormat_Grayscale32: + CopyPixelDataAndComputeMinMax(pixelData); + break; + case Orthanc::PixelFormat_Grayscale16: + CopyPixelDataAndComputeMinMax(pixelData); + break; + case Orthanc::PixelFormat_SignedGrayscale16: + CopyPixelDataAndComputeMinMax(pixelData); + break; + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + volume_->IncrementRevision(); + + pixelDataLoaded_ = true; + BroadcastMessage(OrthancStone::DicomVolumeImage::ContentUpdatedMessage(*volume_)); + } + + bool OrthancMultiframeVolumeLoader::HasGeometry() const + { + return volume_->HasGeometry(); + } + + const OrthancStone::VolumeImageGeometry& OrthancMultiframeVolumeLoader::GetImageGeometry() const + { + return volume_->GetGeometry(); + } + + OrthancMultiframeVolumeLoader::OrthancMultiframeVolumeLoader( + OrthancStone::ILoadersContext& loadersContext, + boost::shared_ptr volume, + float outliersHalfRejectionRate) + : LoaderStateMachine(loadersContext) + , volume_(volume) + , pixelDataLoaded_(false) + , outliersHalfRejectionRate_(outliersHalfRejectionRate) + , distributionRawMin_(0) + , distributionRawMax_(0) + , computedDistributionMin_(0) + , computedDistributionMax_(0) + { + if (volume.get() == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + } + + + boost::shared_ptr + OrthancMultiframeVolumeLoader::Create( + OrthancStone::ILoadersContext& loadersContext, + boost::shared_ptr volume, + float outliersHalfRejectionRate /*= 0.0005*/) + { + boost::shared_ptr obj( + new OrthancMultiframeVolumeLoader( + loadersContext, + volume, + outliersHalfRejectionRate)); + obj->LoaderStateMachine::PostConstructor(); + return obj; + } + + OrthancMultiframeVolumeLoader::~OrthancMultiframeVolumeLoader() + { + LOG(TRACE) << "OrthancMultiframeVolumeLoader::~OrthancMultiframeVolumeLoader()"; + } + + void OrthancMultiframeVolumeLoader::GetDistributionMinMax + (float& minValue, float& maxValue) const + { + if (distributionRawMin_ == 0 && distributionRawMax_ == 0) + { + LOG(WARNING) << "GetDistributionMinMaxWithOutliersRejection called before computation!"; + } + minValue = distributionRawMin_; + maxValue = distributionRawMax_; + } + + void OrthancMultiframeVolumeLoader::GetDistributionMinMaxWithOutliersRejection + (float& minValue, float& maxValue) const + { + if (computedDistributionMin_ == 0 && computedDistributionMax_ == 0) + { + LOG(WARNING) << "GetDistributionMinMaxWithOutliersRejection called before computation!"; + } + minValue = computedDistributionMin_; + maxValue = computedDistributionMax_; + } + + void OrthancMultiframeVolumeLoader::LoadInstance(const std::string& instanceId) + { + Start(); + + instanceId_ = instanceId; + + { + std::unique_ptr command(new OrthancStone::OrthancRestApiCommand); + command->SetHttpHeader("Accept-Encoding", "gzip"); + command->SetUri("/instances/" + instanceId + "/tags"); + command->AcquirePayload(new LoadGeometry(*this)); + Schedule(command.release()); + } + + { + std::unique_ptr command(new OrthancStone::OrthancRestApiCommand); + command->SetUri("/instances/" + instanceId + "/metadata/TransferSyntax"); + command->AcquirePayload(new LoadTransferSyntax(*this)); + Schedule(command.release()); + } + } +} diff -r 04055b6b9e2c -r b1396be5aa27 Framework/Loaders/OrthancMultiframeVolumeLoader.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/OrthancMultiframeVolumeLoader.h Fri Apr 03 16:13:06 2020 +0200 @@ -0,0 +1,123 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "LoaderStateMachine.h" +#include "../../Volumes/DicomVolumeImage.h" +#include "../Volumes/IGeometryProvider.h" + +#include + +namespace OrthancStone +{ + class OrthancMultiframeVolumeLoader : + public LoaderStateMachine, + public OrthancStone::IObservable, + public IGeometryProvider + { + private: + class LoadRTDoseGeometry; + class LoadGeometry; + class LoadTransferSyntax; + class LoadUncompressedPixelData; + + boost::shared_ptr volume_; + std::string instanceId_; + std::string transferSyntaxUid_; + bool pixelDataLoaded_; + float outliersHalfRejectionRate_; + float distributionRawMin_; + float distributionRawMax_; + float computedDistributionMin_; + float computedDistributionMax_; + + const std::string& GetInstanceId() const; + + void ScheduleFrameDownloads(); + + void SetTransferSyntax(const std::string& transferSyntax); + + void SetGeometry(const Orthanc::DicomMap& dicom); + + + /** + This method will : + + - copy the pixel values from the response to the volume image + - compute the maximum and minimum value while discarding the + outliersHalfRejectionRate_ fraction of the outliers from both the start + and the end of the distribution. + + In English, this means that, if the volume dataset contains a few extreme + values very different from the rest (outliers) that we want to get rid of, + this method allows to do so. + + If you supply 0.005, for instance, it means 1% of the extreme values will + be rejected (0.5% on each side of the distribution) + */ + template + void CopyPixelDataAndComputeMinMax(const std::string& pixelData); + + /** Service method for CopyPixelDataAndComputeMinMax*/ + template + void CopyPixelDataAndComputeDistribution( + const std::string& pixelData, + std::map& distribution); + + /** Service method for CopyPixelDataAndComputeMinMax*/ + template + void ComputeMinMaxWithOutlierRejection( + const std::map& distribution); + + void SetUncompressedPixelData(const std::string& pixelData); + + bool HasGeometry() const; + const OrthancStone::VolumeImageGeometry& GetImageGeometry() const; + + protected: + OrthancMultiframeVolumeLoader( + OrthancStone::ILoadersContext& loadersContext, + boost::shared_ptr volume, + float outliersHalfRejectionRate); + public: + + static boost::shared_ptr Create( + OrthancStone::ILoadersContext& loadersContext, + boost::shared_ptr volume, + float outliersHalfRejectionRate = 0.0005); + + virtual ~OrthancMultiframeVolumeLoader(); + + bool IsPixelDataLoaded() const + { + return pixelDataLoaded_; + } + + void GetDistributionMinMax + (float& minValue, float& maxValue) const; + + void GetDistributionMinMaxWithOutliersRejection + (float& minValue, float& maxValue) const; + + void LoadInstance(const std::string& instanceId); + }; +} diff -r 04055b6b9e2c -r b1396be5aa27 Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp Fri Apr 03 16:13:06 2020 +0200 @@ -0,0 +1,577 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "OrthancSeriesVolumeProgressiveLoader.h" + +#include "../../StoneException.h" +#include "../../Loaders/ILoadersContext.h" +#include "../../Loaders/BasicFetchingItemsSorter.h" +#include "../../Loaders/BasicFetchingStrategy.h" +#include "../../Toolbox/GeometryToolbox.h" +#include "../../Volumes/DicomVolumeImageMPRSlicer.h" + +#include +#include + +namespace OrthancStone +{ + using OrthancStone::ILoadersContext; + + class OrthancSeriesVolumeProgressiveLoader::ExtractedSlice : public OrthancStone::DicomVolumeImageMPRSlicer::Slice + { + private: + const OrthancSeriesVolumeProgressiveLoader& that_; + + public: + ExtractedSlice(const OrthancSeriesVolumeProgressiveLoader& that, + const OrthancStone::CoordinateSystem3D& plane) : + OrthancStone::DicomVolumeImageMPRSlicer::Slice(*that.volume_, plane), + that_(that) + { + if (IsValid()) + { + if (GetProjection() == OrthancStone::VolumeProjection_Axial) + { + // For coronal and sagittal projections, we take the global + // revision of the volume because even if a single slice changes, + // this means the projection will yield a different result --> + // we must increase the revision as soon as any slice changes + SetRevision(that_.seriesGeometry_.GetSliceRevision(GetSliceIndex())); + } + + if (that_.strategy_.get() != NULL && + GetProjection() == OrthancStone::VolumeProjection_Axial) + { + that_.strategy_->SetCurrent(GetSliceIndex()); + } + } + } + }; + + void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckSlice( + size_t index, const OrthancStone::DicomInstanceParameters& reference) const + { + const OrthancStone::DicomInstanceParameters& slice = *slices_[index]; + + if (!OrthancStone::GeometryToolbox::IsParallel( + reference.GetGeometry().GetNormal(), + slice.GetGeometry().GetNormal())) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, + "A slice in the volume image is not parallel to the others"); + } + + if (reference.GetExpectedPixelFormat() != slice.GetExpectedPixelFormat()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat, + "The pixel format changes across the slices of the volume image"); + } + + if (reference.GetImageInformation().GetWidth() != slice.GetImageInformation().GetWidth() || + reference.GetImageInformation().GetHeight() != slice.GetImageInformation().GetHeight()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize, + "The width/height of slices are not constant in the volume image"); + } + + if (!OrthancStone::LinearAlgebra::IsNear(reference.GetPixelSpacingX(), slice.GetPixelSpacingX()) || + !OrthancStone::LinearAlgebra::IsNear(reference.GetPixelSpacingY(), slice.GetPixelSpacingY())) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, + "The pixel spacing of the slices change across the volume image"); + } + } + + + void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckVolume() const + { + for (size_t i = 0; i < slices_.size(); i++) + { + assert(slices_[i] != NULL); + if (slices_[i]->GetImageInformation().GetNumberOfFrames() != 1) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, + "This class does not support multi-frame images"); + } + } + + if (slices_.size() != 0) + { + const OrthancStone::DicomInstanceParameters& reference = *slices_[0]; + + for (size_t i = 1; i < slices_.size(); i++) + { + CheckSlice(i, reference); + } + } + } + + + void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::Clear() + { + for (size_t i = 0; i < slices_.size(); i++) + { + assert(slices_[i] != NULL); + delete slices_[i]; + } + + slices_.clear(); + slicesRevision_.clear(); + } + + + void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckSliceIndex(size_t index) const + { + if (!HasGeometry()) + { + LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckSliceIndex(size_t index): (!HasGeometry())"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else if (index >= slices_.size()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + else + { + assert(slices_.size() == GetImageGeometry().GetDepth() && + slices_.size() == slicesRevision_.size()); + } + } + + + // WARNING: The payload of "slices" must be of class "DicomInstanceParameters" + // (called with the slices created in LoadGeometry) + void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::ComputeGeometry(OrthancStone::SlicesSorter& slices) + { + Clear(); + + if (!slices.Sort()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, + "Cannot sort the 3D slices of a DICOM series"); + } + + if (slices.GetSlicesCount() == 0) + { + geometry_.reset(new OrthancStone::VolumeImageGeometry); + } + else + { + slices_.reserve(slices.GetSlicesCount()); + slicesRevision_.resize(slices.GetSlicesCount(), 0); + + for (size_t i = 0; i < slices.GetSlicesCount(); i++) + { + const OrthancStone::DicomInstanceParameters& slice = + dynamic_cast(slices.GetSlicePayload(i)); + slices_.push_back(new OrthancStone::DicomInstanceParameters(slice)); + } + + CheckVolume(); + + double spacingZ; + + if (slices.ComputeSpacingBetweenSlices(spacingZ)) + { + LOG(TRACE) << "Computed spacing between slices: " << spacingZ << "mm"; + + const OrthancStone::DicomInstanceParameters& parameters = *slices_[0]; + + geometry_.reset(new OrthancStone::VolumeImageGeometry); + geometry_->SetSizeInVoxels(parameters.GetImageInformation().GetWidth(), + parameters.GetImageInformation().GetHeight(), + static_cast(slices.GetSlicesCount())); + geometry_->SetAxialGeometry(slices.GetSliceGeometry(0)); + geometry_->SetVoxelDimensions(parameters.GetPixelSpacingX(), + parameters.GetPixelSpacingY(), spacingZ); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, + "The origins of the slices of a volume image are not regularly spaced"); + } + } + } + + + const OrthancStone::VolumeImageGeometry& OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetImageGeometry() const + { + if (!HasGeometry()) + { + LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetImageGeometry(): (!HasGeometry())"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + assert(slices_.size() == geometry_->GetDepth()); + return *geometry_; + } + } + + + const OrthancStone::DicomInstanceParameters& OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetSliceParameters(size_t index) const + { + CheckSliceIndex(index); + return *slices_[index]; + } + + + uint64_t OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetSliceRevision(size_t index) const + { + CheckSliceIndex(index); + return slicesRevision_[index]; + } + + + void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::IncrementSliceRevision(size_t index) + { + CheckSliceIndex(index); + slicesRevision_[index] ++; + } + + + static unsigned int GetSliceIndexPayload(const OrthancStone::OracleCommandBase& command) + { + assert(command.HasPayload()); + return dynamic_cast< const Orthanc::SingleValueObject& >(command.GetPayload()).GetValue(); + } + + + void OrthancSeriesVolumeProgressiveLoader::ScheduleNextSliceDownload() + { + assert(strategy_.get() != NULL); + + unsigned int sliceIndex = 0, quality = 0; + + if (strategy_->GetNext(sliceIndex, quality)) + { + if (!progressiveQuality_) + { + ORTHANC_ASSERT(quality == QUALITY_00, "INTERNAL ERROR. quality != QUALITY_00 in " + << "OrthancSeriesVolumeProgressiveLoader::ScheduleNextSliceDownload"); + } + + const OrthancStone::DicomInstanceParameters& slice = seriesGeometry_.GetSliceParameters(sliceIndex); + + const std::string& instance = slice.GetOrthancInstanceIdentifier(); + if (instance.empty()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + std::unique_ptr command; + + if (!progressiveQuality_ || quality == QUALITY_02) + { + std::unique_ptr tmp(new OrthancStone::GetOrthancImageCommand); + // TODO: review the following comment. + // - Commented out by bgo on 2019-07-19 | reason: Alain has seen cases + // where gzipping the uint16 image took 11 sec to produce 5mb. + // The unzipped request was much much faster. + // - Re-enabled on 2019-07-30. Reason: in Web Assembly, the browser + // does not use the Accept-Encoding header and always requests + // compression. Furthermore, NOT + tmp->SetHttpHeader("Accept-Encoding", "gzip"); + tmp->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Pam))); + tmp->SetInstanceUri(instance, slice.GetExpectedPixelFormat()); + tmp->SetExpectedPixelFormat(slice.GetExpectedPixelFormat()); + //LOG(INFO) + // << "OrthancSeriesVolumeProgressiveLoader.ScheduleNextSliceDownload()" + // << " sliceIndex = " << sliceIndex << " slice quality = " << quality + // << " URI = " << tmp->GetUri(); + command.reset(tmp.release()); + } + else // progressive mode is true AND quality is not final (different from QUALITY_02 + { + std::unique_ptr tmp( + new OrthancStone::GetOrthancWebViewerJpegCommand); + + // TODO: review the following comment. Commented out by bgo on 2019-07-19 + // (gzip for jpeg seems overkill) + //tmp->SetHttpHeader("Accept-Encoding", "gzip"); + tmp->SetInstance(instance); + tmp->SetQuality((quality == 0 ? 50 : 90)); // QUALITY_00 is Jpeg50 while QUALITY_01 is Jpeg90 + tmp->SetExpectedPixelFormat(slice.GetExpectedPixelFormat()); + LOG(TRACE) + << "OrthancSeriesVolumeProgressiveLoader.ScheduleNextSliceDownload()" + << " sliceIndex = " << sliceIndex << " slice quality = " << quality; + command.reset(tmp.release()); + } + + command->AcquirePayload(new Orthanc::SingleValueObject(sliceIndex)); + + { + std::unique_ptr lock(loadersContext_.Lock()); + boost::shared_ptr observer(GetSharedObserver()); + lock->Schedule(observer, 0, command.release()); // TODO: priority! + } + } + else + { + // loading is finished! + volumeImageReadyInHighQuality_ = true; + BroadcastMessage(OrthancSeriesVolumeProgressiveLoader::VolumeImageReadyInHighQuality(*this)); + } + } + +/** + This is called in response to GET "/series/XXXXXXXXXXXXX/instances-tags" +*/ + void OrthancSeriesVolumeProgressiveLoader::LoadGeometry(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) + { + Json::Value body; + message.ParseJsonBody(body); + + if (body.type() != Json::objectValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + + { + Json::Value::Members instances = body.getMemberNames(); + + OrthancStone::SlicesSorter slices; + + for (size_t i = 0; i < instances.size(); i++) + { + Orthanc::DicomMap dicom; + dicom.FromDicomAsJson(body[instances[i]]); + + std::unique_ptr instance(new OrthancStone::DicomInstanceParameters(dicom)); + instance->SetOrthancInstanceIdentifier(instances[i]); + + // the 3D plane corresponding to the slice + OrthancStone::CoordinateSystem3D geometry = instance->GetGeometry(); + slices.AddSlice(geometry, instance.release()); + } + + seriesGeometry_.ComputeGeometry(slices); + } + + size_t slicesCount = seriesGeometry_.GetImageGeometry().GetDepth(); + + if (slicesCount == 0) + { + volume_->Initialize(seriesGeometry_.GetImageGeometry(), Orthanc::PixelFormat_Grayscale8); + } + else + { + const OrthancStone::DicomInstanceParameters& parameters = seriesGeometry_.GetSliceParameters(0); + + volume_->Initialize(seriesGeometry_.GetImageGeometry(), parameters.GetExpectedPixelFormat()); + volume_->SetDicomParameters(parameters); + volume_->GetPixelData().Clear(); + + // If we are in progressive mode, the Fetching strategy will first request QUALITY_00, then QUALITY_01, then + // QUALITY_02... Otherwise, it's only QUALITY_00 + unsigned int maxQuality = QUALITY_00; + if (progressiveQuality_) + maxQuality = QUALITY_02; + + strategy_.reset(new OrthancStone::BasicFetchingStrategy( + sorter_->CreateSorter(static_cast(slicesCount)), + maxQuality)); + + assert(simultaneousDownloads_ != 0); + for (unsigned int i = 0; i < simultaneousDownloads_; i++) + { + ScheduleNextSliceDownload(); + } + } + + slicesQuality_.resize(slicesCount, 0); + + BroadcastMessage(OrthancStone::DicomVolumeImage::GeometryReadyMessage(*volume_)); + } + + + void OrthancSeriesVolumeProgressiveLoader::SetSliceContent(unsigned int sliceIndex, + const Orthanc::ImageAccessor& image, + unsigned int quality) + { + ORTHANC_ASSERT(sliceIndex < slicesQuality_.size() && + slicesQuality_.size() == volume_->GetPixelData().GetDepth()); + + if (!progressiveQuality_) + { + ORTHANC_ASSERT(quality == QUALITY_00); + ORTHANC_ASSERT(slicesQuality_[sliceIndex] == QUALITY_00); + } + + if (quality >= slicesQuality_[sliceIndex]) + { + { + OrthancStone::ImageBuffer3D::SliceWriter writer(volume_->GetPixelData(), + OrthancStone::VolumeProjection_Axial, + sliceIndex); + + Orthanc::ImageProcessing::Copy(writer.GetAccessor(), image); + } + + volume_->IncrementRevision(); + seriesGeometry_.IncrementSliceRevision(sliceIndex); + slicesQuality_[sliceIndex] = quality; + + BroadcastMessage(OrthancStone::DicomVolumeImage::ContentUpdatedMessage(*volume_)); + } + LOG(TRACE) << "SetSliceContent sliceIndex = " << sliceIndex << " -- will " + << " now call ScheduleNextSliceDownload()"; + ScheduleNextSliceDownload(); + } + + void OrthancSeriesVolumeProgressiveLoader::LoadBestQualitySliceContent( + const OrthancStone::GetOrthancImageCommand::SuccessMessage& message) + { + unsigned int quality = QUALITY_00; + if (progressiveQuality_) + quality = QUALITY_02; + + SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), + message.GetImage(), + quality); + } + + void OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent( + const OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage& message) + { + ORTHANC_ASSERT(progressiveQuality_, "INTERNAL ERROR: OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent" + << " called while progressiveQuality_ is false!"); + + LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent"; + unsigned int quality; + + switch (dynamic_cast(message.GetOrigin()).GetQuality()) + { + case 50: + quality = QUALITY_00; + break; + + case 90: + quality = QUALITY_01; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), message.GetImage(), quality); + } + + OrthancSeriesVolumeProgressiveLoader::OrthancSeriesVolumeProgressiveLoader( + OrthancStone::ILoadersContext& loadersContext, + boost::shared_ptr volume, + bool progressiveQuality) + : loadersContext_(loadersContext) + , active_(false) + , progressiveQuality_(progressiveQuality) + , simultaneousDownloads_(4) + , volume_(volume) + , sorter_(new OrthancStone::BasicFetchingItemsSorter::Factory) + , volumeImageReadyInHighQuality_(false) + { + } + + boost::shared_ptr + OrthancSeriesVolumeProgressiveLoader::Create( + OrthancStone::ILoadersContext& loadersContext, + boost::shared_ptr volume, + bool progressiveQuality) + { + std::auto_ptr lock(loadersContext.Lock()); + + boost::shared_ptr obj( + new OrthancSeriesVolumeProgressiveLoader( + loadersContext, volume, progressiveQuality)); + + obj->Register( + lock->GetOracleObservable(), + &OrthancSeriesVolumeProgressiveLoader::LoadGeometry); + + obj->Register( + lock->GetOracleObservable(), + &OrthancSeriesVolumeProgressiveLoader::LoadBestQualitySliceContent); + + obj->Register( + lock->GetOracleObservable(), + &OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent); + + return obj; + } + + + OrthancSeriesVolumeProgressiveLoader::~OrthancSeriesVolumeProgressiveLoader() + { + LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::~OrthancSeriesVolumeProgressiveLoader()"; + } + + void OrthancSeriesVolumeProgressiveLoader::SetSimultaneousDownloads(unsigned int count) + { + if (active_) + { + LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::SetSimultaneousDownloads(): (active_)"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else if (count == 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + else + { + simultaneousDownloads_ = count; + } + } + + + void OrthancSeriesVolumeProgressiveLoader::LoadSeries(const std::string& seriesId) + { + if (active_) + { + LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::LoadSeries(const std::string& seriesId): (active_)"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + active_ = true; + + std::unique_ptr command(new OrthancStone::OrthancRestApiCommand); + command->SetUri("/series/" + seriesId + "/instances-tags"); + { + std::unique_ptr lock(loadersContext_.Lock()); + boost::shared_ptr observer(GetSharedObserver()); + lock->Schedule(observer, 0, command.release()); //TODO: priority! + } + } + } + + + OrthancStone::IVolumeSlicer::IExtractedSlice* + OrthancSeriesVolumeProgressiveLoader::ExtractSlice(const OrthancStone::CoordinateSystem3D& cuttingPlane) + { + if (volume_->HasGeometry()) + { + return new ExtractedSlice(*this, cuttingPlane); + } + else + { + return new IVolumeSlicer::InvalidSlice; + } + } +} diff -r 04055b6b9e2c -r b1396be5aa27 Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h Fri Apr 03 16:13:06 2020 +0200 @@ -0,0 +1,175 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../../Loaders/IFetchingItemsSorter.h" +#include "../../Loaders/IFetchingStrategy.h" +#include "../../Messages/IObservable.h" +#include "../../Messages/ObserverBase.h" +#include "../../Oracle/GetOrthancImageCommand.h" +#include "../../Oracle/GetOrthancWebViewerJpegCommand.h" +#include "../../Oracle/IOracle.h" +#include "../../Oracle/OrthancRestApiCommand.h" +#include "../../Toolbox/SlicesSorter.h" +#include "../../Volumes/DicomVolumeImage.h" +#include "../../Volumes/IVolumeSlicer.h" + +#include "../Volumes/IGeometryProvider.h" + + +#include + +namespace OrthancStone +{ + /** + This class is used to manage the progressive loading of a volume that + is stored in a Dicom series. + */ + class OrthancSeriesVolumeProgressiveLoader : + public OrthancStone::ObserverBase, + public OrthancStone::IObservable, + public OrthancStone::IVolumeSlicer, + public IGeometryProvider + { + private: + static const unsigned int QUALITY_00 = 0; + static const unsigned int QUALITY_01 = 1; + static const unsigned int QUALITY_02 = 2; + + class ExtractedSlice; + + /** Helper class internal to OrthancSeriesVolumeProgressiveLoader */ + class SeriesGeometry : public boost::noncopyable + { + private: + void CheckSlice(size_t index, + const OrthancStone::DicomInstanceParameters& reference) const; + + void CheckVolume() const; + + void Clear(); + + void CheckSliceIndex(size_t index) const; + + std::unique_ptr geometry_; + std::vector slices_; + std::vector slicesRevision_; + + public: + ~SeriesGeometry() + { + Clear(); + } + + void ComputeGeometry(OrthancStone::SlicesSorter& slices); + + virtual bool HasGeometry() const + { + return geometry_.get() != NULL; + } + + virtual const OrthancStone::VolumeImageGeometry& GetImageGeometry() const; + + const OrthancStone::DicomInstanceParameters& GetSliceParameters(size_t index) const; + + uint64_t GetSliceRevision(size_t index) const; + + void IncrementSliceRevision(size_t index); + }; + + void ScheduleNextSliceDownload(); + + void LoadGeometry(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message); + + void SetSliceContent(unsigned int sliceIndex, + const Orthanc::ImageAccessor& image, + unsigned int quality); + + void LoadBestQualitySliceContent(const OrthancStone::GetOrthancImageCommand::SuccessMessage& message); + + void LoadJpegSliceContent(const OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage& message); + + OrthancStone::ILoadersContext& loadersContext_; + bool active_; + bool progressiveQuality_; + unsigned int simultaneousDownloads_; + SeriesGeometry seriesGeometry_; + boost::shared_ptr volume_; + std::unique_ptr sorter_; + std::unique_ptr strategy_; + std::vector slicesQuality_; + bool volumeImageReadyInHighQuality_; + + OrthancSeriesVolumeProgressiveLoader( + OrthancStone::ILoadersContext& loadersContext, + boost::shared_ptr volume, + bool progressiveQuality); + + public: + ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, VolumeImageReadyInHighQuality, OrthancSeriesVolumeProgressiveLoader); + + /** + See doc for the progressiveQuality_ field + */ + static boost::shared_ptr Create( + OrthancStone::ILoadersContext& context, + boost::shared_ptr volume, + bool progressiveQuality = false); + + virtual ~OrthancSeriesVolumeProgressiveLoader(); + + void SetSimultaneousDownloads(unsigned int count); + + bool IsVolumeImageReadyInHighQuality() const + { + return volumeImageReadyInHighQuality_; + } + + void LoadSeries(const std::string& seriesId); + + /** + This getter is used by clients that do not receive the geometry through + subscribing, for instance if they are created or listening only AFTER the + "geometry loaded" message is broadcast + */ + bool HasGeometry() const ORTHANC_OVERRIDE + { + return seriesGeometry_.HasGeometry(); + } + + /** + Same remark as HasGeometry + */ + const OrthancStone::VolumeImageGeometry& GetImageGeometry() const ORTHANC_OVERRIDE + { + return seriesGeometry_.GetImageGeometry(); + } + + /** + When a slice is requested, the strategy algorithm (that defines the + sequence of resources to be loaded from the server) is modified to + take into account this request (this is done in the ExtractedSlice ctor) + */ + virtual IExtractedSlice* + ExtractSlice(const OrthancStone::CoordinateSystem3D& cuttingPlane) ORTHANC_OVERRIDE; + }; +} diff -r 04055b6b9e2c -r b1396be5aa27 Resources/CMake/OrthancStoneConfiguration.cmake --- a/Resources/CMake/OrthancStoneConfiguration.cmake Tue Mar 31 11:01:34 2020 +0200 +++ b/Resources/CMake/OrthancStoneConfiguration.cmake Fri Apr 03 16:13:06 2020 +0200 @@ -395,18 +395,8 @@ # ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/SingleFrameRendererFactory.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/SliceOutlineRenderer.cpp ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/SliceOutlineRenderer.h - ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/DicomStructureSetLoader.cpp - ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/DicomStructureSetLoader.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/DicomStructureSetLoader2.cpp ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/DicomStructureSetLoader2.h - ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/LoaderCache.cpp - ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/LoaderCache.h - ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/LoaderStateMachine.cpp - ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/LoaderStateMachine.h - ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/OrthancMultiframeVolumeLoader.cpp - ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/OrthancMultiframeVolumeLoader.h - ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp - ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/OrthancSeriesVolumeProgressiveLoader.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/BaseWebService.cpp ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/BaseWebService.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/DicomFrameConverter.cpp @@ -537,11 +527,21 @@ ${ORTHANC_STONE_ROOT}/Framework/Loaders/BasicFetchingStrategy.h ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomResourcesLoader.cpp ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomSource.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomStructureSetLoader.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomStructureSetLoader.h ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomVolumeLoader.cpp ${ORTHANC_STONE_ROOT}/Framework/Loaders/IFetchingItemsSorter.h ${ORTHANC_STONE_ROOT}/Framework/Loaders/IFetchingStrategy.h ${ORTHANC_STONE_ROOT}/Framework/Loaders/LoadedDicomResources.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/LoaderCache.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/LoaderCache.h + ${ORTHANC_STONE_ROOT}/Framework/Loaders/LoaderStateMachine.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/LoaderStateMachine.h + ${ORTHANC_STONE_ROOT}/Framework/Loaders/OrthancMultiframeVolumeLoader.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/OrthancMultiframeVolumeLoader.h ${ORTHANC_STONE_ROOT}/Framework/Loaders/OracleScheduler.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h ${ORTHANC_STONE_ROOT}/Framework/Loaders/SeriesFramesLoader.cpp ${ORTHANC_STONE_ROOT}/Framework/Loaders/SeriesMetadataLoader.cpp ${ORTHANC_STONE_ROOT}/Framework/Loaders/SeriesOrderedFrames.cpp