changeset 541:c8347eef225b dicom-rt

volume of structures
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 04 Sep 2013 13:46:08 +0200
parents 505d6deb9947
children a482948c1fd6
files OrthancServer/RadiotherapyRestApi.cpp
diffstat 1 files changed, 214 insertions(+), 57 deletions(-) [+]
line wrap: on
line diff
--- a/OrthancServer/RadiotherapyRestApi.cpp	Tue Sep 03 17:45:26 2013 +0200
+++ b/OrthancServer/RadiotherapyRestApi.cpp	Wed Sep 04 13:46:08 2013 +0200
@@ -42,6 +42,15 @@
 
 // DICOM tags for RT-STRUCT
 
+/**
+ * REFERENCE: http://www.dabsoft.ch/dicom/3/C.8.8.6/
+ *
+ * IMPORTANT: The points/vertices coordinates are reported in [mm]. 
+ * 
+ * TODO: Support "Contour Offset Vector"
+ **/
+
+
 #define REFERENCED_STUDY_SEQUENCE "0008,1110"
 #define REFERENCED_SOP_INSTANCE_UID "0008,1155"
 #define FRAME_OF_REFERENCE_UID "0020,0052"
@@ -59,8 +68,16 @@
 #define NUMBER_OF_CONTOUR_POINTS "3006,0046"
 #define CONTOUR_DATA "3006,0050"
 #define CONTOUR_SLAB_THICKNESS "3006,0044"
+#define SLICE_THICKNESS "0018,0050"
 
 
+
+#include <boost/geometry.hpp>
+#include <boost/geometry/geometries/point.hpp>
+#include <boost/geometry/geometries/point_xy.hpp>
+#include <boost/geometry/geometries/polygon.hpp>
+#include <boost/geometry/geometries/linestring.hpp>
+
 namespace Orthanc
 {
   static bool CheckSeriesModality(Json::Value& study,
@@ -93,7 +110,6 @@
     // Retrieve the instance data
     std::string instanceId = series["Instances"][0].asString();
 
-    Json::Value info;
     context.ReadJson(content, instanceId);
 
     return true;
@@ -258,6 +274,92 @@
   }
 
 
+  static bool GetClosedPlanarPoints(Json::Value& result,
+                                    ServerContext& context,
+                                    const std::string& instanceId,
+                                    const Json::Value& roi,
+                                    unsigned int index)
+  {
+    boost::mutex::scoped_lock lock(context.GetDicomFileMutex());
+    ParsedDicomFile& dicom = context.GetDicomFile(instanceId);
+
+    ParsedDicomFile::SequencePath path;
+    path.push_back(std::make_pair(DicomTag(0x3006, 0x0039 /* ROIContourSequence */), roi["InternalIndex"].asInt()));
+    path.push_back(std::make_pair(DicomTag(0x3006, 0x0040 /* ContourSequence */), index));
+        
+    std::string contourData;
+    std::string numberOfPoints;
+
+    if (dicom.GetTagValue(contourData, path, DicomTag(0x3006, 0x0050 /* ContourData */)) &&
+        dicom.GetTagValue(numberOfPoints, path, DicomTag(0x3006, 0x0046 /* NumberOfContourPoints */)) &&
+        ContourToPoints(result, contourData) &&
+        result.size() == boost::lexical_cast<unsigned int>(numberOfPoints))
+    {
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+
+  static bool LookupReferencedInstance(Json::Value& result,
+                                       ServerContext& context,
+                                       const Json::Value& contour)
+  {
+    if (contour.isMember(CONTOUR_IMAGE_SEQUENCE) &&
+        contour[CONTOUR_IMAGE_SEQUENCE]["Value"].size() == 1 &&
+        contour[CONTOUR_IMAGE_SEQUENCE]["Value"][0].isMember(REFERENCED_SOP_INSTANCE_UID))
+    {
+      std::string uid = contour[CONTOUR_IMAGE_SEQUENCE]["Value"][0][REFERENCED_SOP_INSTANCE_UID]["Value"].asString();
+
+      std::list<std::string> instance;
+      context.GetIndex().LookupTagValue(instance, DICOM_TAG_SOP_INSTANCE_UID, uid);
+
+      if (instance.size() == 1 &&
+          context.GetIndex().LookupResource(result, instance.front(), ResourceType_Instance))
+      {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+
+  static bool GetRtStructuresClosedPlanarThickness(float& result,
+                                                   ServerContext& context,
+                                                   const Json::Value& contour)
+  {
+    if (contour.isMember(CONTOUR_SLAB_THICKNESS))
+    {
+      result = boost::lexical_cast<float>(contour[CONTOUR_SLAB_THICKNESS]["Value"].asString());
+      return true;
+    }
+
+    // No slab thickness is explicitely specified: Fallback to
+    // the thickness of the referred instance
+
+    Json::Value instance;
+    if (LookupReferencedInstance(instance, context, contour))
+    {
+      Json::Value info;
+      context.ReadJson(info, instance["ID"].asString());       
+
+      if (info.isMember(SLICE_THICKNESS))
+      {
+        result = boost::lexical_cast<float>(info[SLICE_THICKNESS]["Value"].asString());
+        return true;
+      }
+    }
+
+    return false;
+  }
+                                       
+
+
   static void GetRtStructuresInfo(RestApi::GetCall& call)
   {
     RETRIEVE_CONTEXT(call);
@@ -406,7 +508,7 @@
   {
      RETRIEVE_CONTEXT(call);
 
-     Json::Value roi, contour;
+     Json::Value roi, contour, result;
      std::string instanceId;
 
      if (GetRtStructuresRoi(roi, contour, instanceId, context, 
@@ -415,21 +517,7 @@
      {
        unsigned int index = boost::lexical_cast<unsigned int>(call.GetUriComponent("polygon", ""));
 
-       boost::mutex::scoped_lock lock(context.GetDicomFileMutex());
-       ParsedDicomFile& dicom = context.GetDicomFile(instanceId);
-
-       ParsedDicomFile::SequencePath path;
-       path.push_back(std::make_pair(DicomTag(0x3006, 0x0039 /* ROIContourSequence */), roi["InternalIndex"].asInt()));
-       path.push_back(std::make_pair(DicomTag(0x3006, 0x0040 /* ContourSequence */), index));
-        
-       Json::Value result;
-       std::string contourData;
-       std::string numberOfPoints;
-
-       if (dicom.GetTagValue(contourData, path, DicomTag(0x3006, 0x0050 /* ContourData */)) &&
-           dicom.GetTagValue(numberOfPoints, path, DicomTag(0x3006, 0x0046 /* NumberOfContourPoints */)) &&
-           ContourToPoints(result, contourData) &&
-           result.size() == boost::lexical_cast<unsigned int>(numberOfPoints))
+       if (GetClosedPlanarPoints(result, context, instanceId, roi, index))
        {
          call.GetOutput().AnswerJson(result);
        }
@@ -450,15 +538,77 @@
      {
        unsigned int index = boost::lexical_cast<unsigned int>(call.GetUriComponent("polygon", ""));
 
-       if (contour[index].isMember(CONTOUR_SLAB_THICKNESS))
+       float thickness;
+       if (GetRtStructuresClosedPlanarThickness(thickness, context, contour[index]))
        {
-         std::cout <<contour[index][CONTOUR_SLAB_THICKNESS] << std::endl;
-         call.GetOutput().AnswerBuffer(contour[index][CONTOUR_SLAB_THICKNESS]["Value"].asString(), "text/plain");
+         call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(thickness), "text/plain");
        }
-       else
+     }
+  }
+
+
+  static bool ComputeClosedPlanarArea(double& area,
+                                      const Json::Value& vertices)
+  {
+    if (vertices.size() <= 1)
+    {
+      area = 0;
+      return true;
+    }
+         
+    // Check that all the points share the same z coordinates
+    for (Json::Value::ArrayIndex i = 1; i < vertices.size(); i++)
+    {
+      static float THRESHOLD = 10.0f * std::numeric_limits<float>::epsilon();
+
+      assert(vertices[i].size() == 3);
+      if (fabs(vertices[i][2].asFloat() - vertices[0][2].asFloat()) > THRESHOLD)
+      {
+        // This point has not the same z coordinate
+        return false;
+      }
+    }
+
+    // Calculate the area of a cartesian polygon
+    // TODO - What happens if self-crossing polygon?
+    typedef boost::geometry::model::d2::point_xy<float> point_type;
+
+    boost::geometry::model::linestring<point_type> points;
+
+    points.resize(vertices.size());
+    for (Json::Value::ArrayIndex i = 0; i < vertices.size(); i++)
+    {
+      float x = vertices[i][0].asFloat();
+      float y = vertices[i][1].asFloat();
+      points[i] = boost::geometry::make<point_type>(x, y);
+    }
+
+    boost::geometry::model::polygon<point_type> poly;
+    boost::geometry::append(poly, points);
+               
+    area = abs(boost::geometry::area(poly));
+    return true;
+  }
+
+
+  static void GetRtStructuresClosedPlanarArea(RestApi::GetCall& call)
+  {
+     RETRIEVE_CONTEXT(call);
+
+     Json::Value roi, contour, vertices;
+     std::string instanceId;
+
+     if (GetRtStructuresRoi(roi, contour, instanceId, context, 
+                            call.GetUriComponent("id", ""),
+                            call.GetUriComponent("roi", "")))
+     {
+       unsigned int index = boost::lexical_cast<unsigned int>(call.GetUriComponent("polygon", ""));
+
+       double area;
+       if (GetClosedPlanarPoints(vertices, context, instanceId, roi, index) &&
+           ComputeClosedPlanarArea(area, vertices))
        {
-         // TODO - No slab thickness is explicitely specified: Should we
-         // fallback to the thickness of the instance?
+         call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(area), "text/plain");
        }
      }
   }
@@ -477,21 +627,10 @@
     {
       unsigned int index = boost::lexical_cast<unsigned int>(call.GetUriComponent("polygon", ""));
 
-      if (contour[index].isMember(CONTOUR_IMAGE_SEQUENCE) &&
-          contour[index][CONTOUR_IMAGE_SEQUENCE]["Value"].size() == 1 &&
-          contour[index][CONTOUR_IMAGE_SEQUENCE]["Value"][0].isMember(REFERENCED_SOP_INSTANCE_UID))
+      Json::Value result;
+      if (LookupReferencedInstance(result, context, contour[index]))
       {
-        std::string uid = contour[index][CONTOUR_IMAGE_SEQUENCE]["Value"][0][REFERENCED_SOP_INSTANCE_UID]["Value"].asString();
-
-        std::list<std::string> instance;
-        context.GetIndex().LookupTagValue(instance, DICOM_TAG_SOP_INSTANCE_UID, uid);
-
-        Json::Value result;
-        if (instance.size() == 1 &&
-            context.GetIndex().LookupResource(result, instance.front(), ResourceType_Instance))
-        {
-          call.GetOutput().AnswerJson(result);
-        }
+        call.GetOutput().AnswerJson(result);
       }
     }
   }
@@ -546,9 +685,6 @@
                             call.GetUriComponent("id", ""),
                             call.GetUriComponent("roi", "")))
      {
-       boost::mutex::scoped_lock lock(context.GetDicomFileMutex());
-       ParsedDicomFile& dicom = context.GetDicomFile(instanceId);
-
        Json::Value result = Json::arrayValue;
 
        for (Json::Value::ArrayIndex i = 0; i < contour.size(); i++)
@@ -563,24 +699,12 @@
          {
            std::string uid = contour[i][CONTOUR_IMAGE_SEQUENCE]["Value"][0][REFERENCED_SOP_INSTANCE_UID]["Value"].asString();
 
-           if (uid == instance["MainDicomTags"]["SOPInstanceUID"].asString())
+           Json::Value points;
+           if (uid == instance["MainDicomTags"]["SOPInstanceUID"].asString() &&
+               GetClosedPlanarPoints(points, context, instanceId, roi, i))
            {
-             ParsedDicomFile::SequencePath path;
-             path.push_back(std::make_pair(DicomTag(0x3006, 0x0039 /* ROIContourSequence */), roi["InternalIndex"].asInt()));
-             path.push_back(std::make_pair(DicomTag(0x3006, 0x0040 /* ContourSequence */), i));
-        
-             Json::Value points;
-             std::string contourData;
-             std::string numberOfPoints;
-
-             if (dicom.GetTagValue(contourData, path, DicomTag(0x3006, 0x0050 /* ContourData */)) &&
-                 dicom.GetTagValue(numberOfPoints, path, DicomTag(0x3006, 0x0046 /* NumberOfContourPoints */)) &&
-                 ContourToPoints(points, contourData) &&
-                 points.size() == boost::lexical_cast<unsigned int>(numberOfPoints))
-             {
-               result.append(points);
-             }             
-           }
+             result.append(points);
+           }             
          }
        }
 
@@ -632,6 +756,37 @@
   }
 
 
+  static void GetRtStructuresVolume(RestApi::GetCall& call)
+  {
+     RETRIEVE_CONTEXT(call);
+
+     Json::Value roi, contour, vertices;
+     std::string instanceId;
+
+     if (GetRtStructuresRoi(roi, contour, instanceId, context, 
+                            call.GetUriComponent("id", ""),
+                            call.GetUriComponent("roi", "")))
+     {
+       double volume = 0;
+
+       for (Json::Value::ArrayIndex i = 0; i < contour.size(); i++)
+       {
+         double area;
+         float thickness;
+
+         if (contour[i].isMember(CONTOUR_GEOMETRIC_TYPE) &&
+             contour[i][CONTOUR_GEOMETRIC_TYPE]["Value"].asString() == "CLOSED_PLANAR" &&
+             GetClosedPlanarPoints(vertices, context, instanceId, roi, i) &&
+             ComputeClosedPlanarArea(area, vertices) &&
+             GetRtStructuresClosedPlanarThickness(thickness, context, contour[i]))
+         {
+           volume += area * static_cast<double>(thickness);
+         }
+       }
+
+       call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(volume), "text/plain");
+     }
+  }
 
 
   RadiotherapyRestApi::RadiotherapyRestApi(ServerContext& context) : OrthancRestApi(context)
@@ -644,9 +799,11 @@
     Register("/series/{id}/rt-structures/roi/{roi}/closed-planar/{polygon}/vertices", GetRtStructuresSingleClosedPlanar);
     Register("/series/{id}/rt-structures/roi/{roi}/closed-planar/{polygon}/thickness", GetRtStructuresClosedPlanarThickness);
     Register("/series/{id}/rt-structures/roi/{roi}/closed-planar/{polygon}/instance", GetRtStructuresInstanceOfClosedPlanar);
+    Register("/series/{id}/rt-structures/roi/{roi}/closed-planar/{polygon}/area", GetRtStructuresClosedPlanarArea);
     Register("/series/{id}/rt-structures/roi/{roi}/instances", GetRtStructuresListOfInstances);
     Register("/series/{id}/rt-structures/roi/{roi}/instances/{instance}/closed-planar", GetRtStructuresClosedPlanarsOfInstance);
     Register("/series/{id}/rt-structures/roi/{roi}/instances/{instance}/points", GetRtStructuresPointsOfInstance);
+    Register("/series/{id}/rt-structures/roi/{roi}/volume", GetRtStructuresVolume);
   }
 
 }