Mercurial > hg > orthanc-stl
changeset 1:0f03a8a0bd6f
encoding of RT-STRUCT as STL
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Mon, 17 Jul 2023 18:54:31 +0200 |
parents | 4e889a8e8be2 |
children | 2bdb9acb7dcf |
files | CMakeLists.txt Resources/CMake/LibVtkConfiguration.cmake Sources/OrthancExplorer.js Sources/Plugin.cpp Sources/viewer.js |
diffstat | 5 files changed, 1355 insertions(+), 11 deletions(-) [+] |
line wrap: on
line diff
--- a/CMakeLists.txt Tue Jul 04 19:15:27 2023 +0200 +++ b/CMakeLists.txt Mon Jul 17 18:54:31 2023 +0200 @@ -19,7 +19,20 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. -cmake_minimum_required(VERSION 2.8) +# Native static compilation: +# $ cmake .. -DCMAKE_BUILD_TYPE=Debug -DSTATIC_BUILD=ON && make -j4 + +# LSB compilation: +# $ LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake .. -DCMAKE_BUILD_TYPE=Release -DSTATIC_BUILD=ON -DUSE_LEGACY_JSONCPP=ON -DUSE_LEGACY_BOOST=ON -DCMAKE_TOOLCHAIN_FILE=${HOME}/Subversion/orthanc/OrthancFramework/Resources/Toolchains/LinuxStandardBaseToolchain.cmake && make -j4 + +# MinGW compilation: +# $ cmake .. -DCMAKE_BUILD_TYPE=Release -DSTATIC_BUILD=ON -DUSE_LEGACY_JSONCPP=ON -DUSE_LEGACY_BOOST=ON -DCMAKE_TOOLCHAIN_FILE=${HOME}/Subversion/orthanc/OrthancFramework/Resources/Toolchains/MinGW-W64-Toolchain32.cmake && make -j4 + + +# We request version 3.12 of CMake, as it is the first version to +# support "--parallel" in "cmake --build": +# https://cmake.org/cmake/help/latest/manual/cmake.1.html#build-a-project +cmake_minimum_required(VERSION 3.12) project(OrthancSTL) set(ORTHANC_STL_VERSION "mainline") @@ -48,6 +61,9 @@ # Advanced parameters to fine-tune linking against system libraries SET(USE_SYSTEM_ORTHANC_SDK ON CACHE BOOL "Use the system version of the Orthanc plugin SDK") +# Parameters to fine-tune linking against system libraries +set(USE_SYSTEM_VTK ON CACHE BOOL "Use the system version of VTK") + ##################################################################### ## Download and setup the Orthanc framework @@ -72,11 +88,10 @@ else() include(${ORTHANC_FRAMEWORK_ROOT}/../Resources/CMake/OrthancFrameworkParameters.cmake) - set(ENABLE_LOCALE OFF) # Disable support for locales (notably in Boost) + set(ENABLE_LOCALE ON) set(ENABLE_ZLIB ON) + set(ENABLE_DCMTK ON) - set(ENABLE_MODULE_DICOM OFF CACHE INTERNAL "") - set(ENABLE_MODULE_IMAGES OFF CACHE INTERNAL "") set(ENABLE_MODULE_JOBS OFF CACHE INTERNAL "") include(${ORTHANC_FRAMEWORK_ROOT}/../Resources/CMake/OrthancFrameworkConfiguration.cmake) @@ -104,6 +119,8 @@ ## Platform-specific configuration ##################################################################### +include(${CMAKE_SOURCE_DIR}/Resources/CMake/LibVtkConfiguration.cmake) + add_definitions( -DHAS_ORTHANC_EXCEPTION=1 -DORTHANC_ENABLE_LOGGING_PLUGIN=1 @@ -183,10 +200,14 @@ ${CMAKE_SOURCE_DIR}/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp ${ORTHANC_CORE_SOURCES_DEPENDENCIES} ${ORTHANC_CORE_SOURCES_INTERNAL} + ${ORTHANC_DICOM_SOURCES_DEPENDENCIES} + ${ORTHANC_DICOM_SOURCES_INTERNAL} ${ORTHANC_STL_SOURCES} ) -add_dependencies(OrthancSTL AutogeneratedTarget) +DefineSourceBasenameForTarget(OrthancSTL) + +add_dependencies(OrthancSTL AutogeneratedTarget VTK) message("Setting the version of the library to ${ORTHANC_STL_VERSION}") @@ -194,6 +215,8 @@ VERSION ${ORTHANC_STL_VERSION} SOVERSION ${ORTHANC_STL_VERSION}) +target_link_libraries(OrthancSTL ${DCMTK_LIBRARIES} ${VTK_LIBRARIES}) + install( TARGETS OrthancSTL RUNTIME DESTINATION lib # Destination for Windows
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/CMake/LibVtkConfiguration.cmake Mon Jul 17 18:54:31 2023 +0200 @@ -0,0 +1,190 @@ +# SPDX-FileCopyrightText: 2023 Sebastien Jodogne, UCLouvain, Belgium +# SPDX-License-Identifier: GPL-3.0-or-later + + +# STL plugin for Orthanc +# Copyright (C) 2023 Sebastien Jodogne, UCLouvain, Belgium +# +# This program is free software: you can redistribute it and/or +# modify it under the terms of the GNU 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 +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +if (NOT STATIC_BUILD AND USE_SYSTEM_VTK) + find_package(VTK REQUIRED COMPONENTS + vtkCommonDataModel + vtkFiltersCore + vtkImagingCore + ) + + include_directories(${VTK_INCLUDE_DIRS}) + + # Add a void target + add_custom_target(VTK) + +else() + set(VTK_MAJOR 7) + set(VTK_MINOR 1) + set(VTK_REVISION 1) + set(VTK_MD5 "daee43460f4e95547f0635240ffbc9cb") + + set(VTK_SOURCES_DIR "${CMAKE_BINARY_DIR}/VTK-${VTK_MAJOR}.${VTK_MINOR}.${VTK_REVISION}") + + DownloadPackage( + "${VTK_MD5}" + "https://orthanc.uclouvain.be/third-party-downloads/VTK-${VTK_MAJOR}.${VTK_MINOR}.${VTK_REVISION}.tar.gz" + "${VTK_SOURCES_DIR}") + + if (CMAKE_TOOLCHAIN_FILE) + # Take absolute path to the toolchain + get_filename_component(TMP ${CMAKE_TOOLCHAIN_FILE} REALPATH BASE ${CMAKE_SOURCE_DIR}/..) + list(APPEND VTKCMakeFlags + -DCMAKE_TOOLCHAIN_FILE=${TMP} + -DLSB_CC=${LSB_CC} + -DLSB_CXX=${LSB_CXX} + ) + endif() + + cmake_host_system_information(RESULT NumberOfPhysicalCores QUERY NUMBER_OF_PHYSICAL_CORES) + + include(ExternalProject) + + if (CMAKE_CROSSCOMPILING) + # First, generate the "VTKCompileToolsConfig.cmake" file on the host + # computer. This is necessary before cross-compiling. Explanations: + # https://cmake.org/cmake/help/book/mastering-cmake/chapter/Cross%20Compiling%20With%20CMake.html#cross-compiling-a-complex-project-vtk + externalproject_add(VTKCompileTools + SOURCE_DIR "${VTK_SOURCES_DIR}" + + CMAKE_ARGS + -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE} + -DCMAKE_CXX_COMPILER=g++ + -DCMAKE_C_COMPILER=gcc + + BUILD_COMMAND + ${CMAKE_COMMAND} --build <BINARY_DIR> --config <CONFIG> --parallel ${NumberOfPhysicalCores} -t + VTKCompileToolsConfig.cmake + + INSTALL_COMMAND "" # Skip the install step + ) + + ExternalProject_Get_Property(VTKCompileTools binary_dir) + list(APPEND VTKCMakeFlags + -DVTKCompileTools_DIR=${binary_dir} + ) + + list(APPEND VTKCMakeFlags + -DCMAKE_REQUIRE_LARGE_FILE_SUPPORT=ON + -DCMAKE_REQUIRE_LARGE_FILE_SUPPORT__TRYRUN_OUTPUT="" + -DKWSYS_LFS_WORKS=ON + -DKWSYS_LFS_WORKS__TRYRUN_OUTPUT="" + -DFILE_OFFSET_BITS=64 + + # This simply disables HDF5, which is not used in Orthanc + -DVTK_USE_SYSTEM_HDF5=ON + ) + else() + add_custom_target(VTKCompileTools) # Empty target + endif() + + if (CMAKE_COMPILER_IS_GNUCXX OR + CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + # The following flag is necessary to statically link VTK within + # the shared library containing the plugin + set(Flags "-fPIC") + else() + set(Flags "") + endif() + + externalproject_add(VTK + SOURCE_DIR "${VTK_SOURCES_DIR}" + + CMAKE_ARGS + ${VTKCMakeFlags} + -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE} + -DBUILD_SHARED_LIBS=OFF + -DVTK_Group_Rendering=OFF + + -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER} + "-DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS} ${Flags}" + -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER} + "-DCMAKE_C_FLAGS=${CMAKE_C_FLAGS} ${Flags}" + -DCMAKE_OSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET} + -DCMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES} + + BUILD_COMMAND + ${CMAKE_COMMAND} --build <BINARY_DIR> --config <CONFIG> --parallel ${NumberOfPhysicalCores} -t + vtkCommonCore + vtkCommonDataModel + vtkCommonMath + vtkCommonMisc + vtkCommonSystem + vtkCommonTransforms + vtkFiltersCore + vtkFiltersPoints + vtkImagingCore + + EXCLUDE_FROM_ALL TRUE + + INSTALL_COMMAND "" # Skip the install step + + DEPENDS VTKCompileTools + ) + + if(MSVC) + set(Suffix ".lib") + set(Prefix "") + else() + set(Suffix ".a") + list(GET CMAKE_FIND_LIBRARY_PREFIXES 0 Prefix) + endif() + + foreach(module IN ITEMS + # WARNING: The order of the modules below *is* important! + ImagingCore + FiltersCore + CommonExecutionModel + CommonDataModel + CommonTransforms + CommonMath + CommonMisc + CommonSystem + CommonCore + sys + ) + list(APPEND VTK_LIBRARIES + ${Prefix}vtk${module}-${VTK_MAJOR}.${VTK_MINOR}${Suffix} + ) + endforeach() + + ExternalProject_Get_Property(VTK binary_dir) + ExternalProject_Get_Property(VTK source_dir) + + link_directories(${binary_dir}/lib) + + foreach(dir IN ITEMS + Common/Core + Common/DataModel + Common/ExecutionModel + Common/Misc + Filters/Core + IO/Image + Imaging/Core + Utilities/KWIML + ) + include_directories( + ${source_dir}/${dir} + ${binary_dir}/${dir} + ) + endforeach() + +endif()
--- a/Sources/OrthancExplorer.js Tue Jul 04 19:15:27 2023 +0200 +++ b/Sources/OrthancExplorer.js Mon Jul 17 18:54:31 2023 +0200 @@ -50,7 +50,7 @@ b.insertAfter($('#series-info')); b.click(function() { if ($.mobile.pageData) { - window.open('../stl/viewer.html?instance=' + instanceId); + window.open('../stl/app/viewer.html?instance=' + instanceId); } }); }
--- a/Sources/Plugin.cpp Tue Jul 04 19:15:27 2023 +0200 +++ b/Sources/Plugin.cpp Mon Jul 17 18:54:31 2023 +0200 @@ -24,11 +24,26 @@ #include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h" +#include <ChunkedBuffer.h> +#include <DicomParsing/ParsedDicomFile.h> +#include <Images/ImageProcessing.h> #include <Logging.h> +#include <OrthancFramework.h> +#include <SerializationToolbox.h> #include <SystemToolbox.h> #include <EmbeddedResources.h> +#include <vtkImageData.h> +#include <vtkImageResize.h> +#include <vtkMarchingCubes.h> +#include <vtkNew.h> +#include <vtkPolyData.h> +#include <vtkTriangle.h> +#include <vtkSmoothPolyDataFilter.h> +#include <vtkPolyDataNormals.h> +#include <vtkImageConstantPad.h> + #include <boost/thread/shared_mutex.hpp> // Forward declaration @@ -125,6 +140,1107 @@ } + + +#include <dcmtk/dcmdata/dcdeftag.h> +#include <dcmtk/dcmdata/dcfilefo.h> +#include <dcmtk/dcmdata/dcitem.h> +#include <dcmtk/dcmdata/dcsequen.h> + +class Extent2D : public boost::noncopyable +{ +private: + bool isEmpty_; + double x1_; + double y1_; + double x2_; + double y2_; + + void CheckNotEmpty() const + { + if (isEmpty_) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + +public: + Extent2D() : + isEmpty_(true), + x1_(0), + y1_(0), + x2_(0), + y2_(0) + { + } + + bool IsEmpty() const + { + return isEmpty_; + } + + double GetMinX() const + { + CheckNotEmpty(); + return x1_; + } + + double GetMaxX() const + { + CheckNotEmpty(); + return x2_; + } + + double GetMinY() const + { + CheckNotEmpty(); + return y1_; + } + + double GetMaxY() const + { + CheckNotEmpty(); + return y2_; + } + + double GetWidth() const + { + CheckNotEmpty(); + return x2_ - x1_; + } + + double GetHeight() const + { + CheckNotEmpty(); + return y2_ - y1_; + } + + void Add(double x, + double y) + { + if (isEmpty_) + { + x1_ = x2_ = x; + y1_ = y2_ = y; + isEmpty_ = false; + } + else + { + x1_ = std::min(x1_, x); + x2_ = std::max(x2_, x); + y1_ = std::min(y1_, y); + y2_ = std::max(y2_, y); + } + } +}; + +static void GetStringValue(std::string& value, + DcmItem& item, + const DcmTagKey& key) +{ + const char* s = NULL; + if (!item.findAndGetString(key, s).good() || + s == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + else + { + value.assign(s); + } +} + + +static void ListStructuresNames(std::set<std::string>& target, + Orthanc::ParsedDicomFile& source) +{ + target.clear(); + + DcmSequenceOfItems* sequence = NULL; + if (!source.GetDcmtkObject().getDataset()->findAndGetSequence(DCM_StructureSetROISequence, sequence).good() || + sequence == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + for (unsigned long i = 0; i < sequence->card(); i++) + { + DcmItem* item = sequence->getItem(i); + if (item == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + else + { + std::string value; + GetStringValue(value, *item, DCM_ROIName); + target.insert(value); + } + } +} + + +static bool IsNear(double a, + double b) +{ + return std::abs(a - b) < 10.0 * std::numeric_limits<double>::epsilon(); +} + + +class Vector3D +{ +private: + double x_; + double y_; + double z_; + +public: + Vector3D() : + x_(0), + y_(0), + z_(0) + { + } + + Vector3D(double x, + double y, + double z) : + x_(x), + y_(y), + z_(z) + { + } + + Vector3D(const Vector3D& from, + const Vector3D& to) : + x_(to.x_ - from.x_), + y_(to.y_ - from.y_), + z_(to.z_ - from.z_) + { + } + + double GetX() const + { + return x_; + } + + double GetY() const + { + return y_; + } + + double GetZ() const + { + return z_; + } + + double ComputeNorm() const + { + return sqrt(x_ * x_ + y_ * y_ + z_ * z_); + } + + void Normalize() + { + double norm = ComputeNorm(); + if (!IsNear(norm, 0)) + { + x_ /= norm; + y_ /= norm; + z_ /= norm; + } + } + + static Vector3D CrossProduct(const Vector3D& u, + const Vector3D& v) + { + return Vector3D(u.GetY() * v.GetZ() - u.GetZ() * v.GetY(), + u.GetZ() * v.GetX() - u.GetX() * v.GetZ(), + u.GetX() * v.GetY() - u.GetY() * v.GetX()); + } + + static double DotProduct(const Vector3D& a, + const Vector3D& b) + { + return a.GetX() * b.GetX() + a.GetY() * b.GetY() + a.GetZ() * b.GetZ(); + } +}; + + + +static bool MyParseDouble(double& value, + const std::string& s) +{ +#if 1 + char* end = NULL; + value = strtod(s.c_str(), &end); + return (end == s.c_str() + s.size()); +#else + return Orthanc::SerializationToolbox::ParseDouble(value, s); +#endif +} + + +class StructurePolygon : public boost::noncopyable +{ +private: + std::string roiName_; + std::string referencedSopInstanceUid_; + uint8_t red_; + uint8_t green_; + uint8_t blue_; + std::vector<Vector3D> points_; + +public: + StructurePolygon(Orthanc::ParsedDicomFile& dicom, + unsigned long roiIndex, + unsigned long contourIndex) + { + DcmDataset& dataset = *dicom.GetDcmtkObject().getDataset(); + + DcmItem* structure = NULL; + DcmItem* roi = NULL; + DcmItem* contour = NULL; + DcmSequenceOfItems* referenced = NULL; + + if (!dataset.findAndGetSequenceItem(DCM_StructureSetROISequence, structure, roiIndex).good() || + structure == NULL || + !dataset.findAndGetSequenceItem(DCM_ROIContourSequence, roi, roiIndex).good() || + roi == NULL || + !roi->findAndGetSequenceItem(DCM_ContourSequence, contour, contourIndex).good() || + contour == NULL || + !contour->findAndGetSequence(DCM_ContourImageSequence, referenced).good() || + referenced == NULL || + referenced->card() != 1) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + GetStringValue(roiName_, *structure, DCM_ROIName); + GetStringValue(referencedSopInstanceUid_, *referenced->getItem(0), DCM_ReferencedSOPInstanceUID); + + std::string s; + GetStringValue(s, *contour, DCM_ContourGeometricType); + if (s != "CLOSED_PLANAR") + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + { + std::string color; + GetStringValue(color, *roi, DCM_ROIDisplayColor); + + std::vector<std::string> tokens; + Orthanc::Toolbox::TokenizeString(tokens, color, '\\'); + + uint32_t r, g, b; + if (tokens.size() != 3 || + !Orthanc::SerializationToolbox::ParseFirstUnsignedInteger32(r, tokens[0]) || + !Orthanc::SerializationToolbox::ParseFirstUnsignedInteger32(g, tokens[1]) || + !Orthanc::SerializationToolbox::ParseFirstUnsignedInteger32(b, tokens[2]) || + r > 255 || + g > 255 || + b > 255) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + red_ = r; + green_ = g; + blue_ = b; + } + + { + GetStringValue(s, *contour, DCM_ContourData); + + std::vector<std::string> tokens; + Orthanc::Toolbox::TokenizeString(tokens, s, '\\'); + + GetStringValue(s, *contour, DCM_NumberOfContourPoints); + + uint32_t countPoints; + if (!Orthanc::SerializationToolbox::ParseUnsignedInteger32(countPoints, s) || + tokens.size() != 3 * countPoints) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + points_.reserve(countPoints); + + for (size_t i = 0; i < tokens.size(); i += 3) + { + double x, y, z; + if (!MyParseDouble(x, tokens[i]) || + !MyParseDouble(y, tokens[i + 1]) || + !MyParseDouble(z, tokens[i + 2])) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + points_.push_back(Vector3D(x, y, z)); + } + + assert(points_.size() == countPoints); + } + } + + const std::string& GetRoiName() const + { + return roiName_; + } + + const std::string& GetReferencedSopInstanceUid() const + { + return referencedSopInstanceUid_; + } + + size_t GetPointsCount() const + { + return points_.size(); + } + + const Vector3D& GetPoint(size_t i) const + { + if (i >= points_.size()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + else + { + return points_[i]; + } + } + + bool IsCoplanar(Vector3D& normal) const + { + if (points_.size() < 3) + { + return false; + } + + bool hasNormal = false; + + for (size_t i = 0; i < points_.size(); i++) + { + normal = Vector3D::CrossProduct(Vector3D(points_[1], points_[0]), + Vector3D(points_[2], points_[0])); + if (!IsNear(normal.ComputeNorm(), 0)) + { + normal.Normalize(); + hasNormal = true; + } + } + + if (!hasNormal) + { + return false; + } + + double a = Vector3D::DotProduct(points_[0], normal); + + for (size_t i = 1; i < points_.size(); i++) + { + double b = Vector3D::DotProduct(points_[i], normal); + if (!IsNear(a, b)) + { + return false; + } + } + + return true; + } + + void Add(Extent2D& extent, + const Vector3D& axisX, + const Vector3D& axisY) const + { + assert(IsNear(1, axisX.ComputeNorm())); + assert(IsNear(1, axisY.ComputeNorm())); + + for (size_t i = 0; i < points_.size(); i++) + { + extent.Add(Vector3D::DotProduct(axisX, points_[i]), + Vector3D::DotProduct(axisY, points_[i])); + } + } +}; + + + +struct IsNearPredicate +{ + bool operator() (const double& a, + const double& b) + { + return IsNear(a, b); + } +}; + +static void RemoveDuplicateValues(std::vector<double>& v) +{ + IsNearPredicate predicate; + std::vector<double>::iterator last = std::unique(v.begin(), v.end(), predicate); + v.erase(last, v.end()); +} + + +class StructureSet : public boost::noncopyable +{ +private: + std::vector<StructurePolygon*> polygons_; + bool hasGeometry_; + Vector3D slicesNormal_; + double slicesSpacing_; + double minProjectionAlongNormal_; + double maxProjectionAlongNormal_; + std::string patientId_; + std::string studyInstanceUid_; + std::string seriesInstanceUid_; + std::string sopInstanceUid_; + + void ComputeGeometry() + { + std::list<double> positionsList; + hasGeometry_ = false; + + for (size_t i = 0; i < polygons_.size(); i++) + { + assert(polygons_[i] != NULL); + + Vector3D n; + if (polygons_[i]->IsCoplanar(n)) + { + const Vector3D& point = polygons_[i]->GetPoint(0); + double z = Vector3D::DotProduct(point, n); + + if (!hasGeometry_) + { + hasGeometry_ = true; + slicesNormal_ = n; + minProjectionAlongNormal_ = z; + maxProjectionAlongNormal_ = z; + } + else if (!IsNear(std::abs(Vector3D::DotProduct(n, slicesNormal_)), 1)) + { + hasGeometry_ = false; + + // RT-STRUCT with non-parallel slices + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + else + { + minProjectionAlongNormal_ = std::min(minProjectionAlongNormal_, z); + maxProjectionAlongNormal_ = std::max(maxProjectionAlongNormal_, z); + } + + positionsList.push_back(Vector3D::DotProduct(n, point)); + } + } + + if (hasGeometry_) + { + std::vector<double> positions(positionsList.begin(), positionsList.end()); + assert(!positions.empty()); + + std::sort(positions.begin(), positions.end()); + RemoveDuplicateValues(positions); + assert(!positions.empty()); + + if (positions.size() == 1) + { + hasGeometry_ = false; + return; + } + + std::vector<double> offsets; + offsets.resize(positions.size() - 1); + + for (size_t i = 0; i < offsets.size(); i++) + { + offsets[i] = positions[i + 1] - positions[i]; + assert(offsets[i] > 0); + } + + std::sort(offsets.begin(), offsets.end()); + RemoveDuplicateValues(offsets); + + slicesSpacing_ = offsets[0]; + + for (size_t i = 1; i < offsets.size(); i++) + { + double d = offsets[i] / slicesSpacing_; + if (!IsNear(d, round(d))) + { + // Irregular spacing between the slices + hasGeometry_ = false; + break; + } + } + } + } + + void CheckHasGeometry() const + { + if (!hasGeometry_) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + +public: + explicit StructureSet(Orthanc::ParsedDicomFile& dicom) : + hasGeometry_(false), + slicesSpacing_(0), + minProjectionAlongNormal_(0), + maxProjectionAlongNormal_(0) + { + DcmDataset& dataset = *dicom.GetDcmtkObject().getDataset(); + GetStringValue(patientId_, dataset, DCM_PatientID); + GetStringValue(studyInstanceUid_, dataset, DCM_StudyInstanceUID); + GetStringValue(seriesInstanceUid_, dataset, DCM_SeriesInstanceUID); + GetStringValue(sopInstanceUid_, dataset, DCM_SOPInstanceUID); + + DcmSequenceOfItems* rois = NULL; + if (!dataset.findAndGetSequence(DCM_ROIContourSequence, rois).good() || + rois == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + std::vector<DcmSequenceOfItems*> contours(rois->card()); + size_t countPolygons = 0; + + for (unsigned long i = 0; i < rois->card(); i++) + { + DcmSequenceOfItems* contour = NULL; + if (!rois->getItem(i)->findAndGetSequence(DCM_ContourSequence, contour).good() || + contour == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + else + { + contours[i] = contour; + countPolygons += contour->card(); + } + } + + polygons_.resize(countPolygons); + + size_t pos = 0; + for (unsigned long i = 0; i < contours.size(); i++) + { + for (unsigned long j = 0; j < contours[i]->card(); j++, pos++) + { + polygons_[pos] = new StructurePolygon(dicom, i, j); + } + } + + assert(pos == countPolygons); + + ComputeGeometry(); + } + + ~StructureSet() + { + for (size_t i = 0; i < polygons_.size(); i++) + { + assert(polygons_[i] != NULL); + delete polygons_[i]; + } + } + + const std::string& GetPatient() const + { + return patientId_; + } + + const std::string& GetStudyInstanceUid() const + { + return studyInstanceUid_; + } + + const std::string& GetSeriesInstanceUid() const + { + return seriesInstanceUid_; + } + + const std::string& GetSopInstanceUid() const + { + return sopInstanceUid_; + } + + std::string HashStudy() const + { + std::string s; + Orthanc::Toolbox::ComputeSHA1(s, patientId_ + "|" + studyInstanceUid_); + return s; + } + + size_t GetPolygonsCount() const + { + return polygons_.size(); + } + + const StructurePolygon& GetPolygon(size_t i) const + { + if (i >= polygons_.size()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + else + { + assert(polygons_[i] != NULL); + return *polygons_[i]; + } + } + + bool HasGeometry() const + { + return hasGeometry_; + } + + const Vector3D& GetSlicesNormal() const + { + CheckHasGeometry(); + return slicesNormal_; + } + + double GetSlicesSpacing() const + { + CheckHasGeometry(); + return slicesSpacing_; + } + + double GetMinProjectionAlongNormal() const + { + CheckHasGeometry(); + return minProjectionAlongNormal_; + } + + double GetMaxProjectionAlongNormal() const + { + CheckHasGeometry(); + return maxProjectionAlongNormal_; + } + + double ProjectAlongNormal(const StructurePolygon& polygon) const + { + CheckHasGeometry(); + return Vector3D::DotProduct(slicesNormal_, polygon.GetPoint(0)); + } + + size_t GetSlicesCount() const + { + CheckHasGeometry(); + double c = (maxProjectionAlongNormal_ - minProjectionAlongNormal_) / slicesSpacing_; + assert(c >= 0); + + if (!IsNear(c, round(c))) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + else + { + return static_cast<size_t>(round(c)) + 1; + } + } + + bool LookupSliceIndex(size_t& slice, + const StructurePolygon& polygon) const + { + CheckHasGeometry(); + + double z = ProjectAlongNormal(polygon); + + if (z < minProjectionAlongNormal_ || + z > maxProjectionAlongNormal_) + { + return false; + } + else + { + double c = (z - minProjectionAlongNormal_) / slicesSpacing_; + + if (IsNear(c, round(c))) + { + slice = static_cast<size_t>(round(c)); + return true; + } + else + { + return false; + } + } + } + + bool LookupReferencedSopInstanceUid(std::string& sopInstanceUid) const + { + if (HasGeometry()) + { + for (size_t i = 0; i < polygons_.size(); i++) + { + assert(polygons_[i] != NULL); + + Vector3D n; + if (polygons_[i]->IsCoplanar(n) && + Vector3D::DotProduct(n, slicesNormal_)) + { + sopInstanceUid = polygons_[i]->GetReferencedSopInstanceUid(); + return true; + } + } + } + + return false; + } +}; + + + + +class XorFiller : public Orthanc::ImageProcessing::IPolygonFiller +{ +private: + Orthanc::ImageAccessor& target_; + +public: + XorFiller(Orthanc::ImageAccessor& target) : + target_(target) + { + } + + virtual void Fill(int y, + int x1, + int x2) ORTHANC_OVERRIDE + { + assert(x1 <= x2); + + if (y >= 0 && + y < static_cast<int>(target_.GetHeight())) + { + x1 = std::max(x1, 0); + x2 = std::min(x2, static_cast<int>(target_.GetWidth()) - 1); + + uint8_t* p = reinterpret_cast<uint8_t*>(target_.GetRow(y)) + x1; + + for (int i = x1; i <= x2; i++, p++) + { + *p = (*p ^ 0xff); + } + } + } +}; + + +static void EncodeSTL(std::string& target /* out */, + vtkPolyData& mesh /* in */) +{ + // TODO - Conversion to little endian on big endian + + Orthanc::ChunkedBuffer buffer; + + uint8_t header[80]; + memset(header, 0, sizeof(header)); + buffer.AddChunk(header, sizeof(header)); + + uint32_t n = mesh.GetNumberOfCells(); + buffer.AddChunk(&n, sizeof(n)); + + for (vtkIdType i = 0; i < mesh.GetNumberOfCells(); i++) + { + vtkCell* cell = mesh.GetCell(i); + vtkTriangle* triangle = dynamic_cast<vtkTriangle*>(cell); + + double p0[3]; + double p1[3]; + double p2[3]; + triangle->GetPoints()->GetPoint(0, p0); + triangle->GetPoints()->GetPoint(1, p1); + triangle->GetPoints()->GetPoint(2, p2); + + double normal[3]; + vtkTriangle::ComputeNormal(p0, p1, p2, normal); + + float d[4 * 3] = { + static_cast<float>(normal[0]), static_cast<float>(normal[1]), static_cast<float>(normal[2]), + static_cast<float>(p0[0]), static_cast<float>(p0[1]), static_cast<float>(p0[2]), + static_cast<float>(p1[0]), static_cast<float>(p1[1]), static_cast<float>(p1[2]), + static_cast<float>(p2[0]), static_cast<float>(p2[1]), static_cast<float>(p2[2]) }; + buffer.AddChunk(d, sizeof(d)); + + uint16_t a = 0; + buffer.AddChunk(&a, sizeof(a)); + } + + buffer.Flatten(target); +} + + +bool EncodeStructureSetMesh(std::string& stl, + const StructureSet& structureSet, + const std::set<std::string>& roiNames, + unsigned int resolution, + bool smooth) +{ + if (!structureSet.HasGeometry()) + { + return false; + } + + if (resolution < 1) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + if (!IsNear(1, structureSet.GetSlicesNormal().ComputeNorm())) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + // TODO - Axes could be retrieved from the referenced CT volume + Vector3D axisX(1, 0, 0); + Vector3D axisY = Vector3D::CrossProduct(structureSet.GetSlicesNormal(), axisX); + + if (!IsNear(1, axisX.ComputeNorm()) || + !IsNear(1, axisY.ComputeNorm())) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + Extent2D extent; + for (size_t i = 0; i < structureSet.GetPolygonsCount(); i++) + { + structureSet.GetPolygon(i).Add(extent, axisX, axisY); + } + + const int depth = structureSet.GetSlicesCount(); + + vtkNew<vtkImageData> volume; + volume->SetDimensions(resolution, resolution, depth); + volume->AllocateScalars(VTK_UNSIGNED_CHAR, 1); + + assert(sizeof(unsigned char) == 1); + memset(volume->GetScalarPointer(), 0, resolution * resolution * depth); + + for (size_t i = 0; i < structureSet.GetPolygonsCount(); i++) + { + const StructurePolygon& polygon = structureSet.GetPolygon(i); + if (roiNames.find(polygon.GetRoiName()) == roiNames.end()) + { + // This polygon doesn't correspond to a ROI of interest + continue; + } + + size_t j; + if (!structureSet.LookupSliceIndex(j, polygon)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + std::vector<Orthanc::ImageProcessing::ImagePoint> points; + points.reserve(polygon.GetPointsCount()); + for (size_t j = 0; j < polygon.GetPointsCount(); j++) + { + const Vector3D& point = polygon.GetPoint(j); + double x = (Vector3D::DotProduct(point, axisX) - extent.GetMinX()) / extent.GetWidth() * static_cast<double>(resolution); + double y = (Vector3D::DotProduct(point, axisY) - extent.GetMinY()) / extent.GetHeight() * static_cast<double>(resolution); + points.push_back(Orthanc::ImageProcessing::ImagePoint(static_cast<int32_t>(std::floor(x)), + static_cast<int32_t>(std::floor(y)))); + } + + Orthanc::ImageAccessor slice; + slice.AssignWritable(Orthanc::PixelFormat_Grayscale8, resolution, resolution, resolution /* pitch */, + reinterpret_cast<uint8_t*>(volume->GetScalarPointer()) + j * resolution * resolution); + + XorFiller filler(slice); + Orthanc::ImageProcessing::FillPolygon(filler, points); + } + + vtkNew<vtkImageResize> resize; + resize->SetOutputDimensions(resolution, resolution, resolution); + resize->SetInputData(volume.Get()); + resize->Update(); + + resize->GetOutput()->SetSpacing( + extent.GetWidth() / static_cast<double>(resolution), + extent.GetHeight() / static_cast<double>(resolution), + (structureSet.GetMaxProjectionAlongNormal() - structureSet.GetMinProjectionAlongNormal()) / static_cast<double>(resolution)); + + // TODO + // resize->GetOutput()->SetOrigin() + + vtkNew<vtkImageConstantPad> padding; + padding->SetConstant(0); + padding->SetOutputNumberOfScalarComponents(1); + padding->SetOutputWholeExtent(-1, resolution, -1, resolution, -1, resolution); + padding->SetInputData(resize->GetOutput()); + padding->Update(); + + vtkNew<vtkMarchingCubes> surface; + surface->SetInputData(padding->GetOutput()); + surface->ComputeNormalsOn(); + surface->SetValue(0, 128 /*isoValue*/); + surface->Update(); + + if (smooth) + { + vtkNew<vtkSmoothPolyDataFilter> smoothFilter; + // Apply volume smoothing + // https://examples.vtk.org/site/Cxx/PolyData/SmoothPolyDataFilter/ + smoothFilter->SetInputConnection(surface->GetOutputPort()); + smoothFilter->SetNumberOfIterations(15); + smoothFilter->SetRelaxationFactor(0.1); + smoothFilter->FeatureEdgeSmoothingOff(); + smoothFilter->BoundarySmoothingOn(); + smoothFilter->Update(); + + vtkNew<vtkPolyDataNormals> normalGenerator; + normalGenerator->SetInputConnection(smoothFilter->GetOutputPort()); + normalGenerator->ComputePointNormalsOn(); + normalGenerator->ComputeCellNormalsOn(); + normalGenerator->Update(); + + EncodeSTL(stl, *normalGenerator->GetOutput()); + } + else + { + EncodeSTL(stl, *surface->GetOutput()); + } + + return true; +} + + +void ListStructures(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + const std::string instanceId(request->groups[0]); + + if (request->method != OrthancPluginHttpMethod_Get) + { + OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "GET"); + return; + } + + std::string dicom; + if (!OrthancPlugins::RestApiGetString(dicom, "/instances/" + instanceId + "/file", false)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource); + } + else + { + Orthanc::ParsedDicomFile parsed(dicom); + + std::set<std::string> names; + ListStructuresNames(names, parsed); + + Json::Value answer = Json::arrayValue; + + for (std::set<std::string>::const_iterator it = names.begin(); it != names.end(); ++it) + { + answer.append(*it); + } + + std::string s = answer.toStyledString(); + OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, s.c_str(), s.size(), Orthanc::MIME_JSON); + } +} + + +void Encode(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + static const char* const KEY_INSTANCE = "Instance"; + static const char* const KEY_RESOLUTION = "Resolution"; + static const char* const KEY_ROI_NAMES = "RoiNames"; + static const char* const KEY_SMOOTH = "Smooth"; + static const char* const KEY_TAGS = "Tags"; + + if (request->method != OrthancPluginHttpMethod_Post) + { + OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "POST"); + return; + } + + Json::Value body; + if (!Orthanc::Toolbox::ReadJson(body, request->body, request->bodySize)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest); + } + + const std::string instanceId = Orthanc::SerializationToolbox::ReadString(body, KEY_INSTANCE); + const bool smooth = (body.isMember(KEY_SMOOTH) ? + Orthanc::SerializationToolbox::ReadBoolean(body, KEY_SMOOTH) : + true /* smooth by default */); + const unsigned int resolution = (body.isMember(KEY_RESOLUTION) ? + Orthanc::SerializationToolbox::ReadUnsignedInteger(body, KEY_RESOLUTION) : + 256 /* default value */); + + std::set<std::string> roiNames; + Orthanc::SerializationToolbox::ReadSetOfStrings(roiNames, body, KEY_ROI_NAMES); + + std::string dicom; + if (!OrthancPlugins::RestApiGetString(dicom, "/instances/" + instanceId + "/file", false)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource); + } + else + { + Orthanc::ParsedDicomFile parsed(dicom); + StructureSet structureSet(parsed); + + std::string stl; + if (!EncodeStructureSetMesh(stl, structureSet, roiNames, resolution, smooth)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "Cannot encode STL"); + } + else + { + std::string content; + Orthanc::Toolbox::EncodeDataUriScheme(content, "model/stl", stl); + + Json::Value create; + create["Content"] = content; + create["Parent"] = structureSet.HashStudy(); + + if (body.isMember(KEY_TAGS)) + { + create[KEY_TAGS] = body[KEY_TAGS]; + } + else + { + std::string description; + for (std::set<std::string>::const_iterator it = roiNames.begin(); it != roiNames.end(); ++it) + { + if (!description.empty()) + { + description += ", "; + } + + description += *it; + } + + create[KEY_TAGS] = Json::objectValue; + create[KEY_TAGS]["SeriesDescription"] = description; + } + + Json::Value answer; + if (OrthancPlugins::RestApiPost(answer, "/tools/create-dicom", create.toStyledString(), false)) + { + std::string s = answer.toStyledString(); + OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, s.c_str(), s.size(), Orthanc::MIME_JSON); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest, "Cannot create DICOM from STL"); + } + } + } +} + + extern "C" { ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context) @@ -150,6 +1266,8 @@ Orthanc::Logging::Initialize(context); #endif + Orthanc::InitializeFramework("", false); + hasCreateDicomStl_ = OrthancPlugins::CheckMinimalOrthancVersion(1, 12, 1); if (!hasCreateDicomStl_) @@ -160,19 +1278,27 @@ OrthancPluginSetDescription(context, "STL plugin for Orthanc."); - OrthancPlugins::RegisterRestCallback<ServeFile>("/stl/(.*)", true); + OrthancPlugins::RegisterRestCallback<ServeFile>("/stl/app/(.*)", true); // Extend the default Orthanc Explorer with custom JavaScript for STL std::string explorer; Orthanc::EmbeddedResources::GetFileResource(explorer, Orthanc::EmbeddedResources::ORTHANC_EXPLORER); OrthancPluginExtendOrthancExplorer(OrthancPlugins::GetGlobalContext(), explorer.c_str()); + OrthancPlugins::RegisterRestCallback<ListStructures>("/stl/rt-struct/([0-9a-f-]+)", true); + + if (hasCreateDicomStl_) + { + OrthancPlugins::RegisterRestCallback<Encode>("/stl/encode", true); + } + return 0; } ORTHANC_PLUGINS_API void OrthancPluginFinalize() { + Orthanc::FinalizeFramework(); }
--- a/Sources/viewer.js Tue Jul 04 19:15:27 2023 +0200 +++ b/Sources/viewer.js Mon Jul 17 18:54:31 2023 +0200 @@ -65,7 +65,7 @@ const loader = new STLLoader() loader.load( - '../instances/' + instanceId + '/content/0042-0011', + '../../instances/' + instanceId + '/content/0042-0011', function (geometry) { const frustumSize = 200; @@ -73,9 +73,14 @@ geometry.translate(-(geometry.boundingBox.min.x + geometry.boundingBox.max.x) / 2.0, -(geometry.boundingBox.min.y + geometry.boundingBox.max.y) / 2.0, -(geometry.boundingBox.min.z + geometry.boundingBox.max.z) / 2.0); - geometry.scale((frustumSize / 2.0) / (geometry.boundingBox.max.x - geometry.boundingBox.min.x), - (frustumSize / 2.0) / (geometry.boundingBox.max.y - geometry.boundingBox.min.y), - (frustumSize / 2.0) / (geometry.boundingBox.max.z - geometry.boundingBox.min.z)); + + var maxSize = Math.max(geometry.boundingBox.max.x - geometry.boundingBox.min.x, + geometry.boundingBox.max.y - geometry.boundingBox.min.y, + geometry.boundingBox.max.z - geometry.boundingBox.min.z); + + geometry.scale((frustumSize / 2.0) / maxSize, + (frustumSize / 2.0) / maxSize, + (frustumSize / 2.0) / maxSize); const mesh = new THREE.Mesh(geometry, material); scene.add(mesh);