view RenderingPlugin/Sources/Plugin.cpp @ 2181:eae006bfeea6 default tip

support images without the "Modality" tag
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 26 Nov 2024 16:37:46 +0100
parents 16c01cc201e7
children
line wrap: on
line source

/**
 * Stone of Orthanc
 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 * Department, University Hospital of Liege, Belgium
 * Copyright (C) 2017-2023 Osimis S.A., Belgium
 * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM 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 "OrthancPluginConnection.h"
#include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h"

#include "../../OrthancStone/Sources/Toolbox/AffineTransform2D.h"
#include "../../OrthancStone/Sources/Toolbox/DicomInstanceParameters.h"
#include "../../OrthancStone/Sources/Toolbox/DicomStructureSet.h"

#include <Cache/MemoryObjectCache.h>
#include <Images/Image.h>
#include <Images/ImageProcessing.h>
#include <Images/NumpyWriter.h>
#include <Logging.h>
#include <SerializationToolbox.h>
#include <Toolbox.h>

#include <boost/math/constants/constants.hpp>


static const char* const INSTANCES = "Instances";  
static const char* const RT_STRUCT_IOD = "1.2.840.10008.5.1.4.1.1.481.3";
static const char* const SOP_CLASS_UID = "0008,0016";
static const char* const STRUCTURES = "Structures";


class DicomStructureCache : public boost::noncopyable
{
private:
  class Item : public Orthanc::ICacheable
  {
  private:
    std::unique_ptr<OrthancStone::DicomStructureSet> rtstruct_;

  public:
    explicit Item(OrthancStone::DicomStructureSet* rtstruct) :
      rtstruct_(rtstruct)
    {
      if (rtstruct == NULL)
      {
        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
      }
    }
    
    virtual size_t GetMemoryUsage() const ORTHANC_OVERRIDE
    {
      return 1;
    }

    OrthancStone::DicomStructureSet& GetRtStruct() const
    {
      return *rtstruct_;
    }
  };

  Orthanc::MemoryObjectCache   cache_;

  DicomStructureCache()  // Singleton design pattern
  {
  }
  
public:
  void Invalidate(const std::string& instanceId)
  {
    cache_.Invalidate(instanceId);
  }

  void SetMaximumNumberOfItems(size_t items)
  {
    cache_.SetMaximumSize(items);
  }

  static DicomStructureCache& GetSingleton()
  {
    static DicomStructureCache instance;
    return instance;
  }

  class Accessor : public boost::noncopyable
  {
  private:
    DicomStructureCache& that_;
    std::string instanceId_;
    Orthanc::MemoryObjectCache::Accessor  lock_;
    std::unique_ptr<OrthancStone::DicomStructureSet>  notCached_;

  public:
    Accessor(DicomStructureCache& that,
             const std::string& instanceId) :
      that_(that),
      instanceId_(instanceId),
      lock_(that.cache_, instanceId, true /* unique, as "GetRtStruct()" is mutable */)
    {
      if (!lock_.IsValid())
      {
        OrthancStone::OrthancPluginConnection connection;
        OrthancStone::FullOrthancDataset dataset(connection, "/instances/" + instanceId + "/tags?ignore-length=3006-0050");
        notCached_.reset(new OrthancStone::DicomStructureSet(dataset));
      }
    }

    ~Accessor()
    {
      if (!lock_.IsValid())
      {
        assert(notCached_.get() != NULL);
      
        try
        {
          that_.cache_.Acquire(instanceId_, new Item(notCached_.release()));
        }
        catch (Orthanc::OrthancException& e)
        {
          LOG(ERROR) << "Cannot insert RT-STRUCT into cache: " << e.What();
        }
      }
    }

    const std::string& GetInstanceId() const
    {
      return instanceId_;
    }

    OrthancStone::DicomStructureSet& GetRtStruct() const
    {
      if (lock_.IsValid())
      {
        return dynamic_cast<Item&>(lock_.GetValue()).GetRtStruct();
      }
      else
      {
        assert(notCached_.get() != NULL);
        return *notCached_;
      }
    }
  };
};


static Orthanc::PixelFormat Convert(OrthancPluginPixelFormat format)
{
  switch (format)
  {
    case OrthancPluginPixelFormat_RGB24:
      return Orthanc::PixelFormat_RGB24;

    case OrthancPluginPixelFormat_Grayscale8:
      return Orthanc::PixelFormat_Grayscale8;

    case OrthancPluginPixelFormat_Grayscale16:
      return Orthanc::PixelFormat_Grayscale16;

    case OrthancPluginPixelFormat_SignedGrayscale16:
      return Orthanc::PixelFormat_SignedGrayscale16;

    default:
      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
  }
}


static bool ParseBoolean(const std::string& key,
                         const std::string& value)
{
  bool result;
  
  if (Orthanc::SerializationToolbox::ParseBoolean(result, value))
  {
    return result;
  }
  else
  {
    throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
                                    "Bad value for " + key + ": " + value);
  }
}


static double ParseDouble(const std::string& key,
                         const std::string& value)
{
  double result;
  
  if (Orthanc::SerializationToolbox::ParseDouble(result, value))
  {
    return result;
  }
  else
  {
    throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
                                    "Bad value for " + key + ": " + value);
  }
}


static unsigned int ParseUnsignedInteger(const std::string& key,
                                         const std::string& value)
{
  uint32_t result;
  
  if (Orthanc::SerializationToolbox::ParseUnsignedInteger32(result, value))
  {
    return result;
  }
  else
  {
    throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
                                    "Bad value for " + key + ": " + value);
  }
}



class DataAugmentationParameters : public boost::noncopyable
{
private:
  double       angleRadians_;
  double       scaling_;
  double       offsetX_;
  double       offsetY_;
  bool         flipX_;
  bool         flipY_;
  bool         hasResize_;
  unsigned int targetWidth_;
  unsigned int targetHeight_;
  bool         hasInterpolation_;
  OrthancStone::ImageInterpolation  interpolation_;

  void ApplyInternal(Orthanc::ImageAccessor& target,
                     const Orthanc::ImageAccessor& source)
  {
    if (source.GetWidth() == 0 ||
        source.GetHeight() == 0)
    {
      Orthanc::ImageProcessing::Set(target, 0);  // Clear the image
    }
    else if (target.GetWidth() == 0 ||
             target.GetHeight() == 0)
    {
      // Nothing to do
    }
    else
    {
      OrthancStone::AffineTransform2D transform = ComputeTransform(source.GetWidth(), source.GetHeight());
      
      OrthancStone::ImageInterpolation interpolation;

      if (hasInterpolation_)
      {
        interpolation = interpolation_;
      }
      else if (source.GetFormat() == Orthanc::PixelFormat_RGB24)
      {
        // Bilinear interpolation for color images is not implemented yet
        interpolation = OrthancStone::ImageInterpolation_Nearest;
      }
      else
      {
        interpolation = OrthancStone::ImageInterpolation_Bilinear;
      }

      transform.Apply(target, source, interpolation, true /* clear */);
    }
  }    


  Orthanc::ImageAccessor* ApplyUnchecked(const Orthanc::ImageAccessor& source)
  {
    std::unique_ptr<Orthanc::ImageAccessor> target;
    
    if (hasResize_)
    {
      target.reset(new Orthanc::Image(source.GetFormat(), targetWidth_, targetHeight_, false));
    }
    else
    {
      target.reset(new Orthanc::Image(source.GetFormat(), source.GetWidth(), source.GetHeight(), false));
    }
    
    ApplyInternal(*target, source);
    return target.release();
  }


public:
  DataAugmentationParameters()
  {
    Clear();
  }
  

  void Clear()
  {
    angleRadians_ = 0;
    scaling_ = 1;
    offsetX_ = 0;
    offsetY_ = 0;
    flipX_ = false;
    flipY_ = false;
    hasResize_ = false;
    targetWidth_ = 0;
    targetHeight_ = 0;
    hasInterpolation_ = false;
    interpolation_ = OrthancStone::ImageInterpolation_Nearest;
  }

  
  OrthancStone::AffineTransform2D ComputeTransform(unsigned int sourceWidth,
                                                   unsigned int sourceHeight) const
  {
    unsigned int w = (hasResize_ ? targetWidth_ : sourceWidth);
    unsigned int h = (hasResize_ ? targetHeight_ : sourceHeight);

    if (w == 0 ||
        h == 0 ||
        sourceWidth == 0 ||
        sourceHeight == 0)
    {
      // Division by zero
      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
    }
    
    double r = std::min(static_cast<double>(w) / static_cast<double>(sourceWidth),
                        static_cast<double>(h) / static_cast<double>(sourceHeight));

    OrthancStone::AffineTransform2D resize = OrthancStone::AffineTransform2D::Combine(
      OrthancStone::AffineTransform2D::CreateOffset(static_cast<double>(w) / 2.0,
                                                    static_cast<double>(h) / 2.0),
      OrthancStone::AffineTransform2D::CreateScaling(r, r));

    OrthancStone::AffineTransform2D dataAugmentation = OrthancStone::AffineTransform2D::Combine(
      OrthancStone::AffineTransform2D::CreateScaling(scaling_, scaling_),
      OrthancStone::AffineTransform2D::CreateOffset(offsetX_, offsetY_),
      OrthancStone::AffineTransform2D::CreateRotation(angleRadians_),
      OrthancStone::AffineTransform2D::CreateOffset(-static_cast<double>(sourceWidth) / 2.0,
                                                    -static_cast<double>(sourceHeight) / 2.0),
      OrthancStone::AffineTransform2D::CreateFlip(flipX_, flipY_, sourceWidth, sourceHeight));

    return OrthancStone::AffineTransform2D::Combine(resize, dataAugmentation);
  }
  
  
  bool ParseParameter(const std::string& key,
                      const std::string& value)
  {
    if (key == "angle")
    {
      double angle = ParseDouble(key, value);
      angleRadians_ = angle / 180.0 * boost::math::constants::pi<double>();
      return true;
    }
    else if (key == "scaling")
    {
      scaling_ = ParseDouble(key, value);
      return true;
    }
    else if (key == "offset-x")
    {
      offsetX_ = ParseDouble(key, value);
      return true;
    }
    else if (key == "offset-y")
    {
      offsetY_ = ParseDouble(key, value);
      return true;
    }
    else if (key == "flip-x")
    {
      flipX_ = ParseBoolean(key, value);
      return true;
    }
    else if (key == "flip-y")
    {
      flipY_ = ParseBoolean(key, value);
      return true;
    }
    else if (key == "resize")
    {
      std::vector<std::string> tokens;
      Orthanc::Toolbox::TokenizeString(tokens, value, ',');
      if (tokens.size() != 2)
      {
        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
                                        "Must provide two integers separated by commas in " + key + ": " + value);
      }
      else
      {
        targetWidth_ = ParseUnsignedInteger(key, tokens[0]);
        targetHeight_ = ParseUnsignedInteger(key, tokens[1]);
        hasResize_ = true;
        return true;
      }
    }
    else if (key == "interpolation")
    {
      if (value == "nearest")
      {
        interpolation_ = OrthancStone::ImageInterpolation_Nearest;
      }
      else if (value == "bilinear")
      {
        interpolation_ = OrthancStone::ImageInterpolation_Bilinear;
      }
      else
      {
        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
                                        "Unknown interpolation (must be \"nearest\" or \"bilinear\"): " + value);
      }

      hasInterpolation_ = true;
      return true;
    }
    else
    {
      return false;
    }
  }

  
  Orthanc::ImageAccessor* Apply(const Orthanc::ImageAccessor& source)
  {
    if (source.GetFormat() != Orthanc::PixelFormat_RGB24 &&
        source.GetFormat() != Orthanc::PixelFormat_Float32)
    {
      throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
    }
    else
    {
      return ApplyUnchecked(source);
    }
  }

  
  Orthanc::ImageAccessor* ApplyBinaryMask(const Orthanc::ImageAccessor& source)
  {
    if (source.GetFormat() != Orthanc::PixelFormat_Grayscale8)
    {
      throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat,
                                      "A segmentation mask should be a grayscale image");
    }
    else
    {
      std::unique_ptr<Orthanc::ImageAccessor> target(ApplyUnchecked(source));

      const unsigned int h = target->GetHeight();
      const unsigned int w = target->GetWidth();

      // Apply thresholding to get back a binary image
      for (unsigned int y = 0; y < h; y++)
      {
        uint8_t* p = reinterpret_cast<uint8_t*>(target->GetRow(y));
        for (unsigned int x = 0; x < w; x++, p++)
        {
          if (*p < 128)
          {
            *p = 0;
          }
          else
          {
            *p = 255;
          }
        }
      }

      return target.release();
    }
  }
};


static OrthancStone::DicomInstanceParameters* GetInstanceParameters(const std::string& orthancId)
{
  OrthancPlugins::MemoryBuffer tags;
  if (!tags.RestApiGet("/instances/" + orthancId + "/tags", false))
  {
    throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem);
  }

  Json::Value json;
  tags.ToJson(json);

  Orthanc::DicomMap m;
  m.FromDicomAsJson(json);

  return new OrthancStone::DicomInstanceParameters(m);
}


static void AnswerNumpyImage(OrthancPluginRestOutput* output,
                             const Orthanc::ImageAccessor& image,
                             bool compress)
{
  std::string answer;
  Orthanc::NumpyWriter writer;
  writer.SetCompressed(compress);
  Orthanc::IImageWriter::WriteToMemory(writer, answer, image);
  
  OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output,
                            answer.c_str(), answer.size(), "application/octet-stream");
}


static void RenderNumpyFrame(OrthancPluginRestOutput* output,
                             const char* url,
                             const OrthancPluginHttpRequest* request)
{
  DataAugmentationParameters dataAugmentation;
  bool compress = false;

  for (uint32_t i = 0; i < request->getCount; i++)
  {
    std::string key(request->getKeys[i]);
    std::string value(request->getValues[i]);

    if (!dataAugmentation.ParseParameter(key, value))
    {
      if (key == "compress")
      {
        compress = ParseBoolean(key, value);
      }
      else
      {
        LOG(WARNING) << "Unsupported option for data augmentation: " << key;
      }
    }
  }

  std::unique_ptr<OrthancStone::DicomInstanceParameters> parameters(GetInstanceParameters(request->groups[0]));
  
  OrthancPlugins::MemoryBuffer dicom;
  dicom.GetDicomInstance(request->groups[0]);

  unsigned int frame = boost::lexical_cast<unsigned int>(request->groups[1]);
  
  OrthancPlugins::OrthancImage image;
  image.DecodeDicomImage(dicom.GetData(), dicom.GetSize(), frame);
  
  Orthanc::ImageAccessor source;
  source.AssignReadOnly(Convert(image.GetPixelFormat()), image.GetWidth(), image.GetHeight(),
                        image.GetPitch(), image.GetBuffer());

  std::unique_ptr<Orthanc::ImageAccessor> modified;

  if (parameters->GetSopClassUid() == OrthancStone::SopClassUid_DicomSeg)
  {
    modified.reset(dataAugmentation.ApplyBinaryMask(source));
  }
  else if (source.GetFormat() == Orthanc::PixelFormat_RGB24)
  {
    modified.reset(dataAugmentation.Apply(source));
  }
  else
  {
    std::unique_ptr<Orthanc::ImageAccessor> converted(parameters->ConvertToFloat(source));
    assert(converted.get() != NULL);
    
    modified.reset(dataAugmentation.Apply(*converted));
  }

  assert(modified.get() != NULL);
  AnswerNumpyImage(output, *modified, compress);
}


static bool IsRtStruct(const std::string& instanceId)
{
  std::string s;
  if (OrthancPlugins::RestApiGetString(s, "/instances/" + instanceId + "/content/" + SOP_CLASS_UID, false) &&
      !s.empty())
  {
    if (s[s.size() - 1] == '\0')  // Deal with DICOM padding
    {
      s.resize(s.size() - 1);
    }
    
    return s == RT_STRUCT_IOD;
  }
  else
  {
    return false;
  }
}


static void ListRtStruct(OrthancPluginRestOutput* output,
                         const char* url,
                         const OrthancPluginHttpRequest* request)
{
  // This is a quick version of "/tools/find" on "SOPClassUID" (the
  // latter would load all the DICOM files from disk)

  Json::Value series;
  OrthancPlugins::RestApiGet(series, "/series?expand", false);

  if (series.type() != Json::arrayValue)
  {
    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
  }

  Json::Value answer = Json::arrayValue;

  for (Json::Value::ArrayIndex i = 0; i < series.size(); i++)
  {
    if (series[i].type() != Json::objectValue ||
        !series[i].isMember(INSTANCES) ||
        series[i][INSTANCES].type() != Json::arrayValue)
    {
      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
    }
    
    const Json::Value& instances = series[i][INSTANCES];

    for (Json::Value::ArrayIndex j = 0; j < instances.size(); j++)
    {
      if (instances[j].type() != Json::stringValue)
      {
        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
      }
    }
    
    if (instances.size() > 0 &&
        IsRtStruct(instances[0].asString()))
    {
      for (Json::Value::ArrayIndex j = 0; j < instances.size(); j++)
      {
        answer.append(instances[j].asString());
      }
    }
  }

  std::string s = answer.toStyledString();
  OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, s.c_str(), s.size(), "application/json");
}


static void GetRtStruct(OrthancPluginRestOutput* output,
                        const char* url,
                        const OrthancPluginHttpRequest* request)
{
  DicomStructureCache::Accessor accessor(DicomStructureCache::GetSingleton(), request->groups[0]);

  Json::Value answer;
  answer[STRUCTURES] = Json::arrayValue;

  for (size_t i = 0; i < accessor.GetRtStruct().GetStructuresCount(); i++)
  {
    Json::Value color = Json::arrayValue;
    color.append(accessor.GetRtStruct().GetStructureColor(i).GetRed());
    color.append(accessor.GetRtStruct().GetStructureColor(i).GetGreen());
    color.append(accessor.GetRtStruct().GetStructureColor(i).GetBlue());
    
    Json::Value structure;
    structure["Name"] = accessor.GetRtStruct().GetStructureName(i);
    structure["Interpretation"] = accessor.GetRtStruct().GetStructureInterpretation(i);
    structure["Color"] = color;
    
    answer[STRUCTURES].append(structure);
  }

  std::set<std::string> sopInstanceUids;
  accessor.GetRtStruct().GetReferencedInstances(sopInstanceUids);

  answer[INSTANCES] = Json::arrayValue;
  for (std::set<std::string>::const_iterator it = sopInstanceUids.begin(); it != sopInstanceUids.end(); ++it)
  {
    OrthancPlugins::OrthancString s;
    s.Assign(OrthancPluginLookupInstance(OrthancPlugins::GetGlobalContext(), it->c_str()));

    std::string t;
    s.ToString(t);

    answer[INSTANCES].append(t);
  }

  std::string s = answer.toStyledString();
  OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, s.c_str(), s.size(), "application/json");
}



static void RenderRtStruct(OrthancPluginRestOutput* output,
                           const char* url,
                           const OrthancPluginHttpRequest* request)
{
  class XorFiller : public Orthanc::ImageProcessing::IPolygonFiller
  {
  private:
    Orthanc::Image  image_;

  public:
    XorFiller(unsigned int width,
              unsigned int height) :
      image_(Orthanc::PixelFormat_Grayscale8, width, height, false)
    {
      Orthanc::ImageProcessing::Set(image_, 0);
    }

    Orthanc::ImageAccessor& GetImage()
    {
      return image_;
    }

    virtual void Fill(int y,
                      int x1,
                      int x2) ORTHANC_OVERRIDE
    {
      assert(x1 <= x2);

      if (y >= 0 &&
          y < static_cast<int>(image_.GetHeight()))
      {
        x1 = std::max(x1, 0);
        x2 = std::min(x2, static_cast<int>(image_.GetWidth()) - 1);

        uint8_t* p = reinterpret_cast<uint8_t*>(image_.GetRow(y)) + x1;

        for (int i = x1; i <= x2; i++, p++)
        {
          *p = (*p ^ 0xff);
        }
      }
    }
  };

  
  class HorizontalSegment
  {
  private:
    int  y_;
    int  x1_;
    int  x2_;
    
  public:
    HorizontalSegment(int y,
                      int x1,
                      int x2) :
      y_(y),
      x1_(std::min(x1, x2)),
      x2_(std::max(x1, x2))
    {
    }
    
    int GetY() const
    {
      return y_;
    }
    
    int GetX1() const
    {
      return x1_;
    }
    
    int GetX2() const
    {
      return x2_;
    }

    void Fill(Orthanc::ImageAccessor& image) const
    {
      assert(x1_ <= x2_);

      if (y_ >= 0 &&
          y_ < static_cast<int>(image.GetHeight()))
      {
        int a = std::max(x1_, 0);
        int b = std::min(x2_, static_cast<int>(image.GetWidth()) - 1);

        uint8_t* p = reinterpret_cast<uint8_t*>(image.GetRow(y_)) + a;

        for (int i = a; i <= b; i++, p++)
        {
          *p = 0xff;
        }
      }
    }
  };


  DataAugmentationParameters dataAugmentation;
  std::vector<std::string> structureNames;
  std::string instanceId;
  bool compress = false;

  for (uint32_t i = 0; i < request->getCount; i++)
  {
    std::string key(request->getKeys[i]);
    std::string value(request->getValues[i]);

    if (!dataAugmentation.ParseParameter(key, value))
    {
      if (key == "structure")
      {
        Orthanc::Toolbox::TokenizeString(structureNames, value, ',');
      }
      else if (key == "instance")
      {
        instanceId = value;
      }
      else if (key == "compress")
      {
        compress = ParseBoolean(key, value);
      }
      else
      {
        LOG(WARNING) << "Unsupported option: " << key;
      }
    }
  }

  if (structureNames.empty())
  {
    throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol,
                                    "Missing option \"structure\" to provide the names of the structures of interest");
  }

  if (instanceId.empty())
  {
    throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol,
                                    "Missing option \"instance\" to provide the Orthanc identifier of the instance of interest");
  }
  
  std::unique_ptr<OrthancStone::DicomInstanceParameters> parameters(GetInstanceParameters(instanceId));

  typedef std::list< std::vector<OrthancStone::Vector> > Polygons;

  Polygons polygons;

  {
    DicomStructureCache::Accessor accessor(DicomStructureCache::GetSingleton(), request->groups[0]);

    for (size_t i = 0; i < structureNames.size(); i++)
    {
      size_t structureIndex;
      if (accessor.GetRtStruct().LookupStructureName(structureIndex, structureNames[i]))
      {
        Polygons p;
        accessor.GetRtStruct().GetStructurePoints(p, structureIndex, parameters->GetSopInstanceUid());
        polygons.splice(polygons.begin(), p);
      }
      else
      {
        LOG(WARNING) << "Missing structure name \"" << structureNames[i]
                     << "\" in RT-STRUCT: " << parameters->GetSopInstanceUid();
      }
    }
  }

  // We use a "XOR" filler for the polygons in order to deal with holes in the RT-STRUCT
  XorFiller filler(parameters->GetWidth(), parameters->GetHeight());
  OrthancStone::AffineTransform2D transform = dataAugmentation.ComputeTransform(parameters->GetWidth(), parameters->GetHeight());
  
  std::list<HorizontalSegment> horizontalSegments;
  
  for (std::list< std::vector<OrthancStone::Vector> >::const_iterator
         it = polygons.begin(); it != polygons.end(); ++it)
  {
    std::vector<Orthanc::ImageProcessing::ImagePoint> points;
    points.reserve(it->size());

    for (size_t i = 0; i < it->size(); i++)
    {
      // The (0.5, 0.5) offset is due to the fact that DICOM
      // coordinates are expressed wrt. the CENTER of the voxels
      
      double x, y;
      parameters->GetGeometry().ProjectPoint(x, y, (*it) [i]);
      x = x / parameters->GetPixelSpacingX() + 0.5;
      y = y / parameters->GetPixelSpacingY() + 0.5;
      
      transform.Apply(x, y);

      points.push_back(Orthanc::ImageProcessing::ImagePoint(std::floor(x), std::floor(y)));
    }
    
    Orthanc::ImageProcessing::FillPolygon(filler, points);

    for (size_t i = 0; i < points.size(); i++)
    {
      size_t next = (i + 1) % points.size();
      if (points[i].GetY() == points[next].GetY())
      {
        horizontalSegments.push_back(HorizontalSegment(points[i].GetY(), points[i].GetX(), points[next].GetX()));
      }
    }
  }

  /**
   * We repeat the filling of the horizontal segments. This is
   * important to deal with horizontal edges that are seen in one
   * direction, then in the reverse direction within the same polygon,
   * which can typically be seen in RT-STRUCT with holes. If this step
   * is not done, only the starting point and the ending point of the
   * segments are drawn.
   **/
  for (std::list<HorizontalSegment>::const_iterator it = horizontalSegments.begin();
       it != horizontalSegments.end(); ++it)
  {
    it->Fill(filler.GetImage());
  }
  
  AnswerNumpyImage(output, filler.GetImage(), compress);
}



OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType,
                                        OrthancPluginResourceType resourceType,
                                        const char* resourceId)
{
  switch (changeType)
  {
    case OrthancPluginChangeType_Deleted:
      if (resourceType == OrthancPluginResourceType_Instance)
      {
        DicomStructureCache::GetSingleton().Invalidate(resourceId);
      }
      
      break;

    case OrthancPluginChangeType_OrthancStarted:
    {
      DicomStructureCache::Accessor accessor(DicomStructureCache::GetSingleton(), "54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9");
      OrthancStone::LinearAlgebra::Print(accessor.GetRtStruct().GetEstimatedNormal());
      printf("Slice thickness: %f\n", accessor.GetRtStruct().GetEstimatedSliceThickness());
      break;
    }

    default:
      break;
  }

  return OrthancPluginErrorCode_Success;
}


extern "C"
{
  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
  {
    OrthancPlugins::SetGlobalContext(context);

#if ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 7, 2)
    Orthanc::Logging::InitializePluginContext(context);
#else
    Orthanc::Logging::Initialize(context);
#endif

    /* Check the version of the Orthanc core */
    if (OrthancPluginCheckVersion(context) == 0)
    {
      char info[1024];
      sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin",
              context->orthancVersion,
              ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
              ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
              ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
      OrthancPluginLogError(context, info);
      return -1;
    }

    try
    {
      DicomStructureCache::GetSingleton().SetMaximumNumberOfItems(1024);  // Cache up to 1024 RT-STRUCT instances
      
      OrthancPlugins::RegisterRestCallback<RenderNumpyFrame>("/stone/instances/([^/]+)/frames/([0-9]+)/numpy", true);
      OrthancPlugins::RegisterRestCallback<ListRtStruct>("/stone/rt-struct", true);
      OrthancPlugins::RegisterRestCallback<GetRtStruct>("/stone/rt-struct/([^/]+)/info", true);
      OrthancPlugins::RegisterRestCallback<RenderRtStruct>("/stone/rt-struct/([^/]+)/numpy", true);
      OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback);
    }
    catch (...)
    {
      OrthancPlugins::LogError("Exception while initializing the Stone Web viewer plugin");
      return -1;
    }

    return 0;
  }


  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
  {
  }


  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
  {
    return PLUGIN_NAME;
  }


  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
  {
    return PLUGIN_VERSION;
  }
}