Mercurial > hg > orthanc-stone
changeset 102:fcec0ab44054 wasm
display volumes
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Wed, 31 May 2017 17:01:18 +0200 |
parents | af312ce4fe59 |
children | 474d85e76499 |
files | Applications/BasicApplicationContext.cpp Applications/BasicApplicationContext.h Applications/IBasicApplication.cpp Applications/Samples/SingleFrameApplication.h Applications/Samples/SingleVolumeApplication.h Applications/Sdl/SdlEngine.cpp Applications/Sdl/SdlSurface.cpp Applications/Sdl/SdlSurface.cpp~ Applications/Sdl/SdlWindow.cpp CMakeLists.txt Framework/Layers/OrthancFrameLayerSource.cpp Framework/Layers/OrthancFrameLayerSource.h Framework/Toolbox/Slice.cpp Framework/Toolbox/Slice.h Framework/dev.h UnitTestsSources/UnitTestsMain.cpp |
diffstat | 16 files changed, 919 insertions(+), 353 deletions(-) [+] |
line wrap: on
line diff
--- a/Applications/BasicApplicationContext.cpp Wed May 31 10:35:20 2017 +0200 +++ b/Applications/BasicApplicationContext.cpp Wed May 31 17:01:18 2017 +0200 @@ -80,33 +80,17 @@ } - VolumeImage& BasicApplicationContext::AddSeriesVolume(const std::string& series, - bool isProgressiveDownload, - size_t downloadThreadCount) + ISlicedVolume& BasicApplicationContext::AddVolume(ISlicedVolume* volume) { - /*std::auto_ptr<VolumeImage> volume - (new VolumeImage(new OrthancSeriesLoader(GetWebService().GetConnection(), series))); - - if (isProgressiveDownload) + if (volume == NULL) { - volume->SetDownloadPolicy(new VolumeImageProgressivePolicy); - } - else - { - volume->SetDownloadPolicy(new VolumeImageSimplePolicy); + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); } - volume->SetThreadCount(downloadThreadCount); - - VolumeImage& result = *volume; - volumes_.push_back(volume.release()); - - return result;*/ - - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + volumes_.push_back(volume); + return *volume; } - DicomStructureSet& BasicApplicationContext::AddStructureSet(const std::string& instance) { /*std::auto_ptr<DicomStructureSet> structureSet @@ -125,7 +109,7 @@ { if (interactor == NULL) { - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); } interactors_.push_back(interactor); @@ -138,13 +122,6 @@ { oracle_.Start(); - // TODO REMOVE THIS - for (Volumes::iterator it = volumes_.begin(); it != volumes_.end(); ++it) - { - assert(*it != NULL); - (*it)->Start(); - } - if (viewport_.HasUpdateContent()) { stopped_ = false; @@ -162,13 +139,6 @@ updateThread_.join(); } - // TODO REMOVE THIS - for (Volumes::iterator it = volumes_.begin(); it != volumes_.end(); ++it) - { - assert(*it != NULL); - (*it)->Stop(); - } - oracle_.Stop(); } }
--- a/Applications/BasicApplicationContext.h Wed May 31 10:35:20 2017 +0200 +++ b/Applications/BasicApplicationContext.h Wed May 31 17:01:18 2017 +0200 @@ -21,7 +21,7 @@ #pragma once -#include "../../Framework/Volumes/VolumeImage.h" +#include "../../Framework/Volumes/ISlicedVolume.h" #include "../../Framework/Viewport/WidgetViewport.h" #include "../../Framework/Widgets/IWorldSceneInteractor.h" #include "../../Framework/Toolbox/DicomStructureSet.h" @@ -35,7 +35,7 @@ class BasicApplicationContext : public boost::noncopyable { private: - typedef std::list<ISliceableVolume*> Volumes; + typedef std::list<ISlicedVolume*> Volumes; typedef std::list<IWorldSceneInteractor*> Interactors; typedef std::list<DicomStructureSet*> StructureSets; @@ -84,9 +84,7 @@ return webService_; } - VolumeImage& AddSeriesVolume(const std::string& series, - bool isProgressiveDownload, - size_t downloadThreadCount); + ISlicedVolume& AddVolume(ISlicedVolume* volume); DicomStructureSet& AddStructureSet(const std::string& instance);
--- a/Applications/IBasicApplication.cpp Wed May 31 10:35:20 2017 +0200 +++ b/Applications/IBasicApplication.cpp Wed May 31 17:01:18 2017 +0200 @@ -21,10 +21,12 @@ #include "IBasicApplication.h" +#include "../Framework/Toolbox/MessagingToolbox.h" +#include "Sdl/SdlEngine.h" + #include "../../Resources/Orthanc/Core/Logging.h" #include "../../Resources/Orthanc/Core/HttpClient.h" #include "../../Resources/Orthanc/Plugins/Samples/Common/OrthancHttpConnection.h" -#include "Sdl/SdlEngine.h" namespace OrthancStone {
--- a/Applications/Samples/SingleFrameApplication.h Wed May 31 10:35:20 2017 +0200 +++ b/Applications/Samples/SingleFrameApplication.h Wed May 31 17:01:18 2017 +0200 @@ -22,7 +22,6 @@ #pragma once #include "SampleApplicationBase.h" -#include "SampleInteractor.h" #include "../../Framework/Layers/OrthancFrameLayerSource.h" #include "../../Framework/Widgets/LayerWidget.h" @@ -81,7 +80,7 @@ KeyboardModifiers modifiers, IStatusBar* statusBar) { - unsigned int scale = (modifiers & KeyboardModifiers_Control ? 10 : 1); + int scale = (modifiers & KeyboardModifiers_Control ? 10 : 1); switch (direction) { @@ -163,9 +162,6 @@ #endif SliceGeometry s(source_->GetSlice(slice_).GetGeometry().GetOrigin(), x, y); - GeometryToolbox::Print(s.GetAxisX()); - GeometryToolbox::Print(s.GetAxisY()); - GeometryToolbox::Print(s.GetNormal()); widget_->SetSlice(s); #endif } @@ -251,7 +247,7 @@ std::auto_ptr<LayerWidget> widget(new LayerWidget); -#if 0 +#if 1 std::auto_ptr<OrthancFrameLayerSource> layer (new OrthancFrameLayerSource(context.GetWebService())); //layer->SetImageQuality(SliceImageQuality_Jpeg50);
--- a/Applications/Samples/SingleVolumeApplication.h Wed May 31 10:35:20 2017 +0200 +++ b/Applications/Samples/SingleVolumeApplication.h Wed May 31 17:01:18 2017 +0200 @@ -21,7 +21,10 @@ #pragma once -#include "SampleInteractor.h" +#include "SampleApplicationBase.h" +#include "../../Framework/dev.h" +//#include "SampleInteractor.h" +#include "../../Framework/Widgets/LayerWidget.h" #include "../../Resources/Orthanc/Core/Toolbox.h" #include "../../Framework/Layers/LineMeasureTracker.h" @@ -32,9 +35,160 @@ { namespace Samples { - class SingleVolumeApplication : public SampleApplicationBase + class SingleVolumeApplication : + public SampleApplicationBase, + private ILayerSource::IObserver { private: + class Interactor : public IWorldSceneInteractor + { + private: + SingleVolumeApplication& application_; + + public: + Interactor(SingleVolumeApplication& application) : + application_(application) + { + } + + virtual IWorldSceneMouseTracker* CreateMouseTracker(WorldSceneWidget& widget, + const ViewportGeometry& view, + MouseButton button, + double x, + double y, + IStatusBar* statusBar) + { + return NULL; + } + + virtual void MouseOver(CairoContext& context, + WorldSceneWidget& widget, + const ViewportGeometry& view, + double x, + double y, + IStatusBar* statusBar) + { + if (statusBar != NULL) + { + Vector p = dynamic_cast<LayerWidget&>(widget).GetSlice().MapSliceToWorldCoordinates(x, y); + + char buf[64]; + sprintf(buf, "X = %.02f Y = %.02f Z = %.02f (in cm)", + p[0] / 10.0, p[1] / 10.0, p[2] / 10.0); + statusBar->SetMessage(buf); + } + } + + virtual void MouseWheel(WorldSceneWidget& widget, + MouseWheelDirection direction, + KeyboardModifiers modifiers, + IStatusBar* statusBar) + { + int scale = (modifiers & KeyboardModifiers_Control ? 10 : 1); + + switch (direction) + { + case MouseWheelDirection_Up: + application_.OffsetSlice(-scale); + break; + + case MouseWheelDirection_Down: + application_.OffsetSlice(scale); + break; + + default: + break; + } + } + + virtual void KeyPressed(WorldSceneWidget& widget, + char key, + KeyboardModifiers modifiers, + IStatusBar* statusBar) + { + switch (key) + { + case 's': + widget.SetDefaultView(); + break; + + default: + break; + } + } + }; + + + LayerWidget* widget_; + OrthancVolumeImage* volume_; + VolumeProjection projection_; + std::auto_ptr<VolumeImageGeometry> slices_; + size_t slice_; + + void OffsetSlice(int offset) + { + if (slices_.get() != NULL) + { + int slice = static_cast<int>(slice_) + offset; + + if (slice < 0) + { + slice = 0; + } + + if (slice >= static_cast<int>(slices_->GetSliceCount())) + { + slice = slices_->GetSliceCount() - 1; + } + + if (slice != static_cast<int>(slice_)) + { + SetSlice(slice); + } + } + } + + void SetSlice(size_t slice) + { + if (slices_.get() != NULL) + { + slice_ = slice; + widget_->SetSlice(slices_->GetSlice(slice_).GetGeometry()); + } + } + + virtual void NotifyGeometryReady(const ILayerSource& source) + { + if (slices_.get() == NULL) + { + slices_.reset(new VolumeImageGeometry(*volume_, projection_)); + SetSlice(slices_->GetSliceCount() / 2); + + widget_->SetDefaultView(); + } + } + + virtual void NotifyGeometryError(const ILayerSource& source) + { + } + + virtual void NotifyContentChange(const ILayerSource& source) + { + } + + virtual void NotifySliceChange(const ILayerSource& source, + const Slice& slice) + { + } + + virtual void NotifyLayerReady(std::auto_ptr<ILayerRenderer>& layer, + const ILayerSource& source, + const Slice& slice, + bool isError) + { + } + +#if 0 class Interactor : public SampleInteractor { private: @@ -204,9 +358,16 @@ } } }; - +#endif + public: + SingleVolumeApplication() : + widget_(NULL), + volume_(NULL) + { + } + virtual void DeclareCommandLineOptions(boost::program_options::options_description& options) { boost::program_options::options_description generic("Sample options"); @@ -243,18 +404,17 @@ std::string tmp = parameters["projection"].as<std::string>(); Orthanc::Toolbox::ToLowerCase(tmp); - VolumeProjection projection; if (tmp == "axial") { - projection = VolumeProjection_Axial; + projection_ = VolumeProjection_Axial; } else if (tmp == "sagittal") { - projection = VolumeProjection_Sagittal; + projection_ = VolumeProjection_Sagittal; } else if (tmp == "coronal") { - projection = VolumeProjection_Coronal; + projection_ = VolumeProjection_Coronal; } else { @@ -262,22 +422,74 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } - VolumeImage& volume = context.AddSeriesVolume(series, true /* progressive download */, threads); + std::auto_ptr<LayerWidget> widget(new LayerWidget); + widget_ = widget.get(); + +#if 0 + std::auto_ptr<OrthancVolumeImage> volume(new OrthancVolumeImage(context.GetWebService())); + volume->ScheduleLoadSeries(series); - std::auto_ptr<Interactor> interactor(new Interactor(volume, projection, reverse)); + volume_ = volume.get(); + + { + std::auto_ptr<VolumeImageSource> source(new VolumeImageSource(*volume)); + source->Register(*this); + widget->AddLayer(source.release()); + } + + context.AddVolume(volume.release()); +#else + std::auto_ptr<OrthancVolumeImage> ct(new OrthancVolumeImage(context.GetWebService())); + ct->ScheduleLoadSeries("dd069910-4f090474-7d2bba07-e5c10783-f9e4fb1d"); + + std::auto_ptr<OrthancVolumeImage> pet(new OrthancVolumeImage(context.GetWebService())); + pet->ScheduleLoadSeries("aabad2e7-80702b5d-e599d26c-4f13398e-38d58a9e"); - std::auto_ptr<LayeredSceneWidget> widget(new LayeredSceneWidget); - widget->AddLayer(new VolumeImage::LayerFactory(volume)); - widget->SetSlice(interactor->GetCursor().GetCurrentSlice()); - widget->SetInteractor(*interactor); + volume_ = pet.get(); + + { + std::auto_ptr<VolumeImageSource> source(new VolumeImageSource(*ct)); + //source->Register(*this); + widget->AddLayer(source.release()); + } + + { + std::auto_ptr<VolumeImageSource> source(new VolumeImageSource(*pet)); + source->Register(*this); + widget->AddLayer(source.release()); + } + + context.AddVolume(ct.release()); + context.AddVolume(pet.release()); - context.AddInteractor(interactor.release()); - context.SetCentralWidget(widget.release()); + { + RenderStyle s; + //s.drawGrid_ = true; + s.alpha_ = 1; + widget->SetLayerStyle(0, s); + } + + { + RenderStyle s; + //s.drawGrid_ = true; + s.SetColor(255, 0, 0); // Draw missing PET layer in red + s.alpha_ = 0.5; + s.applyLut_ = true; + s.lut_ = Orthanc::EmbeddedResources::COLORMAP_JET; + s.interpolation_ = ImageInterpolation_Linear; + widget->SetLayerStyle(1, s); + } +#endif + statusBar.SetMessage("Use the keys \"b\", \"l\" and \"d\" to change Hounsfield windowing"); statusBar.SetMessage("Use the keys \"t\" to track the (X,Y,Z) mouse coordinates"); statusBar.SetMessage("Use the keys \"m\" to measure distances"); statusBar.SetMessage("Use the keys \"c\" to draw circles"); + + widget->SetTransmitMouseOver(true); + widget->SetInteractor(context.AddInteractor(new Interactor(*this))); + context.SetCentralWidget(widget.release()); } }; }
--- a/Applications/Sdl/SdlEngine.cpp Wed May 31 10:35:20 2017 +0200 +++ b/Applications/Sdl/SdlEngine.cpp Wed May 31 17:01:18 2017 +0200 @@ -23,7 +23,7 @@ #if ORTHANC_ENABLE_SDL == 1 -#include "../../../Resources/Orthanc/Core/Logging.h" +#include "../../Resources/Orthanc/Core/Logging.h" #include <SDL.h>
--- a/Applications/Sdl/SdlSurface.cpp Wed May 31 10:35:20 2017 +0200 +++ b/Applications/Sdl/SdlSurface.cpp Wed May 31 17:01:18 2017 +0200 @@ -23,8 +23,8 @@ #if ORTHANC_ENABLE_SDL == 1 -#include "../../../Resources/Orthanc/Core/Logging.h" -#include "../../../Resources/Orthanc/Core/OrthancException.h" +#include "../../Resources/Orthanc/Core/Logging.h" +#include "../../Resources/Orthanc/Core/OrthancException.h" namespace OrthancStone {
--- a/Applications/Sdl/SdlSurface.cpp~ Wed May 31 10:35:20 2017 +0200 +++ b/Applications/Sdl/SdlSurface.cpp~ Wed May 31 17:01:18 2017 +0200 @@ -19,7 +19,7 @@ **/ -#include "SdlBuffering.h" +#include "SdlSurface.h" #if ORTHANC_ENABLE_SDL == 1 @@ -28,14 +28,14 @@ namespace OrthancStone { - SdlBuffering::SdlBuffering() : - sdlSurface_(NULL), - pendingFrame_(false) + SdlSurface::SdlSurface(SdlWindow& window) : + window_(window), + sdlSurface_(NULL) { } - SdlBuffering::~SdlBuffering() + SdlSurface::~SdlSurface() { if (sdlSurface_) { @@ -44,26 +44,14 @@ } - void SdlBuffering::SetSize(unsigned int width, - unsigned int height, - IViewport& viewport) + void SdlSurface::SetSize(unsigned int width, + unsigned int height) { - boost::mutex::scoped_lock lock(mutex_); - - viewport.SetSize(width, height); - - if (offscreenSurface_.get() == NULL || - offscreenSurface_->GetWidth() != width || - offscreenSurface_->GetHeight() != height) + if (cairoSurface_.get() == NULL || + cairoSurface_->GetWidth() != width || + cairoSurface_->GetHeight() != height) { - offscreenSurface_.reset(new CairoSurface(width, height)); - } - - if (onscreenSurface_.get() == NULL || - onscreenSurface_->GetWidth() != width || - onscreenSurface_->GetHeight() != height) - { - onscreenSurface_.reset(new CairoSurface(width, height)); + cairoSurface_.reset(new CairoSurface(width, height)); // TODO Big endian? static const uint32_t rmask = 0x00ff0000; @@ -75,59 +63,30 @@ SDL_FreeSurface(sdlSurface_); } - sdlSurface_ = SDL_CreateRGBSurfaceFrom(onscreenSurface_->GetBuffer(), width, height, 32, - onscreenSurface_->GetPitch(), rmask, gmask, bmask, 0); + sdlSurface_ = SDL_CreateRGBSurfaceFrom(cairoSurface_->GetBuffer(), width, height, 32, + cairoSurface_->GetPitch(), rmask, gmask, bmask, 0); if (!sdlSurface_) { LOG(ERROR) << "Cannot create a SDL surface from a Cairo surface"; throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } - } - - pendingFrame_ = false; - } - - - bool SdlBuffering::RenderOffscreen(IViewport& viewport) - { - boost::mutex::scoped_lock lock(mutex_); - - if (offscreenSurface_.get() == NULL) - { - return false; - } - - Orthanc::ImageAccessor target = offscreenSurface_->GetAccessor(); - - if (viewport.Render(target) && - !pendingFrame_) - { - pendingFrame_ = true; - return true; - } - else - { - return false; + } } } - void SdlBuffering::SwapToScreen(SdlWindow& window) + void SdlSurface::Render(IViewport& viewport) { - if (!pendingFrame_ || - offscreenSurface_.get() == NULL || - onscreenSurface_.get() == NULL) + if (cairoSurface_.get() == NULL) { - return; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } + Orthanc::ImageAccessor target = cairoSurface_->GetAccessor(); + + if (viewport.Render(target)) { - boost::mutex::scoped_lock lock(mutex_); - onscreenSurface_->Copy(*offscreenSurface_); + window_.Render(sdlSurface_); } - - window.Render(sdlSurface_); - pendingFrame_ = false; } }
--- a/Applications/Sdl/SdlWindow.cpp Wed May 31 10:35:20 2017 +0200 +++ b/Applications/Sdl/SdlWindow.cpp Wed May 31 17:01:18 2017 +0200 @@ -23,8 +23,8 @@ #if ORTHANC_ENABLE_SDL == 1 -#include "../../../Resources/Orthanc/Core/Logging.h" -#include "../../../Resources/Orthanc/Core/OrthancException.h" +#include "../../Resources/Orthanc/Core/Logging.h" +#include "../../Resources/Orthanc/Core/OrthancException.h" namespace OrthancStone {
--- a/CMakeLists.txt Wed May 31 10:35:20 2017 +0200 +++ b/CMakeLists.txt Wed May 31 17:01:18 2017 +0200 @@ -34,7 +34,7 @@ BuildSample(OrthancStoneEmpty 1) BuildSample(OrthancStoneTestPattern 2) BuildSample(OrthancStoneSingleFrame 3) -#BuildSample(OrthancStoneSingleVolume 4) +BuildSample(OrthancStoneSingleVolume 4) #BuildSample(OrthancStoneBasicPetCtFusion 5) #BuildSample(OrthancStoneSynchronizedSeries 6) #BuildSample(OrthancStoneLayoutPetCtFusion 7)
--- a/Framework/Layers/OrthancFrameLayerSource.cpp Wed May 31 10:35:20 2017 +0200 +++ b/Framework/Layers/OrthancFrameLayerSource.cpp Wed May 31 17:01:18 2017 +0200 @@ -94,19 +94,7 @@ if (loader_.IsGeometryReady() && loader_.LookupSlice(index, viewportSlice)) { - const Slice& slice = loader_.GetSlice(index); - const SliceGeometry& plane = slice.GetGeometry(); - - double sx = slice.GetPixelSpacingX(); - double sy = slice.GetPixelSpacingY(); - double w = static_cast<double>(slice.GetWidth()); - double h = static_cast<double>(slice.GetHeight()); - - points.clear(); - points.push_back(plane.MapSliceToWorldCoordinates(-0.5 * sx, -0.5 * sy)); - points.push_back(plane.MapSliceToWorldCoordinates((w - 0.5) * sx, -0.5 * sy)); - points.push_back(plane.MapSliceToWorldCoordinates(-0.5 * sx, (h - 0.5) * sy)); - points.push_back(plane.MapSliceToWorldCoordinates((w - 0.5) * sx, (h - 0.5) * sy)); + loader_.GetSlice(index).GetExtent(points); return true; } else
--- a/Framework/Layers/OrthancFrameLayerSource.h Wed May 31 10:35:20 2017 +0200 +++ b/Framework/Layers/OrthancFrameLayerSource.h Wed May 31 17:01:18 2017 +0200 @@ -63,12 +63,12 @@ quality_ = quality; } - virtual size_t GetSliceCount() const + size_t GetSliceCount() const { return loader_.GetSliceCount(); } - virtual const Slice& GetSlice(size_t slice) const + const Slice& GetSlice(size_t slice) const { return loader_.GetSlice(slice); }
--- a/Framework/Toolbox/Slice.cpp Wed May 31 10:35:20 2017 +0200 +++ b/Framework/Toolbox/Slice.cpp Wed May 31 17:01:18 2017 +0200 @@ -187,4 +187,19 @@ GetGeometry().ProjectAlongNormal(plane.GetOrigin()), thickness_ / 2.0)); } + + + void Slice::GetExtent(std::vector<Vector>& points) const + { + double sx = GetPixelSpacingX(); + double sy = GetPixelSpacingY(); + double w = static_cast<double>(GetWidth()); + double h = static_cast<double>(GetHeight()); + + points.clear(); + points.push_back(GetGeometry().MapSliceToWorldCoordinates(-0.5 * sx, -0.5 * sy)); + points.push_back(GetGeometry().MapSliceToWorldCoordinates((w - 0.5) * sx, -0.5 * sy)); + points.push_back(GetGeometry().MapSliceToWorldCoordinates(-0.5 * sx, (h - 0.5) * sy)); + points.push_back(GetGeometry().MapSliceToWorldCoordinates((w - 0.5) * sx, (h - 0.5) * sy)); + } }
--- a/Framework/Toolbox/Slice.h Wed May 31 10:35:20 2017 +0200 +++ b/Framework/Toolbox/Slice.h Wed May 31 17:01:18 2017 +0200 @@ -32,7 +32,7 @@ enum Type { Type_Invalid, - Type_Detached, + Type_Standalone, Type_OrthancInstance // TODO A slice could come from some DICOM file (URL) }; @@ -57,7 +57,7 @@ // layers within LayerWidget? Slice(const SliceGeometry& plane, double thickness) : - type_(Type_Detached), + type_(Type_Standalone), frame_(0), geometry_(plane), pixelSpacingX_(1), @@ -68,6 +68,24 @@ { } + Slice(const SliceGeometry& plane, + double pixelSpacingX, + double pixelSpacingY, + double thickness, + unsigned int width, + unsigned int height, + const DicomFrameConverter& converter) : + type_(Type_Standalone), + geometry_(plane), + pixelSpacingX_(pixelSpacingX), + pixelSpacingY_(pixelSpacingY), + thickness_(thickness), + width_(width), + height_(height), + converter_(converter) + { + } + bool IsValid() const { return type_ != Type_Invalid; @@ -101,5 +119,7 @@ const DicomFrameConverter& GetConverter() const; bool ContainsPlane(const SliceGeometry& plane) const; + + void GetExtent(std::vector<Vector>& points) const; }; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/dev.h Wed May 31 17:01:18 2017 +0200 @@ -0,0 +1,605 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017 Osimis, 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/>. + **/ + + +#pragma once + +#include "Layers/FrameRenderer.h" +#include "Layers/LayerSourceBase.h" +#include "Layers/SliceOutlineRenderer.h" +#include "Toolbox/DownloadStack.h" +#include "Toolbox/OrthancSlicesLoader.h" +#include "Volumes/ImageBuffer3D.h" +#include "Volumes/SlicedVolumeBase.h" + +#include "../Resources/Orthanc/Core/Logging.h" +#include "../Resources/Orthanc/Core/Images/ImageProcessing.h" + +#include <boost/math/special_functions/round.hpp> + + +namespace OrthancStone +{ + class OrthancVolumeImage : + public SlicedVolumeBase, + private OrthancSlicesLoader::ICallback + { + private: + OrthancSlicesLoader loader_; + std::auto_ptr<ImageBuffer3D> image_; + std::auto_ptr<DownloadStack> downloadStack_; + + + void ScheduleSliceDownload() + { + assert(downloadStack_.get() != NULL); + + unsigned int slice; + if (downloadStack_->Pop(slice)) + { + loader_.ScheduleLoadSliceImage(slice, SliceImageQuality_Jpeg90); + } + } + + + static bool IsCompatible(const Slice& a, + const Slice& b) + { + if (!GeometryToolbox::IsParallel(a.GetGeometry().GetNormal(), + b.GetGeometry().GetNormal())) + { + LOG(ERROR) << "Some slice in the volume image is not parallel to the others"; + return false; + } + + if (a.GetConverter().GetExpectedPixelFormat() != b.GetConverter().GetExpectedPixelFormat()) + { + LOG(ERROR) << "The pixel format changes across the slices of the volume image"; + return false; + } + + if (a.GetWidth() != b.GetWidth() || + a.GetHeight() != b.GetHeight()) + { + LOG(ERROR) << "The width/height of the slices change across the volume image"; + return false; + } + + if (!GeometryToolbox::IsNear(a.GetPixelSpacingX(), b.GetPixelSpacingX()) || + !GeometryToolbox::IsNear(a.GetPixelSpacingY(), b.GetPixelSpacingY())) + { + LOG(ERROR) << "The pixel spacing of the slices change across the volume image"; + return false; + } + + return true; + } + + + static double GetDistance(const Slice& a, + const Slice& b) + { + return fabs(a.GetGeometry().ProjectAlongNormal(a.GetGeometry().GetOrigin()) - + a.GetGeometry().ProjectAlongNormal(b.GetGeometry().GetOrigin())); + } + + + virtual void NotifyGeometryReady(const OrthancSlicesLoader& loader) + { + if (loader.GetSliceCount() == 0) + { + LOG(ERROR) << "Empty volume image"; + SlicedVolumeBase::NotifyGeometryError(); + return; + } + + for (size_t i = 1; i < loader.GetSliceCount(); i++) + { + if (!IsCompatible(loader.GetSlice(0), loader.GetSlice(i))) + { + SlicedVolumeBase::NotifyGeometryError(); + return; + } + } + + double spacingZ; + + if (loader.GetSliceCount() > 1) + { + spacingZ = GetDistance(loader.GetSlice(0), loader.GetSlice(1)); + } + else + { + // This is a volume with one single slice: Choose a dummy + // z-dimension for voxels + spacingZ = 1; + } + + for (size_t i = 1; i < loader.GetSliceCount(); i++) + { + if (!GeometryToolbox::IsNear(spacingZ, GetDistance(loader.GetSlice(i - 1), loader.GetSlice(i)), + 0.001 /* this is expressed in mm */)) + { + LOG(ERROR) << "The distance between successive slices is not constant in a volume image"; + SlicedVolumeBase::NotifyGeometryError(); + return; + } + } + + unsigned int width = loader.GetSlice(0).GetWidth(); + unsigned int height = loader.GetSlice(0).GetHeight(); + Orthanc::PixelFormat format = loader.GetSlice(0).GetConverter().GetExpectedPixelFormat(); + LOG(INFO) << "Creating a volume image of size " << width << "x" << height + << "x" << loader.GetSliceCount() << " in " << Orthanc::EnumerationToString(format); + + image_.reset(new ImageBuffer3D(format, width, height, loader.GetSliceCount())); + image_->SetAxialGeometry(loader.GetSlice(0).GetGeometry()); + image_->SetVoxelDimensions(loader.GetSlice(0).GetPixelSpacingX(), + loader.GetSlice(0).GetPixelSpacingY(), spacingZ); + image_->Clear(); + + downloadStack_.reset(new DownloadStack(loader.GetSliceCount())); + + for (unsigned int i = 0; i < 4; i++) // Limit to 4 simultaneous downloads + { + ScheduleSliceDownload(); + } + + // TODO Check the DicomFrameConverter are constant + + SlicedVolumeBase::NotifyGeometryReady(); + } + + virtual void NotifyGeometryError(const OrthancSlicesLoader& loader) + { + LOG(ERROR) << "Unable to download a volume image"; + SlicedVolumeBase::NotifyGeometryError(); + } + + virtual void NotifySliceImageReady(const OrthancSlicesLoader& loader, + unsigned int sliceIndex, + const Slice& slice, + std::auto_ptr<Orthanc::ImageAccessor>& image, + SliceImageQuality quality) + { + { + ImageBuffer3D::SliceWriter writer(*image_, VolumeProjection_Axial, sliceIndex); + Orthanc::ImageProcessing::Copy(writer.GetAccessor(), *image); + } + + SlicedVolumeBase::NotifySliceChange(sliceIndex, slice); + + ScheduleSliceDownload(); + } + + virtual void NotifySliceImageError(const OrthancSlicesLoader& loader, + unsigned int sliceIndex, + const Slice& slice, + SliceImageQuality quality) + { + LOG(ERROR) << "Cannot download slice " << sliceIndex << " in a volume image"; + ScheduleSliceDownload(); + } + + public: + OrthancVolumeImage(IWebService& orthanc) : + loader_(*this, orthanc) + { + } + + void ScheduleLoadSeries(const std::string& seriesId) + { + loader_.ScheduleLoadSeries(seriesId); + } + + void ScheduleLoadInstance(const std::string& instanceId, + unsigned int frame) + { + loader_.ScheduleLoadInstance(instanceId, frame); + } + + virtual size_t GetSliceCount() const + { + return loader_.GetSliceCount(); + } + + virtual const Slice& GetSlice(size_t index) const + { + return loader_.GetSlice(index); + } + + ImageBuffer3D& GetImage() const + { + if (image_.get() == NULL) + { + // The geometry is not ready yet + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + return *image_; + } + } + }; + + + class VolumeImageGeometry + { + private: + unsigned int width_; + unsigned int height_; + size_t depth_; + double pixelSpacingX_; + double pixelSpacingY_; + double sliceThickness_; + SliceGeometry reference_; + DicomFrameConverter converter_; + + double ComputeAxialThickness(const OrthancVolumeImage& volume) const + { + double thickness; + + size_t n = volume.GetSliceCount(); + if (n > 1) + { + const Slice& a = volume.GetSlice(0); + const Slice& b = volume.GetSlice(n - 1); + thickness = ((reference_.ProjectAlongNormal(b.GetGeometry().GetOrigin()) - + reference_.ProjectAlongNormal(a.GetGeometry().GetOrigin())) / + (static_cast<double>(n) - 1.0)); + } + else + { + thickness = volume.GetSlice(0).GetThickness(); + } + + if (thickness <= 0) + { + // The slices should have been sorted with increasing Z + // (along the normal) by the OrthancSlicesLoader + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + else + { + return thickness; + } + } + + void SetupAxial(const OrthancVolumeImage& volume) + { + const Slice& axial = volume.GetSlice(0); + + width_ = axial.GetWidth(); + height_ = axial.GetHeight(); + depth_ = volume.GetSliceCount(); + + pixelSpacingX_ = axial.GetPixelSpacingX(); + pixelSpacingY_ = axial.GetPixelSpacingY(); + sliceThickness_ = ComputeAxialThickness(volume); + + reference_ = axial.GetGeometry(); + } + + void SetupCoronal(const OrthancVolumeImage& volume) + { + const Slice& axial = volume.GetSlice(0); + double axialThickness = ComputeAxialThickness(volume); + + width_ = axial.GetWidth(); + height_ = volume.GetSliceCount(); + depth_ = axial.GetHeight(); + + pixelSpacingX_ = axial.GetPixelSpacingX(); + pixelSpacingY_ = axialThickness; + sliceThickness_ = axial.GetPixelSpacingY(); + + Vector origin = axial.GetGeometry().GetOrigin(); + origin += (static_cast<double>(volume.GetSliceCount() - 1) * + axialThickness * axial.GetGeometry().GetNormal()); + + reference_ = SliceGeometry(origin, + axial.GetGeometry().GetAxisX(), + -axial.GetGeometry().GetNormal()); + } + + void SetupSagittal(const OrthancVolumeImage& volume) + { + const Slice& axial = volume.GetSlice(0); + double axialThickness = ComputeAxialThickness(volume); + + width_ = axial.GetHeight(); + height_ = volume.GetSliceCount(); + depth_ = axial.GetWidth(); + + pixelSpacingX_ = axial.GetPixelSpacingY(); + pixelSpacingY_ = axialThickness; + sliceThickness_ = axial.GetPixelSpacingX(); + + Vector origin = axial.GetGeometry().GetOrigin(); + origin += (static_cast<double>(volume.GetSliceCount() - 1) * + axialThickness * axial.GetGeometry().GetNormal()); + + reference_ = SliceGeometry(origin, + axial.GetGeometry().GetAxisY(), + axial.GetGeometry().GetNormal()); + } + + public: + VolumeImageGeometry(const OrthancVolumeImage& volume, + VolumeProjection projection) + { + if (volume.GetSliceCount() == 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + converter_ = volume.GetSlice(0).GetConverter(); + + switch (projection) + { + case VolumeProjection_Axial: + SetupAxial(volume); + break; + + case VolumeProjection_Coronal: + SetupCoronal(volume); + break; + + case VolumeProjection_Sagittal: + SetupSagittal(volume); + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } + + size_t GetSliceCount() const + { + return depth_; + } + + const Vector& GetNormal() const + { + return reference_.GetNormal(); + } + + bool LookupSlice(size_t& index, + const SliceGeometry& slice) const + { + bool opposite; + if (!GeometryToolbox::IsParallelOrOpposite(opposite, + reference_.GetNormal(), + slice.GetNormal())) + { + return false; + } + + double z = (reference_.ProjectAlongNormal(slice.GetOrigin()) - + reference_.ProjectAlongNormal(reference_.GetOrigin())) / sliceThickness_; + + int s = static_cast<int>(boost::math::iround(z)); + + if (s < 0 || + s >= static_cast<int>(depth_)) + { + return false; + } + else + { + index = static_cast<size_t>(s); + return true; + } + } + + Slice GetSlice(size_t slice) const + { + if (slice < 0 || + slice >= depth_) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + else + { + SliceGeometry origin(reference_.GetOrigin() + + static_cast<double>(slice) * sliceThickness_ * reference_.GetNormal(), + reference_.GetAxisX(), + reference_.GetAxisY()); + + return Slice(origin, pixelSpacingX_, pixelSpacingY_, sliceThickness_, + width_, height_, converter_); + } + } + }; + + + + class VolumeImageSource : + public LayerSourceBase, + private ISlicedVolume::IObserver + { + private: + OrthancVolumeImage& volume_; + std::auto_ptr<VolumeImageGeometry> axialGeometry_; + std::auto_ptr<VolumeImageGeometry> coronalGeometry_; + std::auto_ptr<VolumeImageGeometry> sagittalGeometry_; + + + bool IsGeometryReady() const + { + return axialGeometry_.get() != NULL; + } + + + virtual void NotifyGeometryReady(const ISlicedVolume& volume) + { + // These 3 values are only used to speed up the ILayerSource + axialGeometry_.reset(new VolumeImageGeometry(volume_, VolumeProjection_Axial)); + coronalGeometry_.reset(new VolumeImageGeometry(volume_, VolumeProjection_Coronal)); + sagittalGeometry_.reset(new VolumeImageGeometry(volume_, VolumeProjection_Sagittal)); + + LayerSourceBase::NotifyGeometryReady(); + } + + virtual void NotifyGeometryError(const ISlicedVolume& volume) + { + LayerSourceBase::NotifyGeometryError(); + } + + virtual void NotifyContentChange(const ISlicedVolume& volume) + { + LayerSourceBase::NotifyContentChange(); + } + + virtual void NotifySliceChange(const ISlicedVolume& volume, + const size_t& sliceIndex, + const Slice& slice) + { + //LayerSourceBase::NotifySliceChange(slice); + + // TODO Improve this? + LayerSourceBase::NotifyContentChange(); + } + + + const VolumeImageGeometry& GetProjectionGeometry(VolumeProjection projection) + { + if (!IsGeometryReady()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + switch (projection) + { + case VolumeProjection_Axial: + return *axialGeometry_; + + case VolumeProjection_Sagittal: + return *sagittalGeometry_; + + case VolumeProjection_Coronal: + return *coronalGeometry_; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + + + bool DetectProjection(VolumeProjection& projection, + const SliceGeometry& viewportSlice) + { + bool isOpposite; // Ignored + + if (GeometryToolbox::IsParallelOrOpposite(isOpposite, + viewportSlice.GetNormal(), + axialGeometry_->GetNormal())) + { + projection = VolumeProjection_Axial; + return true; + } + else if (GeometryToolbox::IsParallelOrOpposite(isOpposite, + viewportSlice.GetNormal(), + sagittalGeometry_->GetNormal())) + { + projection = VolumeProjection_Sagittal; + return true; + } + else if (GeometryToolbox::IsParallelOrOpposite(isOpposite, + viewportSlice.GetNormal(), + coronalGeometry_->GetNormal())) + { + projection = VolumeProjection_Coronal; + return true; + } + else + { + return false; + } + } + + + public: + VolumeImageSource(OrthancVolumeImage& volume) : + volume_(volume) + { + volume_.Register(*this); + } + + virtual bool GetExtent(std::vector<Vector>& points, + const SliceGeometry& viewportSlice) + { + VolumeProjection projection; + + if (!IsGeometryReady() || + !DetectProjection(projection, viewportSlice)) + { + return false; + } + else + { + // As the slices of the volumic image are arranged in a box, + // we only consider one single reference slice (the one with index 0). + GetProjectionGeometry(projection).GetSlice(0).GetExtent(points); + + return true; + } + } + + + virtual void ScheduleLayerCreation(const SliceGeometry& viewportSlice) + { + VolumeProjection projection; + + if (IsGeometryReady() && + DetectProjection(projection, viewportSlice)) + { + const VolumeImageGeometry& geometry = GetProjectionGeometry(projection); + + size_t closest; + + if (geometry.LookupSlice(closest, viewportSlice)) + { + bool isFullQuality = true; // TODO + + std::auto_ptr<Orthanc::Image> frame; + + { + ImageBuffer3D::SliceReader reader(volume_.GetImage(), projection, closest); + + // TODO Transfer ownership if non-axial, to avoid memcpy + frame.reset(Orthanc::Image::Clone(reader.GetAccessor())); + } + + Slice slice = geometry.GetSlice(closest); + LayerSourceBase::NotifyLayerReady( + FrameRenderer::CreateRenderer(frame.release(), slice, isFullQuality), + //new SliceOutlineRenderer(slice), + slice, false); + return; + } + } + + // Error + Slice slice; + LayerSourceBase::NotifyLayerReady(NULL, slice, true); + } + }; +}
--- a/UnitTestsSources/UnitTestsMain.cpp Wed May 31 10:35:20 2017 +0200 +++ b/UnitTestsSources/UnitTestsMain.cpp Wed May 31 17:01:18 2017 +0200 @@ -19,6 +19,7 @@ **/ +#include "../Framework/dev.h" #include "gtest/gtest.h" #include "../Platforms/Generic/OracleWebService.h" @@ -31,11 +32,15 @@ #include "../Framework/Volumes/ImageBuffer3D.h" #include "../Framework/Volumes/SlicedVolumeBase.h" #include "../Framework/Toolbox/DownloadStack.h" +#include "../Framework/Layers/LayerSourceBase.h" +#include "../Framework/Layers/FrameRenderer.h" #include "../Resources/Orthanc/Core/Images/ImageProcessing.h" #include <boost/lexical_cast.hpp> #include <boost/date_time/posix_time/posix_time.hpp> #include <boost/thread/thread.hpp> +#include <boost/math/special_functions/round.hpp> + namespace OrthancStone { @@ -60,7 +65,7 @@ virtual void NotifySliceImageReady(const OrthancSlicesLoader& loader, unsigned int sliceIndex, const Slice& slice, - Orthanc::ImageAccessor* image, + std::auto_ptr<Orthanc::ImageAccessor>& image, SliceImageQuality quality) { std::auto_ptr<Orthanc::ImageAccessor> tmp(image); @@ -75,210 +80,6 @@ printf("ERROR 2\n"); } }; - - - class OrthancVolumeImage : - public SlicedVolumeBase, - private OrthancSlicesLoader::ICallback - { - private: - OrthancSlicesLoader loader_; - std::auto_ptr<ImageBuffer3D> image_; - std::auto_ptr<DownloadStack> downloadStack_; - - - void ScheduleSliceDownload() - { - assert(downloadStack_.get() != NULL); - - unsigned int slice; - if (downloadStack_->Pop(slice)) - { - loader_.ScheduleLoadSliceImage(slice, SliceImageQuality_Full); - } - } - - - static bool IsCompatible(const Slice& a, - const Slice& b) - { - if (!GeometryToolbox::IsParallel(a.GetGeometry().GetNormal(), - b.GetGeometry().GetNormal())) - { - LOG(ERROR) << "Some slice in the volume image is not parallel to the others"; - return false; - } - - if (a.GetConverter().GetExpectedPixelFormat() != b.GetConverter().GetExpectedPixelFormat()) - { - LOG(ERROR) << "The pixel format changes across the slices of the volume image"; - return false; - } - - if (a.GetWidth() != b.GetWidth() || - a.GetHeight() != b.GetHeight()) - { - LOG(ERROR) << "The width/height of the slices change across the volume image"; - return false; - } - - if (!GeometryToolbox::IsNear(a.GetPixelSpacingX(), b.GetPixelSpacingX()) || - !GeometryToolbox::IsNear(a.GetPixelSpacingY(), b.GetPixelSpacingY())) - { - LOG(ERROR) << "The pixel spacing of the slices change across the volume image"; - return false; - } - - return true; - } - - - static double GetDistance(const Slice& a, - const Slice& b) - { - return fabs(a.GetGeometry().ProjectAlongNormal(a.GetGeometry().GetOrigin()) - - a.GetGeometry().ProjectAlongNormal(b.GetGeometry().GetOrigin())); - } - - - virtual void NotifyGeometryReady(const OrthancSlicesLoader& loader) - { - if (loader.GetSliceCount() == 0) - { - LOG(ERROR) << "Empty volume image"; - SlicedVolumeBase::NotifyGeometryError(); - return; - } - - for (size_t i = 1; i < loader.GetSliceCount(); i++) - { - if (!IsCompatible(loader.GetSlice(0), loader.GetSlice(i))) - { - SlicedVolumeBase::NotifyGeometryError(); - return; - } - } - - double spacingZ; - - if (loader.GetSliceCount() > 1) - { - spacingZ = GetDistance(loader.GetSlice(0), loader.GetSlice(1)); - } - else - { - // This is a volume with one single slice: Choose a dummy - // z-dimension for voxels - spacingZ = 1; - } - - for (size_t i = 1; i < loader.GetSliceCount(); i++) - { - printf("%d %s %f\n", i, loader.GetSlice(i).GetOrthancInstanceId().c_str(), - GetDistance(loader.GetSlice(i - 1), loader.GetSlice(i))); - - if (!GeometryToolbox::IsNear(spacingZ, GetDistance(loader.GetSlice(i - 1), loader.GetSlice(i)), - 0.001 /* this is expressed in mm */)) - { - LOG(ERROR) << "The distance between successive slices is not constant in a volume image"; - SlicedVolumeBase::NotifyGeometryError(); - return; - } - } - - unsigned int width = loader.GetSlice(0).GetWidth(); - unsigned int height = loader.GetSlice(0).GetHeight(); - Orthanc::PixelFormat format = loader.GetSlice(0).GetConverter().GetExpectedPixelFormat(); - LOG(INFO) << "Creating a volume image of size " << width << "x" << height - << "x" << loader.GetSliceCount() << " in " << Orthanc::EnumerationToString(format); - - image_.reset(new ImageBuffer3D(format, width, height, loader.GetSliceCount())); - image_->SetAxialGeometry(loader.GetSlice(0).GetGeometry()); - image_->SetVoxelDimensions(loader.GetSlice(0).GetPixelSpacingX(), - loader.GetSlice(0).GetPixelSpacingY(), spacingZ); - image_->Clear(); - - downloadStack_.reset(new DownloadStack(loader.GetSliceCount())); - - SlicedVolumeBase::NotifyGeometryReady(); - - for (unsigned int i = 0; i < 4; i++) // Limit to 4 simultaneous downloads - { - ScheduleSliceDownload(); - } - } - - virtual void NotifyGeometryError(const OrthancSlicesLoader& loader) - { - LOG(ERROR) << "Unable to download a volume image"; - } - - virtual void NotifySliceImageReady(const OrthancSlicesLoader& loader, - unsigned int sliceIndex, - const Slice& slice, - Orthanc::ImageAccessor* image, - SliceImageQuality quality) - { - std::auto_ptr<Orthanc::ImageAccessor> protection(image); - - { - ImageBuffer3D::SliceWriter writer(*image_, VolumeProjection_Axial, sliceIndex); - Orthanc::ImageProcessing::Copy(writer.GetAccessor(), *protection); - } - - SlicedVolumeBase::NotifySliceChange(sliceIndex, slice); - - ScheduleSliceDownload(); - } - - virtual void NotifySliceImageError(const OrthancSlicesLoader& loader, - unsigned int sliceIndex, - const Slice& slice, - SliceImageQuality quality) - { - LOG(ERROR) << "Cannot download slice " << sliceIndex << " in a volume image"; - ScheduleSliceDownload(); - } - - public: - OrthancVolumeImage(IWebService& orthanc) : - loader_(*this, orthanc) - { - } - - void ScheduleLoadSeries(const std::string& seriesId) - { - loader_.ScheduleLoadSeries(seriesId); - } - - void ScheduleLoadInstance(const std::string& instanceId, - unsigned int frame) - { - loader_.ScheduleLoadInstance(instanceId, frame); - } - - virtual size_t GetSliceCount() const - { - return loader_.GetSliceCount(); - } - - virtual const Slice& GetSlice(size_t index) const - { - return loader_.GetSlice(index); - } - - ImageBuffer3D& GetImage() - { - if (image_.get() == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - else - { - return *image_; - } - } - }; }