# HG changeset patch # User Sebastien Jodogne # Date 1559073560 -7200 # Node ID df442f1ba0c6d5ef4883bb70f20fecd85d9a4c21 # Parent aead999345e0752382ce097e29af97fcacd76ddf reorganization diff -r aead999345e0 -r df442f1ba0c6 Framework/Loaders/DicomStructureSetLoader.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/DicomStructureSetLoader.cpp Tue May 28 21:59:20 2019 +0200 @@ -0,0 +1,254 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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 "../Toolbox/GeometryToolbox.h" + +namespace OrthancStone +{ + 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 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_++; + } + } + }; + + + // 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 OrthancRestApiCommand::SuccessMessage& message) + { + 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") + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource); + } + + const std::string instanceId = lookup[0]["ID"].asString(); + + { + std::auto_ptr command(new OrthancRestApiCommand); + command->SetHttpHeader("Accept-Encoding", "gzip"); + command->SetUri("/instances/" + instanceId + "/tags"); + command->SetPayload(new AddReferencedInstance(loader, instanceId)); + Schedule(command.release()); + } + } + }; + + + class DicomStructureSetLoader::LoadStructure : public LoaderStateMachine::State + { + public: + LoadStructure(DicomStructureSetLoader& that) : + State(that) + { + } + + virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) + { + DicomStructureSetLoader& loader = GetLoader(); + + { + OrthancPlugins::FullOrthancDataset dicom(message.GetAnswer()); + loader.content_.reset(new DicomStructureSet(dicom)); + } + + std::set instances; + loader.content_->GetReferencedInstances(instances); + + loader.countReferencedInstances_ = instances.size(); + + for (std::set::const_iterator + it = instances.begin(); it != instances.end(); ++it) + { + std::auto_ptr command(new OrthancRestApiCommand); + command->SetUri("/tools/lookup"); + command->SetMethod(Orthanc::HttpMethod_Post); + command->SetBody(*it); + command->SetPayload(new LookupInstance(loader, *it)); + Schedule(command.release()); + } + } + }; + + + class DicomStructureSetLoader::Slice : public IExtractedSlice + { + private: + const DicomStructureSet& content_; + uint64_t revision_; + bool isValid_; + + public: + Slice(const DicomStructureSet& content, + uint64_t revision, + const CoordinateSystem3D& cuttingPlane) : + content_(content), + revision_(revision) + { + bool opposite; + + const Vector normal = content.GetNormal(); + isValid_ = ( + GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetNormal()) || + GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetAxisX()) || + GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetAxisY())); + } + + virtual bool IsValid() + { + return isValid_; + } + + virtual uint64_t GetRevision() + { + return revision_; + } + + virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator, + const CoordinateSystem3D& cuttingPlane) + { + assert(isValid_); + + std::auto_ptr layer(new PolylineSceneLayer); + layer->SetThickness(2); + + for (size_t i = 0; i < content_.GetStructuresCount(); i++) + { + const Color& color = content_.GetStructureColor(i); + + 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].first, polygons[j][k].second); + } + + layer->AddChain(chain, true /* closed */, color); + } + } + } + + return layer.release(); + } + }; + + + DicomStructureSetLoader::DicomStructureSetLoader(IOracle& oracle, + IObservable& oracleObservable) : + LoaderStateMachine(oracle, oracleObservable), + revision_(0), + countProcessedInstances_(0), + countReferencedInstances_(0) + { + } + + + void DicomStructureSetLoader::LoadInstance(const std::string& instanceId) + { + Start(); + + instanceId_ = instanceId; + + { + std::auto_ptr command(new OrthancRestApiCommand); + command->SetHttpHeader("Accept-Encoding", "gzip"); + command->SetUri("/instances/" + instanceId + "/tags?ignore-length=3006-0050"); + command->SetPayload(new LoadStructure(*this)); + Schedule(command.release()); + } + } + + + IVolumeSlicer::IExtractedSlice* DicomStructureSetLoader::ExtractSlice(const CoordinateSystem3D& cuttingPlane) + { + if (content_.get() == NULL) + { + // Geometry is not available yet + return new IVolumeSlicer::InvalidSlice; + } + else + { + return new Slice(*content_, revision_, cuttingPlane); + } + } +} diff -r aead999345e0 -r df442f1ba0c6 Framework/Loaders/DicomStructureSetLoader.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/DicomStructureSetLoader.h Tue May 28 21:59:20 2019 +0200 @@ -0,0 +1,56 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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" + +namespace OrthancStone +{ + class DicomStructureSetLoader : + public LoaderStateMachine, + public IVolumeSlicer + { + private: + class Slice; + + // States of LoaderStateMachine + class AddReferencedInstance; // 3rd state + class LookupInstance; // 2nd state + class LoadStructure; // 1st state + + std::auto_ptr content_; + uint64_t revision_; + std::string instanceId_; + unsigned int countProcessedInstances_; + unsigned int countReferencedInstances_; + + public: + DicomStructureSetLoader(IOracle& oracle, + IObservable& oracleObservable); + + void LoadInstance(const std::string& instanceId); + + virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane); + }; +} diff -r aead999345e0 -r df442f1ba0c6 Framework/Loaders/LoaderStateMachine.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/LoaderStateMachine.cpp Tue May 28 21:59:20 2019 +0200 @@ -0,0 +1,174 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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 + +namespace OrthancStone +{ + void LoaderStateMachine::State::Handle(const OrthancRestApiCommand::SuccessMessage& message) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + + void LoaderStateMachine::State::Handle(const GetOrthancImageCommand::SuccessMessage& message) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + + void LoaderStateMachine::State::Handle(const GetOrthancWebViewerJpegCommand::SuccessMessage& message) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + + void LoaderStateMachine::Schedule(OracleCommandWithPayload* command) + { + std::auto_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() + { + if (active_) + { + 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_) + { + oracle_.Schedule(*this, pendingCommands_.front()); + pendingCommands_.pop_front(); + + activeCommands_++; + } + } + + + void LoaderStateMachine::Clear() + { + for (PendingCommands::iterator it = pendingCommands_.begin(); + it != pendingCommands_.end(); ++it) + { + delete *it; + } + + pendingCommands_.clear(); + } + + + void LoaderStateMachine::HandleExceptionMessage(const OracleCommandExceptionMessage& message) + { + LOG(ERROR) << "Error in the state machine, stopping all processing"; + Clear(); + } + + + template + void LoaderStateMachine::HandleSuccessMessage(const T& message) + { + assert(activeCommands_ > 0); + 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(); + Clear(); + } + } + + + LoaderStateMachine::LoaderStateMachine(IOracle& oracle, + IObservable& oracleObservable) : + IObserver(oracleObservable.GetBroker()), + oracle_(oracle), + active_(false), + simultaneousDownloads_(4), + activeCommands_(0) + { + oracleObservable.RegisterObserverCallback( + new Callable + (*this, &LoaderStateMachine::HandleSuccessMessage)); + + oracleObservable.RegisterObserverCallback( + new Callable + (*this, &LoaderStateMachine::HandleSuccessMessage)); + + oracleObservable.RegisterObserverCallback( + new Callable + (*this, &LoaderStateMachine::HandleSuccessMessage)); + + oracleObservable.RegisterObserverCallback( + new Callable + (*this, &LoaderStateMachine::HandleExceptionMessage)); + } + + + void LoaderStateMachine::SetSimultaneousDownloads(unsigned int count) + { + if (active_) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else if (count == 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + else + { + simultaneousDownloads_ = count; + } + } +} diff -r aead999345e0 -r df442f1ba0c6 Framework/Loaders/LoaderStateMachine.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/LoaderStateMachine.h Tue May 28 21:59:20 2019 +0200 @@ -0,0 +1,119 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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/IObserver.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. + */ + class LoaderStateMachine : public IObserver + { + protected: + class State : public Orthanc::IDynamicObject + { + private: + LoaderStateMachine& that_; + + public: + State(LoaderStateMachine& that) : + that_(that) + { + } + + State(const State& currentState) : + that_(currentState.that_) + { + } + + void Schedule(OracleCommandWithPayload* command) const + { + that_.Schedule(command); + } + + template + T& GetLoader() const + { + return dynamic_cast(that_); + } + + virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message); + + virtual void Handle(const GetOrthancImageCommand::SuccessMessage& message); + + virtual void Handle(const GetOrthancWebViewerJpegCommand::SuccessMessage& message); + }; + + void Schedule(OracleCommandWithPayload* command); + + void Start(); + + private: + void Step(); + + void Clear(); + + void HandleExceptionMessage(const OracleCommandExceptionMessage& message); + + template + void HandleSuccessMessage(const T& message); + + typedef std::list PendingCommands; + + IOracle& oracle_; + bool active_; + unsigned int simultaneousDownloads_; + PendingCommands pendingCommands_; + unsigned int activeCommands_; + + public: + LoaderStateMachine(IOracle& oracle, + IObservable& oracleObservable); + + virtual ~LoaderStateMachine() + { + Clear(); + } + + bool IsActive() const + { + return active_; + } + + void SetSimultaneousDownloads(unsigned int count); + }; +} diff -r aead999345e0 -r df442f1ba0c6 Framework/Loaders/OrthancMultiframeVolumeLoader.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/OrthancMultiframeVolumeLoader.cpp Tue May 28 21:59:20 2019 +0200 @@ -0,0 +1,355 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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 + +namespace OrthancStone +{ + class OrthancMultiframeVolumeLoader::LoadRTDoseGeometry : public LoaderStateMachine::State + { + private: + std::auto_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 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.CopyToString(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 OrthancRestApiCommand::SuccessMessage& message) + { + OrthancMultiframeVolumeLoader& loader = GetLoader(); + + Json::Value body; + message.ParseJsonBody(body); + + if (body.type() != Json::objectValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + + std::auto_ptr dicom(new Orthanc::DicomMap); + dicom->FromDicomAsJson(body); + + if (StringToSopClassUid(GetSopClassUid(*dicom)) == 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::auto_ptr command(new OrthancRestApiCommand); + command->SetUri("/instances/" + loader.GetInstanceId() + "/content/" + + Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR.Format()); + command->SetPayload(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 OrthancRestApiCommand::SuccessMessage& message) + { + GetLoader().SetTransferSyntax(message.GetAnswer()); + } + }; + + + class OrthancMultiframeVolumeLoader::LoadUncompressedPixelData : public State + { + public: + LoadUncompressedPixelData(OrthancMultiframeVolumeLoader& that) : + State(that) + { + } + + virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) + { + GetLoader().SetUncompressedPixelData(message.GetAnswer()); + } + }; + + + const std::string& OrthancMultiframeVolumeLoader::GetInstanceId() const + { + if (IsActive()) + { + return instanceId_; + } + else + { + 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::auto_ptr command(new OrthancRestApiCommand); + command->SetHttpHeader("Accept-Encoding", "gzip"); + command->SetUri("/instances/" + instanceId_ + "/content/" + + Orthanc::DICOM_TAG_PIXEL_DATA.Format() + "/0"); + command->SetPayload(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) + { + 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 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(); + + { + VolumeImageGeometry geometry; + geometry.SetSize(width, height, depth); + geometry.SetAxialGeometry(parameters.GetGeometry()); + geometry.SetVoxelDimensions(parameters.GetPixelSpacingX(), + parameters.GetPixelSpacingY(), spacingZ); + volume_->Initialize(geometry, format); + } + + volume_->GetPixelData().Clear(); + + ScheduleFrameDownloads(); + + BroadcastMessage(DicomVolumeImage::GeometryReadyMessage(*volume_)); + } + + + ORTHANC_FORCE_INLINE + static void CopyPixel(uint32_t& target, + const void* source) + { + // TODO - check alignement? + target = le32toh(*reinterpret_cast(source)); + } + + + template + void OrthancMultiframeVolumeLoader::CopyPixelData(const std::string& pixelData) + { + 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; + } + + const uint8_t* source = reinterpret_cast(pixelData.c_str()); + + for (unsigned int z = 0; z < depth; z++) + { + ImageBuffer3D::SliceWriter writer(target, 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); + + target ++; + source += bpp; + } + } + } + } + + + void OrthancMultiframeVolumeLoader::SetUncompressedPixelData(const std::string& pixelData) + { + switch (volume_->GetPixelData().GetFormat()) + { + case Orthanc::PixelFormat_Grayscale32: + CopyPixelData(pixelData); + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + volume_->IncrementRevision(); + + BroadcastMessage(DicomVolumeImage::ContentUpdatedMessage(*volume_)); + } + + + OrthancMultiframeVolumeLoader::OrthancMultiframeVolumeLoader(const boost::shared_ptr& volume, + IOracle& oracle, + IObservable& oracleObservable) : + LoaderStateMachine(oracle, oracleObservable), + IObservable(oracleObservable.GetBroker()), + volume_(volume) + { + if (volume.get() == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + } + + + void OrthancMultiframeVolumeLoader::LoadInstance(const std::string& instanceId) + { + Start(); + + instanceId_ = instanceId; + + { + std::auto_ptr command(new OrthancRestApiCommand); + command->SetHttpHeader("Accept-Encoding", "gzip"); + command->SetUri("/instances/" + instanceId + "/tags"); + command->SetPayload(new LoadGeometry(*this)); + Schedule(command.release()); + } + + { + std::auto_ptr command(new OrthancRestApiCommand); + command->SetUri("/instances/" + instanceId + "/metadata/TransferSyntax"); + command->SetPayload(new LoadTransferSyntax(*this)); + Schedule(command.release()); + } + } +} diff -r aead999345e0 -r df442f1ba0c6 Framework/Loaders/OrthancMultiframeVolumeLoader.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/OrthancMultiframeVolumeLoader.h Tue May 28 21:59:20 2019 +0200 @@ -0,0 +1,64 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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" + +namespace OrthancStone +{ + class OrthancMultiframeVolumeLoader : + public LoaderStateMachine, + public IObservable + { + private: + class LoadRTDoseGeometry; + class LoadGeometry; + class LoadTransferSyntax; + class LoadUncompressedPixelData; + + boost::shared_ptr volume_; + std::string instanceId_; + std::string transferSyntaxUid_; + + + const std::string& GetInstanceId() const; + + void ScheduleFrameDownloads(); + + void SetTransferSyntax(const std::string& transferSyntax); + + void SetGeometry(const Orthanc::DicomMap& dicom); + + template + void CopyPixelData(const std::string& pixelData); + + void SetUncompressedPixelData(const std::string& pixelData); + + public: + OrthancMultiframeVolumeLoader(const boost::shared_ptr& volume, + IOracle& oracle, + IObservable& oracleObservable); + + void LoadInstance(const std::string& instanceId); + }; +} diff -r aead999345e0 -r df442f1ba0c6 Framework/Volumes/DicomVolumeImageReslicer.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Volumes/DicomVolumeImageReslicer.cpp Tue May 28 21:59:20 2019 +0200 @@ -0,0 +1,116 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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 "DicomVolumeImageReslicer.h" + +#include + +namespace OrthancStone +{ + class DicomVolumeImageReslicer::Slice : public IVolumeSlicer::IExtractedSlice + { + private: + DicomVolumeImageReslicer& that_; + CoordinateSystem3D cuttingPlane_; + + public: + Slice(DicomVolumeImageReslicer& that, + const CoordinateSystem3D& cuttingPlane) : + that_(that), + cuttingPlane_(cuttingPlane) + { + } + + virtual bool IsValid() + { + return true; + } + + virtual uint64_t GetRevision() + { + return that_.volume_->GetRevision(); + } + + virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator, + const CoordinateSystem3D& cuttingPlane) + { + VolumeReslicer& reslicer = that_.reslicer_; + + if (configurator == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, + "Must provide a layer style configurator"); + } + + reslicer.SetOutputFormat(that_.volume_->GetPixelData().GetFormat()); + reslicer.Apply(that_.volume_->GetPixelData(), + that_.volume_->GetGeometry(), + cuttingPlane); + + if (reslicer.IsSuccess()) + { + std::auto_ptr layer + (configurator->CreateTextureFromDicom(reslicer.GetOutputSlice(), + that_.volume_->GetDicomParameters())); + if (layer.get() == NULL) + { + return NULL; + } + + double s = reslicer.GetPixelSpacing(); + layer->SetPixelSpacing(s, s); + layer->SetOrigin(reslicer.GetOutputExtent().GetX1() + 0.5 * s, + reslicer.GetOutputExtent().GetY1() + 0.5 * s); + + // TODO - Angle!! + + return layer.release(); + } + else + { + return NULL; + } + } + }; + + + DicomVolumeImageReslicer::DicomVolumeImageReslicer(const boost::shared_ptr& volume) : + volume_(volume) + { + if (volume.get() == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + } + + + IVolumeSlicer::IExtractedSlice* DicomVolumeImageReslicer::ExtractSlice(const CoordinateSystem3D& cuttingPlane) + { + if (volume_->HasGeometry()) + { + return new Slice(*this, cuttingPlane); + } + else + { + return new IVolumeSlicer::InvalidSlice; + } + } +} diff -r aead999345e0 -r df442f1ba0c6 Framework/Volumes/DicomVolumeImageReslicer.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Volumes/DicomVolumeImageReslicer.h Tue May 28 21:59:20 2019 +0200 @@ -0,0 +1,69 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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 "DicomVolumeImage.h" +#include "IVolumeSlicer.h" +#include "VolumeReslicer.h" + +#include + +namespace OrthancStone +{ + /** + This class is able to supply an extract slice for an arbitrary cutting + plane through a volume image + */ + class DicomVolumeImageReslicer : public IVolumeSlicer + { + private: + class Slice; + + boost::shared_ptr volume_; + VolumeReslicer reslicer_; + + public: + DicomVolumeImageReslicer(const boost::shared_ptr& volume); + + ImageInterpolation GetInterpolation() const + { + return reslicer_.GetInterpolation(); + } + + void SetInterpolation(ImageInterpolation interpolation) + { + reslicer_.SetInterpolation(interpolation); + } + + bool IsFastMode() const + { + return reslicer_.IsFastMode(); + } + + void SetFastMode(bool fast) + { + reslicer_.EnableFastMode(fast); + } + + virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane); + }; +} diff -r aead999345e0 -r df442f1ba0c6 Framework/Volumes/VolumeSceneLayerSource.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Volumes/VolumeSceneLayerSource.cpp Tue May 28 21:59:20 2019 +0200 @@ -0,0 +1,143 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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 "VolumeSceneLayerSource.h" + +#include + +namespace OrthancStone +{ + static bool IsSameCuttingPlane(const CoordinateSystem3D& a, + const CoordinateSystem3D& b) + { + // TODO - What if the normal is reversed? + double distance; + return (CoordinateSystem3D::ComputeDistance(distance, a, b) && + LinearAlgebra::IsCloseToZero(distance)); + } + + + void VolumeSceneLayerSource::ClearLayer() + { + scene_.DeleteLayer(layerDepth_); + lastPlane_.reset(NULL); + } + + + VolumeSceneLayerSource::VolumeSceneLayerSource(Scene2D& scene, + int layerDepth, + const boost::shared_ptr& slicer) : + scene_(scene), + layerDepth_(layerDepth), + slicer_(slicer) + { + if (slicer == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + } + + + void VolumeSceneLayerSource::RemoveConfigurator() + { + configurator_.reset(); + lastPlane_.reset(); + } + + + void VolumeSceneLayerSource::SetConfigurator(ILayerStyleConfigurator* configurator) // Takes ownership + { + if (configurator == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + configurator_.reset(configurator); + + // Invalidate the layer + lastPlane_.reset(NULL); + } + + + ILayerStyleConfigurator& VolumeSceneLayerSource::GetConfigurator() const + { + if (configurator_.get() == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + return *configurator_; + } + + + void VolumeSceneLayerSource::Update(const CoordinateSystem3D& plane) + { + assert(slicer_.get() != NULL); + std::auto_ptr slice(slicer_->ExtractSlice(plane)); + + if (slice.get() == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + if (!slice->IsValid()) + { + // The slicer cannot handle this cutting plane: Clear the layer + ClearLayer(); + } + else if (lastPlane_.get() != NULL && + IsSameCuttingPlane(*lastPlane_, plane) && + lastRevision_ == slice->GetRevision()) + { + // The content of the slice has not changed: Don't update the + // layer content, but possibly update its style + + if (configurator_.get() != NULL && + configurator_->GetRevision() != lastConfiguratorRevision_ && + scene_.HasLayer(layerDepth_)) + { + configurator_->ApplyStyle(scene_.GetLayer(layerDepth_)); + } + } + else + { + // Content has changed: An update is needed + lastPlane_.reset(new CoordinateSystem3D(plane)); + lastRevision_ = slice->GetRevision(); + + std::auto_ptr layer(slice->CreateSceneLayer(configurator_.get(), plane)); + if (layer.get() == NULL) + { + ClearLayer(); + } + else + { + if (configurator_.get() != NULL) + { + lastConfiguratorRevision_ = configurator_->GetRevision(); + configurator_->ApplyStyle(*layer); + } + + scene_.SetLayer(layerDepth_, layer.release()); + } + } + } +} diff -r aead999345e0 -r df442f1ba0c6 Framework/Volumes/VolumeSceneLayerSource.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Volumes/VolumeSceneLayerSource.h Tue May 28 21:59:20 2019 +0200 @@ -0,0 +1,74 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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 "../Scene2D/Scene2D.h" +#include "IVolumeSlicer.h" + +#include + +namespace OrthancStone +{ + /** + This class applies one "volume slicer" to a "3D volume", in order + to create one "2D scene layer" that will be set onto the "2D + scene". The style of the layer can be fine-tuned using a "layer + style configurator". The class only changes the layer if the + cutting plane has been modified since the last call to "Update()". + **/ + class VolumeSceneLayerSource : public boost::noncopyable + { + private: + Scene2D& scene_; + int layerDepth_; + boost::shared_ptr slicer_; + std::auto_ptr configurator_; + std::auto_ptr lastPlane_; + uint64_t lastRevision_; + uint64_t lastConfiguratorRevision_; + + void ClearLayer(); + + public: + VolumeSceneLayerSource(Scene2D& scene, + int layerDepth, + const boost::shared_ptr& slicer); + + const IVolumeSlicer& GetSlicer() const + { + return *slicer_; + } + + void RemoveConfigurator(); + + void SetConfigurator(ILayerStyleConfigurator* configurator); + + bool HasConfigurator() const + { + return configurator_.get() != NULL; + } + + ILayerStyleConfigurator& GetConfigurator() const; + + void Update(const CoordinateSystem3D& plane); + }; +} diff -r aead999345e0 -r df442f1ba0c6 Resources/CMake/OrthancStoneConfiguration.cmake --- a/Resources/CMake/OrthancStoneConfiguration.cmake Tue May 28 21:16:39 2019 +0200 +++ b/Resources/CMake/OrthancStoneConfiguration.cmake Tue May 28 21:59:20 2019 +0200 @@ -390,6 +390,9 @@ ${ORTHANC_STONE_ROOT}/Framework/Fonts/TextBoundingBox.cpp ${ORTHANC_STONE_ROOT}/Framework/Loaders/BasicFetchingItemsSorter.cpp ${ORTHANC_STONE_ROOT}/Framework/Loaders/BasicFetchingStrategy.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomStructureSetLoader.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/LoaderStateMachine.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/OrthancMultiframeVolumeLoader.cpp ${ORTHANC_STONE_ROOT}/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp ${ORTHANC_STONE_ROOT}/Framework/Messages/ICallable.h ${ORTHANC_STONE_ROOT}/Framework/Messages/IMessage.h @@ -494,11 +497,13 @@ ${ORTHANC_STONE_ROOT}/Framework/Toolbox/UndoRedoStack.cpp ${ORTHANC_STONE_ROOT}/Framework/Volumes/DicomVolumeImage.cpp ${ORTHANC_STONE_ROOT}/Framework/Volumes/DicomVolumeImageMPRSlicer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Volumes/DicomVolumeImageReslicer.cpp ${ORTHANC_STONE_ROOT}/Framework/Volumes/IVolumeSlicer.cpp ${ORTHANC_STONE_ROOT}/Framework/Volumes/ImageBuffer3D.cpp ${ORTHANC_STONE_ROOT}/Framework/Volumes/OrientedVolumeBoundingBox.cpp ${ORTHANC_STONE_ROOT}/Framework/Volumes/VolumeImageGeometry.cpp ${ORTHANC_STONE_ROOT}/Framework/Volumes/VolumeReslicer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Volumes/VolumeSceneLayerSource.cpp ${ORTHANC_STONE_ROOT}/Framework/Wrappers/CairoContext.cpp ${ORTHANC_STONE_ROOT}/Framework/Wrappers/CairoSurface.cpp diff -r aead999345e0 -r df442f1ba0c6 Samples/Sdl/Loader.cpp --- a/Samples/Sdl/Loader.cpp Tue May 28 21:16:39 2019 +0200 +++ b/Samples/Sdl/Loader.cpp Tue May 28 21:59:20 2019 +0200 @@ -19,1091 +19,35 @@ **/ +#include "../../Framework/Loaders/DicomStructureSetLoader.h" +#include "../../Framework/Loaders/OrthancMultiframeVolumeLoader.h" #include "../../Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h" -#include "../../Framework/Volumes/DicomVolumeImageMPRSlicer.h" +#include "../../Framework/Oracle/SleepOracleCommand.h" +#include "../../Framework/Oracle/ThreadedOracle.h" +#include "../../Framework/Scene2D/CairoCompositor.h" #include "../../Framework/Scene2D/GrayscaleStyleConfigurator.h" #include "../../Framework/Scene2D/LookupTableStyleConfigurator.h" -#include "../../Framework/Oracle/ThreadedOracle.h" -#include "../../Framework/Oracle/GetOrthancWebViewerJpegCommand.h" -#include "../../Framework/Oracle/GetOrthancImageCommand.h" -#include "../../Framework/Oracle/OrthancRestApiCommand.h" -#include "../../Framework/Oracle/SleepOracleCommand.h" -#include "../../Framework/Oracle/OracleCommandExceptionMessage.h" - -// From Stone -#include "../../Framework/Loaders/BasicFetchingItemsSorter.h" -#include "../../Framework/Loaders/BasicFetchingStrategy.h" -#include "../../Framework/Scene2D/CairoCompositor.h" -#include "../../Framework/Scene2D/Scene2D.h" -#include "../../Framework/Scene2D/PolylineSceneLayer.h" -#include "../../Framework/Scene2D/LookupTableTextureSceneLayer.h" #include "../../Framework/StoneInitialization.h" -#include "../../Framework/Toolbox/GeometryToolbox.h" -#include "../../Framework/Toolbox/SlicesSorter.h" -#include "../../Framework/Toolbox/DicomStructureSet.h" -#include "../../Framework/Volumes/ImageBuffer3D.h" -#include "../../Framework/Volumes/VolumeImageGeometry.h" -#include "../../Framework/Volumes/VolumeReslicer.h" +#include "../../Framework/Volumes/VolumeSceneLayerSource.h" +#include "../../Framework/Volumes/DicomVolumeImageMPRSlicer.h" +#include "../../Framework/Volumes/DicomVolumeImageReslicer.h" // From Orthanc framework -#include -#include #include #include -#include #include #include #include -#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. - */ - class LoaderStateMachine : public IObserver - { - protected: - class State : public Orthanc::IDynamicObject - { - private: - LoaderStateMachine& that_; - - public: - State(LoaderStateMachine& that) : - that_(that) - { - } - - State(const State& currentState) : - that_(currentState.that_) - { - } - - void Schedule(OracleCommandWithPayload* command) const - { - that_.Schedule(command); - } - - template - T& GetLoader() const - { - return dynamic_cast(that_); - } - - virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - - virtual void Handle(const GetOrthancImageCommand::SuccessMessage& message) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - - virtual void Handle(const GetOrthancWebViewerJpegCommand::SuccessMessage& message) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - }; - - void Schedule(OracleCommandWithPayload* command) - { - std::auto_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 Start() - { - if (active_) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - - active_ = true; - - for (size_t i = 0; i < simultaneousDownloads_; i++) - { - Step(); - } - } - - private: - void Step() - { - if (!pendingCommands_.empty() && - activeCommands_ < simultaneousDownloads_) - { - oracle_.Schedule(*this, pendingCommands_.front()); - pendingCommands_.pop_front(); - - activeCommands_++; - } - } - - void Clear() - { - for (PendingCommands::iterator it = pendingCommands_.begin(); - it != pendingCommands_.end(); ++it) - { - delete *it; - } - - pendingCommands_.clear(); - } - - void HandleExceptionMessage(const OracleCommandExceptionMessage& message) - { - LOG(ERROR) << "Error in the state machine, stopping all processing"; - Clear(); - } - - template - void HandleSuccessMessage(const T& message) - { - assert(activeCommands_ > 0); - 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(); - Clear(); - } - } - - typedef std::list PendingCommands; - - IOracle& oracle_; - bool active_; - unsigned int simultaneousDownloads_; - PendingCommands pendingCommands_; - unsigned int activeCommands_; - - public: - LoaderStateMachine(IOracle& oracle, - IObservable& oracleObservable) : - IObserver(oracleObservable.GetBroker()), - oracle_(oracle), - active_(false), - simultaneousDownloads_(4), - activeCommands_(0) - { - oracleObservable.RegisterObserverCallback( - new Callable - (*this, &LoaderStateMachine::HandleSuccessMessage)); - - oracleObservable.RegisterObserverCallback( - new Callable - (*this, &LoaderStateMachine::HandleSuccessMessage)); - - oracleObservable.RegisterObserverCallback( - new Callable - (*this, &LoaderStateMachine::HandleSuccessMessage)); - - oracleObservable.RegisterObserverCallback( - new Callable - (*this, &LoaderStateMachine::HandleExceptionMessage)); - } - - virtual ~LoaderStateMachine() - { - Clear(); - } - - bool IsActive() const - { - return active_; - } - - void SetSimultaneousDownloads(unsigned int count) - { - if (active_) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - else if (count == 0) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); - } - else - { - simultaneousDownloads_ = count; - } - } - }; - - - - class OrthancMultiframeVolumeLoader : - public LoaderStateMachine, - public IObservable - { - private: - class LoadRTDoseGeometry : public State - { - private: - std::auto_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 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.CopyToString(s, Orthanc::DICOM_TAG_SOP_CLASS_UID, false)) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, - "DICOM file without SOP class UID"); - } - else - { - return s; - } - } - - - class LoadGeometry : public State - { - public: - LoadGeometry(OrthancMultiframeVolumeLoader& that) : - State(that) - { - } - - virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) - { - OrthancMultiframeVolumeLoader& loader = GetLoader(); - - Json::Value body; - message.ParseJsonBody(body); - - if (body.type() != Json::objectValue) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); - } - - std::auto_ptr dicom(new Orthanc::DicomMap); - dicom->FromDicomAsJson(body); - - if (StringToSopClassUid(GetSopClassUid(*dicom)) == 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::auto_ptr command(new OrthancRestApiCommand); - command->SetUri("/instances/" + loader.GetInstanceId() + "/content/" + - Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR.Format()); - command->SetPayload(new LoadRTDoseGeometry(loader, dicom.release())); - - Schedule(command.release()); - } - else - { - loader.SetGeometry(*dicom); - } - } - }; - - - - class LoadTransferSyntax : public State - { - public: - LoadTransferSyntax(OrthancMultiframeVolumeLoader& that) : - State(that) - { - } - - virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) - { - GetLoader().SetTransferSyntax(message.GetAnswer()); - } - }; - - - class LoadUncompressedPixelData : public State - { - public: - LoadUncompressedPixelData(OrthancMultiframeVolumeLoader& that) : - State(that) - { - } - - virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) - { - GetLoader().SetUncompressedPixelData(message.GetAnswer()); - } - }; - - - - boost::shared_ptr volume_; - std::string instanceId_; - std::string transferSyntaxUid_; - - - const std::string& GetInstanceId() const - { - if (IsActive()) - { - return instanceId_; - } - else - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - } - - - void 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::auto_ptr command(new OrthancRestApiCommand); - command->SetHttpHeader("Accept-Encoding", "gzip"); - command->SetUri("/instances/" + instanceId_ + "/content/" + - Orthanc::DICOM_TAG_PIXEL_DATA.Format() + "/0"); - command->SetPayload(new LoadUncompressedPixelData(*this)); - Schedule(command.release()); - } - else - { - throw Orthanc::OrthancException( - Orthanc::ErrorCode_NotImplemented, - "No support for multiframe instances with transfer syntax: " + transferSyntaxUid_); - } - } - - - void SetTransferSyntax(const std::string& transferSyntax) - { - transferSyntaxUid_ = Orthanc::Toolbox::StripSpaces(transferSyntax); - ScheduleFrameDownloads(); - } - - - void SetGeometry(const Orthanc::DicomMap& dicom) - { - 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 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(); - - { - VolumeImageGeometry geometry; - geometry.SetSize(width, height, depth); - geometry.SetAxialGeometry(parameters.GetGeometry()); - geometry.SetVoxelDimensions(parameters.GetPixelSpacingX(), - parameters.GetPixelSpacingY(), spacingZ); - volume_->Initialize(geometry, format); - } - - volume_->GetPixelData().Clear(); - - ScheduleFrameDownloads(); - - BroadcastMessage(DicomVolumeImage::GeometryReadyMessage(*volume_)); - } - - - ORTHANC_FORCE_INLINE - static void CopyPixel(uint32_t& target, - const void* source) - { - // TODO - check alignement? - target = le32toh(*reinterpret_cast(source)); - } - - - template - void CopyPixelData(const std::string& pixelData) - { - 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; - } - - const uint8_t* source = reinterpret_cast(pixelData.c_str()); - - for (unsigned int z = 0; z < depth; z++) - { - ImageBuffer3D::SliceWriter writer(target, 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); - - target ++; - source += bpp; - } - } - } - } - - - void SetUncompressedPixelData(const std::string& pixelData) - { - switch (volume_->GetPixelData().GetFormat()) - { - case Orthanc::PixelFormat_Grayscale32: - CopyPixelData(pixelData); - break; - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - - volume_->IncrementRevision(); - - BroadcastMessage(DicomVolumeImage::ContentUpdatedMessage(*volume_)); - } - - - public: - OrthancMultiframeVolumeLoader(const boost::shared_ptr& volume, - IOracle& oracle, - IObservable& oracleObservable) : - LoaderStateMachine(oracle, oracleObservable), - IObservable(oracleObservable.GetBroker()), - volume_(volume) - { - if (volume.get() == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - } - - - void LoadInstance(const std::string& instanceId) - { - Start(); - - instanceId_ = instanceId; - - { - std::auto_ptr command(new OrthancRestApiCommand); - command->SetHttpHeader("Accept-Encoding", "gzip"); - command->SetUri("/instances/" + instanceId + "/tags"); - command->SetPayload(new LoadGeometry(*this)); - Schedule(command.release()); - } - - { - std::auto_ptr command(new OrthancRestApiCommand); - command->SetUri("/instances/" + instanceId + "/metadata/TransferSyntax"); - command->SetPayload(new LoadTransferSyntax(*this)); - Schedule(command.release()); - } - } - }; - - - /** - This class is able to supply an extract slice for an arbitrary cutting - plane through a volume image - */ - class DicomVolumeImageReslicer : public IVolumeSlicer - { - private: - class Slice : public IExtractedSlice - { - private: - DicomVolumeImageReslicer& that_; - CoordinateSystem3D cuttingPlane_; - - public: - Slice(DicomVolumeImageReslicer& that, - const CoordinateSystem3D& cuttingPlane) : - that_(that), - cuttingPlane_(cuttingPlane) - { - } - - virtual bool IsValid() - { - return true; - } - - virtual uint64_t GetRevision() - { - return that_.volume_->GetRevision(); - } - - virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator, - const CoordinateSystem3D& cuttingPlane) - { - VolumeReslicer& reslicer = that_.reslicer_; - - if (configurator == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, - "Must provide a layer style configurator"); - } - - reslicer.SetOutputFormat(that_.volume_->GetPixelData().GetFormat()); - reslicer.Apply(that_.volume_->GetPixelData(), - that_.volume_->GetGeometry(), - cuttingPlane); - - if (reslicer.IsSuccess()) - { - std::auto_ptr layer - (configurator->CreateTextureFromDicom(reslicer.GetOutputSlice(), - that_.volume_->GetDicomParameters())); - if (layer.get() == NULL) - { - return NULL; - } - - double s = reslicer.GetPixelSpacing(); - layer->SetPixelSpacing(s, s); - layer->SetOrigin(reslicer.GetOutputExtent().GetX1() + 0.5 * s, - reslicer.GetOutputExtent().GetY1() + 0.5 * s); - - // TODO - Angle!! - - return layer.release(); - } - else - { - return NULL; - } - } - }; - - boost::shared_ptr volume_; - VolumeReslicer reslicer_; - - public: - DicomVolumeImageReslicer(const boost::shared_ptr& volume) : - volume_(volume) - { - if (volume.get() == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - } - - ImageInterpolation GetInterpolation() const - { - return reslicer_.GetInterpolation(); - } - - void SetInterpolation(ImageInterpolation interpolation) - { - reslicer_.SetInterpolation(interpolation); - } - - bool IsFastMode() const - { - return reslicer_.IsFastMode(); - } - - void SetFastMode(bool fast) - { - reslicer_.EnableFastMode(fast); - } - - virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) - { - if (volume_->HasGeometry()) - { - return new Slice(*this, cuttingPlane); - } - else - { - return new IVolumeSlicer::InvalidSlice; - } - } - }; - - - - class DicomStructureSetLoader : - public LoaderStateMachine, - public IVolumeSlicer - { - private: - class AddReferencedInstance : public State - { - private: - std::string instanceId_; - - public: - AddReferencedInstance(DicomStructureSetLoader& that, - const std::string& instanceId) : - State(that), - instanceId_(instanceId) - { - } - - virtual void Handle(const 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_++; - } - } - }; - - // State that converts a "SOP Instance UID" to an Orthanc identifier - class LookupInstance : public State - { - private: - std::string sopInstanceUid_; - - public: - LookupInstance(DicomStructureSetLoader& that, - const std::string& sopInstanceUid) : - State(that), - sopInstanceUid_(sopInstanceUid) - { - } - - virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) - { - 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") - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource); - } - - const std::string instanceId = lookup[0]["ID"].asString(); - - { - std::auto_ptr command(new OrthancRestApiCommand); - command->SetHttpHeader("Accept-Encoding", "gzip"); - command->SetUri("/instances/" + instanceId + "/tags"); - command->SetPayload(new AddReferencedInstance(loader, instanceId)); - Schedule(command.release()); - } - } - }; - - class LoadStructure : public State - { - public: - LoadStructure(DicomStructureSetLoader& that) : - State(that) - { - } - - virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) - { - DicomStructureSetLoader& loader = GetLoader(); - - { - OrthancPlugins::FullOrthancDataset dicom(message.GetAnswer()); - loader.content_.reset(new DicomStructureSet(dicom)); - } - - std::set instances; - loader.content_->GetReferencedInstances(instances); - - loader.countReferencedInstances_ = instances.size(); - - for (std::set::const_iterator - it = instances.begin(); it != instances.end(); ++it) - { - std::auto_ptr command(new OrthancRestApiCommand); - command->SetUri("/tools/lookup"); - command->SetMethod(Orthanc::HttpMethod_Post); - command->SetBody(*it); - command->SetPayload(new LookupInstance(loader, *it)); - Schedule(command.release()); - } - } - }; - - - - std::auto_ptr content_; - uint64_t revision_; - std::string instanceId_; - unsigned int countProcessedInstances_; - unsigned int countReferencedInstances_; - - - class Slice : public IExtractedSlice - { - private: - const DicomStructureSet& content_; - uint64_t revision_; - bool isValid_; - - public: - Slice(const DicomStructureSet& content, - uint64_t revision, - const CoordinateSystem3D& cuttingPlane) : - content_(content), - revision_(revision) - { - bool opposite; - - const Vector normal = content.GetNormal(); - isValid_ = ( - GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetNormal()) || - GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetAxisX()) || - GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetAxisY())); - } - - virtual bool IsValid() - { - return isValid_; - } - - virtual uint64_t GetRevision() - { - return revision_; - } - - virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator, - const CoordinateSystem3D& cuttingPlane) - { - assert(isValid_); - - std::auto_ptr layer(new PolylineSceneLayer); - layer->SetThickness(2); - - for (size_t i = 0; i < content_.GetStructuresCount(); i++) - { - const Color& color = content_.GetStructureColor(i); - - 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].first, polygons[j][k].second); - } - - layer->AddChain(chain, true /* closed */, color); - } - } - } - - return layer.release(); - } - }; - - public: - DicomStructureSetLoader(IOracle& oracle, - IObservable& oracleObservable) : - LoaderStateMachine(oracle, oracleObservable), - revision_(0), - countProcessedInstances_(0), - countReferencedInstances_(0) - { - } - - - void LoadInstance(const std::string& instanceId) - { - Start(); - - instanceId_ = instanceId; - - { - std::auto_ptr command(new OrthancRestApiCommand); - command->SetHttpHeader("Accept-Encoding", "gzip"); - command->SetUri("/instances/" + instanceId + "/tags?ignore-length=3006-0050"); - command->SetPayload(new LoadStructure(*this)); - Schedule(command.release()); - } - } - - virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) - { - if (content_.get() == NULL) - { - // Geometry is not available yet - return new IVolumeSlicer::InvalidSlice; - } - else - { - return new Slice(*content_, revision_, cuttingPlane); - } - } - }; - - - - class VolumeSceneLayerSource : public boost::noncopyable - { - private: - Scene2D& scene_; - int layerDepth_; - boost::shared_ptr slicer_; - std::auto_ptr configurator_; - std::auto_ptr lastPlane_; - uint64_t lastRevision_; - uint64_t lastConfiguratorRevision_; - - static bool IsSameCuttingPlane(const CoordinateSystem3D& a, - const CoordinateSystem3D& b) - { - // TODO - What if the normal is reversed? - double distance; - return (CoordinateSystem3D::ComputeDistance(distance, a, b) && - LinearAlgebra::IsCloseToZero(distance)); - } - - void ClearLayer() - { - scene_.DeleteLayer(layerDepth_); - lastPlane_.reset(NULL); - } - - public: - VolumeSceneLayerSource(Scene2D& scene, - int layerDepth, - const boost::shared_ptr& slicer) : - scene_(scene), - layerDepth_(layerDepth), - slicer_(slicer) - { - if (slicer == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - } - - const IVolumeSlicer& GetSlicer() const - { - return *slicer_; - } - - void RemoveConfigurator() - { - configurator_.reset(); - lastPlane_.reset(); - } - - void SetConfigurator(ILayerStyleConfigurator* configurator) // Takes ownership - { - if (configurator == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - - configurator_.reset(configurator); - - // Invalidate the layer - lastPlane_.reset(NULL); - } - - bool HasConfigurator() const - { - return configurator_.get() != NULL; - } - - ILayerStyleConfigurator& GetConfigurator() const - { - if (configurator_.get() == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - - return *configurator_; - } - - void Update(const CoordinateSystem3D& plane) - { - assert(slicer_.get() != NULL); - std::auto_ptr slice(slicer_->ExtractSlice(plane)); - - if (slice.get() == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } - - if (!slice->IsValid()) - { - // The slicer cannot handle this cutting plane: Clear the layer - ClearLayer(); - } - else if (lastPlane_.get() != NULL && - IsSameCuttingPlane(*lastPlane_, plane) && - lastRevision_ == slice->GetRevision()) - { - // The content of the slice has not changed: Don't update the - // layer content, but possibly update its style - - if (configurator_.get() != NULL && - configurator_->GetRevision() != lastConfiguratorRevision_ && - scene_.HasLayer(layerDepth_)) - { - configurator_->ApplyStyle(scene_.GetLayer(layerDepth_)); - } - } - else - { - // Content has changed: An update is needed - lastPlane_.reset(new CoordinateSystem3D(plane)); - lastRevision_ = slice->GetRevision(); - - std::auto_ptr layer(slice->CreateSceneLayer(configurator_.get(), plane)); - if (layer.get() == NULL) - { - ClearLayer(); - } - else - { - if (configurator_.get() != NULL) - { - lastConfiguratorRevision_ = configurator_->GetRevision(); - configurator_->ApplyStyle(*layer); - } - - scene_.SetLayer(layerDepth_, layer.release()); - } - } - } - }; - - - - - class NativeApplicationContext : public IMessageEmitter { private: - boost::shared_mutex mutex_; - MessageBroker broker_; - IObservable oracleObservable_; + boost::shared_mutex mutex_; + MessageBroker broker_; + IObservable oracleObservable_; public: NativeApplicationContext() :