# HG changeset patch # User Sebastien Jodogne # Date 1559070999 -7200 # Node ID aead999345e0752382ce097e29af97fcacd76ddf # Parent bc7ee59420a1aba29c21f44f995eb80d13303235 reorganization diff -r bc7ee59420a1 -r aead999345e0 Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp Tue May 28 21:16:39 2019 +0200 @@ -0,0 +1,483 @@ +/** + * 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 "OrthancSeriesVolumeProgressiveLoader.h" + +#include "../Toolbox/GeometryToolbox.h" +#include "../Volumes/DicomVolumeImageMPRSlicer.h" +#include "BasicFetchingItemsSorter.h" +#include "BasicFetchingStrategy.h" + +#include +#include + +namespace OrthancStone +{ + class OrthancSeriesVolumeProgressiveLoader::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()); + } + } + }; + + + + void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::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 OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckVolume() const + { + for (size_t i = 0; i < slices_.size(); i++) + { + assert(slices_[i] != NULL); + if (slices_[i]->GetImageInformation().GetNumberOfFrames() != 1) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, + "This class does not support multi-frame images"); + } + } + + if (slices_.size() != 0) + { + const DicomInstanceParameters& reference = *slices_[0]; + + for (size_t i = 1; i < slices_.size(); i++) + { + CheckSlice(i, reference); + } + } + } + + + void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::Clear() + { + for (size_t i = 0; i < slices_.size(); i++) + { + assert(slices_[i] != NULL); + delete slices_[i]; + } + + slices_.clear(); + slicesRevision_.clear(); + } + + + void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckSliceIndex(size_t index) const + { + if (!HasGeometry()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else if (index >= slices_.size()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + else + { + assert(slices_.size() == GetImageGeometry().GetDepth() && + slices_.size() == slicesRevision_.size()); + } + } + + + // WARNING: The payload of "slices" must be of class "DicomInstanceParameters" + // (called with the slices created in LoadGeometry) + void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::ComputeGeometry(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(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(slices.GetSlicesCount())); + geometry_->SetAxialGeometry(slices.GetSliceGeometry(0)); + geometry_->SetVoxelDimensions(parameters.GetPixelSpacingX(), + parameters.GetPixelSpacingY(), spacingZ); + } + } + + + const VolumeImageGeometry& OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetImageGeometry() const + { + if (!HasGeometry()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + assert(slices_.size() == geometry_->GetDepth()); + return *geometry_; + } + } + + + const DicomInstanceParameters& OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetSliceParameters(size_t index) const + { + CheckSliceIndex(index); + return *slices_[index]; + } + + + uint64_t OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetSliceRevision(size_t index) const + { + CheckSliceIndex(index); + return slicesRevision_[index]; + } + + + void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::IncrementSliceRevision(size_t index) + { + CheckSliceIndex(index); + slicesRevision_[index] ++; + } + + + static unsigned int GetSliceIndexPayload(const OracleCommandWithPayload& command) + { + return dynamic_cast< const Orthanc::SingleValueObject& >(command.GetPayload()).GetValue(); + } + + + void OrthancSeriesVolumeProgressiveLoader::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 command; + + if (quality == BEST_QUALITY) + { + std::auto_ptr 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 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(sliceIndex)); + oracle_.Schedule(*this, command.release()); + } + } + +/** + This is called in response to GET "/series/XXXXXXXXXXXXX/instances-tags" +*/ + void OrthancSeriesVolumeProgressiveLoader::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 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(slicesCount)), BEST_QUALITY)); + + assert(simultaneousDownloads_ != 0); + for (unsigned int i = 0; i < simultaneousDownloads_; i++) + { + ScheduleNextSliceDownload(); + } + } + + slicesQuality_.resize(slicesCount, 0); + + BroadcastMessage(DicomVolumeImage::GeometryReadyMessage(*volume_)); + } + + + void OrthancSeriesVolumeProgressiveLoader::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 OrthancSeriesVolumeProgressiveLoader::LoadBestQualitySliceContent(const GetOrthancImageCommand::SuccessMessage& message) + { + SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), message.GetImage(), BEST_QUALITY); + } + + + void OrthancSeriesVolumeProgressiveLoader::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); + } + + + OrthancSeriesVolumeProgressiveLoader::OrthancSeriesVolumeProgressiveLoader(const boost::shared_ptr& 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 + (*this, &OrthancSeriesVolumeProgressiveLoader::LoadGeometry)); + + oracleObservable.RegisterObserverCallback( + new Callable + (*this, &OrthancSeriesVolumeProgressiveLoader::LoadBestQualitySliceContent)); + + oracleObservable.RegisterObserverCallback( + new Callable + (*this, &OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent)); + } + + + void OrthancSeriesVolumeProgressiveLoader::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 OrthancSeriesVolumeProgressiveLoader::LoadSeries(const std::string& seriesId) + { + if (active_) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + active_ = true; + + std::auto_ptr command(new OrthancRestApiCommand); + command->SetUri("/series/" + seriesId + "/instances-tags"); + + oracle_.Schedule(*this, command.release()); + } + } + + + IVolumeSlicer::IExtractedSlice* + OrthancSeriesVolumeProgressiveLoader::ExtractSlice(const CoordinateSystem3D& cuttingPlane) + { + if (volume_->HasGeometry()) + { + return new ExtractedSlice(*this, cuttingPlane); + } + else + { + return new IVolumeSlicer::InvalidSlice; + } + } +} diff -r bc7ee59420a1 -r aead999345e0 Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h Tue May 28 21:16:39 2019 +0200 @@ -0,0 +1,134 @@ +/** + * 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/OrthancRestApiCommand.h" +#include "../Toolbox/SlicesSorter.h" +#include "../Volumes/DicomVolumeImage.h" +#include "../Volumes/IVolumeSlicer.h" +#include "IFetchingItemsSorter.h" +#include "IFetchingStrategy.h" + +#include + +namespace OrthancStone +{ + /** + This class is used to manage the progressive loading of a volume that + is stored in a Dicom series. + */ + class OrthancSeriesVolumeProgressiveLoader : + public 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; + + class ExtractedSlice; + + /** Helper class internal to OrthancSeriesVolumeProgressiveLoader */ + class SeriesGeometry : public boost::noncopyable + { + private: + void CheckSlice(size_t index, + const DicomInstanceParameters& reference) const; + + void CheckVolume() const; + + void Clear(); + + void CheckSliceIndex(size_t index) const; + + std::auto_ptr geometry_; + std::vector slices_; + std::vector slicesRevision_; + + public: + ~SeriesGeometry() + { + Clear(); + } + + void ComputeGeometry(SlicesSorter& slices); + + bool HasGeometry() const + { + return geometry_.get() != NULL; + } + + const VolumeImageGeometry& GetImageGeometry() const; + + const DicomInstanceParameters& GetSliceParameters(size_t index) const; + + uint64_t GetSliceRevision(size_t index) const; + + void IncrementSliceRevision(size_t index); + }; + + + void ScheduleNextSliceDownload(); + + void LoadGeometry(const OrthancRestApiCommand::SuccessMessage& message); + + void SetSliceContent(unsigned int sliceIndex, + const Orthanc::ImageAccessor& image, + unsigned int quality); + + void LoadBestQualitySliceContent(const GetOrthancImageCommand::SuccessMessage& message); + + void LoadJpegSliceContent(const GetOrthancWebViewerJpegCommand::SuccessMessage& message); + + IOracle& oracle_; + bool active_; + unsigned int simultaneousDownloads_; + SeriesGeometry seriesGeometry_; + boost::shared_ptr volume_; + std::auto_ptr sorter_; + std::auto_ptr strategy_; + std::vector slicesQuality_; + + + public: + OrthancSeriesVolumeProgressiveLoader(const boost::shared_ptr& volume, + IOracle& oracle, + IObservable& oracleObservable); + + void SetSimultaneousDownloads(unsigned int count); + + void LoadSeries(const std::string& seriesId); + + /** + 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); + }; +} diff -r bc7ee59420a1 -r aead999345e0 Framework/Scene2D/GrayscaleStyleConfigurator.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Scene2D/GrayscaleStyleConfigurator.cpp Tue May 28 21:16:39 2019 +0200 @@ -0,0 +1,32 @@ +/** + * 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 "GrayscaleStyleConfigurator.h" + +#include + +namespace OrthancStone +{ + TextureBaseSceneLayer* GrayscaleStyleConfigurator::CreateTextureFromImage(const Orthanc::ImageAccessor& image) const + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } +} diff -r bc7ee59420a1 -r aead999345e0 Framework/Scene2D/GrayscaleStyleConfigurator.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Scene2D/GrayscaleStyleConfigurator.h Tue May 28 21:16:39 2019 +0200 @@ -0,0 +1,62 @@ +/** + * 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 "ILayerStyleConfigurator.h" + +namespace OrthancStone +{ + /** + Creates layers to display the supplied image in grayscale. No dynamic + style is available. + */ + class GrayscaleStyleConfigurator : public ILayerStyleConfigurator + { + private: + uint64_t revision_; + + // TODO - Add windowing + + public: + GrayscaleStyleConfigurator() : + revision_(0) + { + } + + virtual uint64_t GetRevision() const + { + return revision_; + } + + virtual TextureBaseSceneLayer* CreateTextureFromImage(const Orthanc::ImageAccessor& image) const; + + virtual TextureBaseSceneLayer* CreateTextureFromDicom(const Orthanc::ImageAccessor& frame, + const DicomInstanceParameters& parameters) const + { + return parameters.CreateTexture(frame); + } + + virtual void ApplyStyle(ISceneLayer& layer) const + { + } + }; +} diff -r bc7ee59420a1 -r aead999345e0 Framework/Scene2D/ILayerStyleConfigurator.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Scene2D/ILayerStyleConfigurator.h Tue May 28 21:16:39 2019 +0200 @@ -0,0 +1,52 @@ +/** + * 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/DicomInstanceParameters.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; + }; +} diff -r bc7ee59420a1 -r aead999345e0 Framework/Scene2D/LookupTableStyleConfigurator.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Scene2D/LookupTableStyleConfigurator.cpp Tue May 28 21:16:39 2019 +0200 @@ -0,0 +1,90 @@ +/** + * 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 "LookupTableStyleConfigurator.h" + +#include + +namespace OrthancStone +{ + LookupTableStyleConfigurator::LookupTableStyleConfigurator() : + revision_(0), + hasLut_(false), + hasRange_(false) + { + } + + + void LookupTableStyleConfigurator::SetLookupTable(Orthanc::EmbeddedResources::FileResourceId resource) + { + hasLut_ = true; + Orthanc::EmbeddedResources::GetFileResource(lut_, resource); + } + + + void LookupTableStyleConfigurator::SetLookupTable(const std::string& lut) + { + hasLut_ = true; + lut_ = lut; + } + + + void LookupTableStyleConfigurator::SetRange(float minValue, + float maxValue) + { + if (minValue > maxValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + else + { + hasRange_ = true; + minValue_ = minValue; + maxValue_ = maxValue; + } + } + + + TextureBaseSceneLayer* LookupTableStyleConfigurator::CreateTextureFromImage(const Orthanc::ImageAccessor& image) const + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + + void LookupTableStyleConfigurator::ApplyStyle(ISceneLayer& layer) const + { + LookupTableTextureSceneLayer& l = dynamic_cast(layer); + + if (hasLut_) + { + l.SetLookupTable(lut_); + } + + if (hasRange_) + { + l.SetRange(minValue_, maxValue_); + } + else + { + l.FitRange(); + } + } +} diff -r bc7ee59420a1 -r aead999345e0 Framework/Scene2D/LookupTableStyleConfigurator.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Scene2D/LookupTableStyleConfigurator.h Tue May 28 21:16:39 2019 +0200 @@ -0,0 +1,68 @@ +/** + * 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 "ILayerStyleConfigurator.h" + +#include + +namespace OrthancStone +{ + /** + 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(); + + void SetLookupTable(Orthanc::EmbeddedResources::FileResourceId resource); + + void SetLookupTable(const std::string& lut); + + void SetRange(float minValue, + float maxValue); + + virtual uint64_t GetRevision() const + { + return revision_; + } + + virtual TextureBaseSceneLayer* CreateTextureFromImage(const Orthanc::ImageAccessor& image) const; + + virtual TextureBaseSceneLayer* CreateTextureFromDicom(const Orthanc::ImageAccessor& frame, + const DicomInstanceParameters& parameters) const + { + return parameters.CreateLookupTableTexture(frame); + } + + virtual void ApplyStyle(ISceneLayer& layer) const; + }; +} diff -r bc7ee59420a1 -r aead999345e0 Framework/Volumes/DicomVolumeImage.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Volumes/DicomVolumeImage.cpp Tue May 28 21:16:39 2019 +0200 @@ -0,0 +1,94 @@ +/** + * 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 "DicomVolumeImage.h" + +#include + +namespace OrthancStone +{ + void DicomVolumeImage::CheckHasGeometry() const + { + if (!HasGeometry()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + void DicomVolumeImage::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 DicomVolumeImage::SetDicomParameters(const DicomInstanceParameters& parameters) + { + parameters_.reset(parameters.Clone()); + revision_ ++; + } + + + bool DicomVolumeImage::HasGeometry() const + { + return (geometry_.get() != NULL && + image_.get() != NULL); + } + + + ImageBuffer3D& DicomVolumeImage::GetPixelData() + { + CheckHasGeometry(); + return *image_; + } + + + const ImageBuffer3D& DicomVolumeImage::GetPixelData() const + { + CheckHasGeometry(); + return *image_; + } + + + const VolumeImageGeometry& DicomVolumeImage::GetGeometry() const + { + CheckHasGeometry(); + return *geometry_; + } + + + const DicomInstanceParameters& DicomVolumeImage::GetDicomParameters() const + { + if (HasDicomParameters()) + { + return *parameters_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } +} diff -r bc7ee59420a1 -r aead999345e0 Framework/Volumes/DicomVolumeImage.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Volumes/DicomVolumeImage.h Tue May 28 21:16:39 2019 +0200 @@ -0,0 +1,86 @@ +/** + * 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/IMessage.h" +#include "../Toolbox/DicomInstanceParameters.h" +#include "ImageBuffer3D.h" +#include "VolumeImageGeometry.h" + +namespace OrthancStone +{ + /** + 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 geometry_; + std::auto_ptr image_; + std::auto_ptr parameters_; + + void CheckHasGeometry() const; + + public: + DicomVolumeImage() : + revision_(0) + { + } + + void IncrementRevision() + { + revision_ ++; + } + + void Initialize(const VolumeImageGeometry& geometry, + Orthanc::PixelFormat format); + + void SetDicomParameters(const DicomInstanceParameters& parameters); + + uint64_t GetRevision() const + { + return revision_; + } + + bool HasGeometry() const; + + ImageBuffer3D& GetPixelData(); + + const ImageBuffer3D& GetPixelData() const; + + const VolumeImageGeometry& GetGeometry() const; + + bool HasDicomParameters() const + { + return parameters_.get() != NULL; + } + + const DicomInstanceParameters& GetDicomParameters() const; + }; +} diff -r bc7ee59420a1 -r aead999345e0 Framework/Volumes/DicomVolumeImageMPRSlicer.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Volumes/DicomVolumeImageMPRSlicer.cpp Tue May 28 21:16:39 2019 +0200 @@ -0,0 +1,120 @@ +/** + * 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 "DicomVolumeImageMPRSlicer.h" + +#include + +namespace OrthancStone +{ + void DicomVolumeImageMPRSlicer::Slice::CheckValid() const + { + if (!valid_) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + DicomVolumeImageMPRSlicer::Slice::Slice(const DicomVolumeImage& volume, + const CoordinateSystem3D& cuttingPlane) : + volume_(volume) + { + valid_ = (volume_.HasDicomParameters() && + volume_.GetGeometry().DetectSlice(projection_, sliceIndex_, cuttingPlane)); + } + + + VolumeProjection DicomVolumeImageMPRSlicer::Slice::GetProjection() const + { + CheckValid(); + return projection_; + } + + + unsigned int DicomVolumeImageMPRSlicer::Slice::GetSliceIndex() const + { + CheckValid(); + return sliceIndex_; + } + + uint64_t DicomVolumeImageMPRSlicer::Slice::GetRevision() + { + CheckValid(); + return GetRevisionInternal(projection_, sliceIndex_); + } + + + ISceneLayer* DicomVolumeImageMPRSlicer::Slice::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 texture; + + { + const DicomInstanceParameters& parameters = volume_.GetDicomParameters(); + ImageBuffer3D::SliceReader reader(volume_.GetPixelData(), projection_, sliceIndex_); + texture.reset(dynamic_cast + (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(); + } + + + IVolumeSlicer::IExtractedSlice* + DicomVolumeImageMPRSlicer::ExtractSlice(const CoordinateSystem3D& cuttingPlane) + { + if (volume_->HasGeometry()) + { + return new Slice(*volume_, cuttingPlane); + } + else + { + return new IVolumeSlicer::InvalidSlice; + } + } +} diff -r bc7ee59420a1 -r aead999345e0 Framework/Volumes/DicomVolumeImageMPRSlicer.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Volumes/DicomVolumeImageMPRSlicer.h Tue May 28 21:16:39 2019 +0200 @@ -0,0 +1,93 @@ +/** + * 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 + +namespace OrthancStone +{ + /** + 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; + + 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); + + VolumeProjection GetProjection() const; + + unsigned int GetSliceIndex() const; + + virtual bool IsValid() + { + return valid_; + } + + virtual uint64_t GetRevision(); + + virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator, + const CoordinateSystem3D& cuttingPlane); + }; + + private: + boost::shared_ptr volume_; + + public: + DicomVolumeImageMPRSlicer(const boost::shared_ptr& volume) : + volume_(volume) + { + } + + virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) ORTHANC_OVERRIDE; + }; +} diff -r bc7ee59420a1 -r aead999345e0 Framework/Volumes/IVolumeSlicer.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Volumes/IVolumeSlicer.cpp Tue May 28 21:16:39 2019 +0200 @@ -0,0 +1,38 @@ +/** + * 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 "IVolumeSlicer.h" + +#include + +namespace OrthancStone +{ + uint64_t IVolumeSlicer::InvalidSlice::GetRevision() + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + ISceneLayer* IVolumeSlicer::InvalidSlice::CreateSceneLayer(const ILayerStyleConfigurator* configurator, + const CoordinateSystem3D& cuttingPlane) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } +} diff -r bc7ee59420a1 -r aead999345e0 Framework/Volumes/IVolumeSlicer.cpp~ --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Volumes/IVolumeSlicer.cpp~ Tue May 28 21:16:39 2019 +0200 @@ -0,0 +1,113 @@ +/** + * 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 + +namespace OrthancStone +{ + /** + 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; + }; +} diff -r bc7ee59420a1 -r aead999345e0 Framework/Volumes/IVolumeSlicer.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Volumes/IVolumeSlicer.h Tue May 28 21:16:39 2019 +0200 @@ -0,0 +1,110 @@ +/** + * 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/ILayerStyleConfigurator.h" +#include "../Toolbox/CoordinateSystem3D.h" + +namespace OrthancStone +{ + /** + 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(); + + virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator, + const CoordinateSystem3D& cuttingPlane); + }; + + + 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; + }; +} diff -r bc7ee59420a1 -r aead999345e0 Framework/Volumes/IVolumeSlicer.h~ --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Volumes/IVolumeSlicer.h~ Tue May 28 21:16:39 2019 +0200 @@ -0,0 +1,113 @@ +/** + * 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 + +namespace OrthancStone +{ + /** + 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; + }; +} diff -r bc7ee59420a1 -r aead999345e0 Resources/CMake/OrthancStoneConfiguration.cmake --- a/Resources/CMake/OrthancStoneConfiguration.cmake Tue May 28 18:31:44 2019 +0200 +++ b/Resources/CMake/OrthancStoneConfiguration.cmake Tue May 28 21:16:39 2019 +0200 @@ -382,9 +382,46 @@ ${ORTHANC_ROOT}/Plugins/Samples/Common/FullOrthancDataset.cpp ${ORTHANC_ROOT}/Plugins/Samples/Common/IOrthancConnection.cpp + ${ORTHANC_STONE_ROOT}/Framework/Fonts/FontRenderer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Fonts/Glyph.cpp + ${ORTHANC_STONE_ROOT}/Framework/Fonts/GlyphAlphabet.cpp + ${ORTHANC_STONE_ROOT}/Framework/Fonts/GlyphBitmapAlphabet.cpp + ${ORTHANC_STONE_ROOT}/Framework/Fonts/GlyphTextureAlphabet.cpp + ${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/OrthancSeriesVolumeProgressiveLoader.cpp + ${ORTHANC_STONE_ROOT}/Framework/Messages/ICallable.h + ${ORTHANC_STONE_ROOT}/Framework/Messages/IMessage.h + ${ORTHANC_STONE_ROOT}/Framework/Messages/IObservable.cpp + ${ORTHANC_STONE_ROOT}/Framework/Messages/IObserver.h + ${ORTHANC_STONE_ROOT}/Framework/Messages/MessageBroker.h + ${ORTHANC_STONE_ROOT}/Framework/Messages/MessageForwarder.cpp + ${ORTHANC_STONE_ROOT}/Framework/Messages/Promise.h + ${ORTHANC_STONE_ROOT}/Framework/Oracle/GetOrthancImageCommand.cpp + ${ORTHANC_STONE_ROOT}/Framework/Oracle/GetOrthancWebViewerJpegCommand.cpp + ${ORTHANC_STONE_ROOT}/Framework/Oracle/OracleCommandWithPayload.cpp + ${ORTHANC_STONE_ROOT}/Framework/Oracle/OrthancRestApiCommand.cpp + ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyAlphaLayer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyDicomLayer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayerCropTracker.cpp + ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayerMaskTracker.cpp + ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayerMoveTracker.cpp + ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayerResizeTracker.cpp + ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayerRotateTracker.cpp + ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyMaskLayer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyScene.cpp + ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographySceneCommand.cpp + ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographySceneReader.cpp + ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographySceneWriter.cpp + ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyTextLayer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyWidget.cpp + ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyWindowingTracker.cpp ${ORTHANC_STONE_ROOT}/Framework/Scene2D/CairoCompositor.cpp ${ORTHANC_STONE_ROOT}/Framework/Scene2D/ColorTextureSceneLayer.cpp ${ORTHANC_STONE_ROOT}/Framework/Scene2D/FloatTextureSceneLayer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/GrayscaleStyleConfigurator.cpp ${ORTHANC_STONE_ROOT}/Framework/Scene2D/InfoPanelSceneLayer.cpp ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoColorTextureRenderer.cpp ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoFloatTextureRenderer.cpp @@ -394,6 +431,7 @@ ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoTextRenderer.cpp ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CompositorHelper.cpp ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/FixedPointAligner.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/LookupTableStyleConfigurator.cpp ${ORTHANC_STONE_ROOT}/Framework/Scene2D/LookupTableTextureSceneLayer.cpp ${ORTHANC_STONE_ROOT}/Framework/Scene2D/PanSceneTracker.cpp ${ORTHANC_STONE_ROOT}/Framework/Scene2D/PointerEvent.cpp @@ -438,41 +476,6 @@ ${ORTHANC_STONE_ROOT}/Framework/Scene2DViewport/PointerTypes.h ${ORTHANC_STONE_ROOT}/Framework/Scene2DViewport/ViewportController.cpp ${ORTHANC_STONE_ROOT}/Framework/Scene2DViewport/ViewportController.h - ${ORTHANC_STONE_ROOT}/Framework/Fonts/FontRenderer.cpp - ${ORTHANC_STONE_ROOT}/Framework/Fonts/Glyph.cpp - ${ORTHANC_STONE_ROOT}/Framework/Fonts/GlyphAlphabet.cpp - ${ORTHANC_STONE_ROOT}/Framework/Fonts/GlyphBitmapAlphabet.cpp - ${ORTHANC_STONE_ROOT}/Framework/Fonts/GlyphTextureAlphabet.cpp - ${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/Messages/ICallable.h - ${ORTHANC_STONE_ROOT}/Framework/Messages/IMessage.h - ${ORTHANC_STONE_ROOT}/Framework/Messages/IObservable.cpp - ${ORTHANC_STONE_ROOT}/Framework/Messages/IObserver.h - ${ORTHANC_STONE_ROOT}/Framework/Messages/MessageBroker.h - ${ORTHANC_STONE_ROOT}/Framework/Messages/MessageForwarder.cpp - ${ORTHANC_STONE_ROOT}/Framework/Messages/Promise.h - ${ORTHANC_STONE_ROOT}/Framework/Oracle/GetOrthancImageCommand.cpp - ${ORTHANC_STONE_ROOT}/Framework/Oracle/GetOrthancWebViewerJpegCommand.cpp - ${ORTHANC_STONE_ROOT}/Framework/Oracle/OracleCommandWithPayload.cpp - ${ORTHANC_STONE_ROOT}/Framework/Oracle/OrthancRestApiCommand.cpp - ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyAlphaLayer.cpp - ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyDicomLayer.cpp - ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayer.cpp - ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayerCropTracker.cpp - ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayerMaskTracker.cpp - ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayerMoveTracker.cpp - ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayerResizeTracker.cpp - ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayerRotateTracker.cpp - ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyMaskLayer.cpp - ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyScene.cpp - ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographySceneCommand.cpp - ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographySceneReader.cpp - ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographySceneWriter.cpp - ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyTextLayer.cpp - ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyWidget.cpp - ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyWindowingTracker.cpp ${ORTHANC_STONE_ROOT}/Framework/StoneEnumerations.cpp ${ORTHANC_STONE_ROOT}/Framework/StoneException.h ${ORTHANC_STONE_ROOT}/Framework/StoneInitialization.cpp @@ -489,12 +492,15 @@ ${ORTHANC_STONE_ROOT}/Framework/Toolbox/ShearWarpProjectiveTransform.cpp ${ORTHANC_STONE_ROOT}/Framework/Toolbox/SlicesSorter.cpp ${ORTHANC_STONE_ROOT}/Framework/Toolbox/UndoRedoStack.cpp - ${ORTHANC_STONE_ROOT}/Framework/Wrappers/CairoContext.cpp - ${ORTHANC_STONE_ROOT}/Framework/Wrappers/CairoSurface.cpp + ${ORTHANC_STONE_ROOT}/Framework/Volumes/DicomVolumeImage.cpp + ${ORTHANC_STONE_ROOT}/Framework/Volumes/DicomVolumeImageMPRSlicer.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/Wrappers/CairoContext.cpp + ${ORTHANC_STONE_ROOT}/Framework/Wrappers/CairoSurface.cpp ${PLATFORM_SOURCES} ${APPLICATIONS_SOURCES} diff -r bc7ee59420a1 -r aead999345e0 Samples/Sdl/Loader.cpp --- a/Samples/Sdl/Loader.cpp Tue May 28 18:31:44 2019 +0200 +++ b/Samples/Sdl/Loader.cpp Tue May 28 21:16:39 2019 +0200 @@ -19,7 +19,10 @@ **/ -#include "../../Framework/Toolbox/DicomInstanceParameters.h" +#include "../../Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h" +#include "../../Framework/Volumes/DicomVolumeImageMPRSlicer.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" @@ -60,995 +63,6 @@ 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(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 geometry_; - std::auto_ptr image_; - std::auto_ptr 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 texture; - - { - const DicomInstanceParameters& parameters = volume_.GetDicomParameters(); - ImageBuffer3D::SliceReader reader(volume_.GetPixelData(), projection_, sliceIndex_); - texture.reset(dynamic_cast - (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 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 volume_; - - public: - DicomVolumeImageMPRSlicer(const boost::shared_ptr& 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 geometry_; - std::vector slices_; - std::vector 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(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(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& >(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 command; - - if (quality == BEST_QUALITY) - { - std::auto_ptr 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 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(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 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(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 volume_; - std::auto_ptr sorter_; - std::auto_ptr strategy_; - std::vector slicesQuality_; - - - public: - OrthancSeriesVolumeProgressiveLoader(const boost::shared_ptr& 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 - (*this, &OrthancSeriesVolumeProgressiveLoader::LoadGeometry)); - - oracleObservable.RegisterObserverCallback( - new Callable - (*this, &OrthancSeriesVolumeProgressiveLoader::LoadBestQualitySliceContent)); - - oracleObservable.RegisterObserverCallback( - new Callable - (*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 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