changeset 987:d225bccd4d4a

Scaffolding for A/B tests with DicomStructureSet[Loader] (A/B testing)
author Benjamin Golinvaux <bgo@osimis.io>
date Mon, 09 Sep 2019 15:18:24 +0200
parents 4e2de6b8a70b
children 4c9b4c4de814
files Framework/Loaders/DicomStructureSetLoader2.cpp Framework/Loaders/DicomStructureSetLoader2.h Framework/Toolbox/DicomStructureSet2.cpp Framework/Toolbox/DicomStructureSet2.h Resources/CMake/OrthancStoneConfiguration.cmake
diffstat 3 files changed, 1053 insertions(+), 1 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Toolbox/DicomStructureSet2.cpp	Mon Sep 09 15:18:24 2019 +0200
@@ -0,0 +1,836 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "DicomStructureSet2.h"
+
+#include "../Toolbox/GeometryToolbox.h"
+
+#include <Core/Logging.h>
+#include <Core/OrthancException.h>
+#include <Core/Toolbox.h>
+#include <Plugins/Samples/Common/FullOrthancDataset.h>
+#include <Plugins/Samples/Common/DicomDatasetReader.h>
+
+#include <limits>
+#include <stdio.h>
+#include <boost/geometry.hpp>
+#include <boost/geometry/geometries/point_xy.hpp>
+#include <boost/geometry/geometries/polygon.hpp>
+#include <boost/geometry/multi/geometries/multi_polygon.hpp>
+
+typedef boost::geometry::model::d2::point_xy<double> BoostPoint;
+typedef boost::geometry::model::polygon<BoostPoint> BoostPolygon;
+typedef boost::geometry::model::multi_polygon<BoostPolygon>  BoostMultiPolygon;
+
+
+static void Union(BoostMultiPolygon& output,
+                  std::vector<BoostPolygon>& input)
+{
+  for (size_t i = 0; i < input.size(); i++)
+  {
+    boost::geometry::correct(input[i]);
+  }
+  
+  if (input.size() == 0)
+  {
+    output.clear();
+  }
+  else if (input.size() == 1)
+  {
+    output.resize(1);
+    output[0] = input[0];
+  }
+  else
+  {
+    boost::geometry::union_(input[0], input[1], output);
+
+    for (size_t i = 0; i < input.size(); i++)
+    {
+      BoostMultiPolygon tmp;
+      boost::geometry::union_(output, input[i], tmp);
+      output = tmp;
+    }
+  }
+}
+
+
+static BoostPolygon CreateRectangle(float x1, float y1,
+                                    float x2, float y2)
+{
+  BoostPolygon r;
+  boost::geometry::append(r, BoostPoint(x1, y1));
+  boost::geometry::append(r, BoostPoint(x1, y2));
+  boost::geometry::append(r, BoostPoint(x2, y2));
+  boost::geometry::append(r, BoostPoint(x2, y1));
+  return r;
+}
+
+
+
+namespace OrthancStone
+{
+  static const OrthancPlugins::DicomTag DICOM_TAG_CONTOUR_GEOMETRIC_TYPE(0x3006, 0x0042);
+  static const OrthancPlugins::DicomTag DICOM_TAG_CONTOUR_IMAGE_SEQUENCE(0x3006, 0x0016);
+  static const OrthancPlugins::DicomTag DICOM_TAG_CONTOUR_SEQUENCE(0x3006, 0x0040);
+  static const OrthancPlugins::DicomTag DICOM_TAG_CONTOUR_DATA(0x3006, 0x0050);
+  static const OrthancPlugins::DicomTag DICOM_TAG_NUMBER_OF_CONTOUR_POINTS(0x3006, 0x0046);
+  static const OrthancPlugins::DicomTag DICOM_TAG_REFERENCED_SOP_INSTANCE_UID(0x0008, 0x1155);
+  static const OrthancPlugins::DicomTag DICOM_TAG_ROI_CONTOUR_SEQUENCE(0x3006, 0x0039);
+  static const OrthancPlugins::DicomTag DICOM_TAG_ROI_DISPLAY_COLOR(0x3006, 0x002a);
+  static const OrthancPlugins::DicomTag DICOM_TAG_ROI_NAME(0x3006, 0x0026);
+  static const OrthancPlugins::DicomTag DICOM_TAG_RT_ROI_INTERPRETED_TYPE(0x3006, 0x00a4);
+  static const OrthancPlugins::DicomTag DICOM_TAG_RT_ROI_OBSERVATIONS_SEQUENCE(0x3006, 0x0080);
+  static const OrthancPlugins::DicomTag DICOM_TAG_STRUCTURE_SET_ROI_SEQUENCE(0x3006, 0x0020);
+
+
+  static uint8_t ConvertColor(double v)
+  {
+    if (v < 0)
+    {
+      return 0;
+    }
+    else if (v >= 255)
+    {
+      return 255;
+    }
+    else
+    {
+      return static_cast<uint8_t>(v);
+    }
+  }
+
+
+  static bool ParseVector(Vector& target,
+                          const OrthancPlugins::IDicomDataset& dataset,
+                          const OrthancPlugins::DicomPath& tag)
+  {
+    std::string value;
+    return (dataset.GetStringValue(value, tag) &&
+            LinearAlgebra::ParseVector(target, value));
+  }
+
+  void DicomStructureSet2::Polygon::CheckPointIsOnSlice(const Vector& v) const
+  {
+    if (hasSlice_)
+    {
+      double magnitude =
+        GeometryToolbox::ProjectAlongNormal(v, geometry_.GetNormal());
+      if(!LinearAlgebra::IsNear(
+        magnitude,
+        projectionAlongNormal_,
+        sliceThickness_ / 2.0 /* in mm */ ))
+      {
+        LOG(ERROR) << "This RT-STRUCT contains a point that is off the "
+          << "slice of its instance | "
+          << "magnitude = " << magnitude << " | "
+          << "projectionAlongNormal_ = " << projectionAlongNormal_ << " | "
+          << "tolerance (sliceThickness_ / 2.0) = " << (sliceThickness_ / 2.0);
+
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+    }
+  }
+
+  bool DicomStructureSet2::Polygon::IsPointOnSlice(const Vector& v) const
+  {
+    if (hasSlice_)
+    {
+      double magnitude = 
+        GeometryToolbox::ProjectAlongNormal(v, geometry_.GetNormal());
+      bool onSlice = LinearAlgebra::IsNear(
+        magnitude,
+        projectionAlongNormal_,
+        sliceThickness_ / 2.0 /* in mm */);
+      if (!onSlice)
+      {
+        LOG(WARNING) << "This RT-STRUCT contains a point that is off the "
+          << "slice of its instance | "
+          << "magnitude = " << magnitude << " | "
+          << "projectionAlongNormal_ = " << projectionAlongNormal_ << " | "
+          << "tolerance (sliceThickness_ / 2.0) = " << (sliceThickness_ / 2.0);
+      }
+      return onSlice;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  void DicomStructureSet2::Polygon::AddPoint(const Vector& v)
+  {
+#if 1
+    // BGO 2019-09-03
+    if (IsPointOnSlice(v))
+    {
+      points_.push_back(v);
+    }
+#else
+    CheckPoint(v);
+    points_.push_back(v);
+#endif 
+  }
+
+
+  bool DicomStructureSet2::Polygon::UpdateReferencedSlice(const ReferencedSlices& slices)
+  {
+    if (hasSlice_)
+    {
+      return true;
+    }
+    else
+    {
+      ReferencedSlices::const_iterator it = slices.find(sopInstanceUid_);
+      
+      if (it == slices.end())
+      {
+        return false;
+      }
+      else
+      {
+        const CoordinateSystem3D& geometry = it->second.geometry_;
+        
+        hasSlice_ = true;
+        geometry_ = geometry;
+        projectionAlongNormal_ = GeometryToolbox::ProjectAlongNormal(geometry.GetOrigin(), geometry.GetNormal());
+        sliceThickness_ = it->second.thickness_;
+
+        extent_.Reset();
+        
+        for (Points::const_iterator it = points_.begin(); it != points_.end(); ++it)
+        {
+          if (IsPointOnSlice(*it))
+          {
+            double x, y;
+            geometry.ProjectPoint(x, y, *it);
+            extent_.AddPoint(x, y);
+          }
+        }
+        return true;
+      }
+    }
+  }
+
+  bool DicomStructureSet2::Polygon::IsOnSlice(const CoordinateSystem3D& slice) const
+  {
+    bool isOpposite = false;
+
+    if (points_.empty() ||
+        !hasSlice_ ||
+        !GeometryToolbox::IsParallelOrOpposite(isOpposite, slice.GetNormal(), geometry_.GetNormal()))
+    {
+      return false;
+    }
+    
+    double d = GeometryToolbox::ProjectAlongNormal(slice.GetOrigin(), geometry_.GetNormal());
+
+    return (LinearAlgebra::IsNear(d, projectionAlongNormal_,
+                                  sliceThickness_ / 2.0));
+  }
+    
+  bool DicomStructureSet2::Polygon::Project(double& x1,
+                                           double& y1,
+                                           double& x2,
+                                           double& y2,
+                                           const CoordinateSystem3D& slice) const
+  {
+    // TODO: optimize this method using a sweep-line algorithm for polygons
+    
+    if (!hasSlice_ ||
+        points_.size() <= 1)
+    {
+      return false;
+    }
+
+    double x, y;
+    geometry_.ProjectPoint(x, y, slice.GetOrigin());
+      
+    bool isOpposite;
+    if (GeometryToolbox::IsParallelOrOpposite
+        (isOpposite, slice.GetNormal(), geometry_.GetAxisY()))
+    {
+      if (y < extent_.GetY1() ||
+          y > extent_.GetY2())
+      {
+        // The polygon does not intersect the input slice
+        return false;
+      }
+        
+      bool isFirst = true;
+      double xmin = std::numeric_limits<double>::infinity();
+      double xmax = -std::numeric_limits<double>::infinity();
+
+      double prevX, prevY;
+      geometry_.ProjectPoint(prevX, prevY, points_[points_.size() - 1]);
+        
+      for (size_t i = 0; i < points_.size(); i++)
+      {
+        // Reference: ../../Resources/Computations/IntersectSegmentAndHorizontalLine.py
+        double curX, curY;
+        geometry_.ProjectPoint(curX, curY, points_[i]);
+
+        if ((prevY < y && curY > y) ||
+            (prevY > y && curY < y))
+        {
+          double p = (curX * prevY - curY * prevX + y * (prevX - curX)) / (prevY - curY);
+          xmin = std::min(xmin, p);
+          xmax = std::max(xmax, p);
+          isFirst = false;
+        }
+
+        prevX = curX;
+        prevY = curY;
+      }
+        
+      if (isFirst)
+      {
+        return false;
+      }
+      else
+      {
+        Vector p1 = (geometry_.MapSliceToWorldCoordinates(xmin, y) +
+                     sliceThickness_ / 2.0 * geometry_.GetNormal());
+        Vector p2 = (geometry_.MapSliceToWorldCoordinates(xmax, y) -
+                     sliceThickness_ / 2.0 * geometry_.GetNormal());
+          
+        slice.ProjectPoint(x1, y1, p1);
+        slice.ProjectPoint(x2, y2, p2);
+        return true;
+      }
+    }
+    else if (GeometryToolbox::IsParallelOrOpposite
+             (isOpposite, slice.GetNormal(), geometry_.GetAxisX()))
+    {
+      if (x < extent_.GetX1() ||
+          x > extent_.GetX2())
+      {
+        return false;
+      }
+
+      bool isFirst = true;
+      double ymin = std::numeric_limits<double>::infinity();
+      double ymax = -std::numeric_limits<double>::infinity();
+
+      double prevX, prevY;
+      geometry_.ProjectPoint(prevX, prevY, points_[points_.size() - 1]);
+        
+      for (size_t i = 0; i < points_.size(); i++)
+      {
+        // Reference: ../../Resources/Computations/IntersectSegmentAndVerticalLine.py
+        double curX, curY;
+        geometry_.ProjectPoint(curX, curY, points_[i]);
+
+        if ((prevX < x && curX > x) ||
+            (prevX > x && curX < x))
+        {
+          double p = (curX * prevY - curY * prevX + x * (curY - prevY)) / (curX - prevX);
+          ymin = std::min(ymin, p);
+          ymax = std::max(ymax, p);
+          isFirst = false;
+        }
+
+        prevX = curX;
+        prevY = curY;
+      }
+        
+      if (isFirst)
+      {
+        return false;
+      }
+      else
+      {
+        Vector p1 = (geometry_.MapSliceToWorldCoordinates(x, ymin) +
+                     sliceThickness_ / 2.0 * geometry_.GetNormal());
+        Vector p2 = (geometry_.MapSliceToWorldCoordinates(x, ymax) -
+                     sliceThickness_ / 2.0 * geometry_.GetNormal());
+
+        slice.ProjectPoint(x1, y1, p1);
+        slice.ProjectPoint(x2, y2, p2);
+
+        // TODO WHY THIS???
+        y1 = -y1;
+        y2 = -y2;
+
+        return true;
+      }
+    }
+    else
+    {
+      // Should not happen
+      return false;
+    }
+  }
+
+  
+  const DicomStructureSet2::Structure& DicomStructureSet2::GetStructure(size_t index) const
+  {
+    if (index >= structures_.size())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    return structures_[index];
+  }
+
+
+  DicomStructureSet2::Structure& DicomStructureSet2::GetStructure(size_t index)
+  {
+    if (index >= structures_.size())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    return structures_[index];
+  }
+
+  DicomStructureSet2::DicomStructureSet2(const OrthancPlugins::FullOrthancDataset& tags)
+  {
+    OrthancPlugins::DicomDatasetReader reader(tags);
+    
+    size_t count, tmp;
+    if (!tags.GetSequenceSize(count, DICOM_TAG_RT_ROI_OBSERVATIONS_SEQUENCE) ||
+        !tags.GetSequenceSize(tmp, DICOM_TAG_ROI_CONTOUR_SEQUENCE) ||
+        tmp != count ||
+        !tags.GetSequenceSize(tmp, DICOM_TAG_STRUCTURE_SET_ROI_SEQUENCE) ||
+        tmp != count)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+    }
+
+    structures_.resize(count);
+    for (size_t i = 0; i < count; i++)
+    {
+      structures_[i].interpretation_ = reader.GetStringValue
+        (OrthancPlugins::DicomPath(DICOM_TAG_RT_ROI_OBSERVATIONS_SEQUENCE, i,
+                                   DICOM_TAG_RT_ROI_INTERPRETED_TYPE),
+         "No interpretation");
+
+      structures_[i].name_ = reader.GetStringValue
+        (OrthancPlugins::DicomPath(DICOM_TAG_STRUCTURE_SET_ROI_SEQUENCE, i,
+                                   DICOM_TAG_ROI_NAME),
+         "No name");
+
+      Vector color;
+      if (ParseVector(color, tags, OrthancPlugins::DicomPath(DICOM_TAG_ROI_CONTOUR_SEQUENCE, i,
+                                                             DICOM_TAG_ROI_DISPLAY_COLOR)) &&
+          color.size() == 3)
+      {
+        structures_[i].red_ = ConvertColor(color[0]);
+        structures_[i].green_ = ConvertColor(color[1]);
+        structures_[i].blue_ = ConvertColor(color[2]);
+      }
+      else
+      {
+        structures_[i].red_ = 255;
+        structures_[i].green_ = 0;
+        structures_[i].blue_ = 0;
+      }
+
+      size_t countSlices;
+      if (!tags.GetSequenceSize(countSlices, OrthancPlugins::DicomPath(DICOM_TAG_ROI_CONTOUR_SEQUENCE, i,
+                                                                       DICOM_TAG_CONTOUR_SEQUENCE)))
+      {
+        countSlices = 0;
+      }
+
+      LOG(INFO) << "New RT structure: \"" << structures_[i].name_ 
+                   << "\" with interpretation \"" << structures_[i].interpretation_
+                   << "\" containing " << countSlices << " slices (color: " 
+                   << static_cast<int>(structures_[i].red_) << "," 
+                   << static_cast<int>(structures_[i].green_) << ","
+                   << static_cast<int>(structures_[i].blue_) << ")";
+
+      // These temporary variables avoid allocating many vectors in the loop below
+      OrthancPlugins::DicomPath countPointsPath(DICOM_TAG_ROI_CONTOUR_SEQUENCE, i,
+                                                DICOM_TAG_CONTOUR_SEQUENCE, 0,
+                                                DICOM_TAG_NUMBER_OF_CONTOUR_POINTS);
+
+      OrthancPlugins::DicomPath geometricTypePath(DICOM_TAG_ROI_CONTOUR_SEQUENCE, i,
+                                                  DICOM_TAG_CONTOUR_SEQUENCE, 0,
+                                                  DICOM_TAG_CONTOUR_GEOMETRIC_TYPE);
+      
+      OrthancPlugins::DicomPath imageSequencePath(DICOM_TAG_ROI_CONTOUR_SEQUENCE, i,
+                                                  DICOM_TAG_CONTOUR_SEQUENCE, 0,
+                                                  DICOM_TAG_CONTOUR_IMAGE_SEQUENCE);
+
+      // (3006,0039)[i] / (0x3006, 0x0040)[0] / (0x3006, 0x0016)[0] / (0x0008, 0x1155)
+      OrthancPlugins::DicomPath referencedInstancePath(DICOM_TAG_ROI_CONTOUR_SEQUENCE, i,
+                                                       DICOM_TAG_CONTOUR_SEQUENCE, 0,
+                                                       DICOM_TAG_CONTOUR_IMAGE_SEQUENCE, 0,
+                                                       DICOM_TAG_REFERENCED_SOP_INSTANCE_UID);
+
+      OrthancPlugins::DicomPath contourDataPath(DICOM_TAG_ROI_CONTOUR_SEQUENCE, i,
+                                                DICOM_TAG_CONTOUR_SEQUENCE, 0,
+                                                DICOM_TAG_CONTOUR_DATA);
+
+      for (size_t j = 0; j < countSlices; j++)
+      {
+        unsigned int countPoints;
+
+        countPointsPath.SetPrefixIndex(1, j);
+        if (!reader.GetUnsignedIntegerValue(countPoints, countPointsPath))
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+        }
+            
+        //LOG(INFO) << "Parsing slice containing " << countPoints << " vertices";
+
+        geometricTypePath.SetPrefixIndex(1, j);
+        std::string type = reader.GetMandatoryStringValue(geometricTypePath);
+        if (type != "CLOSED_PLANAR")
+        {
+          LOG(WARNING) << "Ignoring contour with geometry type: " << type;
+          continue;
+        }
+
+        size_t size;
+
+        imageSequencePath.SetPrefixIndex(1, j);
+        if (!tags.GetSequenceSize(size, imageSequencePath) || size != 1)
+        {
+          LOG(ERROR) << "The ContourImageSequence sequence (tag 3006,0016) must be present and contain one entry.";
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);          
+        }
+
+        referencedInstancePath.SetPrefixIndex(1, j);
+        std::string sopInstanceUid = reader.GetMandatoryStringValue(referencedInstancePath);
+
+        contourDataPath.SetPrefixIndex(1, j);        
+        std::string slicesData = reader.GetMandatoryStringValue(contourDataPath);
+
+        Vector points;
+        if (!LinearAlgebra::ParseVector(points, slicesData) ||
+            points.size() != 3 * countPoints)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);          
+        }
+
+        // seen in real world
+        if(Orthanc::Toolbox::StripSpaces(sopInstanceUid) == "") 
+        {
+          LOG(ERROR) << "WARNING. The following Dicom tag (Referenced SOP Instance UID) contains an empty value : // (3006,0039)[" << i << "] / (0x3006, 0x0040)[0] / (0x3006, 0x0016)[0] / (0x0008, 0x1155)";
+        }
+
+        Polygon polygon(sopInstanceUid);
+        polygon.Reserve(countPoints);
+
+        for (size_t k = 0; k < countPoints; k++)
+        {
+          Vector v(3);
+          v[0] = points[3 * k];
+          v[1] = points[3 * k + 1];
+          v[2] = points[3 * k + 2];
+          polygon.AddPoint(v);
+        }
+
+        structures_[i].polygons_.push_back(polygon);
+      }
+    }
+  }
+
+
+  Vector DicomStructureSet2::GetStructureCenter(size_t index) const
+  {
+    const Structure& structure = GetStructure(index);
+
+    Vector center;
+    LinearAlgebra::AssignVector(center, 0, 0, 0);
+    if (structure.polygons_.empty())
+    {
+      return center;
+    }
+
+    double n = static_cast<double>(structure.polygons_.size());
+
+    for (Polygons::const_iterator polygon = structure.polygons_.begin();
+         polygon != structure.polygons_.end(); ++polygon)
+    {
+      if (!polygon->GetPoints().empty())
+      {
+        center += polygon->GetPoints().front() / n;
+      }
+    }
+
+    return center;
+  }
+
+
+  const std::string& DicomStructureSet2::GetStructureName(size_t index) const
+  {
+    return GetStructure(index).name_;
+  }
+
+
+  const std::string& DicomStructureSet2::GetStructureInterpretation(size_t index) const
+  {
+    return GetStructure(index).interpretation_;
+  }
+
+
+  Color DicomStructureSet2::GetStructureColor(size_t index) const
+  {
+    const Structure& s = GetStructure(index);
+    return Color(s.red_, s.green_, s.blue_);
+  }
+  
+    
+  void DicomStructureSet2::GetStructureColor(uint8_t& red,
+                                            uint8_t& green,
+                                            uint8_t& blue,
+                                            size_t index) const
+  {
+    const Structure& s = GetStructure(index);
+    red = s.red_;
+    green = s.green_;
+    blue = s.blue_;
+  }
+
+
+  void DicomStructureSet2::GetReferencedInstances(std::set<std::string>& instances)
+  {
+    for (Structures::const_iterator structure = structures_.begin();
+         structure != structures_.end(); ++structure)
+    {
+      for (Polygons::const_iterator polygon = structure->polygons_.begin();
+           polygon != structure->polygons_.end(); ++polygon)
+      {
+        instances.insert(polygon->GetSopInstanceUid());
+      }
+    }
+  }
+
+
+  void DicomStructureSet2::AddReferencedSlice(const std::string& sopInstanceUid,
+                                             const std::string& seriesInstanceUid,
+                                             const CoordinateSystem3D& geometry,
+                                             double thickness)
+  {
+    if (referencedSlices_.find(sopInstanceUid) != referencedSlices_.end())
+    {
+      // This geometry is already known
+      LOG(ERROR) << "DicomStructureSet2::AddReferencedSlice(): (referencedSlices_.find(sopInstanceUid) != referencedSlices_.end()). sopInstanceUid = " << sopInstanceUid;
+     
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      if (thickness < 0)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+        
+      if (!referencedSlices_.empty())
+      {
+        const ReferencedSlice& reference = referencedSlices_.begin()->second;
+
+        if (reference.seriesInstanceUid_ != seriesInstanceUid)
+        {
+          LOG(ERROR) << "This RT-STRUCT refers to several different series";
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+        }
+
+        if (!GeometryToolbox::IsParallel(reference.geometry_.GetNormal(), geometry.GetNormal()))
+        {
+          LOG(ERROR) << "The slices in this RT-STRUCT are not parallel";
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+        }
+      }
+        
+      referencedSlices_[sopInstanceUid] = ReferencedSlice(seriesInstanceUid, geometry, thickness);
+
+      for (Structures::iterator structure = structures_.begin();
+           structure != structures_.end(); ++structure)
+      {
+        for (Polygons::iterator polygon = structure->polygons_.begin();
+             polygon != structure->polygons_.end(); ++polygon)
+        {
+          polygon->UpdateReferencedSlice(referencedSlices_);
+        }
+      }
+    }
+  }
+
+
+  void DicomStructureSet2::AddReferencedSlice(const Orthanc::DicomMap& dataset)
+  {
+    CoordinateSystem3D slice(dataset);
+
+    double thickness = 1;  // 1 mm by default
+
+    std::string s;
+    Vector v;
+    if (dataset.CopyToString(s, Orthanc::DICOM_TAG_SLICE_THICKNESS, false) &&
+        LinearAlgebra::ParseVector(v, s) &&
+        v.size() > 0)
+    {
+      thickness = v[0];
+    }
+
+    std::string instance, series;
+    if (dataset.CopyToString(instance, Orthanc::DICOM_TAG_SOP_INSTANCE_UID, false) &&
+        dataset.CopyToString(series, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false))
+    {
+      AddReferencedSlice(instance, series, slice, thickness);
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+    }
+  }
+
+
+  void DicomStructureSet2::CheckReferencedSlices()
+  {
+    for (Structures::iterator structure = structures_.begin();
+         structure != structures_.end(); ++structure)
+    {
+      for (Polygons::iterator polygon = structure->polygons_.begin();
+           polygon != structure->polygons_.end(); ++polygon)
+      {
+        if (!polygon->UpdateReferencedSlice(referencedSlices_))
+        {
+          std::string sopInstanceUid = polygon->GetSopInstanceUid();
+          if (Orthanc::Toolbox::StripSpaces(sopInstanceUid) == "")
+          {
+            LOG(ERROR) << "DicomStructureSet2::CheckReferencedSlices(): "
+              << " missing information about referenced instance "
+              << "(sopInstanceUid is empty!)";
+          }
+          else
+          {
+            LOG(ERROR) << "DicomStructureSet2::CheckReferencedSlices(): "
+              << " missing information about referenced instance "
+              << "(sopInstanceUid = " << sopInstanceUid << ")";
+          }
+          //throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+        }
+      }
+    }
+  }
+
+
+  Vector DicomStructureSet2::GetNormal() const
+  {
+    if (referencedSlices_.empty())
+    {
+      Vector v;
+      LinearAlgebra::AssignVector(v, 0, 0, 1);
+      return v;
+    }
+    else
+    {
+      return referencedSlices_.begin()->second.geometry_.GetNormal();
+    }
+  }
+
+  
+  bool DicomStructureSet2::ProjectStructure(std::vector< std::vector<PolygonPoint2D> >& polygons,
+                                           const Structure& structure,
+                                           const CoordinateSystem3D& slice) const
+  {
+    polygons.clear();
+
+    Vector normal = GetNormal();
+    
+    bool isOpposite;    
+    if (GeometryToolbox::IsParallelOrOpposite(isOpposite, normal, slice.GetNormal()))
+    {
+      // This is an axial projection
+
+      for (Polygons::const_iterator polygon = structure.polygons_.begin();
+           polygon != structure.polygons_.end(); ++polygon)
+      {
+        if (polygon->IsOnSlice(slice))
+        {
+          polygons.push_back(std::vector<PolygonPoint2D>());
+          
+          for (Points::const_iterator p = polygon->GetPoints().begin();
+               p != polygon->GetPoints().end(); ++p)
+          {
+            double x, y;
+            slice.ProjectPoint(x, y, *p);
+            polygons.back().push_back(std::make_pair(x, y));
+          }
+        }
+      }
+
+      return true;
+    }
+    else if (GeometryToolbox::IsParallelOrOpposite(isOpposite, normal, slice.GetAxisX()) ||
+             GeometryToolbox::IsParallelOrOpposite(isOpposite, normal, slice.GetAxisY()))
+    {
+#if 1
+      // Sagittal or coronal projection
+      std::vector<BoostPolygon> projected;
+  
+      for (Polygons::const_iterator polygon = structure.polygons_.begin();
+           polygon != structure.polygons_.end(); ++polygon)
+      {
+        double x1, y1, x2, y2;
+        if (polygon->Project(x1, y1, x2, y2, slice))
+        {
+          projected.push_back(CreateRectangle(
+            static_cast<float>(x1), 
+            static_cast<float>(y1), 
+            static_cast<float>(x2), 
+            static_cast<float>(y2)));
+        }
+      }
+
+      BoostMultiPolygon merged;
+      Union(merged, projected);
+
+      polygons.resize(merged.size());
+      for (size_t i = 0; i < merged.size(); i++)
+      {
+        const std::vector<BoostPoint>& outer = merged[i].outer();
+
+        polygons[i].resize(outer.size());
+        for (size_t j = 0; j < outer.size(); j++)
+        {
+          polygons[i][j] = std::make_pair(outer[j].x(), outer[j].y());
+        }
+      }  
+#else
+      for (Polygons::iterator polygon = structure.polygons_.begin();
+           polygon != structure.polygons_.end(); ++polygon)
+      {
+        double x1, y1, x2, y2;
+        if (polygon->Project(x1, y1, x2, y2, slice))
+        {
+          std::vector<PolygonPoint2D> p(4);
+          p[0] = std::make_pair(x1, y1);
+          p[1] = std::make_pair(x2, y1);
+          p[2] = std::make_pair(x2, y2);
+          p[3] = std::make_pair(x1, y2);
+          polygons.push_back(p);
+        }
+      }
+#endif
+      
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Toolbox/DicomStructureSet2.h	Mon Sep 09 15:18:24 2019 +0200
@@ -0,0 +1,185 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "CoordinateSystem3D.h"
+#include "Extent2D.h"
+#include "../Scene2D/Color.h"
+
+#include <Plugins/Samples/Common/FullOrthancDataset.h>
+
+#include <list>
+
+namespace OrthancStone
+{
+   class DicomStructureSet2 : public boost::noncopyable
+  {
+  public:
+    typedef std::pair<double, double> PolygonPoint2D;
+    
+  private:
+    struct ReferencedSlice
+    {
+      std::string          seriesInstanceUid_;
+      CoordinateSystem3D   geometry_;
+      double               thickness_;
+
+      ReferencedSlice()
+      {
+      }
+      
+      ReferencedSlice(const std::string& seriesInstanceUid,
+                      const CoordinateSystem3D& geometry,
+                      double thickness) :
+        seriesInstanceUid_(seriesInstanceUid),
+        geometry_(geometry),
+        thickness_(thickness)
+      {
+      }
+    };
+
+    typedef std::map<std::string, ReferencedSlice>  ReferencedSlices;
+    
+    typedef std::vector<Vector>  Points;
+
+    class Polygon
+    {
+    private:
+      std::string         sopInstanceUid_;
+      bool                hasSlice_;
+      CoordinateSystem3D  geometry_;
+      double              projectionAlongNormal_;
+      double              sliceThickness_;  // In millimeters
+      Points              points_;
+      Extent2D            extent_;
+
+      void CheckPointIsOnSlice(const Vector& v) const;
+      bool IsPointOnSlice(const Vector& v) const;
+
+    public:
+      Polygon(const std::string& sopInstanceUid) :
+        sopInstanceUid_(sopInstanceUid),
+        hasSlice_(false)
+      {
+      }
+
+      void Reserve(size_t n)
+      {
+        points_.reserve(n);
+      }
+
+      void AddPoint(const Vector& v);
+
+      bool UpdateReferencedSlice(const ReferencedSlices& slices);
+
+      bool IsOnSlice(const CoordinateSystem3D& geometry) const;
+
+      const std::string& GetSopInstanceUid() const
+      {
+        return sopInstanceUid_;
+      }
+
+      const Points& GetPoints() const
+      {
+        return points_;
+      }
+
+      double GetSliceThickness() const
+      {
+        return sliceThickness_;
+      }
+
+      bool Project(double& x1,
+                   double& y1,
+                   double& x2,
+                   double& y2,
+                   const CoordinateSystem3D& slice) const;
+    };
+
+    typedef std::list<Polygon>  Polygons;
+
+    struct Structure
+    {
+      std::string   name_;
+      std::string   interpretation_;
+      Polygons      polygons_;
+      uint8_t       red_;
+      uint8_t       green_;
+      uint8_t       blue_;
+    };
+
+    typedef std::vector<Structure>  Structures;
+
+    Structures        structures_;
+    ReferencedSlices  referencedSlices_;
+
+    const Structure& GetStructure(size_t index) const;
+
+    Structure& GetStructure(size_t index);
+  
+    bool ProjectStructure(std::vector< std::vector<PolygonPoint2D> >& polygons,
+                          const Structure& structure,
+                          const CoordinateSystem3D& slice) const;
+
+  public:
+    DicomStructureSet2(const OrthancPlugins::FullOrthancDataset& instance);
+
+    size_t GetStructuresCount() const
+    {
+      return structures_.size();
+    }
+
+    Vector GetStructureCenter(size_t index) const;
+
+    const std::string& GetStructureName(size_t index) const;
+
+    const std::string& GetStructureInterpretation(size_t index) const;
+
+    Color GetStructureColor(size_t index) const;
+
+    // TODO - remove
+    void GetStructureColor(uint8_t& red,
+                           uint8_t& green,
+                           uint8_t& blue,
+                           size_t index) const;
+
+    void GetReferencedInstances(std::set<std::string>& instances);
+
+    void AddReferencedSlice(const std::string& sopInstanceUid,
+                            const std::string& seriesInstanceUid,
+                            const CoordinateSystem3D& geometry,
+                            double thickness);
+
+    void AddReferencedSlice(const Orthanc::DicomMap& dataset);
+
+    void CheckReferencedSlices();
+
+    Vector GetNormal() const;
+
+    bool ProjectStructure(std::vector< std::vector<PolygonPoint2D> >& polygons,
+                          size_t index,
+                          const CoordinateSystem3D& slice) const
+    {
+      return ProjectStructure(polygons, GetStructure(index), slice);
+    }
+  };
+}
--- a/Resources/CMake/OrthancStoneConfiguration.cmake	Mon Sep 09 10:41:27 2019 +0200
+++ b/Resources/CMake/OrthancStoneConfiguration.cmake	Mon Sep 09 15:18:24 2019 +0200
@@ -423,14 +423,26 @@
   ${ORTHANC_STONE_ROOT}/Framework/Fonts/GlyphBitmapAlphabet.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Fonts/GlyphTextureAlphabet.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Fonts/TextBoundingBox.cpp
+
   ${ORTHANC_STONE_ROOT}/Framework/Loaders/BasicFetchingItemsSorter.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Loaders/BasicFetchingItemsSorter.h
   ${ORTHANC_STONE_ROOT}/Framework/Loaders/BasicFetchingStrategy.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Loaders/BasicFetchingStrategy.h
   ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomStructureSetLoader.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomStructureSetLoader.h
+  ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomStructureSetLoader2.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomStructureSetLoader2.h
+  ${ORTHANC_STONE_ROOT}/Framework/Loaders/IFetchingItemsSorter.h
+  ${ORTHANC_STONE_ROOT}/Framework/Loaders/IFetchingStrategy.h
+  ${ORTHANC_STONE_ROOT}/Framework/Loaders/LoaderCache.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Loaders/LoaderCache.h
-  ${ORTHANC_STONE_ROOT}/Framework/Loaders/LoaderCache.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Loaders/LoaderStateMachine.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Loaders/LoaderStateMachine.h
   ${ORTHANC_STONE_ROOT}/Framework/Loaders/OrthancMultiframeVolumeLoader.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Loaders/OrthancMultiframeVolumeLoader.h
   ${ORTHANC_STONE_ROOT}/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h
+  
   ${ORTHANC_STONE_ROOT}/Framework/Messages/ICallable.h
   ${ORTHANC_STONE_ROOT}/Framework/Messages/IMessage.h
   ${ORTHANC_STONE_ROOT}/Framework/Messages/IObservable.cpp
@@ -515,19 +527,38 @@
   ${ORTHANC_STONE_ROOT}/Framework/StoneEnumerations.cpp
   ${ORTHANC_STONE_ROOT}/Framework/StoneException.h
   ${ORTHANC_STONE_ROOT}/Framework/StoneInitialization.cpp
+  
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/AffineTransform2D.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Toolbox/AffineTransform2D.h
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/CoordinateSystem3D.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Toolbox/CoordinateSystem3D.h
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/DicomInstanceParameters.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Toolbox/DicomInstanceParameters.h
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/DicomStructureSet.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Toolbox/DicomStructureSet.h
+  ${ORTHANC_STONE_ROOT}/Framework/Toolbox/DicomStructureSet2.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Toolbox/DicomStructureSet2.h
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/DynamicBitmap.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Toolbox/DynamicBitmap.h
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/Extent2D.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Toolbox/Extent2D.h
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/FiniteProjectiveCamera.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Toolbox/FiniteProjectiveCamera.h
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/GeometryToolbox.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Toolbox/GeometryToolbox.h
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/ImageGeometry.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Toolbox/ImageGeometry.h
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/LinearAlgebra.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Toolbox/LinearAlgebra.h
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/ShearWarpProjectiveTransform.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Toolbox/ShearWarpProjectiveTransform.h
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/SlicesSorter.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Toolbox/SlicesSorter.h
+  ${ORTHANC_STONE_ROOT}/Framework/Toolbox/SubpixelReader.h
+  ${ORTHANC_STONE_ROOT}/Framework/Toolbox/SubvoxelReader.h
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/UndoRedoStack.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Toolbox/UndoRedoStack.h
+  
   ${ORTHANC_STONE_ROOT}/Framework/Viewport/ViewportBase.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Volumes/DicomVolumeImage.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Volumes/DicomVolumeImageMPRSlicer.cpp