# HG changeset patch # User Benjamin Golinvaux # Date 1591109172 -7200 # Node ID 1df1c126fb36a183d090e351a4bff0e2be0ecdf1 # Parent 4e233e3ea53b216e732316385d68434e074b25a6# Parent 2a5f0f771fbd19d130f6d5dc07715bbe2fe9e130 Merge the loader plugin injection feature diff -r 4e233e3ea53b -r 1df1c126fb36 Framework/Loaders/DicomStructureSetLoader.cpp --- a/Framework/Loaders/DicomStructureSetLoader.cpp Tue May 19 15:31:38 2020 +0200 +++ b/Framework/Loaders/DicomStructureSetLoader.cpp Tue Jun 02 16:46:12 2020 +0200 @@ -48,8 +48,37 @@ } #endif + // implementation of IInstanceLookupHandler that uses Orthanc REST API calls to retrive the + // geometry of referenced instances + class DicomStructureSetLoader::RestInstanceLookupHandler : public DicomStructureSetLoader::IInstanceLookupHandler, + public LoaderStateMachine + { + public: + static boost::shared_ptr Create(DicomStructureSetLoader& loader) + { + boost::shared_ptr obj(new RestInstanceLookupHandler(loader)); + obj->LoaderStateMachine::PostConstructor(); + return obj; + } - class DicomStructureSetLoader::AddReferencedInstance : public LoaderStateMachine::State + protected: + RestInstanceLookupHandler(DicomStructureSetLoader& loader) + : LoaderStateMachine(loader.loadersContext_) + , loader_(loader) + { + } + + virtual void RetrieveReferencedSlices(const std::set& nonEmptyInstances) ORTHANC_OVERRIDE; + + private: + // these subclasses hold the loading state + class AddReferencedInstance; // 2nd state + class LookupInstance; // 1st state + + DicomStructureSetLoader& loader_; + }; + + class DicomStructureSetLoader::RestInstanceLookupHandler::AddReferencedInstance : public LoaderStateMachine::State { private: std::string instanceId_; @@ -71,27 +100,14 @@ dicom.FromDicomAsJson(tags); DicomStructureSetLoader& loader = GetLoader(); - - loader.content_->AddReferencedSlice(dicom); - loader.countProcessedInstances_ ++; - assert(loader.countProcessedInstances_ <= loader.countReferencedInstances_); - - loader.revision_++; - loader.SetStructuresUpdated(); - - if (loader.countProcessedInstances_ == loader.countReferencedInstances_) - { - // All the referenced instances have been loaded, finalize the RT-STRUCT - loader.content_->CheckReferencedSlices(); - loader.revision_++; - loader.SetStructuresReady(); - } + + loader.AddReferencedSlice(dicom); } }; // State that converts a "SOP Instance UID" to an Orthanc identifier - class DicomStructureSetLoader::LookupInstance : public LoaderStateMachine::State + class DicomStructureSetLoader::RestInstanceLookupHandler::LookupInstance : public LoaderStateMachine::State { private: std::string sopInstanceUid_; @@ -106,9 +122,6 @@ virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) { -#if 0 - LOG(TRACE) << "DicomStructureSetLoader::LookupInstance::Handle() (SUCCESS)"; -#endif DicomStructureSetLoader& loader = GetLoader(); Json::Value lookup; @@ -147,6 +160,21 @@ } }; + void DicomStructureSetLoader::RestInstanceLookupHandler::RetrieveReferencedSlices( + const std::set& nonEmptyInstances) + { + 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::LoadStructure : public LoaderStateMachine::State { @@ -159,53 +187,28 @@ virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) { DicomStructureSetLoader& loader = GetLoader(); - + + // Set the actual structure set content { 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; - } - } } + // initialize visibility flags + SetDefaultStructureVisibility(); + + // retrieve the (non-empty) referenced instances (the CT slices containing the corresponding structures) // Some (admittedly invalid) Dicom files have empty values in the // 0008,1155 tag. We try our best to cope with this. + // this is why we use `nonEmptyInstances` and not `instances` std::set instances; std::set nonEmptyInstances; + + // this traverses the polygon collection for all structures and retrieve the SOPInstanceUID of + // the referenced instances loader.content_->GetReferencedInstances(instances); + for (std::set::const_iterator it = instances.begin(); it != instances.end(); ++it) { @@ -214,20 +217,55 @@ nonEmptyInstances.insert(instance); } - loader.countReferencedInstances_ = - static_cast(nonEmptyInstances.size()); + loader.RetrieveReferencedSlices(nonEmptyInstances); + } + + void SetDefaultStructureVisibility() + { + DicomStructureSetLoader& loader = GetLoader(); + + size_t structureCount = loader.content_->GetStructuresCount(); - for (std::set::const_iterator - it = nonEmptyInstances.begin(); it != nonEmptyInstances.end(); ++it) + 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) { - 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()); + // 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; + } } } + + private: + + }; @@ -347,8 +385,9 @@ , countReferencedInstances_(0) , structuresReady_(false) { + // the default handler to retrieve slice geometry is RestInstanceLookupHandler + instanceLookupHandler_ = RestInstanceLookupHandler::Create(*this); } - boost::shared_ptr DicomStructureSetLoader::Create(OrthancStone::ILoadersContext& loadersContext) { @@ -357,7 +396,31 @@ loadersContext)); obj->LoaderStateMachine::PostConstructor(); return obj; + } + void DicomStructureSetLoader::AddReferencedSlice(const Orthanc::DicomMap& dicom) + { + content_->AddReferencedSlice(dicom); + countProcessedInstances_ ++; + assert(countProcessedInstances_ <= countReferencedInstances_); + + revision_++; + SetStructuresUpdated(); + + if (countProcessedInstances_ == countReferencedInstances_) + { + // All the referenced instances have been loaded, finalize the RT-STRUCT + content_->CheckReferencedSlices(); + revision_++; + SetStructuresReady(); + } + } + + void DicomStructureSetLoader::RetrieveReferencedSlices(const std::set& nonEmptyInstances) + { + // we set the number of referenced instances. This allows to know, in the method above, when we're done + countReferencedInstances_ = static_cast(nonEmptyInstances.size()); + instanceLookupHandler_->RetrieveReferencedSlices(nonEmptyInstances); } void DicomStructureSetLoader::SetStructureDisplayState(size_t structureIndex, bool display) diff -r 4e233e3ea53b -r 1df1c126fb36 Framework/Loaders/DicomStructureSetLoader.h --- a/Framework/Loaders/DicomStructureSetLoader.h Tue May 19 15:31:38 2020 +0200 +++ b/Framework/Loaders/DicomStructureSetLoader.h Tue Jun 02 16:46:12 2020 +0200 @@ -35,12 +35,76 @@ public OrthancStone::IVolumeSlicer, public OrthancStone::IObservable { + public: + ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, StructuresReady, DicomStructureSetLoader); + ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, StructuresUpdated, DicomStructureSetLoader); + + /** + + Once the structure set has been loaded (the LoadStructure state), we need to fill it with geometry information + from the referenced slices (tag (0008,1155) described here: + https://dicom.innolitics.com/ciods/rt-structure-set/general-reference/00081140/00081155 + + This interface allows to customize how this information can be gathered. By default, the RestInstanceLookupHandler + will perform a REST call to the Orthanc API to retrieve this information. + + Injecting another implementation of this interface is useful when where this information can be supplied in + another (faster) way (for instance, if a separate loader for the CT series can be used to supply the slice geometry) + */ + class IInstanceLookupHandler + { + public: + virtual void RetrieveReferencedSlices(const std::set& instances) = 0; + }; + + // predeclaration of the default IInstanceLookupHandler implementation + class RestInstanceLookupHandler; + + static boost::shared_ptr Create( + OrthancStone::ILoadersContext& loadersContext); + + void SetInstanceLookupHandler(boost::shared_ptr instanceLookupHandler) + { + instanceLookupHandler_ = instanceLookupHandler; + } + + 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()); + + void LoadInstanceFullVisibility(const std::string& instanceId); + + + virtual IExtractedSlice* ExtractSlice(const OrthancStone::CoordinateSystem3D& cuttingPlane) ORTHANC_OVERRIDE; + + void SetStructuresReady(); + void SetStructuresUpdated(); + + bool AreStructuresReady() const; + + /** + Called by the IInstanceLookupHandler when slice referenced instance information is available. + When the last referenced slice is received, this method will perform a final check and will warn observers + */ + void AddReferencedSlice(const Orthanc::DicomMap& dicom); + private: class Slice; - // States of LoaderStateMachine - class AddReferencedInstance; // 3rd state - class LookupInstance; // 2nd state + // Only state of LoaderStateMachine class LoadStructure; // 1st state OrthancStone::ILoadersContext& loadersContext_; @@ -70,41 +134,13 @@ */ std::vector structureVisibility_; + + boost::shared_ptr instanceLookupHandler_; + + private: + void RetrieveReferencedSlices(const std::set& nonEmptyInstances); + protected: DicomStructureSetLoader(OrthancStone::ILoadersContext& loadersContext); - - public: - ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, StructuresReady, DicomStructureSetLoader); - ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, StructuresUpdated, 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()); - - void LoadInstanceFullVisibility(const std::string& instanceId); - - - virtual IExtractedSlice* ExtractSlice(const OrthancStone::CoordinateSystem3D& cuttingPlane) ORTHANC_OVERRIDE; - - void SetStructuresReady(); - void SetStructuresUpdated(); - - bool AreStructuresReady() const; }; } diff -r 4e233e3ea53b -r 1df1c126fb36 Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp --- a/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp Tue May 19 15:31:38 2020 +0200 +++ b/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp Tue Jun 02 16:46:12 2020 +0200 @@ -322,7 +322,7 @@ { std::unique_ptr lock(loadersContext_.Lock()); boost::shared_ptr observer(GetSharedObserver()); - lock->Schedule(observer, 0, command.release()); // TODO: priority! + lock->Schedule(observer, sliceSchedulingPriority_, command.release()); } } else @@ -362,6 +362,9 @@ // the 3D plane corresponding to the slice OrthancStone::CoordinateSystem3D geometry = instance->GetGeometry(); slices.AddSlice(geometry, instance.release()); + + if (slicePostProcessor_) + slicePostProcessor_->ProcessCTDicomSlice(dicom); } seriesGeometry_.ComputeGeometry(slices); @@ -476,6 +479,33 @@ SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), message.GetImage(), quality); } + + void OrthancSeriesVolumeProgressiveLoader::SetMetadataSchedulingPriority(int p) + { + medadataSchedulingPriority_ = p; + } + + int OrthancSeriesVolumeProgressiveLoader::GetMetadataSchedulingPriority() const + { + return medadataSchedulingPriority_; + } + + void OrthancSeriesVolumeProgressiveLoader::SetSliceSchedulingPriority(int p) + { + sliceSchedulingPriority_ = p; + } + + int OrthancSeriesVolumeProgressiveLoader::GetSliceSchedulingPriority() const + { + return sliceSchedulingPriority_; + } + + void OrthancSeriesVolumeProgressiveLoader::SetSchedulingPriority(int p) + { + medadataSchedulingPriority_ = p; + sliceSchedulingPriority_ = p; + } + OrthancSeriesVolumeProgressiveLoader::OrthancSeriesVolumeProgressiveLoader( OrthancStone::ILoadersContext& loadersContext, boost::shared_ptr volume, @@ -487,6 +517,8 @@ , volume_(volume) , sorter_(new OrthancStone::BasicFetchingItemsSorter::Factory) , volumeImageReadyInHighQuality_(false) + , medadataSchedulingPriority_(0) + , sliceSchedulingPriority_(0) { } @@ -557,7 +589,7 @@ { std::unique_ptr lock(loadersContext_.Lock()); boost::shared_ptr observer(GetSharedObserver()); - lock->Schedule(observer, 0, command.release()); //TODO: priority! + lock->Schedule(observer, medadataSchedulingPriority_, command.release()); } } } diff -r 4e233e3ea53b -r 1df1c126fb36 Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h --- a/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h Tue May 19 15:31:38 2020 +0200 +++ b/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h Tue Jun 02 16:46:12 2020 +0200 @@ -51,13 +51,21 @@ public OrthancStone::IVolumeSlicer, public IGeometryProvider { + public: + class ISlicePostProcessor + { + public: + virtual void ProcessCTDicomSlice(const Orthanc::DicomMap& dicom) = 0; + }; + 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 { @@ -121,6 +129,14 @@ std::vector slicesQuality_; bool volumeImageReadyInHighQuality_; + boost::shared_ptr slicePostProcessor_; + + /** See priority setters/getters below */ + int medadataSchedulingPriority_; + + /** See priority setters/getters below */ + int sliceSchedulingPriority_; + OrthancSeriesVolumeProgressiveLoader( OrthancStone::ILoadersContext& loadersContext, boost::shared_ptr volume, @@ -141,6 +157,40 @@ void SetSimultaneousDownloads(unsigned int count); + /** + Sets the relative priority of the requests for metadata. + - if p < PRIORITY_HIGH (-1) , the requests will be high priority + - if PRIORITY_LOW (100) > p > PRIORITY_HIGH , the requests will be medium priority + - if p > PRIORITY_LOW , the requests will be low priority + + Default is 0 (medium) + */ + void SetMetadataSchedulingPriority(int p); + + /** @see SetMetadataSchedulingPriority */ + int GetMetadataSchedulingPriority() const; + + /** Same as SetMetadataSchedulingPriority, for slices. Default is 0. */ + void SetSliceSchedulingPriority(int p); + + /** @see SetSliceSchedulingPriority */ + int GetSliceSchedulingPriority() const; + + /** Sets priorities for all requests. @see SetMetadataSchedulingPriority */ + void SetSchedulingPriority(int p); + + void SetDicomSlicePostProcessor(boost::shared_ptr slicePostProcessor) + { + // this will delete the previously stored slice processor, if any + slicePostProcessor_ = slicePostProcessor; + } + + boost::shared_ptr GetDicomSlicePostProcessor() + { + // this could be empty! + return slicePostProcessor_; + } + bool IsVolumeImageReadyInHighQuality() const { return volumeImageReadyInHighQuality_; diff -r 4e233e3ea53b -r 1df1c126fb36 Samples/Common/RtViewerApp.cpp --- a/Samples/Common/RtViewerApp.cpp Tue May 19 15:31:38 2020 +0200 +++ b/Samples/Common/RtViewerApp.cpp Tue Jun 02 16:46:12 2020 +0200 @@ -119,6 +119,10 @@ // "false" means only using hi quality // TODO: add flag for quality ctLoader_ = OrthancSeriesVolumeProgressiveLoader::Create(*loadersContext_, ctVolume_, true); + + // better priority for CT vs dose and struct + ctLoader_->SetSchedulingPriority(-100); + // we need to store the CT loader to ask from geometry details later on when geometry is loaded geometryProvider_ = ctLoader_; diff -r 4e233e3ea53b -r 1df1c126fb36 Samples/Sdl/SdlHelpers.h --- a/Samples/Sdl/SdlHelpers.h Tue May 19 15:31:38 2020 +0200 +++ b/Samples/Sdl/SdlHelpers.h Tue Jun 02 16:46:12 2020 +0200 @@ -94,9 +94,9 @@ } p.AddPosition(compositor.GetPixelCenterCoordinates(event.button.x, event.button.y)); - p.SetAltModifier(modifiers & KeyboardModifiers_Alt); - p.SetControlModifier(modifiers & KeyboardModifiers_Control); - p.SetShiftModifier(modifiers & KeyboardModifiers_Shift); + p.SetAltModifier( (modifiers & KeyboardModifiers_Alt) != 0); + p.SetControlModifier( (modifiers & KeyboardModifiers_Control) != 0); + p.SetShiftModifier( (modifiers & KeyboardModifiers_Shift) != 0); } static boost::shared_ptr GetSdlViewportFromWindowId( diff -r 4e233e3ea53b -r 1df1c126fb36 UnitTestsSources/TestStructureSet.cpp --- a/UnitTestsSources/TestStructureSet.cpp Tue May 19 15:31:38 2020 +0200 +++ b/UnitTestsSources/TestStructureSet.cpp Tue Jun 02 16:46:12 2020 +0200 @@ -32,6 +32,12 @@ #include "Framework/Toolbox/DicomStructureSet2.h" #include "Framework/Toolbox/DisjointDataSet.h" +#include "Framework/Loaders/GenericLoadersContext.h" +#include "Framework/Loaders/DicomStructureSetLoader.h" +#include "Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h" + +#include "boost/date_time/posix_time/posix_time.hpp" + #include #include "gtest/gtest.h" @@ -5406,9 +5412,359 @@ const std::vector& structures = structureSet.structures_; } - #endif // BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - - +namespace +{ + void Initialize(const char* orthancApiUrl, OrthancStone::ILoadersContext& loadersContext) + { + Orthanc::WebServiceParameters p; + + OrthancStone::GenericLoadersContext& typedLoadersContext = + dynamic_cast(loadersContext); + // Default is http://localhost:8042 + // Here's how you may change it + p.SetUrl(orthancApiUrl); + p.SetCredentials("orthanc", "orthanc"); + typedLoadersContext.SetOrthancParameters(p); + + typedLoadersContext.StartOracle(); + } + + void Exitialize(OrthancStone::ILoadersContext& loadersContext) + { + OrthancStone::GenericLoadersContext& typedLoadersContext = + dynamic_cast(loadersContext); + + typedLoadersContext.StopOracle(); + } + + +#if 0 + class TestObserver : public ObserverBase + { + public: + TestObserver() {}; + + virtual void Handle + + }; +#endif + +} + +TEST(StructureSet, DISABLED_StructureSetLoader_injection_feature_2020_05_10) +{ + namespace pt = boost::posix_time; + + std::unique_ptr loadersContext(new OrthancStone::GenericLoadersContext(1,4,1)); + Initialize("http://localhost:8042/", *loadersContext); + + boost::shared_ptr loader = DicomStructureSetLoader::Create(*loadersContext); + + // replace with Orthanc ID of an uploaded RTSTRUCT instance! + loader->LoadInstanceFullVisibility("72c773ac-5059f2c4-2e6a9120-4fd4bca1-45701661"); + + bool bContinue(true); + + pt::ptime initialTime = pt::second_clock::local_time(); + + while (bContinue) + { + bContinue = !loader->AreStructuresReady(); + boost::this_thread::sleep_for(boost::chrono::milliseconds(1000)); + + { + pt::ptime nowTime = pt::second_clock::local_time(); + pt::time_duration diff = nowTime - initialTime; + double seconds = static_cast(diff.total_milliseconds()) * 0.001; + std::cout << seconds << " seconds elapsed...\n"; + if (seconds > 30) + { + std::cout << "More than 30 seconds elapsed... Aborting test :(\n"; + //GTEST_FATAL_FAILURE_("More than 30 seconds elapsed... Aborting test :("); + //bContinue = false; + } + } + } +} + +class SliceProcessor : + public OrthancStone::OrthancSeriesVolumeProgressiveLoader::ISlicePostProcessor, + public OrthancStone::DicomStructureSetLoader::IInstanceLookupHandler +{ +public: + SliceProcessor(OrthancStone::DicomStructureSetLoader& structLoader) : structLoader_(structLoader) + { + } + + virtual void ProcessCTDicomSlice(const Orthanc::DicomMap& instance) ORTHANC_OVERRIDE + { + std::string sopInstanceUid; + if (!instance.LookupStringValue(sopInstanceUid, Orthanc::DICOM_TAG_SOP_INSTANCE_UID, false)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "Missing SOPInstanceUID in a DICOM instance"); + } + slicesDicom_[sopInstanceUid] = boost::shared_ptr(instance.Clone()); + } + + virtual void RetrieveReferencedSlices(const std::set& nonEmptyInstances) ORTHANC_OVERRIDE + { + for (std::set::const_iterator it = nonEmptyInstances.begin(); + it != nonEmptyInstances.end(); + ++it) + { + const std::string nonEmptyInstance = *it; + if (slicesDicom_.find(nonEmptyInstance) == slicesDicom_.end()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "Referenced SOPInstanceUID not found in CT"); + } + boost::shared_ptr instance = slicesDicom_[nonEmptyInstance]; + structLoader_.AddReferencedSlice(*instance); + } + } + + OrthancStone::DicomStructureSetLoader& structLoader_; + std::map > slicesDicom_; +}; + +void LoadCtSeriesBlocking(boost::shared_ptr ctLoader, std::string seriesId) +{ + namespace pt = boost::posix_time; + + // Load the CT + ctLoader->LoadSeries(seriesId); + + // Wait for CT to be loaded + pt::ptime initialTime = pt::second_clock::local_time(); + { + bool bContinue(true); + while (bContinue) + { + bContinue = !ctLoader->IsVolumeImageReadyInHighQuality(); + boost::this_thread::sleep_for(boost::chrono::milliseconds(1000)); + + { + pt::ptime nowTime = pt::second_clock::local_time(); + pt::time_duration diff = nowTime - initialTime; + double seconds = static_cast(diff.total_milliseconds()) * 0.001; + std::cout << seconds << " seconds elapsed...\n"; + if (seconds > 30) + { + const char* msg = "More than 30 seconds elapsed when waiting for CT... Aborting test :(\n"; + GTEST_FATAL_FAILURE_(msg); + bContinue = false; + } + } + } + } +} + + +/** +Will fill planes +*/ +void GetCTPlanes(std::vector& planes, + OrthancStone::VolumeProjection projection, + boost::shared_ptr ctLoader) +{ + planes.clear(); // inefficient : we don't care + + const VolumeImageGeometry& geometry = ctLoader->GetImageGeometry(); + const unsigned int depth = geometry.GetProjectionDepth(projection); + + planes.resize(depth); + + for (unsigned int z = 0; z < depth; z++) + { + planes[z] = geometry.GetProjectionSlice(projection, z); + } +} + +void LoadRtStructBlocking(boost::shared_ptr structLoader, std::string instanceId) +{ + namespace pt = boost::posix_time; + + // Load RTSTRUCT + structLoader->LoadInstanceFullVisibility(instanceId); + + pt::ptime initialTime = pt::second_clock::local_time(); + + // Wait for the loading process to complete + { + bool bContinue(true); + while (bContinue) + { + bContinue = !structLoader->AreStructuresReady(); + boost::this_thread::sleep_for(boost::chrono::milliseconds(1000)); + + { + pt::ptime nowTime = pt::second_clock::local_time(); + pt::time_duration diff = nowTime - initialTime; + double seconds = static_cast(diff.total_milliseconds()) * 0.001; + std::cout << seconds << " seconds elapsed...\n"; + if (seconds > 30) + { + const char* msg = "More than 30 seconds elapsed when waiting for RTSTRUCT... Aborting test :(\n"; + GTEST_FATAL_FAILURE_(msg); + bContinue = false; + } + } + } + } +} + +TEST(StructureSet, DISABLED_Integration_Compound_CT_Struct_Loading) +{ + const double TOLERANCE = 0.0000001; + + // create loaders context + std::unique_ptr loadersContext(new OrthancStone::GenericLoadersContext(1,4,1)); + Initialize("http://localhost:8042/", *loadersContext); + + const char* ctSeriesId = "a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa"; + const char* rtStructInstanceId = "54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9"; + + // we'll compare normal loading and optimized loading with SliceProcessor to store the dicom + + boost::shared_ptr normalStructLoader; + boost::shared_ptr optimizedStructLoader; + + { + // Create the CT volume + boost::shared_ptr volume = boost::make_shared(); + + // Create CT loader + boost::shared_ptr ctLoader = + OrthancStone::OrthancSeriesVolumeProgressiveLoader::Create(*loadersContext, volume); + + // Create struct loader + normalStructLoader = OrthancStone::DicomStructureSetLoader::Create(*loadersContext); + + // Load the CT + LoadCtSeriesBlocking(ctLoader, ctSeriesId); + + const OrthancStone::VolumeImageGeometry& imageGeometry = ctLoader->GetImageGeometry(); + unsigned int width = imageGeometry.GetWidth(); + EXPECT_EQ(512u, width); + unsigned int height = imageGeometry.GetHeight(); + EXPECT_EQ(512u, height); + unsigned int depth = imageGeometry.GetDepth(); + EXPECT_EQ(109u, depth); + + // Load the RTStruct + LoadRtStructBlocking(normalStructLoader, rtStructInstanceId); + } + + std::vector axialPlanes; + std::vector coronalPlanes; + std::vector sagittalPlanes; + + { + // Create the CT volume + boost::shared_ptr volume = boost::make_shared(); + + // Create CT loader + boost::shared_ptr ctLoader = + OrthancStone::OrthancSeriesVolumeProgressiveLoader::Create(*loadersContext, volume); + + // Create struct loader + optimizedStructLoader = OrthancStone::DicomStructureSetLoader::Create(*loadersContext); + + // create the slice processor / instance lookup + boost::shared_ptr sliceProcessor(new SliceProcessor(*optimizedStructLoader)); + + // Inject it into CT loader + ctLoader->SetDicomSlicePostProcessor(sliceProcessor); + + // Inject it into RTSTRUCT loader + optimizedStructLoader->SetInstanceLookupHandler(sliceProcessor); + + // Load the CT + LoadCtSeriesBlocking(ctLoader, ctSeriesId); + + // now, the slices are collected. let's do some checks + EXPECT_EQ(109u, sliceProcessor->slicesDicom_.size()); + + // Load the RTStruct + LoadRtStructBlocking(optimizedStructLoader, rtStructInstanceId); + + GetCTPlanes(axialPlanes, VolumeProjection_Axial, ctLoader); + GetCTPlanes(coronalPlanes, VolumeProjection_Coronal, ctLoader); + GetCTPlanes(sagittalPlanes, VolumeProjection_Sagittal, ctLoader); + } + + // DO NOT DELETE THOSE! + OrthancStone::DicomStructureSet* normalContent = normalStructLoader->GetContent(); + OrthancStone::DicomStructureSet* optimizedContent = optimizedStructLoader->GetContent(); + + EXPECT_EQ(normalContent->GetStructuresCount(), optimizedContent->GetStructuresCount()); + + /*void GetCTPlanes(std::vector& planes, + OrthancStone::VolumeProjection projection, + boost::shared_ptr ctLoader)*/ + + + std::vector allPlanes; + + // let's gather all the possible cutting planes in a single struct + for (size_t i = 0; i < axialPlanes.size(); ++i) + allPlanes.push_back(axialPlanes[i]); + + for (size_t i = 0; i < coronalPlanes.size(); ++i) + allPlanes.push_back(coronalPlanes[i]); + + for (size_t i = 0; i < sagittalPlanes.size(); ++i) + allPlanes.push_back(sagittalPlanes[i]); + + for (size_t i = 0; i < normalContent->GetStructuresCount(); ++i) + { + std::cout << "Testing structure (" << i << "/" << normalContent->GetStructuresCount() << ")\n"; + Vector structureCenter1 = normalContent->GetStructureCenter(i); + const std::string& structureName1 = normalContent->GetStructureName(i); + const std::string& structureInterpretation1 = normalContent->GetStructureInterpretation(i); + Color structureColor1 = normalContent->GetStructureColor(i); + + Vector structureCenter2 = optimizedContent->GetStructureCenter(i); + const std::string& structureName2 = optimizedContent->GetStructureName(i); + const std::string& structureInterpretation2 = optimizedContent->GetStructureInterpretation(i); + Color structureColor2 = optimizedContent->GetStructureColor(i); + + EXPECT_NEAR(structureCenter1[0], structureCenter2[0], TOLERANCE); + EXPECT_NEAR(structureCenter1[1], structureCenter2[1], TOLERANCE); + EXPECT_NEAR(structureCenter1[2], structureCenter2[2], TOLERANCE); + + EXPECT_EQ(structureName1, structureName2); + EXPECT_EQ(structureInterpretation1, structureInterpretation2); + EXPECT_EQ(structureColor1.GetRed(), structureColor2.GetRed()); + EXPECT_EQ(structureColor1.GetGreen(), structureColor2.GetGreen()); + EXPECT_EQ(structureColor1.GetBlue(), structureColor2.GetBlue()); + + // "random" walk through the planes. Processing them all takes too long (~ 1 min) + for (size_t j = 0; j < allPlanes.size(); j += 37) + { + const OrthancStone::CoordinateSystem3D& plane = allPlanes[j]; + + std::vector< std::pair > segments1; + std::vector< std::pair > segments2; + + bool ok1 = normalContent->ProjectStructure(segments1, i, plane); + bool ok2 = optimizedContent->ProjectStructure(segments2, i, plane); + + // checks here + EXPECT_EQ(ok1, ok2); + EXPECT_EQ(segments1.size(), segments2.size()); + + for (size_t k = 0; k < segments1.size(); ++k) + { + EXPECT_NEAR(segments1[k].first.x, segments2[k].first.x, TOLERANCE); + EXPECT_NEAR(segments1[k].first.y, segments2[k].first.y, TOLERANCE); + EXPECT_NEAR(segments1[k].second.x, segments2[k].second.x, TOLERANCE); + EXPECT_NEAR(segments1[k].second.y, segments2[k].second.y, TOLERANCE); + } + } + } + + Exitialize(*loadersContext); +}