# HG changeset patch # User Sebastien Jodogne # Date 1557245512 -7200 # Node ID 2eeb5857eb436604f41f987eedecb0dadf9d6dda # Parent 573e35378999bfda7f3b134f81c2310f000df4ab DicomInstanceParameters diff -r 573e35378999 -r 2eeb5857eb43 Framework/StoneEnumerations.cpp --- a/Framework/StoneEnumerations.cpp Tue May 07 14:28:10 2019 +0200 +++ b/Framework/StoneEnumerations.cpp Tue May 07 18:11:52 2019 +0200 @@ -27,20 +27,18 @@ namespace OrthancStone { - bool StringToSopClassUid(SopClassUid& result, - const std::string& source) + SopClassUid StringToSopClassUid(const std::string& source) { std::string s = Orthanc::Toolbox::StripSpaces(source); if (s == "1.2.840.10008.5.1.4.1.1.481.2") { - result = SopClassUid_RTDose; - return true; + return SopClassUid_RTDose; } else { - //LOG(INFO) << "Unknown SOP class UID: " << source; - return false; + //LOG(INFO) << "Other SOP class UID: " << source; + return SopClassUid_Other; } } diff -r 573e35378999 -r 2eeb5857eb43 Framework/StoneEnumerations.h --- a/Framework/StoneEnumerations.h Tue May 07 14:28:10 2019 +0200 +++ b/Framework/StoneEnumerations.h Tue May 07 18:11:52 2019 +0200 @@ -98,6 +98,7 @@ enum SopClassUid { + SopClassUid_Other, SopClassUid_RTDose }; @@ -192,8 +193,7 @@ }; - bool StringToSopClassUid(SopClassUid& result, - const std::string& source); + SopClassUid StringToSopClassUid(const std::string& source); void ComputeWindowing(float& targetCenter, float& targetWidth, diff -r 573e35378999 -r 2eeb5857eb43 Framework/Toolbox/Slice.cpp --- a/Framework/Toolbox/Slice.cpp Tue May 07 14:28:10 2019 +0200 +++ b/Framework/Toolbox/Slice.cpp Tue May 07 18:11:52 2019 +0200 @@ -194,20 +194,16 @@ geometry_ = CoordinateSystem3D(position, orientation); bool ok = true; - SopClassUid tmp; - if (StringToSopClassUid(tmp, sopClassUid_)) + switch (StringToSopClassUid(sopClassUid_)) { - switch (tmp) - { - case SopClassUid_RTDose: - type_ = Type_OrthancRawFrame; - ok = ComputeRTDoseGeometry(dataset, frame); - break; + case SopClassUid_RTDose: + type_ = Type_OrthancRawFrame; + ok = ComputeRTDoseGeometry(dataset, frame); + break; - default: - break; - } + default: + break; } if (!ok) diff -r 573e35378999 -r 2eeb5857eb43 Samples/Sdl/Loader.cpp --- a/Samples/Sdl/Loader.cpp Tue May 07 14:28:10 2019 +0200 +++ b/Samples/Sdl/Loader.cpp Tue May 07 18:11:52 2019 +0200 @@ -19,22 +19,26 @@ **/ // From Stone -#include "../../Framework/StoneInitialization.h" +#include "../../Framework/Messages/ICallable.h" #include "../../Framework/Messages/IMessage.h" +#include "../../Framework/Messages/IObservable.h" #include "../../Framework/Messages/MessageBroker.h" -#include "../../Framework/Messages/ICallable.h" -#include "../../Framework/Messages/IObservable.h" +#include "../../Framework/StoneInitialization.h" +#include "../../Framework/Toolbox/GeometryToolbox.h" #include "../../Framework/Volumes/ImageBuffer3D.h" // From Orthanc framework +#include +#include +#include #include #include #include #include #include -#include #include #include +#include #include #include @@ -154,7 +158,7 @@ return answer_; } - void GetJsonBody(Json::Value& target) const + void ParseJsonBody(Json::Value& target) const { Json::Reader reader; if (!reader.parse(answer_, target)) @@ -337,8 +341,13 @@ { Orthanc::HttpClient client(orthanc_, command.GetUri()); client.SetMethod(command.GetMethod()); - client.SetBody(command.GetBody()); client.SetTimeout(command.GetTimeout()); + + if (command.GetMethod() == Orthanc::HttpMethod_Post || + command.GetMethod() == Orthanc::HttpMethod_Put) + { + client.SetBody(command.GetBody()); + } { const HttpHeaders& headers = command.GetHttpHeaders(); @@ -383,15 +392,26 @@ { const Item& item = dynamic_cast(*object); - switch (item.GetCommand().GetType()) + try { - case IOracleCommand::Type_OrthancApi: - Execute(item.GetReceiver(), - dynamic_cast(item.GetCommand())); - break; + switch (item.GetCommand().GetType()) + { + case IOracleCommand::Type_OrthancApi: + Execute(item.GetReceiver(), + dynamic_cast(item.GetCommand())); + break; - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } + catch (Orthanc::OrthancException& e) + { + LOG(ERROR) << "Exception within the oracle: " << e.What(); + } + catch (...) + { + LOG(ERROR) << "Native exception within the oracle"; } } } @@ -589,29 +609,405 @@ + class DicomInstanceParameters : public boost::noncopyable + { + private: + Orthanc::DicomImageInformation information_; + OrthancStone::SopClassUid sopClassUid_; + double thickness_; + double pixelSpacingX_; + double pixelSpacingY_; + OrthancStone::CoordinateSystem3D geometry_; + OrthancStone::Vector frameOffsets_; + bool isColor_; + bool hasRescale_; + double rescaleOffset_; + double rescaleSlope_; + bool hasDefaultWindowing_; + float defaultWindowingCenter_; + float defaultWindowingWidth_; + + void ComputeDoseOffsets(const Orthanc::DicomMap& dicom) + { + // http://dicom.nema.org/medical/Dicom/2016a/output/chtml/part03/sect_C.8.8.3.2.html + + { + std::string increment; + + if (dicom.CopyToString(increment, Orthanc::DICOM_TAG_FRAME_INCREMENT_POINTER, false)) + { + Orthanc::Toolbox::ToUpperCase(increment); + if (increment != "3004,000C") // This is the "Grid Frame Offset Vector" tag + { + LOG(ERROR) << "RT-DOSE: Bad value for the \"FrameIncrementPointer\" tag"; + return; + } + } + } + + if (!OrthancStone::LinearAlgebra::ParseVector(frameOffsets_, dicom, Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR) || + frameOffsets_.size() < information_.GetNumberOfFrames()) + { + LOG(ERROR) << "RT-DOSE: No information about the 3D location of some slice(s)"; + frameOffsets_.clear(); + } + else + { + if (frameOffsets_.size() >= 2) + { + thickness_ = frameOffsets_[1] - frameOffsets_[0]; + + if (thickness_ < 0) + { + thickness_ = -thickness_; + } + } + } + } + + public: + DicomInstanceParameters(const Orthanc::DicomMap& dicom) : + information_(dicom) + { + if (information_.GetNumberOfFrames() <= 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + std::string s; + if (!dicom.CopyToString(s, Orthanc::DICOM_TAG_SOP_CLASS_UID, false)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + else + { + sopClassUid_ = OrthancStone::StringToSopClassUid(s); + } + + if (!dicom.ParseDouble(thickness_, Orthanc::DICOM_TAG_SLICE_THICKNESS)) + { + thickness_ = 100.0 * std::numeric_limits::epsilon(); + } + + OrthancStone::GeometryToolbox::GetPixelSpacing(pixelSpacingX_, pixelSpacingY_, dicom); + + std::string position, orientation; + if (dicom.CopyToString(position, Orthanc::DICOM_TAG_IMAGE_POSITION_PATIENT, false) && + dicom.CopyToString(orientation, Orthanc::DICOM_TAG_IMAGE_ORIENTATION_PATIENT, false)) + { + geometry_ = OrthancStone::CoordinateSystem3D(position, orientation); + } + + if (sopClassUid_ == OrthancStone::SopClassUid_RTDose) + { + ComputeDoseOffsets(dicom); + } + + isColor_ = (information_.GetPhotometricInterpretation() != Orthanc::PhotometricInterpretation_Monochrome1 && + information_.GetPhotometricInterpretation() != Orthanc::PhotometricInterpretation_Monochrome2); + + double doseGridScaling; + + if (dicom.ParseDouble(rescaleOffset_, Orthanc::DICOM_TAG_RESCALE_INTERCEPT) && + dicom.ParseDouble(rescaleSlope_, Orthanc::DICOM_TAG_RESCALE_SLOPE)) + { + hasRescale_ = true; + } + else if (dicom.ParseDouble(doseGridScaling, Orthanc::DICOM_TAG_DOSE_GRID_SCALING)) + { + hasRescale_ = true; + rescaleOffset_ = 0; + rescaleSlope_ = doseGridScaling; + } + else + { + hasRescale_ = false; + } + + OrthancStone::Vector c, w; + if (OrthancStone::LinearAlgebra::ParseVector(c, dicom, Orthanc::DICOM_TAG_WINDOW_CENTER) && + OrthancStone::LinearAlgebra::ParseVector(w, dicom, Orthanc::DICOM_TAG_WINDOW_WIDTH) && + c.size() > 0 && + w.size() > 0) + { + hasDefaultWindowing_ = true; + defaultWindowingCenter_ = static_cast(c[0]); + defaultWindowingWidth_ = static_cast(w[0]); + } + else + { + hasDefaultWindowing_ = false; + } + } + + const Orthanc::DicomImageInformation& GetImageInformation() const + { + return information_; + } + + OrthancStone::SopClassUid GetSopClassUid() const + { + return sopClassUid_; + } + + double GetThickness() const + { + return thickness_; + } + + double GetPixelSpacingX() const + { + return pixelSpacingX_; + } + + double GetPixelSpacingY() const + { + return pixelSpacingY_; + } + + const OrthancStone::CoordinateSystem3D& GetGeometry() const + { + return geometry_; + } + + OrthancStone::CoordinateSystem3D GetFrameGeometry(unsigned int frame) const + { + if (frame >= information_.GetNumberOfFrames()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + if (sopClassUid_ == OrthancStone::SopClassUid_RTDose) + { + if (frame >= frameOffsets_.size()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + return OrthancStone::CoordinateSystem3D( + geometry_.GetOrigin() + frameOffsets_[frame] * geometry_.GetNormal(), + geometry_.GetAxisX(), + geometry_.GetAxisY()); + } + } + + bool FrameContainsPlane(unsigned int frame, + const OrthancStone::CoordinateSystem3D& plane) const + { + if (frame >= information_.GetNumberOfFrames()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + OrthancStone::CoordinateSystem3D tmp = geometry_; + + if (frame != 0) + { + tmp = GetFrameGeometry(frame); + } + + bool opposite; // Ignored + return (OrthancStone::GeometryToolbox::IsParallelOrOpposite( + opposite, tmp.GetNormal(), plane.GetNormal()) && + OrthancStone::LinearAlgebra::IsNear( + tmp.ProjectAlongNormal(tmp.GetOrigin()), + tmp.ProjectAlongNormal(plane.GetOrigin()), + thickness_ / 2.0)); + } + + bool IsColor() const + { + return isColor_; + } + + bool HasRescale() const + { + return hasRescale_; + } + + double GetRescaleOffset() const + { + if (hasRescale_) + { + return rescaleOffset_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + double GetRescaleSlope() const + { + if (hasRescale_) + { + return rescaleSlope_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + bool HasDefaultWindowing() const + { + return hasDefaultWindowing_; + } + + float GetDefaultWindowingCenter() const + { + if (hasDefaultWindowing_) + { + return defaultWindowingCenter_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + float GetDefaultWindowingWidth() const + { + if (hasDefaultWindowing_) + { + return defaultWindowingWidth_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + }; + + class AxialVolumeOrthancLoader : public OrthancStone::IObserver { private: - void Handle(const Refactoring::OrthancApiOracleCommand::SuccessMessage& message) + class MessageHandler : public Orthanc::IDynamicObject { - Json::Value v; - message.GetJsonBody(v); + public: + virtual void Handle(const OrthancApiOracleCommand::SuccessMessage& message) const = 0; + }; - printf("ICI [%s]\n", v.toStyledString().c_str()); + void Handle(const OrthancApiOracleCommand::SuccessMessage& message) + { + dynamic_cast(message.GetOrigin().GetPayload()).Handle(message); } + class LoadSeriesGeometryHandler : public MessageHandler + { + private: + AxialVolumeOrthancLoader& that_; + + public: + LoadSeriesGeometryHandler(AxialVolumeOrthancLoader& that) : + that_(that) + { + } + + virtual void Handle(const OrthancApiOracleCommand::SuccessMessage& message) const + { + Json::Value value; + message.ParseJsonBody(value); + + if (value.type() != Json::objectValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + + Json::Value::Members instances = value.getMemberNames(); + + for (size_t i = 0; i < instances.size(); i++) + { + Orthanc::DicomMap dicom; + dicom.FromDicomAsJson(value[instances[i]]); + + DicomInstanceParameters instance(dicom); + } + } + }; + + + class LoadInstanceGeometryHandler : public MessageHandler + { + private: + AxialVolumeOrthancLoader& that_; + + public: + LoadInstanceGeometryHandler(AxialVolumeOrthancLoader& that) : + that_(that) + { + } + + virtual void Handle(const OrthancApiOracleCommand::SuccessMessage& message) const + { + Json::Value value; + message.ParseJsonBody(value); + + if (value.type() != Json::objectValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + + Orthanc::DicomMap dicom; + dicom.FromDicomAsJson(value); + + DicomInstanceParameters instance(dicom); + } + }; + + + bool active_; std::auto_ptr image_; public: AxialVolumeOrthancLoader(OrthancStone::IObservable& oracle) : - IObserver(oracle.GetBroker()) + IObserver(oracle.GetBroker()), + active_(false) { oracle.RegisterObserverCallback( new OrthancStone::Callable (*this, &AxialVolumeOrthancLoader::Handle)); } + + void LoadSeries(IOracle& oracle, + const std::string& seriesId) + { + if (active_) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + active_ = true; + + std::auto_ptr command(new Refactoring::OrthancApiOracleCommand); + command->SetUri("/series/" + seriesId + "/instances-tags"); + command->SetPayload(new LoadSeriesGeometryHandler(*this)); + + oracle.Schedule(*this, command.release()); + } + + void LoadInstance(IOracle& oracle, + const std::string& instanceId) + { + if (active_) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + active_ = true; + + // Tag "3004-000c" is "Grid Frame Offset Vector", which is + // mandatory to read RT DOSE, but is too long to be returned by default + + std::auto_ptr command(new Refactoring::OrthancApiOracleCommand); + command->SetUri("/instances/" + instanceId + "/tags?ignore-length=3004-000c"); + command->SetPayload(new LoadInstanceGeometryHandler(*this)); + + oracle.Schedule(*this, command.release()); + } }; } @@ -624,7 +1020,7 @@ void Handle(const Refactoring::OrthancApiOracleCommand::SuccessMessage& message) { Json::Value v; - message.GetJsonBody(v); + message.ParseJsonBody(v); printf("ICI [%s]\n", v.toStyledString().c_str()); } @@ -648,17 +1044,13 @@ void Run(Refactoring::NativeApplicationContext& context) { std::auto_ptr toto; + std::auto_ptr loader1, loader2; { Refactoring::NativeApplicationContext::WriterLock lock(context); toto.reset(new Toto(lock.GetOracleObservable())); - } - - std::auto_ptr loader; - - { - Refactoring::NativeApplicationContext::WriterLock lock(context); - loader.reset(new Refactoring::AxialVolumeOrthancLoader(lock.GetOracleObservable())); + loader1.reset(new Refactoring::AxialVolumeOrthancLoader(lock.GetOracleObservable())); + loader2.reset(new Refactoring::AxialVolumeOrthancLoader(lock.GetOracleObservable())); } Refactoring::NativeOracle oracle(context); @@ -684,6 +1076,10 @@ oracle.Schedule(*toto, command.release()); } + + // 2017-11-17-Anonymized + loader1->LoadSeries(oracle, "cb3ea4d1-d08f3856-ad7b6314-74d88d77-60b05618"); // CT + loader2->LoadInstance(oracle, "41029085-71718346-811efac4-420e2c15-d39f99b6"); // RT-DOSE boost::this_thread::sleep(boost::posix_time::seconds(1));