changeset 33:2460b376d3f7

reorganization
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 04 Apr 2024 18:50:11 +0200
parents 976da5476810
children bee2017f3088
files CMakeLists.txt Sources/Plugin.cpp Sources/STLToolbox.cpp Sources/STLToolbox.h Sources/StructurePolygon.cpp Sources/StructurePolygon.h Sources/Toolbox.cpp Sources/Toolbox.h Sources/VTKToolbox.cpp Sources/Vector3D.cpp
diffstat 10 files changed, 458 insertions(+), 368 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Thu Apr 04 18:35:54 2024 +0200
+++ b/CMakeLists.txt	Thu Apr 04 18:50:11 2024 +0200
@@ -205,7 +205,8 @@
 add_library(OrthancSTL SHARED
   Sources/Extent2D.cpp
   Sources/Plugin.cpp
-  Sources/Toolbox.cpp
+  Sources/STLToolbox.cpp
+  Sources/StructurePolygon.cpp
   Sources/VTKToolbox.cpp
   Sources/Vector3D.cpp
 
--- a/Sources/Plugin.cpp	Thu Apr 04 18:35:54 2024 +0200
+++ b/Sources/Plugin.cpp	Thu Apr 04 18:50:11 2024 +0200
@@ -22,6 +22,7 @@
  **/
 
 
+#include "StructurePolygon.h"
 #include "VTKToolbox.h"
 #include "Vector3D.h"
 #include "Toolbox.h"
@@ -40,6 +41,8 @@
 #include <SerializationToolbox.h>
 #include <SystemToolbox.h>
 
+#include <vtkNew.h>
+
 #include <boost/thread/shared_mutex.hpp>
 
 #define ORTHANC_PLUGIN_NAME  "stl"
@@ -147,235 +150,11 @@
 
 
 
-#include <dcmtk/dcmdata/dcdeftag.h>
 #include <dcmtk/dcmdata/dcfilefo.h>
-#include <dcmtk/dcmdata/dcitem.h>
 #include <dcmtk/dcmdata/dcsequen.h>
 #include <dcmtk/dcmdata/dcuid.h>
 
 
-static std::string GetStringValue(DcmItem& item,
-                                  const DcmTagKey& key)
-{
-  const char* s = NULL;
-  if (!item.findAndGetString(key, s).good() ||
-      s == NULL)
-  {
-    throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
-  }
-  else
-  {
-    return Orthanc::Toolbox::StripSpaces(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
-    {
-      target.insert(GetStringValue(*item, DCM_ROIName));
-    }
-  }
-}
-
-
-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);
-    }
-
-    roiName_ = GetStringValue(*structure, DCM_ROIName);
-    referencedSopInstanceUid_ = GetStringValue(*referenced->getItem(0), DCM_ReferencedSOPInstanceUID);
-
-    if (GetStringValue(*contour, DCM_ContourGeometricType) != "CLOSED_PLANAR")
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
-    }
-
-    {
-      std::vector<std::string> tokens;
-      Orthanc::Toolbox::TokenizeString(tokens, GetStringValue(*roi, DCM_ROIDisplayColor), '\\');
-
-      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;
-    }
-
-    {
-      std::vector<std::string> tokens;
-      Orthanc::Toolbox::TokenizeString(tokens, GetStringValue(*contour, DCM_ContourData), '\\');
-
-      const std::string s = GetStringValue(*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 (!Toolbox::MyParseDouble(x, tokens[i]) ||
-            !Toolbox::MyParseDouble(y, tokens[i + 1]) ||
-            !Toolbox::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 (!Toolbox::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 (!Toolbox::IsNear(a, b))
-      {
-        return false;
-      }
-    }
-
-    return true;
-  }
-
-  void Add(Extent2D& extent,
-           const Vector3D& axisX,
-           const Vector3D& axisY) const
-  {
-    assert(Toolbox::IsNear(1, axisX.ComputeNorm()));
-    assert(Toolbox::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]));
-    }
-  }
-};
-
-
-
 class StructureSet : public boost::noncopyable
 {
 private:
@@ -392,10 +171,10 @@
     hasFrameOfReferenceUid_(false)
   {
     DcmDataset& dataset = *dicom.GetDcmtkObject().getDataset();
-    patientId_ = GetStringValue(dataset, DCM_PatientID);
-    studyInstanceUid_ = GetStringValue(dataset, DCM_StudyInstanceUID);
-    seriesInstanceUid_ = GetStringValue(dataset, DCM_SeriesInstanceUID);
-    sopInstanceUid_ = GetStringValue(dataset, DCM_SOPInstanceUID);
+    patientId_ = STLToolbox::GetStringValue(dataset, DCM_PatientID);
+    studyInstanceUid_ = STLToolbox::GetStringValue(dataset, DCM_StudyInstanceUID);
+    seriesInstanceUid_ = STLToolbox::GetStringValue(dataset, DCM_SeriesInstanceUID);
+    sopInstanceUid_ = STLToolbox::GetStringValue(dataset, DCM_SOPInstanceUID);
 
     DcmSequenceOfItems* frame = NULL;
     if (!dataset.findAndGetSequence(DCM_ReferencedFrameOfReferenceSequence, frame).good() ||
@@ -523,6 +302,33 @@
       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
     }
   }
+
+  // This static method is faster than constructing the full "StructureSet" object
+  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
+      {
+        target.insert(STLToolbox::GetStringValue(*item, DCM_ROIName));
+      }
+    }
+  }
 };
 
 
@@ -555,7 +361,7 @@
 
     double d = (z - minProjectionAlongNormal_) / slicesSpacing_;
 
-    if (Toolbox::IsNear(d, round(d)))
+    if (STLToolbox::IsNear(d, round(d)))
     {
       if (d < 0.0 ||
           d > static_cast<double>(slicesCount_) - 1.0)
@@ -623,7 +429,7 @@
     // Only keep unique projections
 
     std::sort(projections.begin(), projections.end());
-    Toolbox::RemoveDuplicateValues(projections);
+    STLToolbox::RemoveDuplicateValues(projections);
     assert(!projections.empty());
 
     if (projections.size() == 1)
@@ -650,7 +456,7 @@
       }
 
       std::sort(spacings.begin(), spacings.end());
-      Toolbox::RemoveDuplicateValues(spacings);
+      STLToolbox::RemoveDuplicateValues(spacings);
 
       if (spacings.empty())
       {
@@ -687,7 +493,7 @@
       while (it != candidates.end())
       {
         double d = (projections[*it] - projections[reference]) / slicesSpacing_;
-        if (Toolbox::IsNear(d, round(d)))
+        if (STLToolbox::IsNear(d, round(d)))
         {
           countSupport ++;
         }
@@ -726,7 +532,7 @@
     for (size_t i = 0; i < projections.size(); i++)
     {
       double d = (projections[i] - bestProjection) / slicesSpacing_;
-      if (Toolbox::IsNear(d, round(d)))
+      if (STLToolbox::IsNear(d, round(d)))
       {
         minProjectionAlongNormal_ = std::min(minProjectionAlongNormal_, projections[i]);
         maxProjectionAlongNormal_ = std::max(maxProjectionAlongNormal_, projections[i]);
@@ -734,7 +540,7 @@
     }
 
     double d = (maxProjectionAlongNormal_ - minProjectionAlongNormal_) / slicesSpacing_;
-    if (Toolbox::IsNear(d, round(d)))
+    if (STLToolbox::IsNear(d, round(d)))
     {
       slicesCount_ = static_cast<size_t>(round(d)) + 1;
     }
@@ -902,12 +708,12 @@
             double x1, x2, x3, y1, y2, y3;
 
             if (items.size() == 6 &&
-                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]))
+                STLToolbox::MyParseDouble(x1, items[0]) &&
+                STLToolbox::MyParseDouble(x2, items[1]) &&
+                STLToolbox::MyParseDouble(x3, items[2]) &&
+                STLToolbox::MyParseDouble(y1, items[3]) &&
+                STLToolbox::MyParseDouble(y2, items[4]) &&
+                STLToolbox::MyParseDouble(y3, items[5]))
             {
               axisX = Vector3D(x1, x2, x3);
               axisY = Vector3D(y1, y2, y3);
@@ -938,7 +744,7 @@
     throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
   }
 
-  if (!Toolbox::IsNear(1, geometry.GetSlicesNormal().ComputeNorm()))
+  if (!STLToolbox::IsNear(1, geometry.GetSlicesNormal().ComputeNorm()))
   {
     throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
   }
@@ -948,8 +754,8 @@
 
   Vector3D axisZ = Vector3D::CrossProduct(axisX, axisY);
 
-  if (!Toolbox::IsNear(1, axisX.ComputeNorm()) ||
-      !Toolbox::IsNear(1, axisY.ComputeNorm()) ||
+  if (!STLToolbox::IsNear(1, axisX.ComputeNorm()) ||
+      !STLToolbox::IsNear(1, axisY.ComputeNorm()) ||
       !Vector3D::AreParallel(axisZ, geometry.GetSlicesNormal()))
   {
     throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
@@ -1037,7 +843,7 @@
   std::unique_ptr<Orthanc::ParsedDicomFile> dicom(LoadInstance(instanceId));
 
   std::set<std::string> names;
-  ListStructuresNames(names, *dicom);
+  StructureSet::ListStructuresNames(names, *dicom);
 
   Json::Value answer = Json::arrayValue;
 
@@ -1260,8 +1066,8 @@
   DcmDataset& dataset = *dicom->GetDcmtkObject().getDataset();
 
   std::string stl;
-  if (GetStringValue(dataset, DCM_MIMETypeOfEncapsulatedDocument) != Orthanc::MIME_STL ||
-      GetStringValue(dataset, DCM_SOPClassUID) != UID_EncapsulatedSTLStorage ||
+  if (STLToolbox::GetStringValue(dataset, DCM_MIMETypeOfEncapsulatedDocument) != Orthanc::MIME_STL ||
+      STLToolbox::GetStringValue(dataset, DCM_SOPClassUID) != UID_EncapsulatedSTLStorage ||
       !dicom->GetTagValue(stl, Orthanc::DICOM_TAG_ENCAPSULATED_DOCUMENT))
   {
     throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest, "DICOM instance not encapsulating a STL model: " + instanceId);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Sources/STLToolbox.cpp	Thu Apr 04 18:50:11 2024 +0200
@@ -0,0 +1,91 @@
+/**
+ * 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 "STLToolbox.h"
+
+#include <OrthancException.h>
+#include <Toolbox.h>
+
+#include <algorithm>
+#include <cmath>
+
+
+namespace STLToolbox
+{
+  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 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());
+  }
+
+
+  std::string GetStringValue(DcmItem& item,
+                             const DcmTagKey& key)
+  {
+    const char* s = NULL;
+    if (!item.findAndGetString(key, s).good() ||
+        s == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+    }
+    else
+    {
+      return Orthanc::Toolbox::StripSpaces(s);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Sources/STLToolbox.h	Thu Apr 04 18:50:11 2024 +0200
@@ -0,0 +1,48 @@
+/**
+ * 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>
+
+#include <dcmtk/dcmdata/dcdeftag.h>
+#include <dcmtk/dcmdata/dcitem.h>
+
+
+namespace STLToolbox
+{
+  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);
+
+  std::string GetStringValue(DcmItem& item,
+                             const DcmTagKey& key);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Sources/StructurePolygon.cpp	Thu Apr 04 18:50:11 2024 +0200
@@ -0,0 +1,187 @@
+/**
+ * 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 "StructurePolygon.h"
+
+#include "STLToolbox.h"
+
+#include <OrthancException.h>
+#include <SerializationToolbox.h>
+
+#include <dcmtk/dcmdata/dcfilefo.h>
+#include <dcmtk/dcmdata/dcdeftag.h>
+
+
+StructurePolygon::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);
+  }
+
+  roiName_ = STLToolbox::GetStringValue(*structure, DCM_ROIName);
+  referencedSopInstanceUid_ = STLToolbox::GetStringValue(*referenced->getItem(0), DCM_ReferencedSOPInstanceUID);
+
+  if (STLToolbox::GetStringValue(*contour, DCM_ContourGeometricType) != "CLOSED_PLANAR")
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+  }
+
+  {
+    std::vector<std::string> tokens;
+    Orthanc::Toolbox::TokenizeString(tokens, STLToolbox::GetStringValue(*roi, DCM_ROIDisplayColor), '\\');
+
+    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;
+  }
+
+  {
+    std::vector<std::string> tokens;
+    Orthanc::Toolbox::TokenizeString(tokens, STLToolbox::GetStringValue(*contour, DCM_ContourData), '\\');
+
+    const std::string s = STLToolbox::GetStringValue(*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 (!STLToolbox::MyParseDouble(x, tokens[i]) ||
+          !STLToolbox::MyParseDouble(y, tokens[i + 1]) ||
+          !STLToolbox::MyParseDouble(z, tokens[i + 2]))
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+
+      points_.push_back(Vector3D(x, y, z));
+    }
+
+    assert(points_.size() == countPoints);
+  }
+}
+
+
+const Vector3D& StructurePolygon::GetPoint(size_t i) const
+{
+  if (i >= points_.size())
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+  }
+  else
+  {
+    return points_[i];
+  }
+}
+
+
+bool StructurePolygon::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 (!STLToolbox::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 (!STLToolbox::IsNear(a, b))
+    {
+      return false;
+    }
+  }
+
+  return true;
+}
+
+
+void StructurePolygon::Add(Extent2D& extent,
+                           const Vector3D& axisX,
+                           const Vector3D& axisY) const
+{
+  assert(STLToolbox::IsNear(1, axisX.ComputeNorm()));
+  assert(STLToolbox::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]));
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Sources/StructurePolygon.h	Thu Apr 04 18:50:11 2024 +0200
@@ -0,0 +1,70 @@
+/**
+ * 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 "Extent2D.h"
+#include "Vector3D.h"
+
+#include <DicomParsing/ParsedDicomFile.h>
+
+
+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);
+
+  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;
+
+  bool IsCoplanar(Vector3D& normal) const;
+
+  void Add(Extent2D& extent,
+           const Vector3D& axisX,
+           const Vector3D& axisY) const;
+};
--- a/Sources/Toolbox.cpp	Thu Apr 04 18:35:54 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,72 +0,0 @@
-/**
- * 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());
-  }
-}
--- a/Sources/Toolbox.h	Thu Apr 04 18:35:54 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,42 +0,0 @@
-/**
- * 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);
-}
--- a/Sources/VTKToolbox.cpp	Thu Apr 04 18:35:54 2024 +0200
+++ b/Sources/VTKToolbox.cpp	Thu Apr 04 18:50:11 2024 +0200
@@ -30,12 +30,13 @@
 #include <OrthancException.h>
 #include <Toolbox.h>
 
-#include <vtkTriangle.h>
+#include <vtkImageConstantPad.h>
 #include <vtkImageResize.h>
-#include <vtkImageConstantPad.h>
 #include <vtkMarchingCubes.h>
+#include <vtkNew.h>
+#include <vtkPolyDataNormals.h>
 #include <vtkSmoothPolyDataFilter.h>
-#include <vtkPolyDataNormals.h>
+#include <vtkTriangle.h>
 
 #include <nifti1_io.h>
 
--- a/Sources/Vector3D.cpp	Thu Apr 04 18:35:54 2024 +0200
+++ b/Sources/Vector3D.cpp	Thu Apr 04 18:50:11 2024 +0200
@@ -73,7 +73,7 @@
 void Vector3D::Normalize()
 {
   double norm = ComputeNorm();
-  if (!Toolbox::IsNear(norm, 0))
+  if (!STLToolbox::IsNear(norm, 0))
   {
     x_ /= norm;
     y_ /= norm;
@@ -101,10 +101,10 @@
 bool Vector3D::AreParallel(const Vector3D& a,
                            const Vector3D& b)
 {
-  if (Toolbox::IsNear(a.ComputeSquaredNorm(), 1) &&
-      Toolbox::IsNear(b.ComputeSquaredNorm(), 1))
+  if (STLToolbox::IsNear(a.ComputeSquaredNorm(), 1) &&
+      STLToolbox::IsNear(b.ComputeSquaredNorm(), 1))
   {
-    return Toolbox::IsNear(std::abs(Vector3D::DotProduct(a, b)), 1);
+    return STLToolbox::IsNear(std::abs(Vector3D::DotProduct(a, b)), 1);
   }
   else
   {