# HG changeset patch # User Benjamin Golinvaux # Date 1558077646 -7200 # Node ID 5c551f078c18ddb09447765269595bfc2125f234 # Parent 8b6adfb62a2fd1d980c97251ce765909f8003c11# Parent 9a474e90e8321cfc5330d35ffecffa2a8daa5692 Merge from default diff -r 8b6adfb62a2f -r 5c551f078c18 Applications/Sdl/SdlWindow.cpp --- a/Applications/Sdl/SdlWindow.cpp Wed May 15 16:56:17 2019 +0200 +++ b/Applications/Sdl/SdlWindow.cpp Fri May 17 09:20:46 2019 +0200 @@ -27,7 +27,7 @@ #include #ifdef WIN32 -#include // for SetProcessDpiAware +#include // for SetProcessDpiAware #endif // WIN32 @@ -59,7 +59,7 @@ } // TODO: probably required on MacOS X, too -#ifdef WIN32 +#if defined(WIN32) && (_WIN32_WINNT >= 0x0600) if (!allowDpiScaling) { // if we do NOT allow DPI scaling, it means an SDL pixel will be a real @@ -70,10 +70,10 @@ // THE FOLLOWING HAS BEEN COMMENTED OUT BECAUSE IT WILL CRASH UNDER // OLD WINDOWS VERSIONS // ADD THIS AT THE TOP TO ENABLE IT: - // - //#pragma comment(lib, "Shcore.lib") THIS IS ONLY REQUIRED FOR SetProcessDpiAwareness - //#include - //#include THIS IS ONLY REQUIRED FOR SetProcessDpiAwareness + // + //#pragma comment(lib, "Shcore.lib") THIS IS ONLY REQUIRED FOR SetProcessDpiAwareness + //#include + //#include THIS IS ONLY REQUIRED FOR SetProcessDpiAwareness //#include THIS IS ONLY REQUIRED FOR SetProcessDpiAwareness // SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE); diff -r 8b6adfb62a2f -r 5c551f078c18 Applications/Wasm/StartupParametersBuilder.cpp --- a/Applications/Wasm/StartupParametersBuilder.cpp Wed May 15 16:56:17 2019 +0200 +++ b/Applications/Wasm/StartupParametersBuilder.cpp Fri May 17 09:20:46 2019 +0200 @@ -1,9 +1,11 @@ #include "StartupParametersBuilder.h" #include +#include +#include "emscripten/html5.h" namespace OrthancStone { - void StartupParametersBuilder::Clear() + void StartupParametersBuilder::Clear() { startupParameters_.clear(); } @@ -27,21 +29,28 @@ std::vector argv(startupParameters_.size() + 1); int argCounter = 0; - argvStrings[argCounter] = "Toto.exe"; + argvStrings[argCounter] = "dummy.exe"; argv[argCounter] = argvStrings[argCounter].c_str(); argCounter++; - + std::string cmdLine = ""; for ( StartupParameters::const_iterator it = startupParameters_.begin(); it != startupParameters_.end(); it++) { - std::stringstream argSs; + std::stringstream argSs; - argSs << "--" << std::get<0>(*it); - if(std::get<1>(*it).length() > 0) - argSs << "=" << std::get<1>(*it); + argSs << "--" << std::get<0>(*it); + if(std::get<1>(*it).length() > 0) + argSs << "=" << std::get<1>(*it); + + argvStrings[argCounter] = argSs.str(); + cmdLine = cmdLine + " " + argvStrings[argCounter]; + std::cout << cmdLine << std::endl; + argv[argCounter] = argvStrings[argCounter].c_str(); + argCounter++; + } std::cout << "simulated cmdLine = \"" << cmdLine.c_str() << "\"\n"; @@ -56,6 +65,7 @@ catch (boost::program_options::error& e) { std::cerr << "Error while parsing the command-line arguments: " << - e.what() << std::endl; } + e.what() << std::endl; + } } } diff -r 8b6adfb62a2f -r 5c551f078c18 Framework/Fonts/FontRenderer.cpp --- a/Framework/Fonts/FontRenderer.cpp Wed May 15 16:56:17 2019 +0200 +++ b/Framework/Fonts/FontRenderer.cpp Fri May 17 09:20:46 2019 +0200 @@ -101,7 +101,9 @@ const FT_Byte* data = reinterpret_cast(fontContent_.c_str()); - CheckError(FT_New_Memory_Face(library_, data, fontContent_.size(), 0, &face_)); + CheckError(FT_New_Memory_Face( + library_, data, static_cast(fontContent_.size()), 0, &face_)); + CheckError(FT_Set_Char_Size(face_, // handle to face object 0, // char_width in 1/64th of points fontSize * 64, // char_height in 1/64th of points diff -r 8b6adfb62a2f -r 5c551f078c18 Framework/Fonts/GlyphTextureAlphabet.cpp --- a/Framework/Fonts/GlyphTextureAlphabet.cpp Wed May 15 16:56:17 2019 +0200 +++ b/Framework/Fonts/GlyphTextureAlphabet.cpp Fri May 17 09:20:46 2019 +0200 @@ -89,7 +89,7 @@ column_(0), row_(0) { - int c = boost::math::iround(sqrt(static_cast(countGlyphs))); + int c = boost::math::iround(sqrt(static_cast(countGlyphs))); if (c <= 0) { @@ -239,9 +239,9 @@ sourceAlphabet.GetAlphabet().Apply(size); TextureGenerator generator(alphabet_, - sourceAlphabet.GetAlphabet().GetSize(), - size.GetMaxWidth(), - size.GetMaxHeight()); + static_cast(sourceAlphabet.GetAlphabet().GetSize()), + size.GetMaxWidth(), + size.GetMaxHeight()); sourceAlphabet.GetAlphabet().Apply(generator); texture_.reset(generator.ReleaseTexture()); diff -r 8b6adfb62a2f -r 5c551f078c18 Framework/Fonts/OpenGLTextCoordinates.cpp --- a/Framework/Fonts/OpenGLTextCoordinates.cpp Wed May 15 16:56:17 2019 +0200 +++ b/Framework/Fonts/OpenGLTextCoordinates.cpp Fri May 17 09:20:46 2019 +0200 @@ -35,8 +35,8 @@ const Orthanc::IDynamicObject* payload) { // Rendering coordinates - float rx1 = x - box_.GetLeft(); - float ry1 = y - box_.GetTop(); + float rx1 = static_cast(x - box_.GetLeft()); + float ry1 = static_cast(y - box_.GetTop()); float rx2 = rx1 + static_cast(width); float ry2 = ry1 + static_cast(height); @@ -81,8 +81,8 @@ OpenGLTextCoordinates::OpenGLTextCoordinates(const GlyphTextureAlphabet& alphabet, const std::string& utf8) : box_(alphabet.GetAlphabet(), utf8), - textureWidth_(alphabet.GetTextureWidth()), - textureHeight_(alphabet.GetTextureHeight()) + textureWidth_(static_cast(alphabet.GetTextureWidth())), + textureHeight_(static_cast(alphabet.GetTextureHeight())) { if (textureWidth_ <= 0 || textureHeight_ <= 0) diff -r 8b6adfb62a2f -r 5c551f078c18 Framework/Scene2D/Internals/OpenGLLinesProgram.cpp diff -r 8b6adfb62a2f -r 5c551f078c18 Framework/Scene2D/Internals/OpenGLTextProgram.cpp --- a/Framework/Scene2D/Internals/OpenGLTextProgram.cpp Wed May 15 16:56:17 2019 +0200 +++ b/Framework/Scene2D/Internals/OpenGLTextProgram.cpp Fri May 17 09:20:46 2019 +0200 @@ -155,8 +155,9 @@ program_->Use(); double dx, dy; // In pixels - ComputeAnchorTranslation(dx, dy, data.GetAnchor(), data.GetTextWidth(), - data.GetTextHeight(), static_cast(data.GetBorder())); + ComputeAnchorTranslation(dx, dy, data.GetAnchor(), + data.GetTextWidth(), data.GetTextHeight(), + static_cast(data.GetBorder())); double x = data.GetX(); double y = data.GetY(); diff -r 8b6adfb62a2f -r 5c551f078c18 Framework/Scene2D/Scene2D.h --- a/Framework/Scene2D/Scene2D.h Wed May 15 16:56:17 2019 +0200 +++ b/Framework/Scene2D/Scene2D.h Fri May 17 09:20:46 2019 +0200 @@ -119,4 +119,3 @@ unsigned int canvasHeight); }; } - diff -r 8b6adfb62a2f -r 5c551f078c18 Framework/Scene2D/ScenePoint2D.h --- a/Framework/Scene2D/ScenePoint2D.h Wed May 15 16:56:17 2019 +0200 +++ b/Framework/Scene2D/ScenePoint2D.h Fri May 17 09:20:46 2019 +0200 @@ -23,6 +23,7 @@ #include "../Toolbox/AffineTransform2D.h" + namespace OrthancStone { class ScenePoint2D diff -r 8b6adfb62a2f -r 5c551f078c18 Framework/StoneInitialization.cpp --- a/Framework/StoneInitialization.cpp Wed May 15 16:56:17 2019 +0200 +++ b/Framework/StoneInitialization.cpp Fri May 17 09:20:46 2019 +0200 @@ -22,7 +22,6 @@ #include "StoneInitialization.h" #include -#include #if !defined(ORTHANC_ENABLE_SDL) # error Macro ORTHANC_ENABLE_SDL must be defined @@ -34,9 +33,17 @@ namespace OrthancStone { +#if ORTHANC_ENABLE_LOGGING_PLUGIN == 1 + void StoneInitialize(OrthancPluginContext* context) +#else void StoneInitialize() +#endif { +#if ORTHANC_ENABLE_LOGGING_PLUGIN == 1 + Orthanc::Logging::Initialize(context); +#else Orthanc::Logging::Initialize(); +#endif #if ORTHANC_ENABLE_SDL == 1 OrthancStone::SdlWindow::GlobalInitialize(); diff -r 8b6adfb62a2f -r 5c551f078c18 Framework/StoneInitialization.h --- a/Framework/StoneInitialization.h Wed May 15 16:56:17 2019 +0200 +++ b/Framework/StoneInitialization.h Fri May 17 09:20:46 2019 +0200 @@ -21,9 +21,15 @@ #pragma once +#include + namespace OrthancStone { +#if ORTHANC_ENABLE_LOGGING_PLUGIN == 1 + void StoneInitialize(OrthancPluginContext* context); +#else void StoneInitialize(); +#endif void StoneFinalize(); } diff -r 8b6adfb62a2f -r 5c551f078c18 Framework/Toolbox/DownloadStack.cpp --- a/Framework/Toolbox/DownloadStack.cpp Wed May 15 16:56:17 2019 +0200 +++ b/Framework/Toolbox/DownloadStack.cpp Fri May 17 09:20:46 2019 +0200 @@ -77,8 +77,8 @@ { for (size_t i = 0; i < size; i++) { - nodes_[i].prev_ = i - 1; - nodes_[i].next_ = i + 1; + nodes_[i].prev_ = static_cast(i - 1); + nodes_[i].next_ = static_cast(i + 1); nodes_[i].dequeued_ = false; } diff -r 8b6adfb62a2f -r 5c551f078c18 Framework/Toolbox/FiniteProjectiveCamera.cpp --- a/Framework/Toolbox/FiniteProjectiveCamera.cpp Wed May 15 16:56:17 2019 +0200 +++ b/Framework/Toolbox/FiniteProjectiveCamera.cpp Fri May 17 09:20:46 2019 +0200 @@ -316,7 +316,7 @@ LOG(WARNING) << "Output pixel format: " << Orthanc::EnumerationToString(target.GetFormat()); std::auto_ptr slices(source.GetGeometry(projection)); - const OrthancStone::Vector pixelSpacing = source.GetVoxelDimensions(projection); + const OrthancStone::Vector pixelSpacing = source.GetGeometry().GetVoxelDimensions(projection); const unsigned int targetWidth = target.GetWidth(); const unsigned int targetHeight = target.GetHeight(); @@ -360,7 +360,8 @@ // Read and accumulate the value of the pixel float pixel; - if (pixelReader.GetFloatValue(pixel, ix, iy)) + if (pixelReader.GetFloatValue( + pixel, static_cast(ix), static_cast(iy))) { if (MIP) { diff -r 8b6adfb62a2f -r 5c551f078c18 Framework/Toolbox/OrientedBoundingBox.cpp --- a/Framework/Toolbox/OrientedBoundingBox.cpp Wed May 15 16:56:17 2019 +0200 +++ b/Framework/Toolbox/OrientedBoundingBox.cpp Fri May 17 09:20:46 2019 +0200 @@ -37,8 +37,8 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize); } - const CoordinateSystem3D& geometry = image.GetAxialGeometry(); - Vector dim = image.GetVoxelDimensions(VolumeProjection_Axial); + const CoordinateSystem3D& geometry = image.GetGeometry().GetAxialGeometry(); + Vector dim = image.GetGeometry().GetVoxelDimensions(VolumeProjection_Axial); u_ = geometry.GetAxisX(); v_ = geometry.GetAxisY(); diff -r 8b6adfb62a2f -r 5c551f078c18 Framework/Toolbox/OrthancSlicesLoader.cpp --- a/Framework/Toolbox/OrthancSlicesLoader.cpp Wed May 15 16:56:17 2019 +0200 +++ b/Framework/Toolbox/OrthancSlicesLoader.cpp Fri May 17 09:20:46 2019 +0200 @@ -771,16 +771,22 @@ } orthanc_.GetBinaryAsync(uri, "image/png", - new Callable(*this, &OrthancSlicesLoader::ParseSliceImagePng), - new Callable(*this, &OrthancSlicesLoader::OnSliceImageError), - Operation::DownloadSliceImage(index, slice, SliceImageQuality_FullPng)); - } + new Callable + (*this, &OrthancSlicesLoader::ParseSliceImagePng), + new Callable + (*this, &OrthancSlicesLoader::OnSliceImageError), + Operation::DownloadSliceImage( + static_cast(index), slice, SliceImageQuality_FullPng)); +} void OrthancSlicesLoader::ScheduleSliceImagePam(const Slice& slice, size_t index) { - std::string uri = ("/instances/" + slice.GetOrthancInstanceId() + "/frames/" + - boost::lexical_cast(slice.GetFrame())); + std::string uri = + ("/instances/" + slice.GetOrthancInstanceId() + "/frames/" + + boost::lexical_cast(slice.GetFrame())); switch (slice.GetConverter().GetExpectedPixelFormat()) { @@ -801,9 +807,14 @@ } orthanc_.GetBinaryAsync(uri, "image/x-portable-arbitrarymap", - new Callable(*this, &OrthancSlicesLoader::ParseSliceImagePam), - new Callable(*this, &OrthancSlicesLoader::OnSliceImageError), - Operation::DownloadSliceImage(index, slice, SliceImageQuality_FullPam)); + new Callable + (*this, &OrthancSlicesLoader::ParseSliceImagePam), + new Callable + (*this, &OrthancSlicesLoader::OnSliceImageError), + Operation::DownloadSliceImage(static_cast(index), + slice, SliceImageQuality_FullPam)); } @@ -839,9 +850,14 @@ boost::lexical_cast(slice.GetFrame())); orthanc_.GetJsonAsync(uri, - new Callable(*this, &OrthancSlicesLoader::ParseSliceImageJpeg), - new Callable(*this, &OrthancSlicesLoader::OnSliceImageError), - Operation::DownloadSliceImage(index, slice, quality)); + new Callable + (*this, &OrthancSlicesLoader::ParseSliceImageJpeg), + new Callable + (*this, &OrthancSlicesLoader::OnSliceImageError), + Operation::DownloadSliceImage( + static_cast(index), slice, quality)); } @@ -875,9 +891,14 @@ std::string uri = ("/instances/" + slice.GetOrthancInstanceId() + "/frames/" + boost::lexical_cast(slice.GetFrame()) + "/raw.gz"); orthanc_.GetBinaryAsync(uri, IWebService::HttpHeaders(), - new Callable(*this, &OrthancSlicesLoader::ParseSliceRawImage), - new Callable(*this, &OrthancSlicesLoader::OnSliceImageError), - Operation::DownloadSliceRawImage(index, slice)); + new Callable + (*this, &OrthancSlicesLoader::ParseSliceRawImage), + new Callable + (*this, &OrthancSlicesLoader::OnSliceImageError), + Operation::DownloadSliceRawImage( + static_cast(index), slice)); } } } diff -r 8b6adfb62a2f -r 5c551f078c18 Framework/Toolbox/ParallelSlicesCursor.cpp --- a/Framework/Toolbox/ParallelSlicesCursor.cpp Wed May 15 16:56:17 2019 +0200 +++ b/Framework/Toolbox/ParallelSlicesCursor.cpp Fri May 17 09:20:46 2019 +0200 @@ -110,7 +110,7 @@ return false; } - int count = slices_->GetSliceCount(); + int count = static_cast(slices_->GetSliceCount()); if (count == 0) { return false; @@ -123,7 +123,7 @@ } else { - slice = currentSlice_; + slice = static_cast(currentSlice_); } switch (mode) diff -r 8b6adfb62a2f -r 5c551f078c18 Framework/Toolbox/ShearWarpProjectiveTransform.cpp --- a/Framework/Toolbox/ShearWarpProjectiveTransform.cpp Wed May 15 16:56:17 2019 +0200 +++ b/Framework/Toolbox/ShearWarpProjectiveTransform.cpp Fri May 17 09:20:46 2019 +0200 @@ -195,15 +195,20 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); } - intermediateWidth_ = std::ceil(extent.GetWidth() / maxScaling); - intermediateHeight_ = std::ceil(extent.GetHeight() / maxScaling); + intermediateWidth_ = + static_cast(std::ceil(extent.GetWidth() / maxScaling)); + intermediateHeight_ = + static_cast(std::ceil(extent.GetHeight() / maxScaling)); // This is the product "T * S" in Equation (A.16) on page 209 Matrix TS = LinearAlgebra::Product( - GeometryToolbox::CreateTranslationMatrix(static_cast(intermediateWidth_) / 2.0, - static_cast(intermediateHeight_) / 2.0, 0), - GeometryToolbox::CreateScalingMatrix(1.0 / maxScaling, 1.0 / maxScaling, 1), - GeometryToolbox::CreateTranslationMatrix(-extent.GetCenterX(), -extent.GetCenterY(), 0)); + GeometryToolbox::CreateTranslationMatrix( + static_cast(intermediateWidth_) / 2.0, + static_cast(intermediateHeight_) / 2.0, 0), + GeometryToolbox::CreateScalingMatrix( + 1.0 / maxScaling, 1.0 / maxScaling, 1), + GeometryToolbox::CreateTranslationMatrix( + -extent.GetCenterX(), -extent.GetCenterY(), 0)); // This is Equation (A.16) on page 209. WARNING: There is an // error in Lacroute's thesis: "inv(MM_shear)" is used instead @@ -380,8 +385,8 @@ // Compute the "world" matrix that maps the source volume to the // (0,0,0)->(1,1,1) unit cube - Vector origin = source.GetCoordinates(0, 0, 0); - Vector ps = source.GetVoxelDimensions(VolumeProjection_Axial); + Vector origin = source.GetGeometry().GetCoordinates(0, 0, 0); + Vector ps = source.GetGeometry().GetVoxelDimensions(VolumeProjection_Axial); Matrix world = LinearAlgebra::Product( GeometryToolbox::CreateScalingMatrix(1.0 / ps[0], 1.0 / ps[1], 1.0 / ps[2]), GeometryToolbox::CreateTranslationMatrix(-origin[0], -origin[1], -origin[2])); diff -r 8b6adfb62a2f -r 5c551f078c18 Framework/Toolbox/SlicesSorter.cpp --- a/Framework/Toolbox/SlicesSorter.cpp Wed May 15 16:56:17 2019 +0200 +++ b/Framework/Toolbox/SlicesSorter.cpp Fri May 17 09:20:46 2019 +0200 @@ -285,4 +285,42 @@ return found; } + + + double SlicesSorter::ComputeSpacingBetweenSlices() const + { + if (GetSlicesCount() <= 1) + { + // This is a volume that is empty or that contains one single + // slice: Choose a dummy z-dimension for voxels + return 1.0; + } + + const OrthancStone::CoordinateSystem3D& reference = GetSliceGeometry(0); + + double referencePosition = reference.ProjectAlongNormal(reference.GetOrigin()); + + double p = reference.ProjectAlongNormal(GetSliceGeometry(1).GetOrigin()); + double spacingZ = p - referencePosition; + + if (spacingZ <= 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, + "Please call the Sort() method before"); + } + + for (size_t i = 1; i < GetSlicesCount(); i++) + { + OrthancStone::Vector p = reference.GetOrigin() + spacingZ * static_cast(i) * reference.GetNormal(); + double d = boost::numeric::ublas::norm_2(p - GetSliceGeometry(i).GetOrigin()); + + if (!OrthancStone::LinearAlgebra::IsNear(d, 0, 0.001 /* tolerance expressed in mm */)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, + "The origins of the slices of a volume image are not regularly spaced"); + } + } + + return spacingZ; + } } diff -r 8b6adfb62a2f -r 5c551f078c18 Framework/Toolbox/SlicesSorter.h --- a/Framework/Toolbox/SlicesSorter.h Wed May 15 16:56:17 2019 +0200 +++ b/Framework/Toolbox/SlicesSorter.h Fri May 17 09:20:46 2019 +0200 @@ -80,10 +80,17 @@ const Orthanc::IDynamicObject& GetSlicePayload(size_t i) const; + // WARNING - Apply the sorting algorithm can reduce the number of + // slices. This is notably the case if all the slices are not + // parallel to the reference normal that will be selected. bool Sort(); - + + // TODO - Remove this bool LookupClosestSlice(size_t& index, double& distance, const CoordinateSystem3D& slice) const; + + // WARNING - The slices must have been sorted before calling this method + double ComputeSpacingBetweenSlices() const; }; } diff -r 8b6adfb62a2f -r 5c551f078c18 Framework/Toolbox/ViewportGeometry.cpp --- a/Framework/Toolbox/ViewportGeometry.cpp Wed May 15 16:56:17 2019 +0200 +++ b/Framework/Toolbox/ViewportGeometry.cpp Fri May 17 09:20:46 2019 +0200 @@ -128,7 +128,12 @@ sceneTouches.clear(); for (size_t t = 0; t < displayTouches.size(); t++) { - MapPixelCenterToScene(sceneX, sceneY, displayTouches[t].x, displayTouches[t].y); + MapPixelCenterToScene( + sceneX, + sceneY, + static_cast(displayTouches[t].x), + static_cast(displayTouches[t].y)); + sceneTouches.push_back(Touch((float)sceneX, (float)sceneY)); } } diff -r 8b6adfb62a2f -r 5c551f078c18 Framework/Toolbox/VolumeImageGeometry.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Toolbox/VolumeImageGeometry.cpp Fri May 17 09:20:46 2019 +0200 @@ -0,0 +1,309 @@ +/** + * 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 "VolumeImageGeometry.h" + +#include "../Toolbox/GeometryToolbox.h" + +#include + + +namespace OrthancStone +{ + void VolumeImageGeometry::Invalidate() + { + Vector p = (axialGeometry_.GetOrigin() + + static_cast(depth_ - 1) * voxelDimensions_[2] * axialGeometry_.GetNormal()); + + coronalGeometry_ = CoordinateSystem3D(p, + axialGeometry_.GetAxisX(), + -axialGeometry_.GetNormal()); + + sagittalGeometry_ = CoordinateSystem3D(p, + axialGeometry_.GetAxisY(), + axialGeometry_.GetNormal()); + + Vector origin = ( + axialGeometry_.MapSliceToWorldCoordinates(-0.5 * voxelDimensions_[0], + -0.5 * voxelDimensions_[1]) - + 0.5 * voxelDimensions_[2] * axialGeometry_.GetNormal()); + + Vector scaling; + + if (width_ == 0 || + height_ == 0 || + depth_ == 0) + { + LinearAlgebra::AssignVector(scaling, 1, 1, 1); + } + else + { + scaling = ( + axialGeometry_.GetAxisX() * voxelDimensions_[0] * static_cast(width_) + + axialGeometry_.GetAxisY() * voxelDimensions_[1] * static_cast(height_) + + axialGeometry_.GetNormal() * voxelDimensions_[2] * static_cast(depth_)); + } + + transform_ = LinearAlgebra::Product( + GeometryToolbox::CreateTranslationMatrix(origin[0], origin[1], origin[2]), + GeometryToolbox::CreateScalingMatrix(scaling[0], scaling[1], scaling[2])); + + LinearAlgebra::InvertMatrix(transformInverse_, transform_); + } + + + VolumeImageGeometry::VolumeImageGeometry() : + width_(0), + height_(0), + depth_(0) + { + LinearAlgebra::AssignVector(voxelDimensions_, 1, 1, 1); + Invalidate(); + } + + + void VolumeImageGeometry::SetSize(unsigned int width, + unsigned int height, + unsigned int depth) + { + width_ = width; + height_ = height; + depth_ = depth; + Invalidate(); + } + + + void VolumeImageGeometry::SetAxialGeometry(const CoordinateSystem3D& geometry) + { + axialGeometry_ = geometry; + Invalidate(); + } + + + void VolumeImageGeometry::SetVoxelDimensions(double x, + double y, + double z) + { + if (x <= 0 || + y <= 0 || + z <= 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + else + { + LinearAlgebra::AssignVector(voxelDimensions_, x, y, z); + Invalidate(); + } + } + + + const CoordinateSystem3D& VolumeImageGeometry::GetProjectionGeometry(VolumeProjection projection) const + { + switch (projection) + { + case VolumeProjection_Axial: + return axialGeometry_; + + case VolumeProjection_Coronal: + return coronalGeometry_; + + case VolumeProjection_Sagittal: + return sagittalGeometry_; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } + + + Vector VolumeImageGeometry::GetVoxelDimensions(VolumeProjection projection) const + { + switch (projection) + { + case VolumeProjection_Axial: + return voxelDimensions_; + + case VolumeProjection_Coronal: + return LinearAlgebra::CreateVector(voxelDimensions_[0], voxelDimensions_[2], voxelDimensions_[1]); + + case VolumeProjection_Sagittal: + return LinearAlgebra::CreateVector(voxelDimensions_[1], voxelDimensions_[2], voxelDimensions_[0]); + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } + + + unsigned int VolumeImageGeometry::GetProjectionWidth(VolumeProjection projection) const + { + switch (projection) + { + case VolumeProjection_Axial: + return width_; + + case VolumeProjection_Coronal: + return width_; + + case VolumeProjection_Sagittal: + return height_; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } + + + unsigned int VolumeImageGeometry::GetProjectionHeight(VolumeProjection projection) const + { + switch (projection) + { + case VolumeProjection_Axial: + return height_; + + case VolumeProjection_Coronal: + return depth_; + + case VolumeProjection_Sagittal: + return depth_; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } + + + unsigned int VolumeImageGeometry::GetProjectionDepth(VolumeProjection projection) const + { + switch (projection) + { + case VolumeProjection_Axial: + return depth_; + + case VolumeProjection_Coronal: + return height_; + + case VolumeProjection_Sagittal: + return width_; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } + + + Vector VolumeImageGeometry::GetCoordinates(float x, + float y, + float z) const + { + Vector p = LinearAlgebra::Product(transform_, LinearAlgebra::CreateVector(x, y, z, 1)); + + assert(LinearAlgebra::IsNear(p[3], 1)); // Affine transform, no perspective effect + + // Back to non-homogeneous coordinates + return LinearAlgebra::CreateVector(p[0], p[1], p[2]); + } + + + bool VolumeImageGeometry::DetectProjection(VolumeProjection& projection, + const Vector& planeNormal) const + { + if (GeometryToolbox::IsParallel(planeNormal, axialGeometry_.GetNormal())) + { + projection = VolumeProjection_Axial; + return true; + } + else if (GeometryToolbox::IsParallel(planeNormal, coronalGeometry_.GetNormal())) + { + projection = VolumeProjection_Coronal; + return true; + } + else if (GeometryToolbox::IsParallel(planeNormal, sagittalGeometry_.GetNormal())) + { + projection = VolumeProjection_Sagittal; + return true; + } + else + { + return false; + } + } + + + bool VolumeImageGeometry::DetectSlice(VolumeProjection& projection, + unsigned int& slice, + const CoordinateSystem3D& plane) const + { + if (!DetectProjection(projection, plane.GetNormal())) + { + return false; + } + + // Transforms the coordinates of the origin of the plane, into the + // coordinates of the axial geometry + const Vector& origin = plane.GetOrigin(); + Vector p = LinearAlgebra::Product( + transformInverse_, + LinearAlgebra::CreateVector(origin[0], origin[1], origin[2], 1)); + + assert(LinearAlgebra::IsNear(p[3], 1)); + + double z; + + switch (projection) + { + case VolumeProjection_Axial: + z = p[2]; + break; + + case VolumeProjection_Coronal: + z = p[1]; + break; + + case VolumeProjection_Sagittal: + z = p[0]; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + const unsigned int projectionDepth = GetProjectionDepth(projection); + + z *= static_cast(projectionDepth); + if (z < 0) + { + return false; + } + + unsigned int d = static_cast(std::floor(z)); + if (d >= projectionDepth) + { + return false; + } + else + { + slice = d; + return true; + } + } +} diff -r 8b6adfb62a2f -r 5c551f078c18 Framework/Toolbox/VolumeImageGeometry.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Toolbox/VolumeImageGeometry.h Fri May 17 09:20:46 2019 +0200 @@ -0,0 +1,122 @@ +/** + * 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 "../StoneEnumerations.h" +#include "CoordinateSystem3D.h" + +namespace OrthancStone +{ + class VolumeImageGeometry + { + private: + unsigned int width_; + unsigned int height_; + unsigned int depth_; + CoordinateSystem3D axialGeometry_; + CoordinateSystem3D coronalGeometry_; + CoordinateSystem3D sagittalGeometry_; + Vector voxelDimensions_; + Matrix transform_; + Matrix transformInverse_; + + void Invalidate(); + + public: + VolumeImageGeometry(); + + unsigned int GetWidth() const + { + return width_; + } + + unsigned int GetHeight() const + { + return height_; + } + + unsigned int GetDepth() const + { + return depth_; + } + + const CoordinateSystem3D& GetAxialGeometry() const + { + return axialGeometry_; + } + + const CoordinateSystem3D& GetCoronalGeometry() const + { + return coronalGeometry_; + } + + const CoordinateSystem3D& GetSagittalGeometry() const + { + return sagittalGeometry_; + } + + const CoordinateSystem3D& GetProjectionGeometry(VolumeProjection projection) const; + + const Matrix& GetTransform() const + { + return transform_; + } + + const Matrix& GetTransformInverse() const + { + return transformInverse_; + } + + void SetSize(unsigned int width, + unsigned int height, + unsigned int depth); + + // Set the geometry of the first axial slice (i.e. the one whose + // depth == 0) + void SetAxialGeometry(const CoordinateSystem3D& geometry); + + void SetVoxelDimensions(double x, + double y, + double z); + + Vector GetVoxelDimensions(VolumeProjection projection) const; + + unsigned int GetProjectionWidth(VolumeProjection projection) const; + + unsigned int GetProjectionHeight(VolumeProjection projection) const; + + unsigned int GetProjectionDepth(VolumeProjection projection) const; + + // Get the 3D position of a point in the volume, where x, y and z + // lie in the [0;1] range + Vector GetCoordinates(float x, + float y, + float z) const; + + bool DetectProjection(VolumeProjection& projection, + const Vector& planeNormal) const; + + bool DetectSlice(VolumeProjection& projection, + unsigned int& slice, + const CoordinateSystem3D& plane) const; + }; +} diff -r 8b6adfb62a2f -r 5c551f078c18 Framework/Volumes/ImageBuffer3D.cpp --- a/Framework/Volumes/ImageBuffer3D.cpp Wed May 15 16:56:17 2019 +0200 +++ b/Framework/Volumes/ImageBuffer3D.cpp Fri May 17 09:20:46 2019 +0200 @@ -21,6 +21,8 @@ #include "ImageBuffer3D.h" +#include "../Toolbox/GeometryToolbox.h" + #include #include #include @@ -116,10 +118,11 @@ computeRange_(computeRange), hasRange_(false) { - LinearAlgebra::AssignVector(voxelDimensions_, 1, 1, 1); + geometry_.SetSize(width, height, depth); - LOG(INFO) << "Created an image of " - << (GetEstimatedMemorySize() / (1024ll * 1024ll)) << "MB"; + LOG(INFO) << "Created a 3D image of size " << width << "x" << height + << "x" << depth << " in " << Orthanc::EnumerationToString(format) + << " (" << (GetEstimatedMemorySize() / (1024ll * 1024ll)) << "MB)"; } @@ -129,127 +132,57 @@ } - void ImageBuffer3D::SetAxialGeometry(const CoordinateSystem3D& geometry) - { - axialGeometry_ = geometry; - } - - - void ImageBuffer3D::SetVoxelDimensions(double x, - double y, - double z) - { - if (x <= 0 || - y <= 0 || - z <= 0) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); - } - - { - LinearAlgebra::AssignVector(voxelDimensions_, x, y, z); - } - } - - - Vector ImageBuffer3D::GetVoxelDimensions(VolumeProjection projection) const - { - Vector result; - switch (projection) - { - case VolumeProjection_Axial: - result = voxelDimensions_; - break; - - case VolumeProjection_Coronal: - LinearAlgebra::AssignVector(result, voxelDimensions_[0], voxelDimensions_[2], voxelDimensions_[1]); - break; - - case VolumeProjection_Sagittal: - LinearAlgebra::AssignVector(result, voxelDimensions_[1], voxelDimensions_[2], voxelDimensions_[0]); - break; - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); - } - - return result; - } - - - void ImageBuffer3D::GetSliceSize(unsigned int& width, - unsigned int& height, - VolumeProjection projection) - { - switch (projection) - { - case VolumeProjection_Axial: - width = width_; - height = height_; - break; - - case VolumeProjection_Coronal: - width = width_; - height = depth_; - break; - - case VolumeProjection_Sagittal: - width = height_; - height = depth_; - break; - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); - } - } ParallelSlices* ImageBuffer3D::GetGeometry(VolumeProjection projection) const { + const Vector dimensions = geometry_.GetVoxelDimensions(VolumeProjection_Axial); + const CoordinateSystem3D& axial = geometry_.GetAxialGeometry(); + std::auto_ptr result(new ParallelSlices); switch (projection) { - case VolumeProjection_Axial: - for (unsigned int z = 0; z < depth_; z++) - { - Vector origin = axialGeometry_.GetOrigin(); - origin += static_cast(z) * voxelDimensions_[2] * axialGeometry_.GetNormal(); + case VolumeProjection_Axial: + for (unsigned int z = 0; z < depth_; z++) + { + Vector origin = axial.GetOrigin(); + origin += static_cast(z) * dimensions[2] * axial.GetNormal(); - result->AddSlice(origin, - axialGeometry_.GetAxisX(), - axialGeometry_.GetAxisY()); - } - break; + result->AddSlice(origin, + axial.GetAxisX(), + axial.GetAxisY()); + } + break; - case VolumeProjection_Coronal: - for (unsigned int y = 0; y < height_; y++) - { - Vector origin = axialGeometry_.GetOrigin(); - origin += static_cast(y) * voxelDimensions_[1] * axialGeometry_.GetAxisY(); - origin += static_cast(depth_ - 1) * voxelDimensions_[2] * axialGeometry_.GetNormal(); + case VolumeProjection_Coronal: + for (unsigned int y = 0; y < height_; y++) + { + Vector origin = axial.GetOrigin(); + origin += static_cast(y) * dimensions[1] * axial.GetAxisY(); + origin += static_cast(depth_ - 1) * dimensions[2] * axial.GetNormal(); - result->AddSlice(origin, - axialGeometry_.GetAxisX(), - -axialGeometry_.GetNormal()); - } - break; + result->AddSlice(origin, + axial.GetAxisX(), + -axial.GetNormal()); + } + break; - case VolumeProjection_Sagittal: - for (unsigned int x = 0; x < width_; x++) - { - Vector origin = axialGeometry_.GetOrigin(); - origin += static_cast(x) * voxelDimensions_[0] * axialGeometry_.GetAxisX(); - origin += static_cast(depth_ - 1) * voxelDimensions_[2] * axialGeometry_.GetNormal(); + case VolumeProjection_Sagittal: + for (unsigned int x = 0; x < width_; x++) + { + Vector origin = axial.GetOrigin(); + origin += static_cast(x) * dimensions[0] * axial.GetAxisX(); + origin += static_cast(depth_ - 1) * dimensions[2] * axial.GetNormal(); - result->AddSlice(origin, - axialGeometry_.GetAxisY(), - -axialGeometry_.GetNormal()); - } - break; + result->AddSlice(origin, + axial.GetAxisY(), + -axial.GetNormal()); + } + break; - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } return result.release(); @@ -275,24 +208,24 @@ switch (slice.GetFormat()) { - case Orthanc::PixelFormat_Grayscale8: - case Orthanc::PixelFormat_Grayscale16: - case Orthanc::PixelFormat_Grayscale32: - case Orthanc::PixelFormat_SignedGrayscale16: - { - int64_t a, b; - Orthanc::ImageProcessing::GetMinMaxIntegerValue(a, b, slice); - sliceMin = static_cast(a); - sliceMax = static_cast(b); - break; - } + case Orthanc::PixelFormat_Grayscale8: + case Orthanc::PixelFormat_Grayscale16: + case Orthanc::PixelFormat_Grayscale32: + case Orthanc::PixelFormat_SignedGrayscale16: + { + int64_t a, b; + Orthanc::ImageProcessing::GetMinMaxIntegerValue(a, b, slice); + sliceMin = static_cast(a); + sliceMax = static_cast(b); + break; + } - case Orthanc::PixelFormat_Float32: - Orthanc::ImageProcessing::GetMinMaxFloatValue(sliceMin, sliceMax, slice); - break; + case Orthanc::PixelFormat_Float32: + Orthanc::ImageProcessing::GetMinMaxFloatValue(sliceMin, sliceMax, slice); + break; - default: - return; + default: + return; } if (hasRange_) @@ -331,8 +264,15 @@ if (hasRange_) { style.windowing_ = ImageWindowing_Custom; - style.customWindowCenter_ = converter.Apply((minValue_ + maxValue_) / 2.0); - style.customWindowWidth_ = converter.Apply(maxValue_ - minValue_); + + // casting the narrower type to wider before calling the + operator + // will prevent overflowing (this is why the cast to double is only + // done on the first operand) + style.customWindowCenter_ = static_cast( + converter.Apply((static_cast(minValue_) + maxValue_) / 2.0)); + + style.customWindowWidth_ = static_cast( + converter.Apply(static_cast(maxValue_) - minValue_)); if (style.customWindowWidth_ > 1) { @@ -366,8 +306,8 @@ sagittal_->GetReadOnlyAccessor(accessor_); break; - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } } @@ -410,8 +350,8 @@ sagittal_->GetWriteableAccessor(accessor_); break; - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } } @@ -456,22 +396,4 @@ const void* p = image_.GetConstRow(y + height_ * (depth_ - 1 - z)); return reinterpret_cast(p) [x]; } - - - Vector ImageBuffer3D::GetCoordinates(float x, - float y, - float z) const - { - Vector ps = GetVoxelDimensions(OrthancStone::VolumeProjection_Axial); - - const CoordinateSystem3D& axial = GetAxialGeometry(); - - Vector origin = (axial.MapSliceToWorldCoordinates(-0.5 * ps[0], -0.5 * ps[1]) - - 0.5 * ps[2] * axial.GetNormal()); - - return (origin + - axial.GetAxisX() * ps[0] * x * static_cast(GetWidth()) + - axial.GetAxisY() * ps[1] * y * static_cast(GetHeight()) + - axial.GetNormal() * ps[2] * z * static_cast(GetDepth())); - } } diff -r 8b6adfb62a2f -r 5c551f078c18 Framework/Volumes/ImageBuffer3D.h --- a/Framework/Volumes/ImageBuffer3D.h Wed May 15 16:56:17 2019 +0200 +++ b/Framework/Volumes/ImageBuffer3D.h Fri May 17 09:20:46 2019 +0200 @@ -23,7 +23,7 @@ #include "../StoneEnumerations.h" #include "../Layers/RenderStyle.h" -#include "../Toolbox/CoordinateSystem3D.h" +#include "../Toolbox/VolumeImageGeometry.h" #include "../Toolbox/DicomFrameConverter.h" #include "../Toolbox/ParallelSlices.h" @@ -34,8 +34,7 @@ class ImageBuffer3D : public boost::noncopyable { private: - CoordinateSystem3D axialGeometry_; - Vector voxelDimensions_; + VolumeImageGeometry geometry_; // TODO => Move this out of this class Orthanc::Image image_; Orthanc::PixelFormat format_; unsigned int width_; @@ -45,6 +44,8 @@ bool hasRange_; float minValue_; float maxValue_; + Matrix transform_; + Matrix transformInverse_; void ExtendImageRange(const Orthanc::ImageAccessor& slice); @@ -77,24 +78,15 @@ void Clear(); - // Set the geometry of the first axial slice (i.e. the one whose - // depth == 0) - void SetAxialGeometry(const CoordinateSystem3D& geometry); - - const CoordinateSystem3D& GetAxialGeometry() const + VolumeImageGeometry& GetGeometry() { - return axialGeometry_; + return geometry_; } - void SetVoxelDimensions(double x, - double y, - double z); - - Vector GetVoxelDimensions(VolumeProjection projection) const; - - void GetSliceSize(unsigned int& width, - unsigned int& height, - VolumeProjection projection); + const VolumeImageGeometry& GetGeometry() const + { + return geometry_; + } const Orthanc::ImageAccessor& GetInternalImage() const { @@ -121,6 +113,7 @@ return format_; } + // TODO - Remove ParallelSlices* GetGeometry(VolumeProjection projection) const; uint64_t GetEstimatedMemorySize() const; @@ -160,13 +153,7 @@ unsigned int y, unsigned int z) const; - // Get the 3D position of a point in the volume, where x, y and z - // lie in the [0;1] range - Vector GetCoordinates(float x, - float y, - float z) const; - - + class SliceReader : public boost::noncopyable { private: diff -r 8b6adfb62a2f -r 5c551f078c18 Framework/Volumes/VolumeReslicer.cpp --- a/Framework/Volumes/VolumeReslicer.cpp Wed May 15 16:56:17 2019 +0200 +++ b/Framework/Volumes/VolumeReslicer.cpp Fri May 17 09:20:46 2019 +0200 @@ -749,7 +749,8 @@ { // Choose the default voxel size as the finest voxel dimension // of the source volumetric image - const OrthancStone::Vector dim = source.GetVoxelDimensions(OrthancStone::VolumeProjection_Axial); + const OrthancStone::Vector dim = + source.GetGeometry().GetVoxelDimensions(OrthancStone::VolumeProjection_Axial); double voxelSize = dim[0]; if (dim[1] < voxelSize) diff -r 8b6adfb62a2f -r 5c551f078c18 Framework/Widgets/SliceViewerWidget.cpp --- a/Framework/Widgets/SliceViewerWidget.cpp Wed May 15 16:56:17 2019 +0200 +++ b/Framework/Widgets/SliceViewerWidget.cpp Fri May 17 09:20:46 2019 +0200 @@ -115,7 +115,7 @@ unsigned int GetCountMissing() const { - return countMissing_; + return static_cast(countMissing_); } bool RenderScene(CairoContext& context, diff -r 8b6adfb62a2f -r 5c551f078c18 Framework/Widgets/WorldSceneWidget.cpp --- a/Framework/Widgets/WorldSceneWidget.cpp Wed May 15 16:56:17 2019 +0200 +++ b/Framework/Widgets/WorldSceneWidget.cpp Fri May 17 09:20:46 2019 +0200 @@ -83,8 +83,12 @@ for (size_t t = 0; t < displayTouches.size(); t++) { double sx, sy; - view_.MapPixelCenterToScene(sx, sy, (int)displayTouches[t].x, (int)displayTouches[t].y); - sceneTouches.push_back(Touch(sx, sy)); + + view_.MapPixelCenterToScene( + sx, sy, (int)displayTouches[t].x, (int)displayTouches[t].y); + + sceneTouches.push_back( + Touch(static_cast(sx), static_cast(sy))); } tracker_->MouseMove(x, y, sceneX, sceneY, displayTouches, sceneTouches); } diff -r 8b6adfb62a2f -r 5c551f078c18 Framework/dev.h --- a/Framework/dev.h Wed May 15 16:56:17 2019 +0200 +++ b/Framework/dev.h Fri May 17 09:20:46 2019 +0200 @@ -157,9 +157,9 @@ << "x" << loader_.GetSlicesCount() << " in " << Orthanc::EnumerationToString(format); image_.reset(new ImageBuffer3D(format, width, height, static_cast(loader_.GetSlicesCount()), computeRange_)); - image_->SetAxialGeometry(loader_.GetSlice(0).GetGeometry()); - image_->SetVoxelDimensions(loader_.GetSlice(0).GetPixelSpacingX(), - loader_.GetSlice(0).GetPixelSpacingY(), spacingZ); + image_->GetGeometry().SetAxialGeometry(loader_.GetSlice(0).GetGeometry()); + image_->GetGeometry().SetVoxelDimensions(loader_.GetSlice(0).GetPixelSpacingX(), + loader_.GetSlice(0).GetPixelSpacingY(), spacingZ); image_->Clear(); downloadStack_.reset(new DownloadStack(static_cast(loader_.GetSlicesCount()))); @@ -301,7 +301,7 @@ }; - class VolumeImageGeometry + class OLD_VolumeImageGeometry { private: unsigned int width_; @@ -403,8 +403,8 @@ } public: - VolumeImageGeometry(const OrthancVolumeImage& volume, - VolumeProjection projection) + OLD_VolumeImageGeometry(const OrthancVolumeImage& volume, + VolumeProjection projection) { if (volume.GetSlicesCount() == 0) { @@ -521,9 +521,9 @@ OrthancVolumeImage& volume_; - std::auto_ptr axialGeometry_; - std::auto_ptr coronalGeometry_; - std::auto_ptr sagittalGeometry_; + std::auto_ptr axialGeometry_; + std::auto_ptr coronalGeometry_; + std::auto_ptr sagittalGeometry_; bool IsGeometryReady() const @@ -536,9 +536,9 @@ assert(&message.GetOrigin() == &volume_); // These 3 values are only used to speed up the IVolumeSlicer - axialGeometry_.reset(new VolumeImageGeometry(volume_, VolumeProjection_Axial)); - coronalGeometry_.reset(new VolumeImageGeometry(volume_, VolumeProjection_Coronal)); - sagittalGeometry_.reset(new VolumeImageGeometry(volume_, VolumeProjection_Sagittal)); + axialGeometry_.reset(new OLD_VolumeImageGeometry(volume_, VolumeProjection_Axial)); + coronalGeometry_.reset(new OLD_VolumeImageGeometry(volume_, VolumeProjection_Coronal)); + sagittalGeometry_.reset(new OLD_VolumeImageGeometry(volume_, VolumeProjection_Sagittal)); BroadcastMessage(IVolumeSlicer::GeometryReadyMessage(*this)); } @@ -567,7 +567,7 @@ BroadcastMessage(IVolumeSlicer::ContentChangedMessage(*this)); } - const VolumeImageGeometry& GetProjectionGeometry(VolumeProjection projection) + const OLD_VolumeImageGeometry& GetProjectionGeometry(VolumeProjection projection) { if (!IsGeometryReady()) { @@ -676,7 +676,7 @@ if (IsGeometryReady() && DetectProjection(projection, viewportSlice)) { - const VolumeImageGeometry& geometry = GetProjectionGeometry(projection); + const OLD_VolumeImageGeometry& geometry = GetProjectionGeometry(projection); size_t closest; @@ -716,7 +716,7 @@ private: SliceViewerWidget& widget_; VolumeProjection projection_; - std::auto_ptr slices_; + std::auto_ptr slices_; size_t slice_; protected: @@ -727,7 +727,7 @@ const OrthancVolumeImage& image = dynamic_cast(message.GetOrigin()); - slices_.reset(new VolumeImageGeometry(image, projection_)); + slices_.reset(new OLD_VolumeImageGeometry(image, projection_)); SetSlice(slices_->GetSlicesCount() / 2); widget_.FitContent(); diff -r 8b6adfb62a2f -r 5c551f078c18 Platforms/Wasm/stone-framework-loader.ts --- a/Platforms/Wasm/stone-framework-loader.ts Wed May 15 16:56:17 2019 +0200 +++ b/Platforms/Wasm/stone-framework-loader.ts Fri May 17 09:20:46 2019 +0200 @@ -81,12 +81,6 @@ callback(); } ], - print: function(text : string) { - Logger.defaultLogger.infoFromCpp(text); - }, - printErr: function(text : string) { - Logger.defaultLogger.errorFromCpp(text); - }, totalDependencies: 0 }; diff -r 8b6adfb62a2f -r 5c551f078c18 Resources/CMake/OrthancStoneConfiguration.cmake --- a/Resources/CMake/OrthancStoneConfiguration.cmake Wed May 15 16:56:17 2019 +0200 +++ b/Resources/CMake/OrthancStoneConfiguration.cmake Fri May 17 09:20:46 2019 +0200 @@ -146,13 +146,19 @@ if (ENABLE_OPENGL) - include(FindOpenGL) - if (NOT OPENGL_FOUND) - message(FATAL_ERROR "Cannot find OpenGL on your system") + if (NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + # If including "FindOpenGL.cmake" using Emscripten (targeting + # WebAssembly), the "OPENGL_LIBRARIES" value incorrectly includes + # the "nul" library, which leads to warning message in Emscripten: + # 'shared:WARNING: emcc: cannot find library "nul"'. + include(FindOpenGL) + if (NOT OPENGL_FOUND) + message(FATAL_ERROR "Cannot find OpenGL on your system") + endif() + + link_libraries(${OPENGL_LIBRARIES}) endif() - link_libraries(${OPENGL_LIBRARIES}) - add_definitions( -DGL_GLEXT_PROTOTYPES=1 -DORTHANC_ENABLE_OPENGL=1 @@ -417,6 +423,7 @@ ${ORTHANC_STONE_ROOT}/Framework/Toolbox/SlicesSorter.cpp ${ORTHANC_STONE_ROOT}/Framework/Toolbox/UndoRedoStack.cpp ${ORTHANC_STONE_ROOT}/Framework/Toolbox/ViewportGeometry.cpp + ${ORTHANC_STONE_ROOT}/Framework/Toolbox/VolumeImageGeometry.cpp ${ORTHANC_STONE_ROOT}/Framework/Viewport/CairoContext.cpp ${ORTHANC_STONE_ROOT}/Framework/Viewport/CairoSurface.cpp ${ORTHANC_STONE_ROOT}/Framework/Viewport/IMouseTracker.h diff -r 8b6adfb62a2f -r 5c551f078c18 Resources/CodeGeneration/stonegentool.py --- a/Resources/CodeGeneration/stonegentool.py Wed May 15 16:56:17 2019 +0200 +++ b/Resources/CodeGeneration/stonegentool.py Fri May 17 09:20:46 2019 +0200 @@ -186,6 +186,7 @@ RegisterTemplateFunction(template,NeedsCppConstruction) RegisterTemplateFunction(template, DefaultValueToTs) RegisterTemplateFunction(template, DefaultValueToCpp) + RegisterTemplateFunction(template, sorted) return template def MakeTemplateFromFile(templateFileName): @@ -532,7 +533,7 @@ if not (nextCh == ' ' or nextCh == '\n'): lineNumber = schemaText.count("\n",0,i) + 1 raise RuntimeError("Error at line " + str(lineNumber) + " in the schema: colons must be followed by a space or a newline!") - schema = yaml.load(schemaText) + schema = yaml.load(schemaText, Loader = yaml.SafeLoader) return schema def GetTemplatingDictFromSchemaFilename(fn): diff -r 8b6adfb62a2f -r 5c551f078c18 Resources/CodeGeneration/template.in.h.j2 --- a/Resources/CodeGeneration/template.in.h.j2 Wed May 15 16:56:17 2019 +0200 +++ b/Resources/CodeGeneration/template.in.h.j2 Fri May 17 09:20:46 2019 +0200 @@ -394,9 +394,9 @@ struct {{struct['name']}} { -{% if struct %}{% if struct['fields'] %}{% for key in struct['fields']%} {{CanonToCpp(struct['fields'][key]['type'])}} {{key}}; +{% if struct %}{% if struct['fields'] %}{% for key in sorted(struct['fields']) %} {{CanonToCpp(struct['fields'][key]['type'])}} {{key}}; {% endfor %}{% endif %}{% endif %} - {{struct['name']}}({% if struct %}{% if struct['fields'] %}{% for key in struct['fields']%}{{CanonToCpp(struct['fields'][key]['type'])}} {{key}} = {% if struct['fields'][key]['defaultValue'] %}{{DefaultValueToCpp(rootName,enums,struct['fields'][key])}} {%else%} {{CanonToCpp(struct['fields'][key]['type'])}}() {%endif%} {{ ", " if not loop.last }}{% endfor %}{% endif %}{% endif %}) + {{struct['name']}}({% if struct %}{% if struct['fields'] %}{% for key in sorted(struct['fields']) %}{{CanonToCpp(struct['fields'][key]['type'])}} {{key}} = {% if struct['fields'][key]['defaultValue'] %}{{DefaultValueToCpp(rootName,enums,struct['fields'][key])}} {%else%} {{CanonToCpp(struct['fields'][key]['type'])}}() {%endif%} {{ ", " if not loop.last }}{% endfor %}{% endif %}{% endif %}) { {% if struct %}{% if struct['fields'] %}{% for key in struct['fields']%} this->{{key}} = {{key}}; {% endfor %}{% endif %}{% endif %} } diff -r 8b6adfb62a2f -r 5c551f078c18 Samples/Sdl/Loader.cpp --- a/Samples/Sdl/Loader.cpp Wed May 15 16:56:17 2019 +0200 +++ b/Samples/Sdl/Loader.cpp Fri May 17 09:20:46 2019 +0200 @@ -25,8 +25,9 @@ #include "../../Framework/Messages/MessageBroker.h" #include "../../Framework/StoneInitialization.h" #include "../../Framework/Toolbox/GeometryToolbox.h" +#include "../../Framework/Toolbox/SlicesSorter.h" #include "../../Framework/Volumes/ImageBuffer3D.h" -#include "../../Framework/Toolbox/SlicesSorter.h" +#include "../../Framework/Scene2D/Scene2D.h" // From Orthanc framework #include @@ -44,8 +45,8 @@ #include #include #include +#include #include -#include #include #include @@ -101,6 +102,17 @@ + class IVolumeSlicer : public boost::noncopyable + { + public: + virtual ~IVolumeSlicer() + { + } + + virtual void SetViewportPlane(const OrthancStone::CoordinateSystem3D& plane) = 0; + }; + + class OracleCommandWithPayload : public IOracleCommand { @@ -354,9 +366,11 @@ private: - std::string uri_; - HttpHeaders headers_; - unsigned int timeout_; + std::string uri_; + HttpHeaders headers_; + unsigned int timeout_; + bool hasExpectedFormat_; + Orthanc::PixelFormat expectedFormat_; std::auto_ptr< OrthancStone::MessageHandler > successCallback_; std::auto_ptr< OrthancStone::MessageHandler > failureCallback_; @@ -364,7 +378,8 @@ public: GetOrthancImageCommand() : uri_("/"), - timeout_(10) + timeout_(10), + hasExpectedFormat_(false) { } @@ -373,6 +388,12 @@ return Type_GetOrthancImage; } + void SetExpectedPixelFormat(Orthanc::PixelFormat format) + { + hasExpectedFormat_ = true; + expectedFormat_ = format; + } + void SetUri(const std::string& uri) { uri_ = uri; @@ -455,6 +476,20 @@ std::string(Orthanc::EnumerationToString(contentType))); } + if (hasExpectedFormat_) + { + if (expectedFormat_ == Orthanc::PixelFormat_SignedGrayscale16 && + image->GetFormat() == Orthanc::PixelFormat_Grayscale16) + { + image->SetFormat(Orthanc::PixelFormat_SignedGrayscale16); + } + + if (expectedFormat_ != image->GetFormat()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat); + } + } + SuccessMessage message(*this, image.release(), contentType); emitter.EmitMessage(receiver, message); } @@ -515,7 +550,7 @@ return Type_GetOrthancWebViewerJpeg; } - void SetExpectedFormat(Orthanc::PixelFormat format) + void SetExpectedPixelFormat(Orthanc::PixelFormat format) { expectedFormat_ = format; } @@ -549,7 +584,7 @@ headers_[key] = value; } - Orthanc::PixelFormat GetExpectedFormat() const + Orthanc::PixelFormat GetExpectedPixelFormat() const { return expectedFormat_; } @@ -734,6 +769,974 @@ + class DicomInstanceParameters : + public Orthanc::IDynamicObject /* to be used as a payload of SlicesSorter */ + { + private: + struct Data // Struct to ease the copy constructor + { + std::string orthancInstanceId_; + std::string studyInstanceUid_; + std::string seriesInstanceUid_; + std::string sopInstanceUid_; + Orthanc::DicomImageInformation imageInformation_; + OrthancStone::SopClassUid sopClassUid_; + double thickness_; + double pixelSpacingX_; + double pixelSpacingY_; + OrthancStone::CoordinateSystem3D geometry_; + OrthancStone::Vector frameOffsets_; + bool isColor_; + bool hasRescale_; + double rescaleOffset_; + double rescaleSlope_; + bool hasDefaultWindowing_; + float defaultWindowingCenter_; + float defaultWindowingWidth_; + Orthanc::PixelFormat expectedPixelFormat_; + + void ComputeDoseOffsets(const Orthanc::DicomMap& dicom) + { + // http://dicom.nema.org/medical/Dicom/2016a/output/chtml/part03/sect_C.8.8.3.2.html + + { + std::string increment; + + if (dicom.CopyToString(increment, Orthanc::DICOM_TAG_FRAME_INCREMENT_POINTER, false)) + { + Orthanc::Toolbox::ToUpperCase(increment); + if (increment != "3004,000C") // This is the "Grid Frame Offset Vector" tag + { + LOG(ERROR) << "RT-DOSE: Bad value for the \"FrameIncrementPointer\" tag"; + return; + } + } + } + + if (!OrthancStone::LinearAlgebra::ParseVector(frameOffsets_, dicom, Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR) || + frameOffsets_.size() < imageInformation_.GetNumberOfFrames()) + { + LOG(ERROR) << "RT-DOSE: No information about the 3D location of some slice(s)"; + frameOffsets_.clear(); + } + else + { + if (frameOffsets_.size() >= 2) + { + thickness_ = frameOffsets_[1] - frameOffsets_[0]; + + if (thickness_ < 0) + { + thickness_ = -thickness_; + } + } + } + } + + Data(const Orthanc::DicomMap& dicom) : + imageInformation_(dicom) + { + if (imageInformation_.GetNumberOfFrames() <= 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + if (!dicom.CopyToString(studyInstanceUid_, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) || + !dicom.CopyToString(seriesInstanceUid_, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false) || + !dicom.CopyToString(sopInstanceUid_, Orthanc::DICOM_TAG_SOP_INSTANCE_UID, false)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + std::string s; + if (!dicom.CopyToString(s, Orthanc::DICOM_TAG_SOP_CLASS_UID, false)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + else + { + sopClassUid_ = OrthancStone::StringToSopClassUid(s); + } + + if (!dicom.ParseDouble(thickness_, Orthanc::DICOM_TAG_SLICE_THICKNESS)) + { + thickness_ = 100.0 * std::numeric_limits::epsilon(); + } + + OrthancStone::GeometryToolbox::GetPixelSpacing(pixelSpacingX_, pixelSpacingY_, dicom); + + std::string position, orientation; + if (dicom.CopyToString(position, Orthanc::DICOM_TAG_IMAGE_POSITION_PATIENT, false) && + dicom.CopyToString(orientation, Orthanc::DICOM_TAG_IMAGE_ORIENTATION_PATIENT, false)) + { + geometry_ = OrthancStone::CoordinateSystem3D(position, orientation); + } + + if (sopClassUid_ == OrthancStone::SopClassUid_RTDose) + { + ComputeDoseOffsets(dicom); + } + + isColor_ = (imageInformation_.GetPhotometricInterpretation() != Orthanc::PhotometricInterpretation_Monochrome1 && + imageInformation_.GetPhotometricInterpretation() != Orthanc::PhotometricInterpretation_Monochrome2); + + double doseGridScaling; + + if (dicom.ParseDouble(rescaleOffset_, Orthanc::DICOM_TAG_RESCALE_INTERCEPT) && + dicom.ParseDouble(rescaleSlope_, Orthanc::DICOM_TAG_RESCALE_SLOPE)) + { + hasRescale_ = true; + } + else if (dicom.ParseDouble(doseGridScaling, Orthanc::DICOM_TAG_DOSE_GRID_SCALING)) + { + hasRescale_ = true; + rescaleOffset_ = 0; + rescaleSlope_ = doseGridScaling; + } + else + { + hasRescale_ = false; + } + + OrthancStone::Vector c, w; + if (OrthancStone::LinearAlgebra::ParseVector(c, dicom, Orthanc::DICOM_TAG_WINDOW_CENTER) && + OrthancStone::LinearAlgebra::ParseVector(w, dicom, Orthanc::DICOM_TAG_WINDOW_WIDTH) && + c.size() > 0 && + w.size() > 0) + { + hasDefaultWindowing_ = true; + defaultWindowingCenter_ = static_cast(c[0]); + defaultWindowingWidth_ = static_cast(w[0]); + } + else + { + hasDefaultWindowing_ = false; + } + + if (sopClassUid_ == OrthancStone::SopClassUid_RTDose) + { + switch (imageInformation_.GetBitsStored()) + { + case 16: + expectedPixelFormat_ = Orthanc::PixelFormat_Grayscale16; + break; + + case 32: + expectedPixelFormat_ = Orthanc::PixelFormat_Grayscale32; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } + else if (isColor_) + { + expectedPixelFormat_ = Orthanc::PixelFormat_RGB24; + } + else if (imageInformation_.IsSigned()) + { + expectedPixelFormat_ = Orthanc::PixelFormat_SignedGrayscale16; + } + else + { + expectedPixelFormat_ = Orthanc::PixelFormat_Grayscale16; + } + } + + OrthancStone::CoordinateSystem3D GetFrameGeometry(unsigned int frame) const + { + if (frame == 0) + { + return geometry_; + } + else if (frame >= imageInformation_.GetNumberOfFrames()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + else if (sopClassUid_ == OrthancStone::SopClassUid_RTDose) + { + if (frame >= frameOffsets_.size()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + return OrthancStone::CoordinateSystem3D( + geometry_.GetOrigin() + frameOffsets_[frame] * geometry_.GetNormal(), + geometry_.GetAxisX(), + geometry_.GetAxisY()); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } + + // TODO - Is this necessary? + bool FrameContainsPlane(unsigned int frame, + const OrthancStone::CoordinateSystem3D& plane) const + { + if (frame >= imageInformation_.GetNumberOfFrames()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + OrthancStone::CoordinateSystem3D tmp = geometry_; + + if (frame != 0) + { + tmp = GetFrameGeometry(frame); + } + + double distance; + + return (OrthancStone::CoordinateSystem3D::GetDistance(distance, tmp, plane) && + distance <= thickness_ / 2.0); + } + }; + + Data data_; + + + public: + DicomInstanceParameters(const DicomInstanceParameters& other) : + data_(other.data_) + { + } + + DicomInstanceParameters(const Orthanc::DicomMap& dicom) : + data_(dicom) + { + } + + void SetOrthancInstanceIdentifier(const std::string& id) + { + data_.orthancInstanceId_ = id; + } + + const std::string& GetOrthancInstanceIdentifier() const + { + return data_.orthancInstanceId_; + } + + const Orthanc::DicomImageInformation& GetImageInformation() const + { + return data_.imageInformation_; + } + + const std::string& GetStudyInstanceUid() const + { + return data_.studyInstanceUid_; + } + + const std::string& GetSeriesInstanceUid() const + { + return data_.seriesInstanceUid_; + } + + const std::string& GetSopInstanceUid() const + { + return data_.sopInstanceUid_; + } + + OrthancStone::SopClassUid GetSopClassUid() const + { + return data_.sopClassUid_; + } + + double GetThickness() const + { + return data_.thickness_; + } + + double GetPixelSpacingX() const + { + return data_.pixelSpacingX_; + } + + double GetPixelSpacingY() const + { + return data_.pixelSpacingY_; + } + + const OrthancStone::CoordinateSystem3D& GetGeometry() const + { + return data_.geometry_; + } + + OrthancStone::CoordinateSystem3D GetFrameGeometry(unsigned int frame) const + { + return data_.GetFrameGeometry(frame); + } + + // TODO - Is this necessary? + bool FrameContainsPlane(unsigned int frame, + const OrthancStone::CoordinateSystem3D& plane) const + { + return data_.FrameContainsPlane(frame, plane); + } + + bool IsColor() const + { + return data_.isColor_; + } + + bool HasRescale() const + { + return data_.hasRescale_; + } + + double GetRescaleOffset() const + { + if (data_.hasRescale_) + { + return data_.rescaleOffset_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + double GetRescaleSlope() const + { + if (data_.hasRescale_) + { + return data_.rescaleSlope_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + bool HasDefaultWindowing() const + { + return data_.hasDefaultWindowing_; + } + + float GetDefaultWindowingCenter() const + { + if (data_.hasDefaultWindowing_) + { + return data_.defaultWindowingCenter_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + float GetDefaultWindowingWidth() const + { + if (data_.hasDefaultWindowing_) + { + return data_.defaultWindowingWidth_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + Orthanc::PixelFormat GetExpectedPixelFormat() const + { + return data_.expectedPixelFormat_; + } + }; + + + class DicomVolumeImage : public boost::noncopyable + { + private: + std::auto_ptr image_; + std::vector slices_; + uint64_t revision_; + std::vector slicesRevision_; + + void CheckSlice(size_t index, + const DicomInstanceParameters& reference) const + { + const DicomInstanceParameters& slice = *slices_[index]; + + if (!OrthancStone::GeometryToolbox::IsParallel( + reference.GetGeometry().GetNormal(), + slice.GetGeometry().GetNormal())) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, + "A slice in the volume image is not parallel to the others"); + } + + if (reference.GetExpectedPixelFormat() != slice.GetExpectedPixelFormat()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat, + "The pixel format changes across the slices of the volume image"); + } + + if (reference.GetImageInformation().GetWidth() != slice.GetImageInformation().GetWidth() || + reference.GetImageInformation().GetHeight() != slice.GetImageInformation().GetHeight()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize, + "The width/height of slices are not constant in the volume image"); + } + + if (!OrthancStone::LinearAlgebra::IsNear(reference.GetPixelSpacingX(), slice.GetPixelSpacingX()) || + !OrthancStone::LinearAlgebra::IsNear(reference.GetPixelSpacingY(), slice.GetPixelSpacingY())) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, + "The pixel spacing of the slices change across the volume image"); + } + } + + + void CheckVolume() const + { + for (size_t i = 0; i < slices_.size(); i++) + { + assert(slices_[i] != NULL); + if (slices_[i]->GetImageInformation().GetNumberOfFrames() != 1) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, + "This class does not support multi-frame images"); + } + } + + if (slices_.size() != 0) + { + const DicomInstanceParameters& reference = *slices_[0]; + + for (size_t i = 1; i < slices_.size(); i++) + { + CheckSlice(i, reference); + } + } + } + + + void Clear() + { + image_.reset(); + + for (size_t i = 0; i < slices_.size(); i++) + { + assert(slices_[i] != NULL); + delete slices_[i]; + } + } + + + void CheckSliceIndex(size_t index) const + { + assert(slices_.size() == image_->GetDepth() && + slices_.size() == slicesRevision_.size()); + + if (!HasGeometry()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else if (index >= slices_.size()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } + + + public: + DicomVolumeImage() + { + } + + ~DicomVolumeImage() + { + Clear(); + } + + // WARNING: The payload of "slices" must be of class "DicomInstanceParameters" + void SetGeometry(OrthancStone::SlicesSorter& slices) + { + Clear(); + + if (!slices.Sort()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, + "Cannot sort the 3D slices of a DICOM series"); + } + + if (slices.GetSlicesCount() == 0) + { + // Empty volume + image_.reset(new OrthancStone::ImageBuffer3D(Orthanc::PixelFormat_Grayscale8, 0, 0, 0, + false /* don't compute range */)); + } + else + { + slices_.reserve(slices.GetSlicesCount()); + slicesRevision_.resize(slices.GetSlicesCount()); + + for (size_t i = 0; i < slices.GetSlicesCount(); i++) + { + const DicomInstanceParameters& slice = + dynamic_cast(slices.GetSlicePayload(i)); + slices_.push_back(new DicomInstanceParameters(slice)); + slicesRevision_[i] = 0; + } + + CheckVolume(); + + const double spacingZ = slices.ComputeSpacingBetweenSlices(); + LOG(INFO) << "Computed spacing between slices: " << spacingZ << "mm"; + + const DicomInstanceParameters& parameters = *slices_[0]; + + image_.reset(new OrthancStone::ImageBuffer3D(parameters.GetExpectedPixelFormat(), + parameters.GetImageInformation().GetWidth(), + parameters.GetImageInformation().GetHeight(), + slices.GetSlicesCount(), false /* don't compute range */)); + + image_->GetGeometry().SetAxialGeometry(slices.GetSliceGeometry(0)); + image_->GetGeometry().SetVoxelDimensions(parameters.GetPixelSpacingX(), + parameters.GetPixelSpacingY(), spacingZ); + } + + image_->Clear(); + + revision_++; + } + + uint64_t GetRevision() const + { + return revision_; + } + + bool HasGeometry() const + { + return (image_.get() != NULL); + } + + const OrthancStone::ImageBuffer3D& GetImage() const + { + if (!HasGeometry()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + return *image_; + } + } + + size_t GetSlicesCount() const + { + if (!HasGeometry()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + return slices_.size(); + } + } + + const DicomInstanceParameters& GetSliceParameters(size_t index) const + { + CheckSliceIndex(index); + return *slices_[index]; + } + + uint64_t GetSliceRevision(size_t index) const + { + CheckSliceIndex(index); + return slicesRevision_[index]; + } + + void SetSliceContent(size_t index, + const Orthanc::ImageAccessor& image) + { + CheckSliceIndex(index); + + { + OrthancStone::ImageBuffer3D::SliceWriter writer + (*image_, OrthancStone::VolumeProjection_Axial, index); + Orthanc::ImageProcessing::Copy(writer.GetAccessor(), image); + } + + revision_ ++; + slicesRevision_[index] += 1; + } + }; + + + + class VolumeSeriesOrthancLoader : public OrthancStone::IObserver + { + private: + class MessageHandler : public Orthanc::IDynamicObject + { + public: + virtual void Handle(const Json::Value& body) const + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + virtual void Handle(const Orthanc::ImageAccessor& image) const + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + }; + + void Handle(const OrthancRestApiCommand::SuccessMessage& message) + { + Json::Value body; + message.ParseJsonBody(body); + + if (body.type() != Json::objectValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + + dynamic_cast(message.GetOrigin().GetPayload()).Handle(body); + } + + void Handle(const Refactoring::GetOrthancImageCommand::SuccessMessage& message) + { + dynamic_cast(message.GetOrigin().GetPayload()).Handle(message.GetImage()); + } + + void Handle(const Refactoring::GetOrthancWebViewerJpegCommand::SuccessMessage& message) + { + dynamic_cast(message.GetOrigin().GetPayload()).Handle(message.GetImage()); + } + + + class LoadSliceImage : public MessageHandler + { + private: + DicomVolumeImage& target_; + size_t slice_; + + public: + LoadSliceImage(DicomVolumeImage& target, + size_t slice) : + target_(target), + slice_(slice) + { + } + + virtual void Handle(const Orthanc::ImageAccessor& image) const + { + target_.SetSliceContent(slice_, image); + } + }; + + + class LoadSeriesGeometryHandler : public MessageHandler + { + private: + VolumeSeriesOrthancLoader& that_; + + public: + LoadSeriesGeometryHandler(VolumeSeriesOrthancLoader& that) : + that_(that) + { + } + + virtual void Handle(const Json::Value& body) const + { + Json::Value::Members instances = body.getMemberNames(); + + OrthancStone::SlicesSorter slices; + + for (size_t i = 0; i < instances.size(); i++) + { + Orthanc::DicomMap dicom; + dicom.FromDicomAsJson(body[instances[i]]); + + std::auto_ptr instance(new DicomInstanceParameters(dicom)); + instance->SetOrthancInstanceIdentifier(instances[i]); + + OrthancStone::CoordinateSystem3D geometry = instance->GetGeometry(); + slices.AddSlice(geometry, instance.release()); + } + + that_.volume_.SetGeometry(slices); + + { + OrthancStone::LinearAlgebra::Print(that_.volume_.GetImage().GetGeometry().GetCoordinates(0, 0, 0)); + OrthancStone::LinearAlgebra::Print(that_.volume_.GetImage().GetGeometry().GetCoordinates(1, 1, 1)); + return; + } + + for (size_t i = 0; i < that_.volume_.GetSlicesCount(); i++) + { + const DicomInstanceParameters& slice = that_.volume_.GetSliceParameters(i); + + const std::string& instance = slice.GetOrthancInstanceIdentifier(); + if (instance.empty()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + +#if 0 + std::auto_ptr command( + new Refactoring::GetOrthancWebViewerJpegCommand); + command->SetInstance(instance); + command->SetQuality(95); +#else + std::string uri = "/instances/" + instance; + + switch (slice.GetExpectedPixelFormat()) + { + case Orthanc::PixelFormat_RGB24: + uri += "/preview"; + break; + + case Orthanc::PixelFormat_Grayscale16: + uri += "/image-uint16"; + break; + + case Orthanc::PixelFormat_SignedGrayscale16: + uri += "/image-int16"; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + std::auto_ptr command( + new Refactoring::GetOrthancImageCommand); + command->SetHttpHeader("Accept-Encoding", "gzip"); + command->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Pam))); + command->SetUri(uri); +#endif + + command->SetExpectedPixelFormat(slice.GetExpectedPixelFormat()); + command->SetPayload(new LoadSliceImage(that_.volume_, i)); + + that_.oracle_.Schedule(that_, command.release()); + } + } + }; + + + class LoadInstanceGeometryHandler : public MessageHandler + { + private: + VolumeSeriesOrthancLoader& that_; + + public: + LoadInstanceGeometryHandler(VolumeSeriesOrthancLoader& that) : + that_(that) + { + } + + virtual void Handle(const Json::Value& body) const + { + Orthanc::DicomMap dicom; + dicom.FromDicomAsJson(body); + + DicomInstanceParameters instance(dicom); + } + }; + + + IOracle& oracle_; + bool active_; + DicomVolumeImage volume_; + + public: + VolumeSeriesOrthancLoader(IOracle& oracle, + OrthancStone::IObservable& oracleObservable) : + IObserver(oracleObservable.GetBroker()), + oracle_(oracle), + active_(false) + { + oracleObservable.RegisterObserverCallback( + new OrthancStone::Callable + (*this, &VolumeSeriesOrthancLoader::Handle)); + + oracleObservable.RegisterObserverCallback( + new OrthancStone::Callable + (*this, &VolumeSeriesOrthancLoader::Handle)); + + oracleObservable.RegisterObserverCallback( + new OrthancStone::Callable + (*this, &VolumeSeriesOrthancLoader::Handle)); + } + + void LoadSeries(const std::string& seriesId) + { + if (active_) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + active_ = true; + + std::auto_ptr command(new Refactoring::OrthancRestApiCommand); + command->SetUri("/series/" + seriesId + "/instances-tags"); + command->SetPayload(new LoadSeriesGeometryHandler(*this)); + + oracle_.Schedule(*this, command.release()); + } + + 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 Refactoring::OrthancRestApiCommand); + command->SetUri("/instances/" + instanceId + "/tags?ignore-length=3004-000c"); + command->SetPayload(new LoadInstanceGeometryHandler(*this)); + + oracle_.Schedule(*this, command.release()); + } + }; + + + + + /* class VolumeSlicerBase : public IVolumeSlicer + { + private: + OrthancStone::Scene2D& scene_; + int layerDepth_; + bool first_; + OrthancStone::CoordinateSystem3D lastPlane_; + + protected: + bool HasViewportPlaneChanged(const OrthancStone::CoordinateSystem3D& plane) const + { + if (first_ || + !OrthancStone::LinearAlgebra::IsCloseToZero( + boost::numeric::ublas::norm_2(lastPlane_.GetNormal() - plane.GetNormal()))) + { + // This is the first rendering, or the plane has not the same orientation + return false; + } + else + { + double offset1 = lastPlane_.ProjectAlongNormal(plane.GetOrigin()); + double offset2 = lastPlane_.ProjectAlongNormal(lastPlane_.GetOrigin()); + return OrthancStone::LinearAlgebra::IsCloseToZero(offset2 - offset1); + } + } + + void SetLastViewportPlane(const OrthancStone::CoordinateSystem3D& plane) + { + first_ = false; + lastPlane_ = plane; + } + + void SetLayer(OrthancStone::ISceneLayer* layer) + { + scene_.SetLayer(layerDepth_, layer); + } + + void DeleteLayer() + { + scene_.DeleteLayer(layerDepth_); + } + + public: + VolumeSlicerBase(OrthancStone::Scene2D& scene, + int layerDepth) : + scene_(scene), + layerDepth_(layerDepth), + first_(true) + { + } + };*/ + + + + class DicomVolumeSlicer : public IVolumeSlicer + { + private: + OrthancStone::Scene2D& scene_; + int layerDepth_; + const DicomVolumeImage& volume_; + bool first_; + OrthancStone::VolumeProjection lastProjection_; + unsigned int lastSliceIndex_; + uint64_t lastSliceRevision_; + + public: + DicomVolumeSlicer(OrthancStone::Scene2D& scene, + int layerDepth, + const DicomVolumeImage& volume) : + scene_(scene), + layerDepth_(layerDepth), + volume_(volume), + first_(true) + { + } + + virtual void SetViewportPlane(const OrthancStone::CoordinateSystem3D& plane) + { + if (!volume_.HasGeometry()) + { + scene_.DeleteLayer(layerDepth_); + return; + } + + OrthancStone::VolumeProjection projection; + unsigned int sliceIndex; + if (!volume_.GetImage().GetGeometry().DetectSlice(projection, sliceIndex, plane)) + { + // The cutting plane is neither axial, nor coronal, nor + // sagittal. Could use "VolumeReslicer" here. + scene_.DeleteLayer(layerDepth_); + return; + } + + uint64_t sliceRevision; + if (projection == OrthancStone::VolumeProjection_Axial) + { + sliceRevision = volume_.GetSliceRevision(sliceIndex); + } + else + { + // For coronal and sagittal projections, we take the global + // revision of the volume + sliceRevision = volume_.GetRevision(); + } + + if (first_ || + lastProjection_ == projection || + lastSliceIndex_ == sliceIndex || + lastSliceRevision_ == sliceRevision) + { + // Eiter the viewport plane, or the content of the slice have not + // changed since the last time the layer was set: Update is needed + + first_ = false; + lastProjection_ = projection; + lastSliceIndex_ = sliceIndex; + lastSliceRevision_ = sliceRevision; + + { + OrthancStone::ImageBuffer3D::SliceReader reader(volume_.GetImage(), projection, sliceIndex); + + // TODO: Convert the image to Float32 or RGB24 + + // TODO: Set the layer + } + } + } + }; + + + + + class NativeOracle : public IOracle { private: @@ -1067,7 +2070,6 @@ }; - class NativeApplicationContext : public IMessageEmitter { private: @@ -1136,463 +2138,6 @@ } }; }; - - - - class DicomInstanceParameters : - public Orthanc::IDynamicObject /* to be used as a payload of SlicesSorter */ - { - private: - Orthanc::DicomImageInformation imageInformation_; - OrthancStone::SopClassUid sopClassUid_; - double thickness_; - double pixelSpacingX_; - double pixelSpacingY_; - OrthancStone::CoordinateSystem3D geometry_; - OrthancStone::Vector frameOffsets_; - bool isColor_; - bool hasRescale_; - double rescaleOffset_; - double rescaleSlope_; - bool hasDefaultWindowing_; - float defaultWindowingCenter_; - float defaultWindowingWidth_; - Orthanc::PixelFormat expectedPixelFormat_; - - void ComputeDoseOffsets(const Orthanc::DicomMap& dicom) - { - // http://dicom.nema.org/medical/Dicom/2016a/output/chtml/part03/sect_C.8.8.3.2.html - - { - std::string increment; - - if (dicom.CopyToString(increment, Orthanc::DICOM_TAG_FRAME_INCREMENT_POINTER, false)) - { - Orthanc::Toolbox::ToUpperCase(increment); - if (increment != "3004,000C") // This is the "Grid Frame Offset Vector" tag - { - LOG(ERROR) << "RT-DOSE: Bad value for the \"FrameIncrementPointer\" tag"; - return; - } - } - } - - if (!OrthancStone::LinearAlgebra::ParseVector(frameOffsets_, dicom, Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR) || - frameOffsets_.size() < imageInformation_.GetNumberOfFrames()) - { - LOG(ERROR) << "RT-DOSE: No information about the 3D location of some slice(s)"; - frameOffsets_.clear(); - } - else - { - if (frameOffsets_.size() >= 2) - { - thickness_ = frameOffsets_[1] - frameOffsets_[0]; - - if (thickness_ < 0) - { - thickness_ = -thickness_; - } - } - } - } - - public: - DicomInstanceParameters(const Orthanc::DicomMap& dicom) : - imageInformation_(dicom) - { - if (imageInformation_.GetNumberOfFrames() <= 0) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); - } - - std::string s; - if (!dicom.CopyToString(s, Orthanc::DICOM_TAG_SOP_CLASS_UID, false)) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); - } - else - { - sopClassUid_ = OrthancStone::StringToSopClassUid(s); - } - - if (!dicom.ParseDouble(thickness_, Orthanc::DICOM_TAG_SLICE_THICKNESS)) - { - thickness_ = 100.0 * std::numeric_limits::epsilon(); - } - - OrthancStone::GeometryToolbox::GetPixelSpacing(pixelSpacingX_, pixelSpacingY_, dicom); - - std::string position, orientation; - if (dicom.CopyToString(position, Orthanc::DICOM_TAG_IMAGE_POSITION_PATIENT, false) && - dicom.CopyToString(orientation, Orthanc::DICOM_TAG_IMAGE_ORIENTATION_PATIENT, false)) - { - geometry_ = OrthancStone::CoordinateSystem3D(position, orientation); - } - - if (sopClassUid_ == OrthancStone::SopClassUid_RTDose) - { - ComputeDoseOffsets(dicom); - } - - isColor_ = (imageInformation_.GetPhotometricInterpretation() != Orthanc::PhotometricInterpretation_Monochrome1 && - imageInformation_.GetPhotometricInterpretation() != Orthanc::PhotometricInterpretation_Monochrome2); - - double doseGridScaling; - - if (dicom.ParseDouble(rescaleOffset_, Orthanc::DICOM_TAG_RESCALE_INTERCEPT) && - dicom.ParseDouble(rescaleSlope_, Orthanc::DICOM_TAG_RESCALE_SLOPE)) - { - hasRescale_ = true; - } - else if (dicom.ParseDouble(doseGridScaling, Orthanc::DICOM_TAG_DOSE_GRID_SCALING)) - { - hasRescale_ = true; - rescaleOffset_ = 0; - rescaleSlope_ = doseGridScaling; - } - else - { - hasRescale_ = false; - } - - OrthancStone::Vector c, w; - if (OrthancStone::LinearAlgebra::ParseVector(c, dicom, Orthanc::DICOM_TAG_WINDOW_CENTER) && - OrthancStone::LinearAlgebra::ParseVector(w, dicom, Orthanc::DICOM_TAG_WINDOW_WIDTH) && - c.size() > 0 && - w.size() > 0) - { - hasDefaultWindowing_ = true; - defaultWindowingCenter_ = static_cast(c[0]); - defaultWindowingWidth_ = static_cast(w[0]); - } - else - { - hasDefaultWindowing_ = false; - } - - if (sopClassUid_ == OrthancStone::SopClassUid_RTDose) - { - switch (imageInformation_.GetBitsStored()) - { - case 16: - expectedPixelFormat_ = Orthanc::PixelFormat_Grayscale16; - break; - - case 32: - expectedPixelFormat_ = Orthanc::PixelFormat_Grayscale32; - break; - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - } - else if (isColor_) - { - expectedPixelFormat_ = Orthanc::PixelFormat_RGB24; - } - else if (imageInformation_.IsSigned()) - { - expectedPixelFormat_ = Orthanc::PixelFormat_SignedGrayscale16; - } - else - { - expectedPixelFormat_ = Orthanc::PixelFormat_Grayscale16; - } - } - - const Orthanc::DicomImageInformation& GetImageInformation() const - { - return imageInformation_; - } - - OrthancStone::SopClassUid GetSopClassUid() const - { - return sopClassUid_; - } - - double GetThickness() const - { - return thickness_; - } - - double GetPixelSpacingX() const - { - return pixelSpacingX_; - } - - double GetPixelSpacingY() const - { - return pixelSpacingY_; - } - - const OrthancStone::CoordinateSystem3D& GetGeometry() const - { - return geometry_; - } - - OrthancStone::CoordinateSystem3D GetFrameGeometry(unsigned int frame) const - { - if (frame == 0) - { - return geometry_; - } - else if (frame >= imageInformation_.GetNumberOfFrames()) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); - } - else if (sopClassUid_ == OrthancStone::SopClassUid_RTDose) - { - if (frame >= frameOffsets_.size()) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } - - return OrthancStone::CoordinateSystem3D( - geometry_.GetOrigin() + frameOffsets_[frame] * geometry_.GetNormal(), - geometry_.GetAxisX(), - geometry_.GetAxisY()); - } - else - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - } - - bool FrameContainsPlane(unsigned int frame, - const OrthancStone::CoordinateSystem3D& plane) const - { - if (frame >= imageInformation_.GetNumberOfFrames()) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); - } - - OrthancStone::CoordinateSystem3D tmp = geometry_; - - if (frame != 0) - { - tmp = GetFrameGeometry(frame); - } - - double distance; - - return (OrthancStone::CoordinateSystem3D::GetDistance(distance, tmp, plane) && - distance <= thickness_ / 2.0); - } - - bool IsColor() const - { - return isColor_; - } - - bool HasRescale() const - { - return hasRescale_; - } - - double GetRescaleOffset() const - { - if (hasRescale_) - { - return rescaleOffset_; - } - else - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - } - - double GetRescaleSlope() const - { - if (hasRescale_) - { - return rescaleSlope_; - } - else - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - } - - bool HasDefaultWindowing() const - { - return hasDefaultWindowing_; - } - - float GetDefaultWindowingCenter() const - { - if (hasDefaultWindowing_) - { - return defaultWindowingCenter_; - } - else - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - } - - float GetDefaultWindowingWidth() const - { - if (hasDefaultWindowing_) - { - return defaultWindowingWidth_; - } - else - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - } - - Orthanc::PixelFormat GetExpectedPixelFormat() const - { - return expectedPixelFormat_; - } - }; - - - class AxialVolumeOrthancLoader : public OrthancStone::IObserver - { - private: - class MessageHandler : public Orthanc::IDynamicObject - { - public: - virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) const = 0; - }; - - void Handle(const OrthancRestApiCommand::SuccessMessage& message) - { - dynamic_cast(message.GetOrigin().GetPayload()).Handle(message); - } - - - class LoadSeriesGeometryHandler : public MessageHandler - { - private: - AxialVolumeOrthancLoader& that_; - - public: - LoadSeriesGeometryHandler(AxialVolumeOrthancLoader& that) : - that_(that) - { - } - - virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) const - { - Json::Value value; - message.ParseJsonBody(value); - - if (value.type() != Json::objectValue) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); - } - - Json::Value::Members instances = value.getMemberNames(); - - for (size_t i = 0; i < instances.size(); i++) - { - Orthanc::DicomMap dicom; - dicom.FromDicomAsJson(value[instances[i]]); - - std::auto_ptr instance(new DicomInstanceParameters(dicom)); - - OrthancStone::CoordinateSystem3D geometry = instance->GetGeometry(); - that_.slices_.AddSlice(geometry, instance.release()); - } - - if (!that_.slices_.Sort()) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, - "Cannot sort the 3D slices of a DICOM series"); - } - - printf("series sorted\n"); - } - }; - - - class LoadInstanceGeometryHandler : public MessageHandler - { - private: - AxialVolumeOrthancLoader& that_; - - public: - LoadInstanceGeometryHandler(AxialVolumeOrthancLoader& that) : - that_(that) - { - } - - virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) const - { - Json::Value value; - message.ParseJsonBody(value); - - if (value.type() != Json::objectValue) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); - } - - Orthanc::DicomMap dicom; - dicom.FromDicomAsJson(value); - - DicomInstanceParameters instance(dicom); - } - }; - - - bool active_; - std::auto_ptr image_; - OrthancStone::SlicesSorter slices_; - - public: - AxialVolumeOrthancLoader(OrthancStone::IObservable& oracle) : - IObserver(oracle.GetBroker()), - active_(false) - { - oracle.RegisterObserverCallback( - new OrthancStone::Callable - (*this, &AxialVolumeOrthancLoader::Handle)); - } - - void LoadSeries(IOracle& oracle, - const std::string& seriesId) - { - if (active_) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - - active_ = true; - - std::auto_ptr command(new Refactoring::OrthancRestApiCommand); - command->SetUri("/series/" + seriesId + "/instances-tags"); - command->SetPayload(new LoadSeriesGeometryHandler(*this)); - - oracle.Schedule(*this, command.release()); - } - - void LoadInstance(IOracle& oracle, - 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 Refactoring::OrthancRestApiCommand); - command->SetUri("/instances/" + instanceId + "/tags?ignore-length=3004-000c"); - command->SetPayload(new LoadInstanceGeometryHandler(*this)); - - oracle.Schedule(*this, command.release()); - } - }; - } @@ -1657,29 +2202,19 @@ }; -void Run(Refactoring::NativeApplicationContext& context) +void Run(Refactoring::NativeApplicationContext& context, + Refactoring::IOracle& oracle) { std::auto_ptr toto; - std::auto_ptr loader1, loader2; + std::auto_ptr loader1, loader2; { Refactoring::NativeApplicationContext::WriterLock lock(context); toto.reset(new Toto(lock.GetOracleObservable())); - loader1.reset(new Refactoring::AxialVolumeOrthancLoader(lock.GetOracleObservable())); - loader2.reset(new Refactoring::AxialVolumeOrthancLoader(lock.GetOracleObservable())); + loader1.reset(new Refactoring::VolumeSeriesOrthancLoader(oracle, lock.GetOracleObservable())); + loader2.reset(new Refactoring::VolumeSeriesOrthancLoader(oracle, lock.GetOracleObservable())); } - Refactoring::NativeOracle oracle(context); - - { - Orthanc::WebServiceParameters p; - //p.SetUrl("http://localhost:8043/"); - p.SetCredentials("orthanc", "orthanc"); - oracle.SetOrthancParameters(p); - } - - oracle.Start(); - if (1) { Json::Value v = Json::objectValue; @@ -1746,14 +2281,15 @@ // 2017-11-17-Anonymized - loader1->LoadSeries(oracle, "cb3ea4d1-d08f3856-ad7b6314-74d88d77-60b05618"); // CT - loader2->LoadInstance(oracle, "41029085-71718346-811efac4-420e2c15-d39f99b6"); // RT-DOSE + //loader1->LoadSeries("cb3ea4d1-d08f3856-ad7b6314-74d88d77-60b05618"); // CT + loader2->LoadInstance("41029085-71718346-811efac4-420e2c15-d39f99b6"); // RT-DOSE + + // Delphine + loader1->LoadSeries("5990e39c-51e5f201-fe87a54c-31a55943-e59ef80e"); // CT LOG(WARNING) << "...Waiting for Ctrl-C..."; Orthanc::SystemToolbox::ServerBarrier(); //boost::this_thread::sleep(boost::posix_time::seconds(1)); - - oracle.Stop(); } @@ -1771,7 +2307,21 @@ try { Refactoring::NativeApplicationContext context; - Run(context); + + Refactoring::NativeOracle oracle(context); + + { + Orthanc::WebServiceParameters p; + //p.SetUrl("http://localhost:8043/"); + p.SetCredentials("orthanc", "orthanc"); + oracle.SetOrthancParameters(p); + } + + oracle.Start(); + + Run(context, oracle); + + oracle.Stop(); } catch (Orthanc::OrthancException& e) { diff -r 8b6adfb62a2f -r 5c551f078c18 Samples/Sdl/TrackerSampleApp.cpp --- a/Samples/Sdl/TrackerSampleApp.cpp Wed May 15 16:56:17 2019 +0200 +++ b/Samples/Sdl/TrackerSampleApp.cpp Fri May 17 09:20:46 2019 +0200 @@ -349,6 +349,7 @@ TrackerSampleApp::TrackerSampleApp(MessageBroker& broker) : IObserver(broker) + , scene_(broker) , currentTool_(GuiTool_Rotate) { controller_ = ViewportControllerPtr(new ViewportController(broker)); diff -r 8b6adfb62a2f -r 5c551f078c18 Samples/WebAssembly/BasicScene.cpp --- a/Samples/WebAssembly/BasicScene.cpp Wed May 15 16:56:17 2019 +0200 +++ b/Samples/WebAssembly/BasicScene.cpp Fri May 17 09:20:46 2019 +0200 @@ -153,8 +153,10 @@ void SetupEvents(const std::string& canvas); public: - WebAssemblyViewport(const std::string& canvas) : + WebAssemblyViewport(MessageBroker& broker, + const std::string& canvas) : context_(canvas), + scene_(broker), compositor_(context_, scene_) { compositor_.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT, @@ -361,6 +363,7 @@ std::auto_ptr viewport1_; std::auto_ptr viewport2_; std::auto_ptr viewport3_; +OrthancStone::MessageBroker broker_; EM_BOOL OnWindowResize(int eventType, const EmscriptenUiEvent *uiEvent, void *userData) @@ -396,15 +399,15 @@ EMSCRIPTEN_KEEPALIVE void Initialize() { - viewport1_.reset(new OrthancStone::WebAssemblyViewport("mycanvas1")); + viewport1_.reset(new OrthancStone::WebAssemblyViewport(broker_, "mycanvas1")); PrepareScene(viewport1_->GetScene()); viewport1_->UpdateSize(); - viewport2_.reset(new OrthancStone::WebAssemblyViewport("mycanvas2")); + viewport2_.reset(new OrthancStone::WebAssemblyViewport(broker_, "mycanvas2")); PrepareScene(viewport2_->GetScene()); viewport2_->UpdateSize(); - viewport3_.reset(new OrthancStone::WebAssemblyViewport("mycanvas3")); + viewport3_.reset(new OrthancStone::WebAssemblyViewport(broker_, "mycanvas3")); PrepareScene(viewport3_->GetScene()); viewport3_->UpdateSize(); diff -r 8b6adfb62a2f -r 5c551f078c18 UnitTestsSources/UnitTestsMain.cpp --- a/UnitTestsSources/UnitTestsMain.cpp Wed May 15 16:56:17 2019 +0200 +++ b/UnitTestsSources/UnitTestsMain.cpp Fri May 17 09:20:46 2019 +0200 @@ -730,6 +730,82 @@ ASSERT_TRUE(OrthancStone::MessagingToolbox::ParseJson(response, source.c_str(), source.size())); } +TEST(VolumeImageGeometry, Basic) +{ + OrthancStone::VolumeImageGeometry g; + g.SetSize(10, 20, 30); + g.SetVoxelDimensions(1, 2, 3); + + OrthancStone::Vector p = g.GetCoordinates(0, 0, 0); + ASSERT_EQ(3u, p.size()); + ASSERT_FLOAT_EQ(-1.0 / 2.0, p[0]); + ASSERT_FLOAT_EQ(-2.0 / 2.0, p[1]); + ASSERT_FLOAT_EQ(-3.0 / 2.0, p[2]); + + p = g.GetCoordinates(1, 1, 1); + ASSERT_FLOAT_EQ(-1.0 / 2.0 + 10.0 * 1.0, p[0]); + ASSERT_FLOAT_EQ(-2.0 / 2.0 + 20.0 * 2.0, p[1]); + ASSERT_FLOAT_EQ(-3.0 / 2.0 + 30.0 * 3.0, p[2]); + + OrthancStone::VolumeProjection proj; + ASSERT_TRUE(g.DetectProjection(proj, g.GetAxialGeometry().GetNormal())); + ASSERT_EQ(OrthancStone::VolumeProjection_Axial, proj); + ASSERT_TRUE(g.DetectProjection(proj, g.GetCoronalGeometry().GetNormal())); + ASSERT_EQ(OrthancStone::VolumeProjection_Coronal, proj); + ASSERT_TRUE(g.DetectProjection(proj, g.GetSagittalGeometry().GetNormal())); + ASSERT_EQ(OrthancStone::VolumeProjection_Sagittal, proj); + + ASSERT_EQ(10u, g.GetProjectionWidth(OrthancStone::VolumeProjection_Axial)); + ASSERT_EQ(20u, g.GetProjectionHeight(OrthancStone::VolumeProjection_Axial)); + ASSERT_EQ(30u, g.GetProjectionDepth(OrthancStone::VolumeProjection_Axial)); + ASSERT_EQ(10u, g.GetProjectionWidth(OrthancStone::VolumeProjection_Coronal)); + ASSERT_EQ(30u, g.GetProjectionHeight(OrthancStone::VolumeProjection_Coronal)); + ASSERT_EQ(20u, g.GetProjectionDepth(OrthancStone::VolumeProjection_Coronal)); + ASSERT_EQ(20u, g.GetProjectionWidth(OrthancStone::VolumeProjection_Sagittal)); + ASSERT_EQ(30u, g.GetProjectionHeight(OrthancStone::VolumeProjection_Sagittal)); + ASSERT_EQ(10u, g.GetProjectionDepth(OrthancStone::VolumeProjection_Sagittal)); + + p = g.GetVoxelDimensions(OrthancStone::VolumeProjection_Axial); + ASSERT_EQ(3u, p.size()); + ASSERT_FLOAT_EQ(1, p[0]); + ASSERT_FLOAT_EQ(2, p[1]); + ASSERT_FLOAT_EQ(3, p[2]); + p = g.GetVoxelDimensions(OrthancStone::VolumeProjection_Coronal); + ASSERT_EQ(3u, p.size()); + ASSERT_FLOAT_EQ(1, p[0]); + ASSERT_FLOAT_EQ(3, p[1]); + ASSERT_FLOAT_EQ(2, p[2]); + p = g.GetVoxelDimensions(OrthancStone::VolumeProjection_Sagittal); + ASSERT_EQ(3u, p.size()); + ASSERT_FLOAT_EQ(2, p[0]); + ASSERT_FLOAT_EQ(3, p[1]); + ASSERT_FLOAT_EQ(1, p[2]); + + ASSERT_EQ(0, (int) OrthancStone::VolumeProjection_Axial); + ASSERT_EQ(1, (int) OrthancStone::VolumeProjection_Coronal); + ASSERT_EQ(2, (int) OrthancStone::VolumeProjection_Sagittal); + + for (int p = 0; p < 3; p++) + { + OrthancStone::VolumeProjection projection = (OrthancStone::VolumeProjection) p; + const OrthancStone::CoordinateSystem3D& s = g.GetProjectionGeometry(projection); + + for (unsigned int i = 0; i < g.GetProjectionDepth(projection); i++) + { + OrthancStone::CoordinateSystem3D plane( + s.GetOrigin() + static_cast(i) * s.GetNormal() * g.GetVoxelDimensions(projection)[2], + s.GetAxisX(), + s.GetAxisY()); + + unsigned int slice; + OrthancStone::VolumeProjection q; + ASSERT_TRUE(g.DetectSlice(q, slice, plane)); + ASSERT_EQ(projection, q); + ASSERT_EQ(i, slice); + } + } +} + int main(int argc, char **argv) { Orthanc::Logging::Initialize();