Mercurial > hg > orthanc-stl
changeset 32:976da5476810
reorganization
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Thu, 04 Apr 2024 18:35:54 +0200 (13 months ago) |
parents | ab231760799d |
children | 2460b376d3f7 |
files | .hgignore CMakeLists.txt Sources/Extent2D.cpp Sources/Extent2D.h Sources/Plugin.cpp Sources/Toolbox.cpp Sources/Toolbox.h Sources/VTKToolbox.cpp Sources/VTKToolbox.h Sources/Vector3D.cpp Sources/Vector3D.h |
diffstat | 11 files changed, 870 insertions(+), 537 deletions(-) [+] |
line wrap: on
line diff
--- a/.hgignore Thu Apr 04 18:05:13 2024 +0200 +++ b/.hgignore Thu Apr 04 18:35:54 2024 +0200 @@ -7,3 +7,4 @@ Three/three.js-*.tar.gz Three/dist i/ +*.orig
--- a/CMakeLists.txt Thu Apr 04 18:05:13 2024 +0200 +++ b/CMakeLists.txt Thu Apr 04 18:35:54 2024 +0200 @@ -203,7 +203,12 @@ ##################################################################### add_library(OrthancSTL SHARED + Sources/Extent2D.cpp Sources/Plugin.cpp + Sources/Toolbox.cpp + Sources/VTKToolbox.cpp + Sources/Vector3D.cpp + ${AUTOGENERATED_SOURCES} ${CMAKE_SOURCE_DIR}/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp ${ORTHANC_CORE_SOURCES_DEPENDENCIES}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/Extent2D.cpp Thu Apr 04 18:35:54 2024 +0200 @@ -0,0 +1,107 @@ +/** + * SPDX-FileCopyrightText: 2023-2024 Sebastien Jodogne, UCLouvain, Belgium + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +/** + * STL plugin for Orthanc + * Copyright (C) 2023-2024 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/>. + **/ + + +#include "Extent2D.h" + +#include <OrthancException.h> + + +void Extent2D::CheckNotEmpty() const +{ + if (isEmpty_) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } +} + + +Extent2D::Extent2D() : + isEmpty_(true), + x1_(0), + y1_(0), + x2_(0), + y2_(0) +{ +} + + +double Extent2D::GetMinX() const +{ + CheckNotEmpty(); + return x1_; +} + + +double Extent2D::GetMaxX() const +{ + CheckNotEmpty(); + return x2_; +} + + +double Extent2D::GetMinY() const +{ + CheckNotEmpty(); + return y1_; +} + + +double Extent2D::GetMaxY() const +{ + CheckNotEmpty(); + return y2_; +} + + +double Extent2D::GetWidth() const +{ + CheckNotEmpty(); + return x2_ - x1_; +} + + +double Extent2D::GetHeight() const +{ + CheckNotEmpty(); + return y2_ - y1_; +} + + +void Extent2D::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); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/Extent2D.h Thu Apr 04 18:35:54 2024 +0200 @@ -0,0 +1,63 @@ +/** + * SPDX-FileCopyrightText: 2023-2024 Sebastien Jodogne, UCLouvain, Belgium + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +/** + * STL plugin for Orthanc + * Copyright (C) 2023-2024 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/>. + **/ + + +#pragma once + +#include <boost/noncopyable.hpp> + + +class Extent2D : public boost::noncopyable +{ +private: + bool isEmpty_; + double x1_; + double y1_; + double x2_; + double y2_; + + void CheckNotEmpty() const; + +public: + Extent2D(); + + bool IsEmpty() const + { + return isEmpty_; + } + + double GetMinX() const; + + double GetMaxX() const; + + double GetMinY() const; + + double GetMaxY() const; + + double GetWidth() const; + + double GetHeight() const; + + void Add(double x, + double y); +};
--- a/Sources/Plugin.cpp Thu Apr 04 18:05:13 2024 +0200 +++ b/Sources/Plugin.cpp Thu Apr 04 18:35:54 2024 +0200 @@ -22,13 +22,16 @@ **/ +#include "VTKToolbox.h" +#include "Vector3D.h" +#include "Toolbox.h" +#include "Extent2D.h" + #include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h" #include <EmbeddedResources.h> #include <DicomFormat/DicomInstanceHasher.h> -#include <Compression/GzipCompressor.h> -#include <ChunkedBuffer.h> #include <DicomParsing/FromDcmtkBridge.h> #include <DicomParsing/ParsedDicomFile.h> #include <Images/ImageProcessing.h> @@ -37,20 +40,8 @@ #include <SerializationToolbox.h> #include <SystemToolbox.h> -#include <vtkImageConstantPad.h> -#include <vtkImageData.h> -#include <vtkImageResize.h> -#include <vtkMarchingCubes.h> -#include <vtkNew.h> -#include <vtkPolyData.h> -#include <vtkPolyDataNormals.h> -#include <vtkSmoothPolyDataFilter.h> -#include <vtkTriangle.h> - #include <boost/thread/shared_mutex.hpp> -#include <nifti1_io.h> - #define ORTHANC_PLUGIN_NAME "stl" @@ -162,92 +153,6 @@ #include <dcmtk/dcmdata/dcsequen.h> #include <dcmtk/dcmdata/dcuid.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 std::string GetStringValue(DcmItem& item, const DcmTagKey& key) @@ -292,128 +197,6 @@ } -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 ComputeSquaredNorm() const - { - return x_ * x_ + y_ * y_ + z_ * z_; - } - - double ComputeNorm() const - { - return sqrt(ComputeSquaredNorm()); - } - - 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 AreParallel(const Vector3D& a, - const Vector3D& b) - { - if (IsNear(a.ComputeSquaredNorm(), 1) && - IsNear(b.ComputeSquaredNorm(), 1)) - { - return IsNear(std::abs(Vector3D::DotProduct(a, b)), 1); - } - else - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, - "Only applicable to normalized vectors"); - } - } -}; - - - -static bool MyParseDouble(double& value, - const std::string& s) -{ -#if 1 - // This version is much faster, as "ParseDouble()" internally uses - // "boost::lexical_cast<double>()" - 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: @@ -496,9 +279,9 @@ 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])) + if (!Toolbox::MyParseDouble(x, tokens[i]) || + !Toolbox::MyParseDouble(y, tokens[i + 1]) || + !Toolbox::MyParseDouble(z, tokens[i + 2])) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); } @@ -550,7 +333,7 @@ { normal = Vector3D::CrossProduct(Vector3D(points_[1], points_[0]), Vector3D(points_[2], points_[0])); - if (!IsNear(normal.ComputeNorm(), 0)) + if (!Toolbox::IsNear(normal.ComputeNorm(), 0)) { normal.Normalize(); hasNormal = true; @@ -567,7 +350,7 @@ for (size_t i = 1; i < points_.size(); i++) { double b = Vector3D::DotProduct(points_[i], normal); - if (!IsNear(a, b)) + if (!Toolbox::IsNear(a, b)) { return false; } @@ -580,8 +363,8 @@ const Vector3D& axisX, const Vector3D& axisY) const { - assert(IsNear(1, axisX.ComputeNorm())); - assert(IsNear(1, axisY.ComputeNorm())); + assert(Toolbox::IsNear(1, axisX.ComputeNorm())); + assert(Toolbox::IsNear(1, axisY.ComputeNorm())); for (size_t i = 0; i < points_.size(); i++) { @@ -593,23 +376,6 @@ -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: @@ -789,7 +555,7 @@ double d = (z - minProjectionAlongNormal_) / slicesSpacing_; - if (IsNear(d, round(d))) + if (Toolbox::IsNear(d, round(d))) { if (d < 0.0 || d > static_cast<double>(slicesCount_) - 1.0) @@ -857,7 +623,7 @@ // Only keep unique projections std::sort(projections.begin(), projections.end()); - RemoveDuplicateValues(projections); + Toolbox::RemoveDuplicateValues(projections); assert(!projections.empty()); if (projections.size() == 1) @@ -884,7 +650,7 @@ } std::sort(spacings.begin(), spacings.end()); - RemoveDuplicateValues(spacings); + Toolbox::RemoveDuplicateValues(spacings); if (spacings.empty()) { @@ -921,7 +687,7 @@ while (it != candidates.end()) { double d = (projections[*it] - projections[reference]) / slicesSpacing_; - if (IsNear(d, round(d))) + if (Toolbox::IsNear(d, round(d))) { countSupport ++; } @@ -960,7 +726,7 @@ for (size_t i = 0; i < projections.size(); i++) { double d = (projections[i] - bestProjection) / slicesSpacing_; - if (IsNear(d, round(d))) + if (Toolbox::IsNear(d, round(d))) { minProjectionAlongNormal_ = std::min(minProjectionAlongNormal_, projections[i]); maxProjectionAlongNormal_ = std::max(maxProjectionAlongNormal_, projections[i]); @@ -968,7 +734,7 @@ } double d = (maxProjectionAlongNormal_ - minProjectionAlongNormal_) / slicesSpacing_; - if (IsNear(d, round(d))) + if (Toolbox::IsNear(d, round(d))) { slicesCount_ = static_cast<size_t>(round(d)) + 1; } @@ -1077,171 +843,6 @@ }; -static void WriteFloat(Orthanc::ChunkedBuffer& buffer, - float value, - Orthanc::Endianness endianness) -{ - switch (endianness) - { - case Orthanc::Endianness_Little: - buffer.AddChunk(&value, sizeof(float)); - break; - - case Orthanc::Endianness_Big: - { - uint8_t tmp[4]; - tmp[0] = reinterpret_cast<uint8_t*>(&value) [3]; - tmp[1] = reinterpret_cast<uint8_t*>(&value) [2]; - tmp[2] = reinterpret_cast<uint8_t*>(&value) [1]; - tmp[3] = reinterpret_cast<uint8_t*>(&value) [0]; - buffer.AddChunk(&tmp, sizeof(float)); - break; - } - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } -} - - -static void WriteInteger(Orthanc::ChunkedBuffer& buffer, - uint32_t value, - Orthanc::Endianness endianness) -{ - switch (endianness) - { - case Orthanc::Endianness_Little: - buffer.AddChunk(&value, sizeof(uint32_t)); - break; - - case Orthanc::Endianness_Big: - { - uint8_t tmp[4]; - tmp[0] = reinterpret_cast<uint8_t*>(&value) [3]; - tmp[1] = reinterpret_cast<uint8_t*>(&value) [2]; - tmp[2] = reinterpret_cast<uint8_t*>(&value) [1]; - tmp[3] = reinterpret_cast<uint8_t*>(&value) [0]; - buffer.AddChunk(&tmp, sizeof(uint32_t)); - break; - } - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } -} - - -static void EncodeSTL(std::string& target /* out */, - vtkPolyData& mesh /* in */) -{ - const Orthanc::Endianness endianness = Orthanc::Toolbox::DetectEndianness(); - - Orthanc::ChunkedBuffer buffer; - - uint8_t header[80]; - memset(header, 0, sizeof(header)); - buffer.AddChunk(header, sizeof(header)); // This doesn't depend on endianness - - WriteInteger(buffer, mesh.GetNumberOfCells(), endianness); - - 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); - - WriteFloat(buffer, normal[0], endianness); - WriteFloat(buffer, normal[1], endianness); - WriteFloat(buffer, normal[2], endianness); - WriteFloat(buffer, p0[0], endianness); - WriteFloat(buffer, p0[1], endianness); - WriteFloat(buffer, p0[2], endianness); - WriteFloat(buffer, p1[0], endianness); - WriteFloat(buffer, p1[1], endianness); - WriteFloat(buffer, p1[2], endianness); - WriteFloat(buffer, p2[0], endianness); - WriteFloat(buffer, p2[1], endianness); - WriteFloat(buffer, p2[2], endianness); - - uint16_t a = 0; - buffer.AddChunk(&a, sizeof(a)); // This doesn't depend on endianness - } - - buffer.Flatten(target); -} - - -bool EncodeVolume(std::string& stl, - vtkImageData* volume, - unsigned int resolution, - bool smooth) -{ - if (volume == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - - vtkNew<vtkImageResize> resize; - resize->SetOutputDimensions(resolution, resolution, resolution); - resize->SetInputData(volume); - resize->Update(); - - vtkNew<vtkImageConstantPad> padding; - padding->SetConstant(0); - padding->SetOutputNumberOfScalarComponents(1); - padding->SetOutputWholeExtent(-1, resolution, -1, resolution, -1, resolution); - padding->SetInputData(resize->GetOutput()); - padding->Update(); - - double range[2]; - padding->GetOutput()->GetScalarRange(range); - - const double isoValue = (range[0] + range[1]) / 2.0; - - vtkNew<vtkMarchingCubes> surface; - surface->SetInputData(padding->GetOutput()); - surface->ComputeNormalsOn(); - surface->SetValue(0, 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; -} - - static Orthanc::ParsedDicomFile* LoadInstance(const std::string& instanceId) { std::string dicom; @@ -1301,12 +902,12 @@ double x1, x2, x3, y1, y2, y3; if (items.size() == 6 && - MyParseDouble(x1, items[0]) && - MyParseDouble(x2, items[1]) && - MyParseDouble(x3, items[2]) && - MyParseDouble(y1, items[3]) && - MyParseDouble(y2, items[4]) && - MyParseDouble(y3, items[5])) + Toolbox::MyParseDouble(x1, items[0]) && + Toolbox::MyParseDouble(x2, items[1]) && + Toolbox::MyParseDouble(x3, items[2]) && + Toolbox::MyParseDouble(y1, items[3]) && + Toolbox::MyParseDouble(y2, items[4]) && + Toolbox::MyParseDouble(y3, items[5])) { axisX = Vector3D(x1, x2, x3); axisY = Vector3D(y1, y2, y3); @@ -1337,7 +938,7 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } - if (!IsNear(1, geometry.GetSlicesNormal().ComputeNorm())) + if (!Toolbox::IsNear(1, geometry.GetSlicesNormal().ComputeNorm())) { throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); } @@ -1347,8 +948,8 @@ Vector3D axisZ = Vector3D::CrossProduct(axisX, axisY); - if (!IsNear(1, axisX.ComputeNorm()) || - !IsNear(1, axisY.ComputeNorm()) || + if (!Toolbox::IsNear(1, axisX.ComputeNorm()) || + !Toolbox::IsNear(1, axisY.ComputeNorm()) || !Vector3D::AreParallel(axisZ, geometry.GetSlicesNormal())) { throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); @@ -1417,7 +1018,7 @@ volume->SetSpacing(pixelSpacingX, pixelSpacingY, pixelSpacingZ); - return EncodeVolume(stl, volume.Get(), resolution, smooth); + return VTKToolbox::EncodeVolume(stl, volume.Get(), resolution, smooth); } @@ -1674,114 +1275,6 @@ -class NiftiHeader : public boost::noncopyable -{ -private: - nifti_image* image_; - -public: - NiftiHeader(const std::string& nifti) - { - nifti_1_header header; - if (nifti.size() < sizeof(header)) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); - } - - memcpy(&header, nifti.c_str(), sizeof(header)); - if (!nifti_hdr_looks_good(&header)) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); - } - - image_ = nifti_convert_nhdr2nim(header, "dummy_filename"); - if (image_ == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); - } - } - - ~NiftiHeader() - { - nifti_image_free(image_); - } - - const nifti_image& GetInfo() const - { - assert(image_ != NULL); - return *image_; - } -}; - - -static void LoadNifti(vtkImageData* volume, - std::string& nifti) -{ - if (volume == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - - const uint8_t* p = reinterpret_cast<const uint8_t*>(nifti.c_str()); - - if (nifti.size() >= 2 && - p[0] == 0x1f && - p[1] == 0x8b) - { - Orthanc::GzipCompressor compressor; - std::string uncompressed; - Orthanc::IBufferCompressor::Uncompress(uncompressed, compressor, nifti); - nifti.swap(uncompressed); - } - - NiftiHeader header(nifti); - - if (header.GetInfo().ndim != 3) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, - "Only 3D NIfTI volumes are allowed"); - } - - size_t itemSize; - int vtkType; - - switch (header.GetInfo().datatype) - { - case DT_UNSIGNED_CHAR: - itemSize = 1; - vtkType = VTK_UNSIGNED_CHAR; - break; - - case DT_FLOAT: - itemSize = sizeof(float); - vtkType = VTK_FLOAT; - break; - - case DT_DOUBLE: - itemSize = sizeof(double); - vtkType = VTK_DOUBLE; - break; - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - - assert(static_cast<int>(header.GetInfo().nvox) == header.GetInfo().nx * header.GetInfo().ny * header.GetInfo().nz); - - const size_t pixelDataOffset = sizeof(nifti_1_header) + 4 /* extension */; - - if (nifti.size() != pixelDataOffset + header.GetInfo().nvox * itemSize) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_CorruptedFile); - } - - volume->SetDimensions(header.GetInfo().nx, header.GetInfo().ny, header.GetInfo().nz); - volume->AllocateScalars(vtkType, 1); - volume->SetSpacing(header.GetInfo().dx, header.GetInfo().dy, header.GetInfo().dz); - memcpy(volume->GetScalarPointer(), &nifti[pixelDataOffset], header.GetInfo().nvox * itemSize); -} - - void EncodeNifti(OrthancPluginRestOutput* output, const char* url, const OrthancPluginHttpRequest* request) @@ -1818,10 +1311,10 @@ 256 /* default value */); vtkNew<vtkImageData> volume; - LoadNifti(volume.Get(), nifti); + VTKToolbox::LoadNifti(volume.Get(), nifti); std::string stl; - if (!EncodeVolume(stl, volume.Get(), resolution, smooth)) + if (!VTKToolbox::EncodeVolume(stl, volume.Get(), resolution, smooth)) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "Cannot encode STL from NIfTI"); }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/Toolbox.cpp Thu Apr 04 18:35:54 2024 +0200 @@ -0,0 +1,72 @@ +/** + * SPDX-FileCopyrightText: 2023-2024 Sebastien Jodogne, UCLouvain, Belgium + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +/** + * STL plugin for Orthanc + * Copyright (C) 2023-2024 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/>. + **/ + + +#include "Toolbox.h" + +#include <algorithm> +#include <cmath> + + +namespace Toolbox +{ + bool IsNear(double a, + double b) + { + return std::abs(a - b) < 10.0 * std::numeric_limits<double>::epsilon(); + } + + + 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 + } + + + namespace + { + struct IsNearPredicate + { + bool operator() (const double& a, + const double& b) + { + return Toolbox::IsNear(a, b); + } + }; + } + + + 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()); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/Toolbox.h Thu Apr 04 18:35:54 2024 +0200 @@ -0,0 +1,42 @@ +/** + * SPDX-FileCopyrightText: 2023-2024 Sebastien Jodogne, UCLouvain, Belgium + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +/** + * STL plugin for Orthanc + * Copyright (C) 2023-2024 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/>. + **/ + + +#pragma once + +#include <string> +#include <vector> + + +namespace Toolbox +{ + bool IsNear(double a, + double b); + + // This version is much faster, as "ParseDouble()" internally uses + // "boost::lexical_cast<double>()" + bool MyParseDouble(double& value, + const std::string& s); + + void RemoveDuplicateValues(std::vector<double>& v); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/VTKToolbox.cpp Thu Apr 04 18:35:54 2024 +0200 @@ -0,0 +1,319 @@ +/** + * SPDX-FileCopyrightText: 2023-2024 Sebastien Jodogne, UCLouvain, Belgium + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +/** + * STL plugin for Orthanc + * Copyright (C) 2023-2024 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/>. + **/ + + +#include "VTKToolbox.h" + +#include <ChunkedBuffer.h> +#include <Compression/GzipCompressor.h> +#include <Endianness.h> +#include <OrthancException.h> +#include <Toolbox.h> + +#include <vtkTriangle.h> +#include <vtkImageResize.h> +#include <vtkImageConstantPad.h> +#include <vtkMarchingCubes.h> +#include <vtkSmoothPolyDataFilter.h> +#include <vtkPolyDataNormals.h> + +#include <nifti1_io.h> + + +namespace VTKToolbox +{ + static void WriteFloat(Orthanc::ChunkedBuffer& buffer, + float value, + Orthanc::Endianness endianness) + { + switch (endianness) + { + case Orthanc::Endianness_Little: + buffer.AddChunk(&value, sizeof(float)); + break; + + case Orthanc::Endianness_Big: + { + uint8_t tmp[4]; + tmp[0] = reinterpret_cast<uint8_t*>(&value) [3]; + tmp[1] = reinterpret_cast<uint8_t*>(&value) [2]; + tmp[2] = reinterpret_cast<uint8_t*>(&value) [1]; + tmp[3] = reinterpret_cast<uint8_t*>(&value) [0]; + buffer.AddChunk(&tmp, sizeof(float)); + break; + } + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } + + + static void WriteInteger(Orthanc::ChunkedBuffer& buffer, + uint32_t value, + Orthanc::Endianness endianness) + { + switch (endianness) + { + case Orthanc::Endianness_Little: + buffer.AddChunk(&value, sizeof(uint32_t)); + break; + + case Orthanc::Endianness_Big: + { + uint8_t tmp[4]; + tmp[0] = reinterpret_cast<uint8_t*>(&value) [3]; + tmp[1] = reinterpret_cast<uint8_t*>(&value) [2]; + tmp[2] = reinterpret_cast<uint8_t*>(&value) [1]; + tmp[3] = reinterpret_cast<uint8_t*>(&value) [0]; + buffer.AddChunk(&tmp, sizeof(uint32_t)); + break; + } + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } + + + void EncodeSTL(std::string& target /* out */, + vtkPolyData& mesh /* in */) + { + const Orthanc::Endianness endianness = Orthanc::Toolbox::DetectEndianness(); + + Orthanc::ChunkedBuffer buffer; + + uint8_t header[80]; + memset(header, 0, sizeof(header)); + buffer.AddChunk(header, sizeof(header)); // This doesn't depend on endianness + + WriteInteger(buffer, mesh.GetNumberOfCells(), endianness); + + 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); + + WriteFloat(buffer, normal[0], endianness); + WriteFloat(buffer, normal[1], endianness); + WriteFloat(buffer, normal[2], endianness); + WriteFloat(buffer, p0[0], endianness); + WriteFloat(buffer, p0[1], endianness); + WriteFloat(buffer, p0[2], endianness); + WriteFloat(buffer, p1[0], endianness); + WriteFloat(buffer, p1[1], endianness); + WriteFloat(buffer, p1[2], endianness); + WriteFloat(buffer, p2[0], endianness); + WriteFloat(buffer, p2[1], endianness); + WriteFloat(buffer, p2[2], endianness); + + uint16_t a = 0; + buffer.AddChunk(&a, sizeof(a)); // This doesn't depend on endianness + } + + buffer.Flatten(target); + } + + + bool EncodeVolume(std::string& stl, + vtkImageData* volume, + unsigned int resolution, + bool smooth) + { + if (volume == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + vtkNew<vtkImageResize> resize; + resize->SetOutputDimensions(resolution, resolution, resolution); + resize->SetInputData(volume); + resize->Update(); + + vtkNew<vtkImageConstantPad> padding; + padding->SetConstant(0); + padding->SetOutputNumberOfScalarComponents(1); + padding->SetOutputWholeExtent(-1, resolution, -1, resolution, -1, resolution); + padding->SetInputData(resize->GetOutput()); + padding->Update(); + + double range[2]; + padding->GetOutput()->GetScalarRange(range); + + const double isoValue = (range[0] + range[1]) / 2.0; + + vtkNew<vtkMarchingCubes> surface; + surface->SetInputData(padding->GetOutput()); + surface->ComputeNormalsOn(); + surface->SetValue(0, 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; + } + + + namespace + { + class NiftiHeader : public boost::noncopyable + { + private: + nifti_image* image_; + + public: + NiftiHeader(const std::string& nifti) + { + nifti_1_header header; + if (nifti.size() < sizeof(header)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + memcpy(&header, nifti.c_str(), sizeof(header)); + if (!nifti_hdr_looks_good(&header)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + image_ = nifti_convert_nhdr2nim(header, "dummy_filename"); + if (image_ == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + } + + ~NiftiHeader() + { + nifti_image_free(image_); + } + + const nifti_image& GetInfo() const + { + assert(image_ != NULL); + return *image_; + } + }; + } + + + void LoadNifti(vtkImageData* volume /* out */, + std::string& nifti /* in */) + { + if (volume == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + const uint8_t* p = reinterpret_cast<const uint8_t*>(nifti.c_str()); + + if (nifti.size() >= 2 && + p[0] == 0x1f && + p[1] == 0x8b) + { + Orthanc::GzipCompressor compressor; + std::string uncompressed; + Orthanc::IBufferCompressor::Uncompress(uncompressed, compressor, nifti); + nifti.swap(uncompressed); + } + + NiftiHeader header(nifti); + + if (header.GetInfo().ndim != 3) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, + "Only 3D NIfTI volumes are allowed"); + } + + size_t itemSize; + int vtkType; + + switch (header.GetInfo().datatype) + { + case DT_UNSIGNED_CHAR: + itemSize = 1; + vtkType = VTK_UNSIGNED_CHAR; + break; + + case DT_FLOAT: + itemSize = sizeof(float); + vtkType = VTK_FLOAT; + break; + + case DT_DOUBLE: + itemSize = sizeof(double); + vtkType = VTK_DOUBLE; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + assert(static_cast<int>(header.GetInfo().nvox) == header.GetInfo().nx * header.GetInfo().ny * header.GetInfo().nz); + + const size_t pixelDataOffset = sizeof(nifti_1_header) + 4 /* extension */; + + if (nifti.size() != pixelDataOffset + header.GetInfo().nvox * itemSize) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_CorruptedFile); + } + + volume->SetDimensions(header.GetInfo().nx, header.GetInfo().ny, header.GetInfo().nz); + volume->AllocateScalars(vtkType, 1); + volume->SetSpacing(header.GetInfo().dx, header.GetInfo().dy, header.GetInfo().dz); + memcpy(volume->GetScalarPointer(), &nifti[pixelDataOffset], header.GetInfo().nvox * itemSize); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/VTKToolbox.h Thu Apr 04 18:35:54 2024 +0200 @@ -0,0 +1,43 @@ +/** + * SPDX-FileCopyrightText: 2023-2024 Sebastien Jodogne, UCLouvain, Belgium + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +/** + * STL plugin for Orthanc + * Copyright (C) 2023-2024 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/>. + **/ + + +#pragma once + +#include <vtkPolyData.h> +#include <vtkImageData.h> + + +namespace VTKToolbox +{ + void EncodeSTL(std::string& target /* out */, + vtkPolyData& mesh /* in */); + + bool EncodeVolume(std::string& stl, + vtkImageData* volume, + unsigned int resolution, + bool smooth); + + void LoadNifti(vtkImageData* volume /* out */, + std::string& nifti /* in */); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/Vector3D.cpp Thu Apr 04 18:35:54 2024 +0200 @@ -0,0 +1,114 @@ +/** + * SPDX-FileCopyrightText: 2023-2024 Sebastien Jodogne, UCLouvain, Belgium + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +/** + * STL plugin for Orthanc + * Copyright (C) 2023-2024 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/>. + **/ + + +#include "Vector3D.h" + +#include "Toolbox.h" + +#include <OrthancException.h> + +#include <cmath> + + +Vector3D::Vector3D() : + x_(0), + y_(0), + z_(0) +{ +} + + +Vector3D::Vector3D(double x, + double y, + double z) : + x_(x), + y_(y), + z_(z) +{ +} + + +Vector3D::Vector3D(const Vector3D& from, + const Vector3D& to) : + x_(to.x_ - from.x_), + y_(to.y_ - from.y_), + z_(to.z_ - from.z_) +{ +} + + +double Vector3D::ComputeSquaredNorm() const +{ + return x_ * x_ + y_ * y_ + z_ * z_; +} + + +double Vector3D::ComputeNorm() const +{ + return sqrt(ComputeSquaredNorm()); +} + + +void Vector3D::Normalize() +{ + double norm = ComputeNorm(); + if (!Toolbox::IsNear(norm, 0)) + { + x_ /= norm; + y_ /= norm; + z_ /= norm; + } +} + + +Vector3D 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()); +} + + +double Vector3D::DotProduct(const Vector3D& a, + const Vector3D& b) +{ + return a.GetX() * b.GetX() + a.GetY() * b.GetY() + a.GetZ() * b.GetZ(); +} + + +bool Vector3D::AreParallel(const Vector3D& a, + const Vector3D& b) +{ + if (Toolbox::IsNear(a.ComputeSquaredNorm(), 1) && + Toolbox::IsNear(b.ComputeSquaredNorm(), 1)) + { + return Toolbox::IsNear(std::abs(Vector3D::DotProduct(a, b)), 1); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, + "Only applicable to normalized vectors"); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/Vector3D.h Thu Apr 04 18:35:54 2024 +0200 @@ -0,0 +1,74 @@ +/** + * SPDX-FileCopyrightText: 2023-2024 Sebastien Jodogne, UCLouvain, Belgium + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +/** + * STL plugin for Orthanc + * Copyright (C) 2023-2024 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/>. + **/ + + +#pragma once + + +class Vector3D +{ +private: + double x_; + double y_; + double z_; + +public: + Vector3D(); + + Vector3D(double x, + double y, + double z); + + Vector3D(const Vector3D& from, + const Vector3D& to); + + double GetX() const + { + return x_; + } + + double GetY() const + { + return y_; + } + + double GetZ() const + { + return z_; + } + + double ComputeSquaredNorm() const; + + double ComputeNorm() const; + + void Normalize(); + + static Vector3D CrossProduct(const Vector3D& u, + const Vector3D& v); + + static double DotProduct(const Vector3D& a, + const Vector3D& b); + + static bool AreParallel(const Vector3D& a, + const Vector3D& b); +};