Mercurial > hg > orthanc-stone
view Samples/Sdl/Loader.cpp @ 808:269522196741
deprecating ISeriesLoader
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Tue, 28 May 2019 17:50:09 +0200 |
parents | f4d64e088838 |
children | abc08ac721d3 |
line wrap: on
line source
/** * 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 <http://www.gnu.org/licenses/>. **/ #include "../../Framework/Toolbox/DicomInstanceParameters.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" // From Orthanc framework #include <Core/DicomFormat/DicomArray.h> #include <Core/Images/Image.h> #include <Core/Images/ImageProcessing.h> #include <Core/Images/PngWriter.h> #include <Core/Endianness.h> #include <Core/Logging.h> #include <Core/OrthancException.h> #include <Core/SystemToolbox.h> #include <Core/Toolbox.h> #include <EmbeddedResources.h> namespace OrthancStone { /** This interface is implemented by objects able to create an ISceneLayer suitable to display the Orthanc image supplied to the CreateTextureXX factory methods (taking Dicom parameters into account if relevant). It can also refresh the style of an existing layer afterwards, to match the configurator settings. */ class ILayerStyleConfigurator { public: virtual ~ILayerStyleConfigurator() { } virtual uint64_t GetRevision() const = 0; virtual TextureBaseSceneLayer* CreateTextureFromImage(const Orthanc::ImageAccessor& image) const = 0; virtual TextureBaseSceneLayer* CreateTextureFromDicom(const Orthanc::ImageAccessor& frame, const DicomInstanceParameters& parameters) const = 0; virtual void ApplyStyle(ISceneLayer& layer) const = 0; }; /** This configurator supplies an API to set a display range and a LUT. */ class LookupTableStyleConfigurator : public ILayerStyleConfigurator { private: uint64_t revision_; bool hasLut_; std::string lut_; bool hasRange_; float minValue_; float maxValue_; public: LookupTableStyleConfigurator() : revision_(0), hasLut_(false), hasRange_(false) { } void SetLookupTable(Orthanc::EmbeddedResources::FileResourceId resource) { hasLut_ = true; Orthanc::EmbeddedResources::GetFileResource(lut_, resource); } void SetLookupTable(const std::string& lut) { hasLut_ = true; lut_ = lut; } void SetRange(float minValue, float maxValue) { if (minValue > maxValue) { throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } else { hasRange_ = true; minValue_ = minValue; maxValue_ = maxValue; } } virtual uint64_t GetRevision() const { return revision_; } virtual TextureBaseSceneLayer* CreateTextureFromImage(const Orthanc::ImageAccessor& image) const { throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); } virtual TextureBaseSceneLayer* CreateTextureFromDicom(const Orthanc::ImageAccessor& frame, const DicomInstanceParameters& parameters) const { return parameters.CreateLookupTableTexture(frame); } virtual void ApplyStyle(ISceneLayer& layer) const { LookupTableTextureSceneLayer& l = dynamic_cast<LookupTableTextureSceneLayer&>(layer); if (hasLut_) { l.SetLookupTable(lut_); } if (hasRange_) { l.SetRange(minValue_, maxValue_); } else { l.FitRange(); } } }; /** Creates layers to display the supplied image in grayscale. No dynamic style is available. */ class GrayscaleStyleConfigurator : public ILayerStyleConfigurator { private: uint64_t revision_; public: GrayscaleStyleConfigurator() : revision_(0) { } virtual uint64_t GetRevision() const { return revision_; } virtual TextureBaseSceneLayer* CreateTextureFromImage(const Orthanc::ImageAccessor& image) const { throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); } virtual TextureBaseSceneLayer* CreateTextureFromDicom(const Orthanc::ImageAccessor& frame, const DicomInstanceParameters& parameters) const { return parameters.CreateTexture(frame); } virtual void ApplyStyle(ISceneLayer& layer) const { } }; /** This interface is implemented by objects representing 3D volume data and that are able to return an object that: - represent a slice of their data - are able to create the corresponding slice visual representation. */ class IVolumeSlicer : public boost::noncopyable { public: /** This interface is implemented by objects representing a slice of volume data and that are able to create a 2D layer to display a this slice. The CreateSceneLayer factory method is called with an optional configurator that possibly impacts the ISceneLayer subclass that is created (for instance, if a LUT must be applied on the texture when displaying it) */ class IExtractedSlice : public boost::noncopyable { public: virtual ~IExtractedSlice() { } /** Invalid slices are created when the data is not ready yet or if the cut is outside of the available geometry. */ virtual bool IsValid() = 0; /** This retrieves the *revision* that gets incremented every time the underlying object undergoes a mutable operation (that it, changes its state). This **must** be a cheap call. */ virtual uint64_t GetRevision() = 0; /** Creates the slice visual representation */ virtual ISceneLayer* CreateSceneLayer( const ILayerStyleConfigurator* configurator, // possibly absent const CoordinateSystem3D& cuttingPlane) = 0; }; /** See IExtractedSlice.IsValid() */ class InvalidSlice : public IExtractedSlice { public: virtual bool IsValid() { return false; } virtual uint64_t GetRevision() { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator, const CoordinateSystem3D& cuttingPlane) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } }; virtual ~IVolumeSlicer() { } /** This method is implemented by the objects representing volumetric data and must returns an IExtractedSlice subclass that contains all the data needed to, later on, create its visual representation through CreateSceneLayer. Subclasses a.o.: - InvalidSlice, - DicomVolumeImageMPRSlicer::Slice, - DicomVolumeImageReslicer::Slice - DicomStructureSetLoader::Slice */ virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) = 0; }; /** This class combines a 3D image buffer, a 3D volume geometry and information about the DICOM parameters of the series. (MPR means MultiPlanar Reconstruction) */ class DicomVolumeImage : public boost::noncopyable { public: ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, GeometryReadyMessage, DicomVolumeImage); ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, ContentUpdatedMessage, DicomVolumeImage); private: uint64_t revision_; std::auto_ptr<VolumeImageGeometry> geometry_; std::auto_ptr<ImageBuffer3D> image_; std::auto_ptr<DicomInstanceParameters> parameters_; void CheckHasGeometry() const { if (!HasGeometry()) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } } public: DicomVolumeImage() : revision_(0) { } void IncrementRevision() { revision_ ++; } void Initialize(const VolumeImageGeometry& geometry, Orthanc::PixelFormat format) { geometry_.reset(new VolumeImageGeometry(geometry)); image_.reset(new ImageBuffer3D(format, geometry_->GetWidth(), geometry_->GetHeight(), geometry_->GetDepth(), false /* don't compute range */)); revision_ ++; } void SetDicomParameters(const DicomInstanceParameters& parameters) { parameters_.reset(parameters.Clone()); revision_ ++; } uint64_t GetRevision() const { return revision_; } bool HasGeometry() const { return (geometry_.get() != NULL && image_.get() != NULL); } ImageBuffer3D& GetPixelData() { CheckHasGeometry(); return *image_; } const ImageBuffer3D& GetPixelData() const { CheckHasGeometry(); return *image_; } const VolumeImageGeometry& GetGeometry() const { CheckHasGeometry(); return *geometry_; } bool HasDicomParameters() const { return parameters_.get() != NULL; } const DicomInstanceParameters& GetDicomParameters() const { if (HasDicomParameters()) { return *parameters_; } else { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } } }; /** Implements the IVolumeSlicer on Dicom volume data when the cutting plane that is supplied to the slicer is either axial, sagittal or coronal. Arbitrary planes are *not* supported */ class DicomVolumeImageMPRSlicer : public IVolumeSlicer { public: class Slice : public IExtractedSlice { private: const DicomVolumeImage& volume_; bool valid_; VolumeProjection projection_; unsigned int sliceIndex_; void CheckValid() const { if (!valid_) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } } protected: // Can be overloaded in subclasses virtual uint64_t GetRevisionInternal(VolumeProjection projection, unsigned int sliceIndex) const { return volume_.GetRevision(); } public: /** Represents a slice of a volume image that is parallel to the coordinate system axis. The constructor initializes the type of projection (axial, sagittal or coronal) and the corresponding slice index, from the cutting plane. */ Slice(const DicomVolumeImage& volume, const CoordinateSystem3D& cuttingPlane) : volume_(volume) { valid_ = (volume_.HasDicomParameters() && volume_.GetGeometry().DetectSlice(projection_, sliceIndex_, cuttingPlane)); } VolumeProjection GetProjection() const { CheckValid(); return projection_; } unsigned int GetSliceIndex() const { CheckValid(); return sliceIndex_; } virtual bool IsValid() { return valid_; } virtual uint64_t GetRevision() { CheckValid(); return GetRevisionInternal(projection_, sliceIndex_); } virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator, const CoordinateSystem3D& cuttingPlane) { CheckValid(); if (configurator == NULL) { throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer, "A style configurator is mandatory for textures"); } std::auto_ptr<TextureBaseSceneLayer> texture; { const DicomInstanceParameters& parameters = volume_.GetDicomParameters(); ImageBuffer3D::SliceReader reader(volume_.GetPixelData(), projection_, sliceIndex_); texture.reset(dynamic_cast<TextureBaseSceneLayer*> (configurator->CreateTextureFromDicom(reader.GetAccessor(), parameters))); } const CoordinateSystem3D& system = volume_.GetGeometry().GetProjectionGeometry(projection_); double x0, y0, x1, y1; cuttingPlane.ProjectPoint(x0, y0, system.GetOrigin()); cuttingPlane.ProjectPoint(x1, y1, system.GetOrigin() + system.GetAxisX()); texture->SetOrigin(x0, y0); double dx = x1 - x0; double dy = y1 - y0; if (!LinearAlgebra::IsCloseToZero(dx) || !LinearAlgebra::IsCloseToZero(dy)) { texture->SetAngle(atan2(dy, dx)); } Vector tmp = volume_.GetGeometry().GetVoxelDimensions(projection_); texture->SetPixelSpacing(tmp[0], tmp[1]); return texture.release(); #if 0 double w = texture->GetTexture().GetWidth() * tmp[0]; double h = texture->GetTexture().GetHeight() * tmp[1]; printf("%.1f %.1f %.1f => %.1f %.1f => %.1f %.1f\n", system.GetOrigin() [0], system.GetOrigin() [1], system.GetOrigin() [2], x0, y0, x0 + w, y0 + h); std::auto_ptr<PolylineSceneLayer> toto(new PolylineSceneLayer); PolylineSceneLayer::Chain c; c.push_back(ScenePoint2D(x0, y0)); c.push_back(ScenePoint2D(x0 + w, y0)); c.push_back(ScenePoint2D(x0 + w, y0 + h)); c.push_back(ScenePoint2D(x0, y0 + h)); toto->AddChain(c, true); return toto.release(); #endif } }; private: boost::shared_ptr<DicomVolumeImage> volume_; public: DicomVolumeImageMPRSlicer(const boost::shared_ptr<DicomVolumeImage>& volume) : volume_(volume) { } virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) ORTHANC_OVERRIDE { if (volume_->HasGeometry()) { return new Slice(*volume_, cuttingPlane); } else { return new IVolumeSlicer::InvalidSlice; } } }; /** This class is used to manage the progressive loading of a volume that is stored in a Dicom series. // TODO - Refactor using LoaderStateMachine? // TODO: */ class OrthancSeriesVolumeProgressiveLoader : public IObserver, public IObservable, public IVolumeSlicer { private: static const unsigned int LOW_QUALITY = 0; static const unsigned int MIDDLE_QUALITY = 1; static const unsigned int BEST_QUALITY = 2; /** Helper class internal to OrthancSeriesVolumeProgressiveLoader */ class SeriesGeometry : public boost::noncopyable { private: void CheckSlice(size_t index, const DicomInstanceParameters& reference) const { const DicomInstanceParameters& slice = *slices_[index]; if (!GeometryToolbox::IsParallel( reference.GetGeometry().GetNormal(), slice.GetGeometry().GetNormal())) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, "A slice in the volume image is not parallel to the others"); } if (reference.GetExpectedPixelFormat() != slice.GetExpectedPixelFormat()) { throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat, "The pixel format changes across the slices of the volume image"); } if (reference.GetImageInformation().GetWidth() != slice.GetImageInformation().GetWidth() || reference.GetImageInformation().GetHeight() != slice.GetImageInformation().GetHeight()) { throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize, "The width/height of slices are not constant in the volume image"); } if (!LinearAlgebra::IsNear(reference.GetPixelSpacingX(), slice.GetPixelSpacingX()) || !LinearAlgebra::IsNear(reference.GetPixelSpacingY(), slice.GetPixelSpacingY())) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, "The pixel spacing of the slices change across the volume image"); } } void CheckVolume() const { for (size_t i = 0; i < slices_.size(); i++) { assert(slices_[i] != NULL); if (slices_[i]->GetImageInformation().GetNumberOfFrames() != 1) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, "This class does not support multi-frame images"); } } if (slices_.size() != 0) { const DicomInstanceParameters& reference = *slices_[0]; for (size_t i = 1; i < slices_.size(); i++) { CheckSlice(i, reference); } } } void Clear() { for (size_t i = 0; i < slices_.size(); i++) { assert(slices_[i] != NULL); delete slices_[i]; } slices_.clear(); slicesRevision_.clear(); } void CheckSliceIndex(size_t index) const { if (!HasGeometry()) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } else if (index >= slices_.size()) { throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } else { assert(slices_.size() == GetImageGeometry().GetDepth() && slices_.size() == slicesRevision_.size()); } } std::auto_ptr<VolumeImageGeometry> geometry_; std::vector<DicomInstanceParameters*> slices_; std::vector<uint64_t> slicesRevision_; public: ~SeriesGeometry() { Clear(); } // WARNING: The payload of "slices" must be of class "DicomInstanceParameters" // (called with the slices created in LoadGeometry) void ComputeGeometry(SlicesSorter& slices) { Clear(); if (!slices.Sort()) { throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, "Cannot sort the 3D slices of a DICOM series"); } if (slices.GetSlicesCount() == 0) { geometry_.reset(new VolumeImageGeometry); } else { slices_.reserve(slices.GetSlicesCount()); slicesRevision_.resize(slices.GetSlicesCount(), 0); for (size_t i = 0; i < slices.GetSlicesCount(); i++) { const DicomInstanceParameters& slice = dynamic_cast<const DicomInstanceParameters&>(slices.GetSlicePayload(i)); slices_.push_back(new DicomInstanceParameters(slice)); } CheckVolume(); const double spacingZ = slices.ComputeSpacingBetweenSlices(); LOG(INFO) << "Computed spacing between slices: " << spacingZ << "mm"; const DicomInstanceParameters& parameters = *slices_[0]; geometry_.reset(new VolumeImageGeometry); geometry_->SetSize(parameters.GetImageInformation().GetWidth(), parameters.GetImageInformation().GetHeight(), static_cast<unsigned int>(slices.GetSlicesCount())); geometry_->SetAxialGeometry(slices.GetSliceGeometry(0)); geometry_->SetVoxelDimensions(parameters.GetPixelSpacingX(), parameters.GetPixelSpacingY(), spacingZ); } } bool HasGeometry() const { return geometry_.get() != NULL; } const VolumeImageGeometry& GetImageGeometry() const { if (!HasGeometry()) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } else { assert(slices_.size() == geometry_->GetDepth()); return *geometry_; } } const DicomInstanceParameters& GetSliceParameters(size_t index) const { CheckSliceIndex(index); return *slices_[index]; } uint64_t GetSliceRevision(size_t index) const { CheckSliceIndex(index); return slicesRevision_[index]; } void IncrementSliceRevision(size_t index) { CheckSliceIndex(index); slicesRevision_[index] ++; } }; class ExtractedSlice : public DicomVolumeImageMPRSlicer::Slice { private: const OrthancSeriesVolumeProgressiveLoader& that_; protected: virtual uint64_t GetRevisionInternal(VolumeProjection projection, unsigned int sliceIndex) const { if (projection == VolumeProjection_Axial) { return that_.seriesGeometry_.GetSliceRevision(sliceIndex); } else { // For coronal and sagittal projections, we take the global // revision of the volume because even if a single slice changes, // this means the projection will yield a different result --> // we must increase the revision as soon as any slice changes return that_.volume_->GetRevision(); } } public: ExtractedSlice(const OrthancSeriesVolumeProgressiveLoader& that, const CoordinateSystem3D& plane) : DicomVolumeImageMPRSlicer::Slice(*that.volume_, plane), that_(that) { if (that_.strategy_.get() != NULL && IsValid() && GetProjection() == VolumeProjection_Axial) { that_.strategy_->SetCurrent(GetSliceIndex()); } } }; static unsigned int GetSliceIndexPayload(const OracleCommandWithPayload& command) { return dynamic_cast< const Orthanc::SingleValueObject<unsigned int>& >(command.GetPayload()).GetValue(); } void ScheduleNextSliceDownload() { assert(strategy_.get() != NULL); unsigned int sliceIndex, quality; if (strategy_->GetNext(sliceIndex, quality)) { assert(quality <= BEST_QUALITY); const DicomInstanceParameters& slice = seriesGeometry_.GetSliceParameters(sliceIndex); const std::string& instance = slice.GetOrthancInstanceIdentifier(); if (instance.empty()) { throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); } std::auto_ptr<OracleCommandWithPayload> command; if (quality == BEST_QUALITY) { std::auto_ptr<GetOrthancImageCommand> tmp(new GetOrthancImageCommand); tmp->SetHttpHeader("Accept-Encoding", "gzip"); tmp->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Pam))); tmp->SetInstanceUri(instance, slice.GetExpectedPixelFormat()); tmp->SetExpectedPixelFormat(slice.GetExpectedPixelFormat()); command.reset(tmp.release()); } else { std::auto_ptr<GetOrthancWebViewerJpegCommand> tmp(new GetOrthancWebViewerJpegCommand); tmp->SetHttpHeader("Accept-Encoding", "gzip"); tmp->SetInstance(instance); tmp->SetQuality((quality == 0 ? 50 : 90)); tmp->SetExpectedPixelFormat(slice.GetExpectedPixelFormat()); command.reset(tmp.release()); } command->SetPayload(new Orthanc::SingleValueObject<unsigned int>(sliceIndex)); oracle_.Schedule(*this, command.release()); } } /** This is called in response to GET "/series/XXXXXXXXXXXXX/instances-tags" */ void LoadGeometry(const OrthancRestApiCommand::SuccessMessage& message) { Json::Value body; message.ParseJsonBody(body); if (body.type() != Json::objectValue) { throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); } { Json::Value::Members instances = body.getMemberNames(); SlicesSorter slices; for (size_t i = 0; i < instances.size(); i++) { Orthanc::DicomMap dicom; dicom.FromDicomAsJson(body[instances[i]]); std::auto_ptr<DicomInstanceParameters> instance(new DicomInstanceParameters(dicom)); instance->SetOrthancInstanceIdentifier(instances[i]); // the 3D plane corresponding to the slice CoordinateSystem3D geometry = instance->GetGeometry(); slices.AddSlice(geometry, instance.release()); } seriesGeometry_.ComputeGeometry(slices); } size_t slicesCount = seriesGeometry_.GetImageGeometry().GetDepth(); if (slicesCount == 0) { volume_->Initialize(seriesGeometry_.GetImageGeometry(), Orthanc::PixelFormat_Grayscale8); } else { const DicomInstanceParameters& parameters = seriesGeometry_.GetSliceParameters(0); volume_->Initialize(seriesGeometry_.GetImageGeometry(), parameters.GetExpectedPixelFormat()); volume_->SetDicomParameters(parameters); volume_->GetPixelData().Clear(); strategy_.reset(new BasicFetchingStrategy(sorter_->CreateSorter(static_cast<unsigned int>(slicesCount)), BEST_QUALITY)); assert(simultaneousDownloads_ != 0); for (unsigned int i = 0; i < simultaneousDownloads_; i++) { ScheduleNextSliceDownload(); } } slicesQuality_.resize(slicesCount, 0); BroadcastMessage(DicomVolumeImage::GeometryReadyMessage(*volume_)); } void SetSliceContent(unsigned int sliceIndex, const Orthanc::ImageAccessor& image, unsigned int quality) { assert(sliceIndex < slicesQuality_.size() && slicesQuality_.size() == volume_->GetPixelData().GetDepth()); if (quality >= slicesQuality_[sliceIndex]) { { ImageBuffer3D::SliceWriter writer(volume_->GetPixelData(), VolumeProjection_Axial, sliceIndex); Orthanc::ImageProcessing::Copy(writer.GetAccessor(), image); } volume_->IncrementRevision(); seriesGeometry_.IncrementSliceRevision(sliceIndex); slicesQuality_[sliceIndex] = quality; BroadcastMessage(DicomVolumeImage::ContentUpdatedMessage(*volume_)); } ScheduleNextSliceDownload(); } void LoadBestQualitySliceContent(const GetOrthancImageCommand::SuccessMessage& message) { SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), message.GetImage(), BEST_QUALITY); } void LoadJpegSliceContent(const GetOrthancWebViewerJpegCommand::SuccessMessage& message) { unsigned int quality; switch (message.GetOrigin().GetQuality()) { case 50: quality = LOW_QUALITY; break; case 90: quality = MIDDLE_QUALITY; break; default: throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); } SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), message.GetImage(), quality); } IOracle& oracle_; bool active_; unsigned int simultaneousDownloads_; SeriesGeometry seriesGeometry_; boost::shared_ptr<DicomVolumeImage> volume_; std::auto_ptr<IFetchingItemsSorter::IFactory> sorter_; std::auto_ptr<IFetchingStrategy> strategy_; std::vector<unsigned int> slicesQuality_; public: OrthancSeriesVolumeProgressiveLoader(const boost::shared_ptr<DicomVolumeImage>& volume, IOracle& oracle, IObservable& oracleObservable) : IObserver(oracleObservable.GetBroker()), IObservable(oracleObservable.GetBroker()), oracle_(oracle), active_(false), simultaneousDownloads_(4), volume_(volume), sorter_(new BasicFetchingItemsSorter::Factory) { oracleObservable.RegisterObserverCallback( new Callable<OrthancSeriesVolumeProgressiveLoader, OrthancRestApiCommand::SuccessMessage> (*this, &OrthancSeriesVolumeProgressiveLoader::LoadGeometry)); oracleObservable.RegisterObserverCallback( new Callable<OrthancSeriesVolumeProgressiveLoader, GetOrthancImageCommand::SuccessMessage> (*this, &OrthancSeriesVolumeProgressiveLoader::LoadBestQualitySliceContent)); oracleObservable.RegisterObserverCallback( new Callable<OrthancSeriesVolumeProgressiveLoader, GetOrthancWebViewerJpegCommand::SuccessMessage> (*this, &OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent)); } 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; } } void LoadSeries(const std::string& seriesId) { if (active_) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } else { active_ = true; std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand); command->SetUri("/series/" + seriesId + "/instances-tags"); oracle_.Schedule(*this, command.release()); } } /** When a slice is requested, the strategy algorithm (that defines the sequence of resources to be loaded from the server) is modified to take into account this request (this is done in the ExtractedSlice ctor) */ virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) { if (volume_->HasGeometry()) { return new ExtractedSlice(*this, cuttingPlane); } else { return new IVolumeSlicer::InvalidSlice; } } }; /** 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 <typename T> T& GetLoader() const { return dynamic_cast<T&>(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<OracleCommandWithPayload> 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 <typename T> void HandleSuccessMessage(const T& message) { assert(activeCommands_ > 0); activeCommands_--; try { dynamic_cast<State&>(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<IOracleCommand*> 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<LoaderStateMachine, OrthancRestApiCommand::SuccessMessage> (*this, &LoaderStateMachine::HandleSuccessMessage)); oracleObservable.RegisterObserverCallback( new Callable<LoaderStateMachine, GetOrthancImageCommand::SuccessMessage> (*this, &LoaderStateMachine::HandleSuccessMessage)); oracleObservable.RegisterObserverCallback( new Callable<LoaderStateMachine, GetOrthancWebViewerJpegCommand::SuccessMessage> (*this, &LoaderStateMachine::HandleSuccessMessage)); oracleObservable.RegisterObserverCallback( new Callable<LoaderStateMachine, OracleCommandExceptionMessage> (*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<Orthanc::DicomMap> 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<OrthancMultiframeVolumeLoader>().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<OrthancMultiframeVolumeLoader>(); Json::Value body; message.ParseJsonBody(body); if (body.type() != Json::objectValue) { throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); } std::auto_ptr<Orthanc::DicomMap> 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<OrthancRestApiCommand> 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<OrthancMultiframeVolumeLoader>().SetTransferSyntax(message.GetAnswer()); } }; class LoadUncompressedPixelData : public State { public: LoadUncompressedPixelData(OrthancMultiframeVolumeLoader& that) : State(that) { } virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) { GetLoader<OrthancMultiframeVolumeLoader>().SetUncompressedPixelData(message.GetAnswer()); } }; boost::shared_ptr<DicomVolumeImage> 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<OrthancRestApiCommand> 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<const uint32_t*>(source)); } template <typename T> 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<const uint8_t*>(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<T*>(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<uint32_t>(pixelData); break; default: throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); } volume_->IncrementRevision(); BroadcastMessage(DicomVolumeImage::ContentUpdatedMessage(*volume_)); } public: OrthancMultiframeVolumeLoader(const boost::shared_ptr<DicomVolumeImage>& 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<OrthancRestApiCommand> command(new OrthancRestApiCommand); command->SetHttpHeader("Accept-Encoding", "gzip"); command->SetUri("/instances/" + instanceId + "/tags"); command->SetPayload(new LoadGeometry(*this)); Schedule(command.release()); } { std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand); command->SetUri("/instances/" + instanceId + "/metadata/TransferSyntax"); command->SetPayload(new LoadTransferSyntax(*this)); Schedule(command.release()); } } }; 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<TextureBaseSceneLayer> 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<DicomVolumeImage> volume_; VolumeReslicer reslicer_; public: DicomVolumeImageReslicer(const boost::shared_ptr<DicomVolumeImage>& 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<DicomStructureSetLoader>(); 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<DicomStructureSetLoader>(); 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<OrthancRestApiCommand> 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<DicomStructureSetLoader>(); { OrthancPlugins::FullOrthancDataset dicom(message.GetAnswer()); loader.content_.reset(new DicomStructureSet(dicom)); } std::set<std::string> instances; loader.content_->GetReferencedInstances(instances); loader.countReferencedInstances_ = instances.size(); for (std::set<std::string>::const_iterator it = instances.begin(); it != instances.end(); ++it) { std::auto_ptr<OrthancRestApiCommand> 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<DicomStructureSet> 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<PolylineSceneLayer> 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<DicomStructureSet::PolygonPoint> > 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<OrthancRestApiCommand> 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<IVolumeSlicer> slicer_; std::auto_ptr<ILayerStyleConfigurator> configurator_; std::auto_ptr<CoordinateSystem3D> 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<IVolumeSlicer>& 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<IVolumeSlicer::IExtractedSlice> 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<ISceneLayer> 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_; public: NativeApplicationContext() : oracleObservable_(broker_) { } virtual void EmitMessage(const IObserver& observer, const IMessage& message) { try { boost::unique_lock<boost::shared_mutex> lock(mutex_); oracleObservable_.EmitMessage(observer, message); } catch (Orthanc::OrthancException& e) { LOG(ERROR) << "Exception while emitting a message: " << e.What(); } } class ReaderLock : public boost::noncopyable { private: NativeApplicationContext& that_; boost::shared_lock<boost::shared_mutex> lock_; public: ReaderLock(NativeApplicationContext& that) : that_(that), lock_(that.mutex_) { } }; class WriterLock : public boost::noncopyable { private: NativeApplicationContext& that_; boost::unique_lock<boost::shared_mutex> lock_; public: WriterLock(NativeApplicationContext& that) : that_(that), lock_(that.mutex_) { } MessageBroker& GetBroker() { return that_.broker_; } IObservable& GetOracleObservable() { return that_.oracleObservable_; } }; }; } class Toto : public OrthancStone::IObserver { private: OrthancStone::CoordinateSystem3D plane_; OrthancStone::IOracle& oracle_; OrthancStone::Scene2D scene_; std::auto_ptr<OrthancStone::VolumeSceneLayerSource> source1_, source2_, source3_; void Refresh() { if (source1_.get() != NULL) { source1_->Update(plane_); } if (source2_.get() != NULL) { source2_->Update(plane_); } if (source3_.get() != NULL) { source3_->Update(plane_); } scene_.FitContent(1024, 768); { OrthancStone::CairoCompositor compositor(scene_, 1024, 768); compositor.Refresh(); Orthanc::ImageAccessor accessor; compositor.GetCanvas().GetReadOnlyAccessor(accessor); Orthanc::Image tmp(Orthanc::PixelFormat_RGB24, accessor.GetWidth(), accessor.GetHeight(), false); Orthanc::ImageProcessing::Convert(tmp, accessor); static unsigned int count = 0; char buf[64]; sprintf(buf, "scene-%06d.png", count++); Orthanc::PngWriter writer; writer.WriteToFile(buf, tmp); } } void Handle(const OrthancStone::DicomVolumeImage::GeometryReadyMessage& message) { printf("Geometry ready\n"); //plane_ = message.GetOrigin().GetGeometry().GetSagittalGeometry(); //plane_ = message.GetOrigin().GetGeometry().GetAxialGeometry(); plane_ = message.GetOrigin().GetGeometry().GetCoronalGeometry(); plane_.SetOrigin(message.GetOrigin().GetGeometry().GetCoordinates(0.5f, 0.5f, 0.5f)); Refresh(); } void Handle(const OrthancStone::SleepOracleCommand::TimeoutMessage& message) { if (message.GetOrigin().HasPayload()) { printf("TIMEOUT! %d\n", dynamic_cast<const Orthanc::SingleValueObject<unsigned int>& >(message.GetOrigin().GetPayload()).GetValue()); } else { printf("TIMEOUT\n"); Refresh(); /** * The sleep() leads to a crash if the oracle is still running, * while this object is destroyed. Always stop the oracle before * destroying active objects. (*) **/ // boost::this_thread::sleep(boost::posix_time::seconds(2)); oracle_.Schedule(*this, new OrthancStone::SleepOracleCommand(message.GetOrigin().GetDelay())); } } void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) { Json::Value v; message.ParseJsonBody(v); printf("ICI [%s]\n", v.toStyledString().c_str()); } void Handle(const OrthancStone::GetOrthancImageCommand::SuccessMessage& message) { printf("IMAGE %dx%d\n", message.GetImage().GetWidth(), message.GetImage().GetHeight()); } void Handle(const OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage& message) { printf("WebViewer %dx%d\n", message.GetImage().GetWidth(), message.GetImage().GetHeight()); } void Handle(const OrthancStone::OracleCommandExceptionMessage& message) { printf("EXCEPTION: [%s] on command type %d\n", message.GetException().What(), message.GetCommand().GetType()); switch (message.GetCommand().GetType()) { case OrthancStone::IOracleCommand::Type_GetOrthancWebViewerJpeg: printf("URI: [%s]\n", dynamic_cast<const OrthancStone::GetOrthancWebViewerJpegCommand&> (message.GetCommand()).GetUri().c_str()); break; default: break; } } public: Toto(OrthancStone::IOracle& oracle, OrthancStone::IObservable& oracleObservable) : IObserver(oracleObservable.GetBroker()), oracle_(oracle) { oracleObservable.RegisterObserverCallback (new OrthancStone::Callable <Toto, OrthancStone::SleepOracleCommand::TimeoutMessage>(*this, &Toto::Handle)); oracleObservable.RegisterObserverCallback (new OrthancStone::Callable <Toto, OrthancStone::OrthancRestApiCommand::SuccessMessage>(*this, &Toto::Handle)); oracleObservable.RegisterObserverCallback (new OrthancStone::Callable <Toto, OrthancStone::GetOrthancImageCommand::SuccessMessage>(*this, &Toto::Handle)); oracleObservable.RegisterObserverCallback (new OrthancStone::Callable <Toto, OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage>(*this, &Toto::Handle)); oracleObservable.RegisterObserverCallback (new OrthancStone::Callable <Toto, OrthancStone::OracleCommandExceptionMessage>(*this, &Toto::Handle)); } void SetReferenceLoader(OrthancStone::IObservable& loader) { loader.RegisterObserverCallback (new OrthancStone::Callable <Toto, OrthancStone::DicomVolumeImage::GeometryReadyMessage>(*this, &Toto::Handle)); } void SetVolume1(int depth, const boost::shared_ptr<OrthancStone::IVolumeSlicer>& volume, OrthancStone::ILayerStyleConfigurator* style) { source1_.reset(new OrthancStone::VolumeSceneLayerSource(scene_, depth, volume)); if (style != NULL) { source1_->SetConfigurator(style); } } void SetVolume2(int depth, const boost::shared_ptr<OrthancStone::IVolumeSlicer>& volume, OrthancStone::ILayerStyleConfigurator* style) { source2_.reset(new OrthancStone::VolumeSceneLayerSource(scene_, depth, volume)); if (style != NULL) { source2_->SetConfigurator(style); } } void SetStructureSet(int depth, const boost::shared_ptr<OrthancStone::DicomStructureSetLoader>& volume) { source3_.reset(new OrthancStone::VolumeSceneLayerSource(scene_, depth, volume)); } }; void Run(OrthancStone::NativeApplicationContext& context, OrthancStone::ThreadedOracle& oracle) { boost::shared_ptr<OrthancStone::DicomVolumeImage> ct(new OrthancStone::DicomVolumeImage); boost::shared_ptr<OrthancStone::DicomVolumeImage> dose(new OrthancStone::DicomVolumeImage); boost::shared_ptr<Toto> toto; boost::shared_ptr<OrthancStone::OrthancSeriesVolumeProgressiveLoader> ctLoader; boost::shared_ptr<OrthancStone::OrthancMultiframeVolumeLoader> doseLoader; boost::shared_ptr<OrthancStone::DicomStructureSetLoader> rtstructLoader; { OrthancStone::NativeApplicationContext::WriterLock lock(context); toto.reset(new Toto(oracle, lock.GetOracleObservable())); ctLoader.reset(new OrthancStone::OrthancSeriesVolumeProgressiveLoader(ct, oracle, lock.GetOracleObservable())); doseLoader.reset(new OrthancStone::OrthancMultiframeVolumeLoader(dose, oracle, lock.GetOracleObservable())); rtstructLoader.reset(new OrthancStone::DicomStructureSetLoader(oracle, lock.GetOracleObservable())); } //toto->SetReferenceLoader(*ctLoader); toto->SetReferenceLoader(*doseLoader); #if 1 toto->SetVolume1(0, ctLoader, new OrthancStone::GrayscaleStyleConfigurator); #else { boost::shared_ptr<OrthancStone::IVolumeSlicer> reslicer(new OrthancStone::DicomVolumeImageReslicer(ct)); toto->SetVolume1(0, reslicer, new OrthancStone::GrayscaleStyleConfigurator); } #endif { std::auto_ptr<OrthancStone::LookupTableStyleConfigurator> config(new OrthancStone::LookupTableStyleConfigurator); config->SetLookupTable(Orthanc::EmbeddedResources::COLORMAP_HOT); boost::shared_ptr<OrthancStone::DicomVolumeImageMPRSlicer> tmp(new OrthancStone::DicomVolumeImageMPRSlicer(dose)); toto->SetVolume2(1, tmp, config.release()); } toto->SetStructureSet(2, rtstructLoader); oracle.Schedule(*toto, new OrthancStone::SleepOracleCommand(100)); if (0) { Json::Value v = Json::objectValue; v["Level"] = "Series"; v["Query"] = Json::objectValue; std::auto_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand); command->SetMethod(Orthanc::HttpMethod_Post); command->SetUri("/tools/find"); command->SetBody(v); oracle.Schedule(*toto, command.release()); } if (0) { std::auto_ptr<OrthancStone::GetOrthancImageCommand> command(new OrthancStone::GetOrthancImageCommand); command->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Jpeg))); command->SetUri("/instances/6687cc73-07cae193-52ff29c8-f646cb16-0753ed92/preview"); oracle.Schedule(*toto, command.release()); } if (0) { std::auto_ptr<OrthancStone::GetOrthancImageCommand> command(new OrthancStone::GetOrthancImageCommand); command->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Png))); command->SetUri("/instances/6687cc73-07cae193-52ff29c8-f646cb16-0753ed92/preview"); oracle.Schedule(*toto, command.release()); } if (0) { std::auto_ptr<OrthancStone::GetOrthancImageCommand> command(new OrthancStone::GetOrthancImageCommand); command->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Png))); command->SetUri("/instances/6687cc73-07cae193-52ff29c8-f646cb16-0753ed92/image-uint16"); oracle.Schedule(*toto, command.release()); } if (0) { std::auto_ptr<OrthancStone::GetOrthancImageCommand> command(new OrthancStone::GetOrthancImageCommand); command->SetHttpHeader("Accept-Encoding", "gzip"); command->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Pam))); command->SetUri("/instances/6687cc73-07cae193-52ff29c8-f646cb16-0753ed92/image-uint16"); oracle.Schedule(*toto, command.release()); } if (0) { std::auto_ptr<OrthancStone::GetOrthancImageCommand> command(new OrthancStone::GetOrthancImageCommand); command->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Pam))); command->SetUri("/instances/6687cc73-07cae193-52ff29c8-f646cb16-0753ed92/image-uint16"); oracle.Schedule(*toto, command.release()); } if (0) { std::auto_ptr<OrthancStone::GetOrthancWebViewerJpegCommand> command(new OrthancStone::GetOrthancWebViewerJpegCommand); command->SetHttpHeader("Accept-Encoding", "gzip"); command->SetInstance("e6c7c20b-c9f65d7e-0d76f2e2-830186f2-3e3c600e"); command->SetQuality(90); oracle.Schedule(*toto, command.release()); } if (0) { for (unsigned int i = 0; i < 10; i++) { std::auto_ptr<OrthancStone::SleepOracleCommand> command(new OrthancStone::SleepOracleCommand(i * 1000)); command->SetPayload(new Orthanc::SingleValueObject<unsigned int>(42 * i)); oracle.Schedule(*toto, command.release()); } } // 2017-11-17-Anonymized #if 0 // BGO data ctLoader->LoadSeries("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa"); // CT doseLoader->LoadInstance("830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb"); // RT-DOSE //rtstructLoader->LoadInstance("54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9"); // RT-STRUCT #else //ctLoader->LoadSeries("cb3ea4d1-d08f3856-ad7b6314-74d88d77-60b05618"); // CT //doseLoader->LoadInstance("41029085-71718346-811efac4-420e2c15-d39f99b6"); // RT-DOSE //rtstructLoader->LoadInstance("83d9c0c3-913a7fee-610097d7-cbf0522d-fd75bee6"); // RT-STRUCT // 2017-05-16 ctLoader->LoadSeries("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa"); // CT doseLoader->LoadInstance("eac822ef-a395f94e-e8121fe0-8411fef8-1f7bffad"); // RT-DOSE rtstructLoader->LoadInstance("54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9"); // RT-STRUCT #endif // 2015-01-28-Multiframe //doseLoader->LoadInstance("88f71e2a-5fad1c61-96ed14d6-5b3d3cf7-a5825279"); // Multiframe CT // Delphine //ctLoader->LoadSeries("5990e39c-51e5f201-fe87a54c-31a55943-e59ef80e"); // CT //ctLoader->LoadSeries("67f1b334-02c16752-45026e40-a5b60b6b-030ecab5"); // Lung 1/10mm { LOG(WARNING) << "...Waiting for Ctrl-C..."; oracle.Start(); Orthanc::SystemToolbox::ServerBarrier(); /** * WARNING => The oracle must be stopped BEFORE the objects using * it are destroyed!!! This forces to wait for the completion of * the running callback methods. Otherwise, the callbacks methods * might still be running while their parent object is destroyed, * resulting in crashes. This is very visible if adding a sleep(), * as in (*). **/ oracle.Stop(); } } /** * IMPORTANT: The full arguments to "main()" are needed for SDL on * Windows. Otherwise, one gets the linking error "undefined reference * to `SDL_main'". https://wiki.libsdl.org/FAQWindows **/ int main(int argc, char* argv[]) { OrthancStone::StoneInitialize(); //Orthanc::Logging::EnableInfoLevel(true); try { OrthancStone::NativeApplicationContext context; OrthancStone::ThreadedOracle oracle(context); //oracle.SetThreadsCount(1); { Orthanc::WebServiceParameters p; //p.SetUrl("http://localhost:8043/"); p.SetCredentials("orthanc", "orthanc"); oracle.SetOrthancParameters(p); } //oracle.Start(); Run(context, oracle); //oracle.Stop(); } catch (Orthanc::OrthancException& e) { LOG(ERROR) << "EXCEPTION: " << e.What(); } OrthancStone::StoneFinalize(); return 0; }