# HG changeset patch # User Benjamin Golinvaux # Date 1558706424 -7200 # Node ID cf1102295ae5371515c576652208b96481487c55 # Parent 66ac7a2d1e3a634f7f144331b545d5f9b7e56970# Parent b8dfd966b5f432ca30aa402f15b9f8bd0d3eb998 Merge from default diff -r 66ac7a2d1e3a -r cf1102295ae5 Framework/Oracle/IOracle.h --- a/Framework/Oracle/IOracle.h Fri May 24 15:59:51 2019 +0200 +++ b/Framework/Oracle/IOracle.h Fri May 24 16:00:24 2019 +0200 @@ -32,6 +32,10 @@ { } + virtual void Start() = 0; + + virtual void Stop() = 0; + virtual void Schedule(const IObserver& receiver, IOracleCommand* command) = 0; // Takes ownership }; diff -r 66ac7a2d1e3a -r cf1102295ae5 Framework/Oracle/ThreadedOracle.cpp --- a/Framework/Oracle/ThreadedOracle.cpp Fri May 24 15:59:51 2019 +0200 +++ b/Framework/Oracle/ThreadedOracle.cpp Fri May 24 16:00:24 2019 +0200 @@ -204,6 +204,9 @@ Orthanc::GzipCompressor compressor; compressor.Uncompress(answer, compressed.c_str(), compressed.size()); + + LOG(INFO) << "Uncompressing gzip Encoding: from " << compressed.size() + << " to " << answer.size() << " bytes"; } } @@ -424,6 +427,29 @@ } + ThreadedOracle::~ThreadedOracle() + { + if (state_ == State_Running) + { + LOG(ERROR) << "The threaded oracle is still running, explicit call to " + << "Stop() is mandatory to avoid crashes"; + } + + try + { + StopInternal(); + } + catch (Orthanc::OrthancException& e) + { + LOG(ERROR) << "Exception while stopping the threaded oracle: " << e.What(); + } + catch (...) + { + LOG(ERROR) << "Native exception while stopping the threaded oracle"; + } + } + + void ThreadedOracle::SetOrthancParameters(const Orthanc::WebServiceParameters& orthanc) { boost::mutex::scoped_lock lock(mutex_); diff -r 66ac7a2d1e3a -r cf1102295ae5 Framework/Oracle/ThreadedOracle.h --- a/Framework/Oracle/ThreadedOracle.h Fri May 24 15:59:51 2019 +0200 +++ b/Framework/Oracle/ThreadedOracle.h Fri May 24 16:00:24 2019 +0200 @@ -72,10 +72,7 @@ public: ThreadedOracle(IMessageEmitter& emitter); - virtual ~ThreadedOracle() - { - StopInternal(); - } + virtual ~ThreadedOracle(); void SetOrthancParameters(const Orthanc::WebServiceParameters& orthanc); @@ -83,9 +80,9 @@ void SetSleepingTimeResolution(unsigned int milliseconds); - void Start(); + virtual void Start(); - void Stop() + virtual void Stop() { StopInternal(); } diff -r 66ac7a2d1e3a -r cf1102295ae5 Framework/Scene2D/CairoCompositor.cpp --- a/Framework/Scene2D/CairoCompositor.cpp Fri May 24 15:59:51 2019 +0200 +++ b/Framework/Scene2D/CairoCompositor.cpp Fri May 24 16:00:24 2019 +0200 @@ -24,6 +24,7 @@ #include "Internals/CairoColorTextureRenderer.h" #include "Internals/CairoFloatTextureRenderer.h" #include "Internals/CairoInfoPanelRenderer.h" +#include "Internals/CairoLookupTableTextureRenderer.h" #include "Internals/CairoPolylineRenderer.h" #include "Internals/CairoTextRenderer.h" @@ -60,6 +61,9 @@ case ISceneLayer::Type_FloatTexture: return new Internals::CairoFloatTextureRenderer(*this, layer); + case ISceneLayer::Type_LookupTableTexture: + return new Internals::CairoLookupTableTextureRenderer(*this, layer); + case ISceneLayer::Type_Text: { const TextSceneLayer& l = dynamic_cast(layer); diff -r 66ac7a2d1e3a -r cf1102295ae5 Framework/Scene2D/ColorTextureSceneLayer.h --- a/Framework/Scene2D/ColorTextureSceneLayer.h Fri May 24 15:59:51 2019 +0200 +++ b/Framework/Scene2D/ColorTextureSceneLayer.h Fri May 24 16:00:24 2019 +0200 @@ -28,6 +28,7 @@ class ColorTextureSceneLayer : public TextureBaseSceneLayer { public: + // If using RGBA32, premultiplied alpha is assumed ColorTextureSceneLayer(const Orthanc::ImageAccessor& texture); virtual ISceneLayer* Clone() const; diff -r 66ac7a2d1e3a -r cf1102295ae5 Framework/Scene2D/FloatTextureSceneLayer.cpp --- a/Framework/Scene2D/FloatTextureSceneLayer.cpp Fri May 24 15:59:51 2019 +0200 +++ b/Framework/Scene2D/FloatTextureSceneLayer.cpp Fri May 24 16:00:24 2019 +0200 @@ -86,6 +86,13 @@ } + void FloatTextureSceneLayer::SetInverted(bool inverted) + { + inverted_ = inverted; + IncrementRevision(); + } + + void FloatTextureSceneLayer::FitRange() { float minValue, maxValue; @@ -116,6 +123,7 @@ cloned->windowing_ = windowing_; cloned->customCenter_ = customCenter_; cloned->customWidth_ = customWidth_; + cloned->inverted_ = inverted_; return cloned.release(); } diff -r 66ac7a2d1e3a -r cf1102295ae5 Framework/Scene2D/FloatTextureSceneLayer.h --- a/Framework/Scene2D/FloatTextureSceneLayer.h Fri May 24 15:59:51 2019 +0200 +++ b/Framework/Scene2D/FloatTextureSceneLayer.h Fri May 24 16:00:24 2019 +0200 @@ -31,9 +31,10 @@ ImageWindowing windowing_; float customCenter_; float customWidth_; + bool inverted_; public: - // The pixel format must be "Float32" + // The pixel format must be convertible to "Float32" FloatTextureSceneLayer(const Orthanc::ImageAccessor& texture); void SetWindowing(ImageWindowing windowing); @@ -49,6 +50,14 @@ return windowing_; } + // To achieve MONOCHROME1 photometric interpretation + void SetInverted(bool inverted); + + bool IsInverted() const + { + return inverted_; + } + void FitRange(); virtual ISceneLayer* Clone() const; diff -r 66ac7a2d1e3a -r cf1102295ae5 Framework/Scene2D/ISceneLayer.h --- a/Framework/Scene2D/ISceneLayer.h Fri May 24 15:59:51 2019 +0200 +++ b/Framework/Scene2D/ISceneLayer.h Fri May 24 16:00:24 2019 +0200 @@ -37,7 +37,8 @@ Type_ColorTexture, Type_Polyline, Type_Text, - Type_FloatTexture + Type_FloatTexture, + Type_LookupTableTexture }; virtual ~ISceneLayer() diff -r 66ac7a2d1e3a -r cf1102295ae5 Framework/Scene2D/Internals/CairoColorTextureRenderer.cpp --- a/Framework/Scene2D/Internals/CairoColorTextureRenderer.cpp Fri May 24 15:59:51 2019 +0200 +++ b/Framework/Scene2D/Internals/CairoColorTextureRenderer.cpp Fri May 24 16:00:24 2019 +0200 @@ -44,13 +44,17 @@ isLinearInterpolation_ = l.IsLinearInterpolation(); } - - void CairoColorTextureRenderer::Render(const AffineTransform2D& transform) + + void CairoColorTextureRenderer::RenderColorTexture(ICairoContextProvider& target, + const AffineTransform2D& transform, + CairoSurface& texture, + const AffineTransform2D& textureTransform, + bool isLinearInterpolation) { - cairo_t* cr = target_.GetCairoContext(); + cairo_t* cr = target.GetCairoContext(); AffineTransform2D t = - AffineTransform2D::Combine(transform, textureTransform_); + AffineTransform2D::Combine(transform, textureTransform); Matrix h = t.GetHomogeneousMatrix(); cairo_save(cr); @@ -60,9 +64,9 @@ cairo_transform(cr, &m); cairo_set_operator(cr, CAIRO_OPERATOR_OVER); - cairo_set_source_surface(cr, texture_.GetObject(), 0, 0); + cairo_set_source_surface(cr, texture.GetObject(), 0, 0); - if (isLinearInterpolation_) + if (isLinearInterpolation) { cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_BILINEAR); } diff -r 66ac7a2d1e3a -r cf1102295ae5 Framework/Scene2D/Internals/CairoColorTextureRenderer.h --- a/Framework/Scene2D/Internals/CairoColorTextureRenderer.h Fri May 24 15:59:51 2019 +0200 +++ b/Framework/Scene2D/Internals/CairoColorTextureRenderer.h Fri May 24 16:00:24 2019 +0200 @@ -43,7 +43,17 @@ virtual void Update(const ISceneLayer& layer); - virtual void Render(const AffineTransform2D& transform); + virtual void Render(const AffineTransform2D& transform) + { + RenderColorTexture(target_, transform, texture_, + textureTransform_, isLinearInterpolation_); + } + + static void RenderColorTexture(ICairoContextProvider& target, + const AffineTransform2D& transform, + CairoSurface& texture, + const AffineTransform2D& textureTransform, + bool isLinearInterpolation); }; } } diff -r 66ac7a2d1e3a -r cf1102295ae5 Framework/Scene2D/Internals/CairoFloatTextureRenderer.cpp --- a/Framework/Scene2D/Internals/CairoFloatTextureRenderer.cpp Fri May 24 15:59:51 2019 +0200 +++ b/Framework/Scene2D/Internals/CairoFloatTextureRenderer.cpp Fri May 24 16:00:24 2019 +0200 @@ -21,6 +21,7 @@ #include "CairoFloatTextureRenderer.h" +#include "CairoColorTextureRenderer.h" #include "../FloatTextureSceneLayer.h" namespace OrthancStone @@ -37,8 +38,8 @@ float windowCenter, windowWidth; l.GetWindowing(windowCenter, windowWidth); - const float a = windowCenter - windowWidth; - const float slope = 256.0f / (2.0f * windowWidth); + const float a = windowCenter - windowWidth / 2.0f; + const float slope = 256.0f / windowWidth; const Orthanc::ImageAccessor& source = l.GetTexture(); const unsigned int width = source.GetWidth(); @@ -71,6 +72,11 @@ uint8_t vv = static_cast(v); + if (l.IsInverted()) + { + vv = 255 - vv; + } + q[0] = vv; q[1] = vv; q[2] = vv; @@ -84,33 +90,8 @@ void CairoFloatTextureRenderer::Render(const AffineTransform2D& transform) { - cairo_t* cr = target_.GetCairoContext(); - - AffineTransform2D t = - AffineTransform2D::Combine(transform, textureTransform_); - Matrix h = t.GetHomogeneousMatrix(); - - cairo_save(cr); - - cairo_matrix_t m; - cairo_matrix_init(&m, h(0, 0), h(1, 0), h(0, 1), h(1, 1), h(0, 2), h(1, 2)); - cairo_transform(cr, &m); - - cairo_set_operator(cr, CAIRO_OPERATOR_OVER); - cairo_set_source_surface(cr, texture_.GetObject(), 0, 0); - - if (isLinearInterpolation_) - { - cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_BILINEAR); - } - else - { - cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST); - } - - cairo_paint(cr); - - cairo_restore(cr); + CairoColorTextureRenderer::RenderColorTexture(target_, transform, texture_, + textureTransform_, isLinearInterpolation_); } } } diff -r 66ac7a2d1e3a -r cf1102295ae5 Framework/Scene2D/Internals/CairoLookupTableTextureRenderer.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Scene2D/Internals/CairoLookupTableTextureRenderer.cpp Fri May 24 16:00:24 2019 +0200 @@ -0,0 +1,108 @@ +/** + * 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 "CairoLookupTableTextureRenderer.h" + +#include "CairoColorTextureRenderer.h" +#include "../LookupTableTextureSceneLayer.h" + +#include + +namespace OrthancStone +{ + namespace Internals + { + void CairoLookupTableTextureRenderer::Update(const ISceneLayer& layer) + { + const LookupTableTextureSceneLayer& l = dynamic_cast(layer); + + textureTransform_ = l.GetTransform(); + isLinearInterpolation_ = l.IsLinearInterpolation(); + + const float a = l.GetMinValue(); + float slope; + + if (l.GetMinValue() >= l.GetMaxValue()) + { + slope = 0; + } + else + { + slope = 256.0f / (l.GetMaxValue() - l.GetMinValue()); + } + + const Orthanc::ImageAccessor& source = l.GetTexture(); + const unsigned int width = source.GetWidth(); + const unsigned int height = source.GetHeight(); + texture_.SetSize(width, height, true /* alpha channel is enabled */); + + Orthanc::ImageAccessor target; + texture_.GetWriteableAccessor(target); + + const std::vector& lut = l.GetLookupTable(); + if (lut.size() != 4 * 256) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + assert(source.GetFormat() == Orthanc::PixelFormat_Float32 && + target.GetFormat() == Orthanc::PixelFormat_BGRA32 && + sizeof(float) == 4); + + for (unsigned int y = 0; y < height; y++) + { + const float* p = reinterpret_cast(source.GetConstRow(y)); + uint8_t* q = reinterpret_cast(target.GetRow(y)); + + for (unsigned int x = 0; x < width; x++) + { + float v = (*p - a) * slope; + if (v <= 0) + { + v = 0; + } + else if (v >= 255) + { + v = 255; + } + + uint8_t vv = static_cast(v); + + q[0] = lut[4 * vv + 2]; // B + q[1] = lut[4 * vv + 1]; // G + q[2] = lut[4 * vv + 0]; // R + q[3] = lut[4 * vv + 3]; // A + + p++; + q += 4; + } + } + + cairo_surface_mark_dirty(texture_.GetObject()); + } + + void CairoLookupTableTextureRenderer::Render(const AffineTransform2D& transform) + { + CairoColorTextureRenderer::RenderColorTexture(target_, transform, texture_, + textureTransform_, isLinearInterpolation_); + } + } +} diff -r 66ac7a2d1e3a -r cf1102295ae5 Framework/Scene2D/Internals/CairoLookupTableTextureRenderer.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Scene2D/Internals/CairoLookupTableTextureRenderer.h Fri May 24 16:00:24 2019 +0200 @@ -0,0 +1,53 @@ +/** + * 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 "../../Viewport/CairoSurface.h" +#include "CompositorHelper.h" +#include "ICairoContextProvider.h" + +namespace OrthancStone +{ + namespace Internals + { + class CairoLookupTableTextureRenderer : public CompositorHelper::ILayerRenderer + { + private: + ICairoContextProvider& target_; + CairoSurface texture_; + AffineTransform2D textureTransform_; + bool isLinearInterpolation_; + + public: + CairoLookupTableTextureRenderer(ICairoContextProvider& target, + const ISceneLayer& layer) : + target_(target) + { + Update(layer); + } + + virtual void Update(const ISceneLayer& layer); + + virtual void Render(const AffineTransform2D& transform); + }; + } +} diff -r 66ac7a2d1e3a -r cf1102295ae5 Framework/Scene2D/Internals/OpenGLFloatTextureProgram.cpp --- a/Framework/Scene2D/Internals/OpenGLFloatTextureProgram.cpp Fri May 24 15:59:51 2019 +0200 +++ b/Framework/Scene2D/Internals/OpenGLFloatTextureProgram.cpp Fri May 24 16:00:24 2019 +0200 @@ -29,28 +29,28 @@ static const char* FRAGMENT_SHADER = ORTHANC_STONE_OPENGL_SHADER_VERSION_DIRECTIVE - "uniform float u_offset; \n" - "uniform float u_slope; \n" - "uniform float u_windowCenter; \n" - "uniform float u_windowWidth; \n" - "uniform sampler2D u_texture; \n" - "varying vec2 v_texcoord; \n" - "void main() \n" - "{ \n" - " vec4 t = texture2D(u_texture, v_texcoord); \n" - " float v = (t.r * 256.0 + t.g) * 256.0; \n" - " v = v * u_slope + u_offset; \n" // (*) - " float a = u_windowCenter - u_windowWidth; \n" - " float dy = 1.0 / (2.0 * u_windowWidth); \n" - " if (v <= a) \n" - " v = 0.0; \n" - " else \n" - " { \n" - " v = (v - a) * dy; \n" - " if (v >= 1.0) \n" - " v = 1.0; \n" - " } \n" - " gl_FragColor = vec4(v, v, v, 1); \n" + "uniform float u_offset; \n" + "uniform float u_slope; \n" + "uniform float u_windowCenter; \n" + "uniform float u_windowWidth; \n" + "uniform sampler2D u_texture; \n" + "varying vec2 v_texcoord; \n" + "void main() \n" + "{ \n" + " vec4 t = texture2D(u_texture, v_texcoord); \n" + " float v = (t.r * 256.0 + t.g) * 256.0; \n" + " v = v * u_slope + u_offset; \n" // (*) + " float a = u_windowCenter - u_windowWidth / 2.0; \n" + " float dy = 1.0 / u_windowWidth; \n" + " if (v <= a) \n" + " v = 0.0; \n" + " else \n" + " { \n" + " v = (v - a) * dy; \n" + " if (v >= 1.0) \n" + " v = 1.0; \n" + " } \n" + " gl_FragColor = vec4(v, v, v, 1); \n" "}"; diff -r 66ac7a2d1e3a -r cf1102295ae5 Framework/Scene2D/LookupTableTextureSceneLayer.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Scene2D/LookupTableTextureSceneLayer.cpp Fri May 24 16:00:24 2019 +0200 @@ -0,0 +1,178 @@ +/** + * 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 "LookupTableTextureSceneLayer.h" + +#include +#include +#include + +namespace OrthancStone +{ + static void StringToVector(std::vector& target, + const std::string& source) + { + target.resize(source.size()); + + for (size_t i = 0; i < source.size(); i++) + { + target[i] = source[i]; + } + } + + + LookupTableTextureSceneLayer::LookupTableTextureSceneLayer(const Orthanc::ImageAccessor& texture) + { + { + std::auto_ptr t( + new Orthanc::Image(Orthanc::PixelFormat_Float32, + texture.GetWidth(), + texture.GetHeight(), + false)); + + Orthanc::ImageProcessing::Convert(*t, texture); + SetTexture(t.release()); + } + + SetLookupTableGrayscale(); + SetRange(0, 1); + } + + + void LookupTableTextureSceneLayer::SetLookupTableGrayscale() + { + std::vector rgb(3 * 256); + + for (size_t i = 0; i < 256; i++) + { + rgb[3 * i] = i; + rgb[3 * i + 1] = i; + rgb[3 * i + 2] = i; + } + + SetLookupTableRgb(rgb); + } + + + void LookupTableTextureSceneLayer::SetLookupTableRgb(const std::vector& lut) + { + if (lut.size() != 3 * 256) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + lut_.resize(4 * 256); + + for (size_t i = 0; i < 256; i++) + { + // Premultiplied alpha + + if (i == 0) + { + // Make zero transparent + lut_[4 * i] = 0; // R + lut_[4 * i + 1] = 0; // G + lut_[4 * i + 2] = 0; // B + lut_[4 * i + 3] = 0; // A + } + else + { + float a = static_cast(i) / 255.0f; + + float r = static_cast(lut[3 * i]) * a; + float g = static_cast(lut[3 * i + 1]) * a; + float b = static_cast(lut[3 * i + 2]) * a; + + lut_[4 * i] = static_cast(std::floor(r)); + lut_[4 * i + 1] = static_cast(std::floor(g)); + lut_[4 * i + 2] = static_cast(std::floor(b)); + lut_[4 * i + 3] = static_cast(std::floor(a * 255.0f)); + } + } + + IncrementRevision(); + } + + + void LookupTableTextureSceneLayer::SetLookupTable(const std::vector& lut) + { + if (lut.size() == 4 * 256) + { + lut_ = lut; + IncrementRevision(); + } + else if (lut.size() == 3 * 256) + { + SetLookupTableRgb(lut); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } + + + void LookupTableTextureSceneLayer::SetLookupTable(const std::string& lut) + { + std::vector tmp; + StringToVector(tmp, lut); + SetLookupTable(tmp); + } + + + void LookupTableTextureSceneLayer::SetRange(float minValue, + float maxValue) + { + if (minValue > maxValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + else + { + minValue_ = minValue; + maxValue_ = maxValue; + IncrementRevision(); + } + } + + + void LookupTableTextureSceneLayer::FitRange() + { + Orthanc::ImageProcessing::GetMinMaxFloatValue(minValue_, maxValue_, GetTexture()); + assert(minValue_ <= maxValue_); + + IncrementRevision(); + } + + + ISceneLayer* LookupTableTextureSceneLayer::Clone() const + { + std::auto_ptr cloned + (new LookupTableTextureSceneLayer(GetTexture())); + + cloned->CopyParameters(*this); + cloned->minValue_ = minValue_; + cloned->maxValue_ = maxValue_; + cloned->lut_ = lut_; + + return cloned.release(); + } +} diff -r 66ac7a2d1e3a -r cf1102295ae5 Framework/Scene2D/LookupTableTextureSceneLayer.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Scene2D/LookupTableTextureSceneLayer.h Fri May 24 16:00:24 2019 +0200 @@ -0,0 +1,78 @@ +/** + * 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 "TextureBaseSceneLayer.h" + +namespace OrthancStone +{ + class LookupTableTextureSceneLayer : public TextureBaseSceneLayer + { + private: + ImageWindowing windowing_; + float minValue_; + float maxValue_; + std::vector lut_; + + void SetLookupTableRgb(const std::vector& lut); + + public: + // The pixel format must be convertible to Float32 + LookupTableTextureSceneLayer(const Orthanc::ImageAccessor& texture); + + void SetLookupTableGrayscale(); + + // The vector must contain either 3 * 256 values (RGB), or 4 * 256 + // (RGBA). In the RGB case, an alpha channel will be automatically added. + void SetLookupTable(const std::vector& lut); + + void SetLookupTable(const std::string& lut); + + void SetRange(float minValue, + float maxValue); + + void FitRange(); + + float GetMinValue() const + { + return minValue_; + } + + float GetMaxValue() const + { + return maxValue_; + } + + // This returns a vector of 4 * 256 values between 0 and 255, in RGBA. + const std::vector& GetLookupTable() const + { + return lut_; + } + + virtual ISceneLayer* Clone() const; + + virtual Type GetType() const + { + return Type_LookupTableTexture; + } + }; +} diff -r 66ac7a2d1e3a -r cf1102295ae5 Framework/Toolbox/DicomInstanceParameters.cpp --- a/Framework/Toolbox/DicomInstanceParameters.cpp Fri May 24 15:59:51 2019 +0200 +++ b/Framework/Toolbox/DicomInstanceParameters.cpp Fri May 24 16:00:24 2019 +0200 @@ -62,12 +62,7 @@ { if (frameOffsets_.size() >= 2) { - thickness_ = frameOffsets_[1] - frameOffsets_[0]; - - if (thickness_ < 0) - { - thickness_ = -thickness_; - } + thickness_ = std::abs(frameOffsets_[1] - frameOffsets_[0]); } } } @@ -325,7 +320,24 @@ } - TextureBaseSceneLayer* DicomInstanceParameters::CreateTexture(const Orthanc::ImageAccessor& pixelData) const + Orthanc::ImageAccessor* DicomInstanceParameters::ConvertToFloat(const Orthanc::ImageAccessor& pixelData) const + { + std::auto_ptr converted(new Orthanc::Image(Orthanc::PixelFormat_Float32, + pixelData.GetWidth(), + pixelData.GetHeight(), + false)); + Orthanc::ImageProcessing::Convert(*converted, pixelData); + + // Correct rescale slope/intercept if need be + data_.ApplyRescale(*converted, (pixelData.GetFormat() == Orthanc::PixelFormat_Grayscale32)); + + return converted.release(); + } + + + + TextureBaseSceneLayer* DicomInstanceParameters::CreateTexture + (const Orthanc::ImageAccessor& pixelData) const { assert(sizeof(float) == 4); @@ -343,26 +355,16 @@ } else { - if (sourceFormat != Orthanc::PixelFormat_Grayscale16 && - sourceFormat != Orthanc::PixelFormat_Grayscale32 && - sourceFormat != Orthanc::PixelFormat_SignedGrayscale16) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } + // This is the case of a grayscale frame. Convert it to Float32. + std::auto_ptr texture; - std::auto_ptr texture; - + if (pixelData.GetFormat() == Orthanc::PixelFormat_Float32) { - // This is the case of a grayscale frame. Convert it to Float32. - std::auto_ptr converted(new Orthanc::Image(Orthanc::PixelFormat_Float32, - pixelData.GetWidth(), - pixelData.GetHeight(), - false)); - Orthanc::ImageProcessing::Convert(*converted, pixelData); - - // Correct rescale slope/intercept if need be - data_.ApplyRescale(*converted, (sourceFormat == Orthanc::PixelFormat_Grayscale32)); - + texture.reset(new FloatTextureSceneLayer(pixelData)); + } + else + { + std::auto_ptr converted(ConvertToFloat(pixelData)); texture.reset(new FloatTextureSceneLayer(*converted)); } @@ -375,4 +377,21 @@ return texture.release(); } } + + + LookupTableTextureSceneLayer* DicomInstanceParameters::CreateLookupTableTexture + (const Orthanc::ImageAccessor& pixelData) const + { + std::auto_ptr texture; + + if (pixelData.GetFormat() == Orthanc::PixelFormat_Float32) + { + return new LookupTableTextureSceneLayer(pixelData); + } + else + { + std::auto_ptr converted(ConvertToFloat(pixelData)); + return new LookupTableTextureSceneLayer(*converted); + } + } } diff -r 66ac7a2d1e3a -r cf1102295ae5 Framework/Toolbox/DicomInstanceParameters.h --- a/Framework/Toolbox/DicomInstanceParameters.h Fri May 24 15:59:51 2019 +0200 +++ b/Framework/Toolbox/DicomInstanceParameters.h Fri May 24 16:00:24 2019 +0200 @@ -22,7 +22,7 @@ #pragma once #include "../StoneEnumerations.h" -#include "../Scene2D/TextureBaseSceneLayer.h" +#include "../Scene2D/LookupTableTextureSceneLayer.h" #include "../Toolbox/CoordinateSystem3D.h" #include @@ -72,6 +72,9 @@ }; + Orthanc::ImageAccessor* ConvertToFloat(const Orthanc::ImageAccessor& pixelData) const; + + Data data_; @@ -181,5 +184,7 @@ } TextureBaseSceneLayer* CreateTexture(const Orthanc::ImageAccessor& pixelData) const; + + LookupTableTextureSceneLayer* CreateLookupTableTexture(const Orthanc::ImageAccessor& pixelData) const; }; } diff -r 66ac7a2d1e3a -r cf1102295ae5 Framework/Volumes/ImageBuffer3D.h --- a/Framework/Volumes/ImageBuffer3D.h Fri May 24 15:59:51 2019 +0200 +++ b/Framework/Volumes/ImageBuffer3D.h Fri May 24 16:00:24 2019 +0200 @@ -99,6 +99,11 @@ return format_; } + unsigned int GetBytesPerPixel() const + { + return Orthanc::GetBytesPerPixel(format_); + } + uint64_t GetEstimatedMemorySize() const; bool GetRange(float& minValue, diff -r 66ac7a2d1e3a -r cf1102295ae5 Resources/CMake/OrthancStoneConfiguration.cmake --- a/Resources/CMake/OrthancStoneConfiguration.cmake Fri May 24 15:59:51 2019 +0200 +++ b/Resources/CMake/OrthancStoneConfiguration.cmake Fri May 24 16:00:24 2019 +0200 @@ -380,10 +380,12 @@ ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoColorTextureRenderer.cpp ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoFloatTextureRenderer.cpp ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoInfoPanelRenderer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoLookupTableTextureRenderer.cpp ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoPolylineRenderer.cpp ${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/LookupTableTextureSceneLayer.cpp ${ORTHANC_STONE_ROOT}/Framework/Scene2D/PanSceneTracker.cpp ${ORTHANC_STONE_ROOT}/Framework/Scene2D/PointerEvent.cpp ${ORTHANC_STONE_ROOT}/Framework/Scene2D/PolylineSceneLayer.cpp diff -r 66ac7a2d1e3a -r cf1102295ae5 Samples/Sdl/Loader.cpp --- a/Samples/Sdl/Loader.cpp Fri May 24 15:59:51 2019 +0200 +++ b/Samples/Sdl/Loader.cpp Fri May 24 16:00:24 2019 +0200 @@ -32,6 +32,8 @@ #include "../../Framework/Loaders/BasicFetchingStrategy.h" #include "../../Framework/Scene2D/CairoCompositor.h" #include "../../Framework/Scene2D/Scene2D.h" +#include "../../Framework/Scene2D/PolylineSceneLayer.h" +#include "../../Framework/Scene2D/LookupTableTextureSceneLayer.h" #include "../../Framework/StoneInitialization.h" #include "../../Framework/Toolbox/GeometryToolbox.h" #include "../../Framework/Toolbox/SlicesSorter.h" @@ -39,12 +41,17 @@ #include "../../Framework/Volumes/VolumeImageGeometry.h" // From Orthanc framework +#include #include #include #include #include #include #include +#include + + +#include namespace OrthancStone @@ -65,7 +72,7 @@ virtual uint64_t GetRevision() = 0; // This call can take some time - virtual ISceneLayer* CreateSceneLayer() = 0; + virtual ISceneLayer* CreateSceneLayer(const CoordinateSystem3D& cuttingPlane) = 0; }; virtual ~IVolumeSlicer() @@ -76,6 +83,15 @@ }; + class IVolumeImageSlicer : public IVolumeSlicer + { + public: + virtual bool HasGeometry() const = 0; + + virtual const VolumeImageGeometry& GetGeometry() const = 0; + }; + + class InvalidExtractedSlice : public IVolumeSlicer::ExtractedSlice { public: @@ -89,7 +105,7 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } - virtual ISceneLayer* CreateSceneLayer() + virtual ISceneLayer* CreateSceneLayer(const CoordinateSystem3D& cuttingPlane) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } @@ -153,7 +169,7 @@ return GetRevisionInternal(projection_, sliceIndex_); } - virtual ISceneLayer* CreateSceneLayer() + virtual ISceneLayer* CreateSceneLayer(const CoordinateSystem3D& cuttingPlane) { CheckValid(); @@ -162,14 +178,32 @@ { const DicomInstanceParameters& parameters = GetDicomParameters(projection_, sliceIndex_); ImageBuffer3D::SliceReader reader(image_, projection_, sliceIndex_); - texture.reset(parameters.CreateTexture(reader.GetAccessor())); + + static unsigned int i = 1; + + if (i % 2) + { + texture.reset(parameters.CreateTexture(reader.GetAccessor())); + } + else + { + std::string lut; + Orthanc::EmbeddedResources::GetFileResource(lut, Orthanc::EmbeddedResources::COLORMAP_HOT); + + std::auto_ptr tmp(parameters.CreateLookupTableTexture(reader.GetAccessor())); + tmp->FitRange(); + tmp->SetLookupTable(lut); + texture.reset(tmp.release()); + } + + i++; } const CoordinateSystem3D& system = geometry_.GetProjectionGeometry(projection_); - + double x0, y0, x1, y1; - system.ProjectPoint(x0, y0, system.GetOrigin()); - system.ProjectPoint(x1, y1, system.GetOrigin() + system.GetAxisX()); + cuttingPlane.ProjectPoint(x0, y0, system.GetOrigin()); + cuttingPlane.ProjectPoint(x1, y1, system.GetOrigin() + system.GetAxisX()); texture->SetOrigin(x0, y0); double dx = x1 - x0; @@ -184,6 +218,28 @@ 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 } }; @@ -193,7 +249,7 @@ class DicomSeriesVolumeImage : public boost::noncopyable { public: - class ExtractedSlice : public DicomVolumeImageOrthogonalSlice + class ExtractedOrthogonalSlice : public DicomVolumeImageOrthogonalSlice { private: const DicomSeriesVolumeImage& that_; @@ -221,8 +277,8 @@ } public: - ExtractedSlice(const DicomSeriesVolumeImage& that, - const CoordinateSystem3D& plane) : + ExtractedOrthogonalSlice(const DicomSeriesVolumeImage& that, + const CoordinateSystem3D& plane) : DicomVolumeImageOrthogonalSlice(that.GetImage(), that.GetGeometry(), plane), that_(that) { @@ -331,7 +387,8 @@ public: - DicomSeriesVolumeImage() + DicomSeriesVolumeImage() : + revision_(0) { } @@ -479,8 +536,7 @@ class OrthancSeriesVolumeProgressiveLoader : - public IObserver, - public IVolumeSlicer + public IObserver { private: static const unsigned int LOW_QUALITY = 0; @@ -624,7 +680,61 @@ std::auto_ptr sorter_; std::auto_ptr strategy_; + + IVolumeSlicer::ExtractedSlice* ExtractOrthogonalSlice(const CoordinateSystem3D& cuttingPlane) const + { + if (volume_.HasGeometry() && + volume_.GetSlicesCount() != 0) + { + std::auto_ptr slice + (new DicomSeriesVolumeImage::ExtractedOrthogonalSlice(volume_, cuttingPlane)); + + assert(slice.get() != NULL && + strategy_.get() != NULL); + + if (slice->IsValid() && + slice->GetProjection() == VolumeProjection_Axial) + { + strategy_->SetCurrent(slice->GetSliceIndex()); + } + + return slice.release(); + } + else + { + return new InvalidExtractedSlice; + } + } + + public: + class MPRSlicer : public IVolumeImageSlicer + { + private: + boost::shared_ptr that_; + + public: + MPRSlicer(const boost::shared_ptr& that) : + that_(that) + { + } + + virtual ExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) const + { + return that_->ExtractOrthogonalSlice(cuttingPlane); + } + + virtual bool HasGeometry() const + { + return that_->GetVolume().HasGeometry(); + } + + virtual const VolumeImageGeometry& GetGeometry() const + { + return that_->GetVolume().GetGeometry(); + } + }; + OrthancSeriesVolumeProgressiveLoader(IOracle& oracle, IObservable& oracleObservable) : IObserver(oracleObservable.GetBroker()), @@ -684,64 +794,494 @@ { return volume_; } + }; + + + + class OrthancMultiframeVolumeLoader : public IObserver + { + private: + class State : public Orthanc::IDynamicObject + { + private: + OrthancMultiframeVolumeLoader& that_; + + protected: + void Schedule(OrthancRestApiCommand* command) const + { + that_.oracle_.Schedule(that_, command); + } + + OrthancMultiframeVolumeLoader& GetTarget() const + { + return that_; + } + + public: + State(OrthancMultiframeVolumeLoader& that) : + that_(that) + { + } + + virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) const = 0; + }; + + void Handle(const OrthancRestApiCommand::SuccessMessage& message) + { + dynamic_cast(message.GetOrigin().GetPayload()).Handle(message); + } + + + class LoadRTDoseGeometry : public State + { + private: + std::auto_ptr dicom_; + + public: + LoadRTDoseGeometry(OrthancMultiframeVolumeLoader& that, + Orthanc::DicomMap* dicom) : + State(that), + dicom_(dicom) + { + if (dicom == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + } + + virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) const + { + // Complete the DICOM tags with just-received "Grid Frame Offset Vector" + std::string s = Orthanc::Toolbox::StripSpaces(message.GetAnswer()); + dicom_->SetValue(Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR, s, false); + + GetTarget().SetGeometry(*dicom_); + } + }; - virtual IVolumeSlicer::ExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) const + static std::string GetSopClassUid(const Orthanc::DicomMap& dicom) { - if (volume_.HasGeometry() && - volume_.GetSlicesCount() != 0) + std::string s; + if (!dicom.CopyToString(s, Orthanc::DICOM_TAG_SOP_CLASS_UID, false)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, + "DICOM file without SOP class UID"); + } + else { - std::auto_ptr slice - (new DicomSeriesVolumeImage::ExtractedSlice(volume_, cuttingPlane)); + return s; + } + } + - assert(slice.get() != NULL && - strategy_.get() != NULL); - - if (slice->GetProjection() == VolumeProjection_Axial) + class LoadGeometry : public State + { + public: + LoadGeometry(OrthancMultiframeVolumeLoader& that) : + State(that) + { + } + + virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) const + { + Json::Value body; + message.ParseJsonBody(body); + + if (body.type() != Json::objectValue) { - strategy_->SetCurrent(slice->GetSliceIndex()); + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); } - return slice.release(); + std::auto_ptr dicom(new Orthanc::DicomMap); + dicom->FromDicomAsJson(body); + + if (StringToSopClassUid(GetSopClassUid(*dicom)) == SopClassUid_RTDose) + { + // Download the "Grid Frame Offset Vector" DICOM tag, that is + // mandatory for RT-DOSE, but is too long to be returned by default + + std::auto_ptr command(new OrthancRestApiCommand); + command->SetUri("/instances/" + GetTarget().GetInstanceId() + "/content/" + + Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR.Format()); + command->SetPayload(new LoadRTDoseGeometry(GetTarget(), dicom.release())); + + Schedule(command.release()); + } + else + { + GetTarget().SetGeometry(*dicom); + } + } + }; + + + + class LoadTransferSyntax : public State + { + public: + LoadTransferSyntax(OrthancMultiframeVolumeLoader& that) : + State(that) + { + } + + virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) const + { + GetTarget().SetTransferSyntax(message.GetAnswer()); + } + }; + + + class LoadUncompressedPixelData : public State + { + public: + LoadUncompressedPixelData(OrthancMultiframeVolumeLoader& that) : + State(that) + { + } + + virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) const + { + GetTarget().SetUncompressedPixelData(message.GetAnswer()); + } + }; + + + + IOracle& oracle_; + bool active_; + std::string instanceId_; + std::string transferSyntaxUid_; + uint64_t revision_; + + std::auto_ptr dicom_; + std::auto_ptr geometry_; + std::auto_ptr image_; + + + const std::string& GetInstanceId() const + { + if (active_) + { + return instanceId_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + void ScheduleFrameDownloads() + { + if (transferSyntaxUid_.empty() || + !HasGeometry()) + { + return; + } + + if (transferSyntaxUid_ == "1.2.840.10008.1.2" || + transferSyntaxUid_ == "1.2.840.10008.1.2.1" || + transferSyntaxUid_ == "1.2.840.10008.1.2.2") + { + std::auto_ptr command(new OrthancRestApiCommand); + command->SetHttpHeader("Accept-Encoding", "gzip"); + command->SetUri("/instances/" + instanceId_ + "/content/" + + Orthanc::DICOM_TAG_PIXEL_DATA.Format() + "/0"); + command->SetPayload(new LoadUncompressedPixelData(*this)); + oracle_.Schedule(*this, command.release()); } else { - return new InvalidExtractedSlice; + throw Orthanc::OrthancException( + Orthanc::ErrorCode_NotImplemented, + "No support for multiframe instances with transfer syntax: " + transferSyntaxUid_); + } + } + + + void SetTransferSyntax(const std::string& transferSyntax) + { + transferSyntaxUid_ = Orthanc::Toolbox::StripSpaces(transferSyntax); + ScheduleFrameDownloads(); + } + + + void SetGeometry(const Orthanc::DicomMap& dicom) + { + dicom_.reset(new DicomInstanceParameters(dicom)); + + Orthanc::PixelFormat format; + if (!dicom_->GetImageInformation().ExtractPixelFormat(format, true)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + double spacingZ; + switch (dicom_->GetSopClassUid()) + { + case SopClassUid_RTDose: + spacingZ = dicom_->GetThickness(); + break; + + default: + throw Orthanc::OrthancException( + Orthanc::ErrorCode_NotImplemented, + "No support for multiframe instances with SOP class UID: " + GetSopClassUid(dicom)); + } + + const unsigned int width = dicom_->GetImageInformation().GetWidth(); + const unsigned int height = dicom_->GetImageInformation().GetHeight(); + const unsigned int depth = dicom_->GetImageInformation().GetNumberOfFrames(); + + geometry_.reset(new VolumeImageGeometry); + geometry_->SetSize(width, height, depth); + geometry_->SetAxialGeometry(dicom_->GetGeometry()); + geometry_->SetVoxelDimensions(dicom_->GetPixelSpacingX(), + dicom_->GetPixelSpacingY(), + spacingZ); + + image_.reset(new ImageBuffer3D(format, width, height, depth, + false /* don't compute range */)); + image_->Clear(); + + ScheduleFrameDownloads(); + } + + + ORTHANC_FORCE_INLINE + static void CopyPixel(uint32_t& target, + const void* source) + { + // TODO - check alignement? + target = le32toh(*reinterpret_cast(source)); + } + + + template + void CopyPixelData(const std::string& pixelData) + { + const Orthanc::PixelFormat format = image_->GetFormat(); + const unsigned int bpp = image_->GetBytesPerPixel(); + const unsigned int width = image_->GetWidth(); + const unsigned int height = image_->GetHeight(); + const unsigned int depth = image_->GetDepth(); + + if (pixelData.size() != bpp * width * height * depth) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, + "The pixel data has not the proper size"); + } + + if (pixelData.empty()) + { + return; + } + + const uint8_t* source = reinterpret_cast(pixelData.c_str()); + + for (unsigned int z = 0; z < depth; z++) + { + ImageBuffer3D::SliceWriter writer(*image_, VolumeProjection_Axial, z); + + assert (writer.GetAccessor().GetWidth() == width && + writer.GetAccessor().GetHeight() == height); + + for (unsigned int y = 0; y < height; y++) + { + assert(sizeof(T) == Orthanc::GetBytesPerPixel(format)); + + T* target = reinterpret_cast(writer.GetAccessor().GetRow(y)); + + for (unsigned int x = 0; x < width; x++) + { + CopyPixel(*target, source); + + target ++; + source += bpp; + } + } + } + } + + + void SetUncompressedPixelData(const std::string& pixelData) + { + switch (image_->GetFormat()) + { + case Orthanc::PixelFormat_Grayscale32: + CopyPixelData(pixelData); + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + revision_ ++; + } + + + private: + class ExtractedOrthogonalSlice : public DicomVolumeImageOrthogonalSlice + { + private: + const OrthancMultiframeVolumeLoader& that_; + + protected: + virtual uint64_t GetRevisionInternal(VolumeProjection projection, + unsigned int sliceIndex) const + { + return that_.revision_; + } + + virtual const DicomInstanceParameters& GetDicomParameters(VolumeProjection projection, + unsigned int sliceIndex) const + { + return that_.GetDicomParameters(); + } + + public: + ExtractedOrthogonalSlice(const OrthancMultiframeVolumeLoader& that, + const CoordinateSystem3D& plane) : + DicomVolumeImageOrthogonalSlice(that.GetImage(), that.GetGeometry(), plane), + that_(that) + { + } + }; + + + public: + class MPRSlicer : public IVolumeImageSlicer + { + private: + boost::shared_ptr that_; + + public: + MPRSlicer(const boost::shared_ptr& that) : + that_(that) + { + } + + virtual ExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) const + { + if (that_->HasGeometry()) + { + return new ExtractedOrthogonalSlice(*that_, cuttingPlane); + } + else + { + return new InvalidExtractedSlice; + } + } + + virtual bool HasGeometry() const + { + return that_->HasGeometry(); + } + + virtual const VolumeImageGeometry& GetGeometry() const + { + return that_->GetGeometry(); + } + }; + + + OrthancMultiframeVolumeLoader(IOracle& oracle, + IObservable& oracleObservable) : + IObserver(oracleObservable.GetBroker()), + oracle_(oracle), + active_(false), + revision_(0) + { + oracleObservable.RegisterObserverCallback( + new Callable + (*this, &OrthancMultiframeVolumeLoader::Handle)); + } + + + bool HasGeometry() const + { + return (dicom_.get() != NULL && + geometry_.get() != NULL && + image_.get() != NULL); + } + + + const ImageBuffer3D& GetImage() const + { + if (!HasGeometry()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + return *image_; + } + } + + + const VolumeImageGeometry& GetGeometry() const + { + if (!HasGeometry()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + return *geometry_; + } + } + + + const DicomInstanceParameters& GetDicomParameters() const + { + if (!HasGeometry()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + return *dicom_; + } + } + + + void LoadInstance(const std::string& instanceId) + { + if (active_) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + active_ = true; + instanceId_ = instanceId; + + { + std::auto_ptr command(new OrthancRestApiCommand); + command->SetHttpHeader("Accept-Encoding", "gzip"); + command->SetUri("/instances/" + instanceId + "/tags"); + command->SetPayload(new LoadGeometry(*this)); + oracle_.Schedule(*this, command.release()); + } + + { + std::auto_ptr command(new OrthancRestApiCommand); + command->SetUri("/instances/" + instanceId + "/metadata/TransferSyntax"); + command->SetPayload(new LoadTransferSyntax(*this)); + oracle_.Schedule(*this, command.release()); + } } } }; -#if 0 - void LoadInstance(const std::string& instanceId) - { - if (active_) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - - active_ = true; - - // Tag "3004-000c" is "Grid Frame Offset Vector", which is - // mandatory to read RT DOSE, but is too long to be returned by default - - // TODO => Should be part of a second call if needed - - std::auto_ptr command(new OrthancRestApiCommand); - command->SetUri("/instances/" + instanceId + "/tags?ignore-length=3004-000c"); - command->SetPayload(new LoadInstanceGeometryHandler(*this)); - - oracle_.Schedule(*this, command.release()); - } -#endif - - - class SceneVolumeSlicer : public boost::noncopyable + class VolumeSceneLayerSource : public boost::noncopyable { private: int layerDepth_; - std::auto_ptr volume_; + boost::shared_ptr slicer_; bool linearInterpolation_; std::auto_ptr lastPlane_; uint64_t lastRevision_; @@ -755,13 +1295,13 @@ } public: - SceneVolumeSlicer(int layerDepth, - IVolumeSlicer* volume) : // Takes ownership + VolumeSceneLayerSource(int layerDepth, + IVolumeSlicer* slicer) : // Takes ownership layerDepth_(layerDepth), - volume_(volume), + slicer_(slicer), linearInterpolation_(false) { - if (volume == NULL) + if (slicer == NULL) { throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); } @@ -769,7 +1309,7 @@ const IVolumeSlicer& GetSlicer() const { - return *volume_; + return *slicer_; } void SetLinearInterpolation(bool enabled) @@ -785,8 +1325,8 @@ void Update(Scene2D& scene, const CoordinateSystem3D& plane) { - assert(volume_.get() != NULL); - std::auto_ptr slice(volume_->ExtractSlice(plane)); + assert(slicer_.get() != NULL); + std::auto_ptr slice(slicer_->ExtractSlice(plane)); if (slice.get() == NULL) { @@ -811,7 +1351,7 @@ lastPlane_.reset(new CoordinateSystem3D(plane)); lastRevision_ = slice->GetRevision(); - std::auto_ptr layer(slice->CreateSceneLayer()); + std::auto_ptr layer(slice->CreateSceneLayer(plane)); if (layer.get() == NULL) { throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); @@ -909,7 +1449,28 @@ private: OrthancStone::IOracle& oracle_; OrthancStone::Scene2D scene_; - std::auto_ptr slicer_; + std::auto_ptr source1_, source2_; + + + OrthancStone::CoordinateSystem3D GetSamplePlane + (const OrthancStone::VolumeSceneLayerSource& source) const + { + const OrthancStone::IVolumeImageSlicer& slicer = + dynamic_cast(source.GetSlicer()); + + OrthancStone::CoordinateSystem3D plane; + + if (slicer.HasGeometry()) + { + //plane = slicer.GetGeometry().GetSagittalGeometry(); + //plane = slicer.GetGeometry().GetAxialGeometry(); + plane = slicer.GetGeometry().GetCoronalGeometry(); + plane.SetOrigin(slicer.GetGeometry().GetCoordinates(0.5f, 0.5f, 0.5f)); + } + + return plane; + } + void Handle(const OrthancStone::SleepOracleCommand::TimeoutMessage& message) { @@ -921,40 +1482,53 @@ { printf("TIMEOUT\n"); - if (slicer_.get() != NULL) - { - OrthancStone::CoordinateSystem3D plane; + OrthancStone::CoordinateSystem3D plane; - const OrthancStone::OrthancSeriesVolumeProgressiveLoader& loader = - dynamic_cast(slicer_->GetSlicer()); - - if (loader.GetVolume().HasGeometry()) - { - plane = loader.GetVolume().GetGeometry().GetSagittalGeometry(); - plane.SetOrigin(loader.GetVolume().GetGeometry().GetCoordinates(0.5f, 0.5f, 0.5f)); - } + if (source1_.get() != NULL) + { + plane = GetSamplePlane(*source1_); + } + else if (source2_.get() != NULL) + { + plane = GetSamplePlane(*source2_); + } - slicer_->Update(scene_, plane); - scene_.FitContent(1024, 768); + if (source1_.get() != NULL) + { + source1_->Update(scene_, plane); + } + + if (source2_.get() != NULL) + { + source2_->Update(scene_, plane); + } - { - OrthancStone::CairoCompositor compositor(scene_, 1024, 768); - compositor.Refresh(); - - Orthanc::ImageAccessor accessor; - compositor.GetCanvas().GetReadOnlyAccessor(accessor); + scene_.FitContent(1024, 768); + + { + OrthancStone::CairoCompositor compositor(scene_, 1024, 768); + compositor.Refresh(); + + Orthanc::ImageAccessor accessor; + compositor.GetCanvas().GetReadOnlyAccessor(accessor); - Orthanc::Image tmp(Orthanc::PixelFormat_RGB24, accessor.GetWidth(), accessor.GetHeight(), false); - Orthanc::ImageProcessing::Convert(tmp, accessor); + Orthanc::Image tmp(Orthanc::PixelFormat_RGB24, accessor.GetWidth(), accessor.GetHeight(), false); + Orthanc::ImageProcessing::Convert(tmp, accessor); + + static unsigned int count = 0; + char buf[64]; + sprintf(buf, "scene-%06d.png", count++); + + Orthanc::PngWriter writer; + writer.WriteToFile(buf, tmp); + } - static unsigned int count = 0; - char buf[64]; - sprintf(buf, "scene-%06d.png", count++); - - Orthanc::PngWriter writer; - writer.WriteToFile(buf, tmp); - } - } + /** + * The sleep() leads to a crash if the oracle is still running, + * while this object is destroyed. Always stop the oracle before + * destroying active objects. (*) + **/ + // boost::this_thread::sleep(boost::posix_time::seconds(2)); oracle_.Schedule(*this, new OrthancStone::SleepOracleCommand(message.GetOrigin().GetDelay())); } @@ -1021,25 +1595,33 @@ (*this, &Toto::Handle)); } - void SetVolume(int depth, - OrthancStone::IVolumeSlicer* volume) + void SetVolume1(int depth, + OrthancStone::IVolumeSlicer* volume) { - slicer_.reset(new OrthancStone::SceneVolumeSlicer(0, volume)); + source1_.reset(new OrthancStone::VolumeSceneLayerSource(depth, volume)); + } + + void SetVolume2(int depth, + OrthancStone::IVolumeSlicer* volume) + { + source2_.reset(new OrthancStone::VolumeSceneLayerSource(depth, volume)); } }; void Run(OrthancStone::NativeApplicationContext& context, - OrthancStone::IOracle& oracle) + OrthancStone::ThreadedOracle& oracle) { - std::auto_ptr toto; - std::auto_ptr loader1, loader2; + boost::shared_ptr toto; + boost::shared_ptr loader1, loader2; + boost::shared_ptr loader3; { OrthancStone::NativeApplicationContext::WriterLock lock(context); toto.reset(new Toto(oracle, lock.GetOracleObservable())); loader1.reset(new OrthancStone::OrthancSeriesVolumeProgressiveLoader(oracle, lock.GetOracleObservable())); loader2.reset(new OrthancStone::OrthancSeriesVolumeProgressiveLoader(oracle, lock.GetOracleObservable())); + loader3.reset(new OrthancStone::OrthancMultiframeVolumeLoader(oracle, lock.GetOracleObservable())); } oracle.Schedule(*toto, new OrthancStone::SleepOracleCommand(100)); @@ -1120,19 +1702,37 @@ } // 2017-11-17-Anonymized - //loader1->LoadSeries("cb3ea4d1-d08f3856-ad7b6314-74d88d77-60b05618"); // CT - //loader2->LoadInstance("41029085-71718346-811efac4-420e2c15-d39f99b6"); // RT-DOSE + loader1->LoadSeries("cb3ea4d1-d08f3856-ad7b6314-74d88d77-60b05618"); // CT + loader3->LoadInstance("41029085-71718346-811efac4-420e2c15-d39f99b6"); // RT-DOSE + // 2015-01-28-Multiframe + //loader3->LoadInstance("88f71e2a-5fad1c61-96ed14d6-5b3d3cf7-a5825279"); // Multiframe CT + // Delphine //loader1->LoadSeries("5990e39c-51e5f201-fe87a54c-31a55943-e59ef80e"); // CT - loader1->LoadSeries("67f1b334-02c16752-45026e40-a5b60b6b-030ecab5"); // Lung 1/10mm + //loader1->LoadSeries("67f1b334-02c16752-45026e40-a5b60b6b-030ecab5"); // Lung 1/10mm - toto->SetVolume(0, loader1.release()); + toto->SetVolume2(1, new OrthancStone::OrthancMultiframeVolumeLoader::MPRSlicer(loader3)); + toto->SetVolume1(0, new OrthancStone::OrthancSeriesVolumeProgressiveLoader::MPRSlicer(loader1)); + + { + oracle.Start(); + + LOG(WARNING) << "...Waiting for Ctrl-C..."; + Orthanc::SystemToolbox::ServerBarrier(); - LOG(WARNING) << "...Waiting for Ctrl-C..."; - Orthanc::SystemToolbox::ServerBarrier(); - //boost::this_thread::sleep(boost::posix_time::seconds(1)); + /** + * WARNING => The oracle must be stopped BEFORE the objects using + * it are destroyed!!! This forces to wait for the completion of + * the running callback methods. Otherwise, the callbacks methods + * might still be running while their parent object is destroyed, + * resulting in crashes. This is very visible if adding a sleep(), + * as in (*). + **/ + + oracle.Stop(); + } } @@ -1152,7 +1752,7 @@ OrthancStone::NativeApplicationContext context; OrthancStone::ThreadedOracle oracle(context); - oracle.SetThreadsCount(1); + //oracle.SetThreadsCount(1); { Orthanc::WebServiceParameters p; @@ -1161,11 +1761,11 @@ oracle.SetOrthancParameters(p); } - oracle.Start(); + //oracle.Start(); Run(context, oracle); - - oracle.Stop(); + + //oracle.Stop(); } catch (Orthanc::OrthancException& e) {