changeset 1821:36430d73e36c

introducing measure units in AnnotationsSceneLayer
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 26 May 2021 14:02:12 +0200
parents 5baaad557d58
children 0489fe25ce48
files Applications/Samples/Sdl/SingleFrameViewer/SdlSimpleViewer.cpp Applications/Samples/Sdl/SingleFrameViewer/SdlSimpleViewerApplication.h OrthancStone/Sources/Scene2D/AnnotationsSceneLayer.cpp OrthancStone/Sources/Scene2D/AnnotationsSceneLayer.h OrthancStone/Sources/StoneEnumerations.h OrthancStone/Sources/Toolbox/DicomInstanceParameters.cpp OrthancStone/Sources/Toolbox/DicomInstanceParameters.h OrthancStone/Sources/Toolbox/GeometryToolbox.cpp OrthancStone/Sources/Toolbox/GeometryToolbox.h
diffstat 9 files changed, 174 insertions(+), 43 deletions(-) [+]
line wrap: on
line diff
--- a/Applications/Samples/Sdl/SingleFrameViewer/SdlSimpleViewer.cpp	Wed May 26 13:08:49 2021 +0200
+++ b/Applications/Samples/Sdl/SingleFrameViewer/SdlSimpleViewer.cpp	Wed May 26 14:02:12 2021 +0200
@@ -194,16 +194,6 @@
         OrthancStone::AnnotationsSceneLayer annotations(10);
         annotations.SetActiveTool(OrthancStone::AnnotationsSceneLayer::Tool_Edit);
 
-        /*
-        annotations.AddSegmentAnnotation(OrthancStone::ScenePoint2D(0, 0),
-                                         OrthancStone::ScenePoint2D(100, 100));
-        annotations.AddAngleAnnotation(OrthancStone::ScenePoint2D(100, 50),
-                                       OrthancStone::ScenePoint2D(150, 40),
-                                       OrthancStone::ScenePoint2D(200, 50));
-        annotations.AddCircleAnnotation(OrthancStone::ScenePoint2D(50, 200),
-                                        OrthancStone::ScenePoint2D(100, 250));
-        */
-        
 #else
         ActiveTool activeTool = ActiveTool_None;
 
@@ -233,6 +223,8 @@
           bool stop = false;
           while (!stop)
           {
+            annotations.SetUnits(application->GetUnits());
+
             bool paint = false;
             SDL_Event event;
             while (SDL_PollEvent(&event))
--- a/Applications/Samples/Sdl/SingleFrameViewer/SdlSimpleViewerApplication.h	Wed May 26 13:08:49 2021 +0200
+++ b/Applications/Samples/Sdl/SingleFrameViewer/SdlSimpleViewerApplication.h	Wed May 26 14:02:12 2021 +0200
@@ -101,16 +101,23 @@
     lock->Invalidate();
   }
 
+  OrthancStone::Units GetUnits() const
+  {
+    return units_;
+  }
+
 private:
   ILoadersContext& context_;
   boost::shared_ptr<IViewport>             viewport_;
   boost::shared_ptr<DicomResourcesLoader>  dicomLoader_;
   boost::shared_ptr<SeriesFramesLoader>    framesLoader_;
+  OrthancStone::Units                      units_;
 
   SdlSimpleViewerApplication(ILoadersContext& context,
                              boost::shared_ptr<IViewport> viewport) :
     context_(context),
-    viewport_(viewport)
+    viewport_(viewport),
+    units_(OrthancStone::Units_Pixels)
   {
   }
 
@@ -141,6 +148,26 @@
       throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
     }
 
+    OrthancStone::DicomInstanceParameters parameters(message.GetResources()->GetResource(0));
+    if (parameters.HasPixelSpacing())
+    {
+      /**
+       * TODO - Ultrasound (US) images store an equivalent to
+       * "PixelSpacing" in the "SequenceOfUltrasoundRegions"
+       * (0018,6011) sequence, cf. tags "PhysicalDeltaX" (0018,602c)
+       * and "PhysicalDeltaY" (0018,602e) => This would require
+       * storing the full JSON into the "LoadedDicomResources" class
+       * or to use DCMTK
+       **/
+      
+      LOG(INFO) << "Using millimeters units, as the DICOM instance contains the PixelSpacing tag";
+      units_ = OrthancStone::Units_Millimeters;
+    }
+    else
+    {
+      LOG(INFO) << "Using pixels units, as the DICOM instance does *not* contain the PixelSpacing tag";
+    }
+    
     //message.GetResources()->GetResource(0).Print(stdout);
 
     {
--- a/OrthancStone/Sources/Scene2D/AnnotationsSceneLayer.cpp	Wed May 26 13:08:49 2021 +0200
+++ b/OrthancStone/Sources/Scene2D/AnnotationsSceneLayer.cpp	Wed May 26 14:02:12 2021 +0200
@@ -42,10 +42,13 @@
 static const char* const KEY_Y2 = "y2";
 static const char* const KEY_X3 = "x3";
 static const char* const KEY_Y3 = "y3";
+static const char* const KEY_UNITS = "units";
 
 static const char* const VALUE_ANGLE = "angle";
 static const char* const VALUE_CIRCLE = "circle";
 static const char* const VALUE_SEGMENT = "segment";
+static const char* const VALUE_MILLIMETERS = "millimeters";
+static const char* const VALUE_PIXELS = "pixels";
 
 #if 0
 static OrthancStone::Color COLOR_PRIMITIVES(192, 192, 192);
@@ -166,10 +169,13 @@
       
     AnnotationsSceneLayer&  that_;
     GeometricPrimitives     primitives_;
+    Units                   units_;
       
   public:
-    explicit Annotation(AnnotationsSceneLayer& that) :
-      that_(that)
+    explicit Annotation(AnnotationsSceneLayer& that,
+                        Units units) :
+      that_(that),
+      units_(units)
     {
       that.AddAnnotation(this);
     }
@@ -182,6 +188,11 @@
       }
     }
 
+    Units GetUnits() const
+    {
+      return units_;
+    }
+
     GeometricPrimitive* AddPrimitive(GeometricPrimitive* primitive)
     {
       if (primitive == NULL)
@@ -772,7 +783,21 @@
         double dx = x1 - x2;
         double dy = y1 - y2;
         char buf[32];
-        sprintf(buf, "%0.2f cm", sqrt(dx * dx + dy * dy) / 10.0);
+
+        switch (GetUnits())
+        {
+          case Units_Millimeters:
+            sprintf(buf, "%0.2f cm", sqrt(dx * dx + dy * dy) / 10.0);
+            break;
+
+          case Units_Pixels:
+            sprintf(buf, "%0.1f px", sqrt(dx * dx + dy * dy));
+            break;
+
+          default:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+        }
+            
         content.SetText(buf);
 
         label_.SetContent(content);
@@ -781,10 +806,11 @@
 
   public:
     SegmentAnnotation(AnnotationsSceneLayer& that,
+                      Units units,
                       bool showLabel,
                       const ScenePoint2D& p1,
                       const ScenePoint2D& p2) :
-      Annotation(that),
+      Annotation(that, units),
       showLabel_(showLabel),
       handle1_(AddTypedPrimitive<Handle>(new Handle(*this, p1))),
       handle2_(AddTypedPrimitive<Handle>(new Handle(*this, p2))),
@@ -832,6 +858,7 @@
     }
 
     static void Unserialize(AnnotationsSceneLayer& target,
+                            Units units,
                             const Json::Value& source)
     {
       if (source.isMember(KEY_X1) &&
@@ -843,7 +870,7 @@
           source[KEY_X2].isNumeric() &&
           source[KEY_Y2].isNumeric())
       {
-        new SegmentAnnotation(target, true,
+        new SegmentAnnotation(target, units, true,
                               ScenePoint2D(source[KEY_X1].asDouble(), source[KEY_Y1].asDouble()),
                               ScenePoint2D(source[KEY_X2].asDouble(), source[KEY_Y2].asDouble()));
       }
@@ -898,10 +925,11 @@
 
   public:
     AngleAnnotation(AnnotationsSceneLayer& that,
+                    Units units,
                     const ScenePoint2D& start,
                     const ScenePoint2D& middle,
                     const ScenePoint2D& end) :
-      Annotation(that),
+      Annotation(that, units),
       startHandle_(AddTypedPrimitive<Handle>(new Handle(*this, start))),
       middleHandle_(AddTypedPrimitive<Handle>(new Handle(*this, middle))),
       endHandle_(AddTypedPrimitive<Handle>(new Handle(*this, end))),
@@ -970,6 +998,7 @@
     }
 
     static void Unserialize(AnnotationsSceneLayer& target,
+                            Units units,
                             const Json::Value& source)
     {
       if (source.isMember(KEY_X1) &&
@@ -985,7 +1014,7 @@
           source[KEY_X3].isNumeric() &&
           source[KEY_Y3].isNumeric())
       {
-        new AngleAnnotation(target,
+        new AngleAnnotation(target, units,
                             ScenePoint2D(source[KEY_X1].asDouble(), source[KEY_Y1].asDouble()),
                             ScenePoint2D(source[KEY_X2].asDouble(), source[KEY_Y2].asDouble()),
                             ScenePoint2D(source[KEY_X3].asDouble(), source[KEY_Y3].asDouble()));
@@ -1036,10 +1065,25 @@
       double area = PI * diameter * diameter / 4.0;
         
       char buf[32];
-      sprintf(buf, "%0.2f cm\n%0.2f cm%c%c",
-              diameter / 10.0,
-              area / 100.0,
-              0xc2, 0xb2 /* two bytes corresponding to two power in UTF-8 */);
+
+      switch (GetUnits())
+      {
+        case Units_Millimeters:
+          sprintf(buf, "%0.2f cm\n%0.2f cm%c%c",
+                  diameter / 10.0,
+                  area / 100.0,
+                  0xc2, 0xb2 /* two bytes corresponding to two power in UTF-8 */);
+          break;
+
+        case Units_Pixels:
+          // Don't report area (pixel-times-pixel is a strange unit)
+          sprintf(buf, "%0.1f px", diameter);
+          break;
+          
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
+
       content.SetText(buf);
 
       label_.SetContent(content);
@@ -1047,9 +1091,10 @@
 
   public:
     CircleAnnotation(AnnotationsSceneLayer& that,
+                     Units units,
                      const ScenePoint2D& p1,
                      const ScenePoint2D& p2) :
-      Annotation(that),
+      Annotation(that, units),
       handle1_(AddTypedPrimitive<Handle>(new Handle(*this, p1))),
       handle2_(AddTypedPrimitive<Handle>(new Handle(*this, p2))),
       segment_(AddTypedPrimitive<Segment>(new Segment(*this, p1, p2))),
@@ -1094,6 +1139,7 @@
     }
 
     static void Unserialize(AnnotationsSceneLayer& target,
+                            Units units,
                             const Json::Value& source)
     {
       if (source.isMember(KEY_X1) &&
@@ -1105,7 +1151,7 @@
           source[KEY_X2].isNumeric() &&
           source[KEY_Y2].isNumeric())
       {
-        new CircleAnnotation(target,
+        new CircleAnnotation(target, units,
                              ScenePoint2D(source[KEY_X1].asDouble(), source[KEY_Y1].asDouble()),
                              ScenePoint2D(source[KEY_X2].asDouble(), source[KEY_Y2].asDouble()));
       }
@@ -1127,6 +1173,7 @@
       
   public:
     CreateSegmentOrCircleTracker(AnnotationsSceneLayer& that,
+                                 Units units,
                                  bool isCircle,
                                  const ScenePoint2D& sceneClick,
                                  const AffineTransform2D& canvasToScene) :
@@ -1137,12 +1184,12 @@
     {
       if (isCircle)
       {
-        annotation_ = new CircleAnnotation(that, sceneClick, sceneClick);
+        annotation_ = new CircleAnnotation(that, units, sceneClick, sceneClick);
         handle2_ = &dynamic_cast<CircleAnnotation*>(annotation_)->GetHandle2();
       }
       else
       {
-        annotation_ = new SegmentAnnotation(that, true /* show label */, sceneClick, sceneClick);
+        annotation_ = new SegmentAnnotation(that, units, true /* show label */, sceneClick, sceneClick);
         handle2_ = &dynamic_cast<SegmentAnnotation*>(annotation_)->GetHandle2();
       }
         
@@ -1199,6 +1246,7 @@
       
   public:
     CreateAngleTracker(AnnotationsSceneLayer& that,
+                       Units units,
                        const ScenePoint2D& sceneClick,
                        const AffineTransform2D& canvasToScene) :
       that_(that),
@@ -1206,7 +1254,7 @@
       angle_(NULL),
       canvasToScene_(canvasToScene)
     {
-      segment_ = new SegmentAnnotation(that, false /* no length label */, sceneClick, sceneClick);
+      segment_ = new SegmentAnnotation(that, units, false /* no length label */, sceneClick, sceneClick);
     }
 
     virtual void PointerMove(const PointerEvent& event) ORTHANC_OVERRIDE
@@ -1232,7 +1280,7 @@
       {
         // End of first step: The first segment is available, now create the angle
 
-        angle_ = new AngleAnnotation(that_, segment_->GetHandle1().GetCenter(),
+        angle_ = new AngleAnnotation(that_, segment_->GetUnits(), segment_->GetHandle1().GetCenter(),
                                      segment_->GetHandle2().GetCenter(),
                                      segment_->GetHandle2().GetCenter());
           
@@ -1348,7 +1396,8 @@
   AnnotationsSceneLayer::AnnotationsSceneLayer(size_t macroLayerIndex) :
     activeTool_(Tool_Edit),
     macroLayerIndex_(macroLayerIndex),
-    polylineSubLayer_(0)  // dummy initialization
+    polylineSubLayer_(0),  // dummy initialization
+    units_(Units_Pixels)
   {
   }
     
@@ -1366,18 +1415,28 @@
     ClearHover();
   }
 
+  
+  void AnnotationsSceneLayer::SetUnits(Units units)
+  {
+    if (units_ != units)
+    {
+      Clear();
+      units_ = units;
+    }
+  }
+
 
   void AnnotationsSceneLayer::AddSegmentAnnotation(const ScenePoint2D& p1,
                                                    const ScenePoint2D& p2)
   {
-    annotations_.insert(new SegmentAnnotation(*this, true /* show label */, p1, p2));
+    annotations_.insert(new SegmentAnnotation(*this, units_, true /* show label */, p1, p2));
   }
   
 
   void AnnotationsSceneLayer::AddCircleAnnotation(const ScenePoint2D& p1,
                                                   const ScenePoint2D& p2)
   {
-    annotations_.insert(new CircleAnnotation(*this, p1, p2));
+    annotations_.insert(new CircleAnnotation(*this, units_, p1, p2));
   }
   
 
@@ -1385,7 +1444,7 @@
                                                  const ScenePoint2D& p2,
                                                  const ScenePoint2D& p3)
   {
-    annotations_.insert(new AngleAnnotation(*this, p1, p2, p3));
+    annotations_.insert(new AngleAnnotation(*this, units_, p1, p2, p3));
   }
   
 
@@ -1524,13 +1583,13 @@
         switch (activeTool_)
         {
           case Tool_Segment:
-            return new CreateSegmentOrCircleTracker(*this, false /* segment */, s, scene.GetCanvasToSceneTransform());
+            return new CreateSegmentOrCircleTracker(*this, units_, false /* segment */, s, scene.GetCanvasToSceneTransform());
 
           case Tool_Circle:
-            return new CreateSegmentOrCircleTracker(*this, true /* circle */, s, scene.GetCanvasToSceneTransform());
+            return new CreateSegmentOrCircleTracker(*this, units_, true /* circle */, s, scene.GetCanvasToSceneTransform());
 
           case Tool_Angle:
-            return new CreateAngleTracker(*this, s, scene.GetCanvasToSceneTransform());
+            return new CreateAngleTracker(*this, units_, s, scene.GetCanvasToSceneTransform());
 
           default:
             return NULL;
@@ -1555,6 +1614,20 @@
 
     target = Json::objectValue;
     target[KEY_ANNOTATIONS] = annotations;
+
+    switch (units_)
+    {
+      case Units_Millimeters:
+        target[KEY_UNITS] = VALUE_MILLIMETERS;
+        break;
+
+      case Units_Pixels:
+        target[KEY_UNITS] = VALUE_PIXELS;
+        break;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
   }
 
 
@@ -1564,11 +1637,28 @@
       
     if (serialized.type() != Json::objectValue ||
         !serialized.isMember(KEY_ANNOTATIONS) ||
-        serialized[KEY_ANNOTATIONS].type() != Json::arrayValue)
+        !serialized.isMember(KEY_UNITS) ||
+        serialized[KEY_ANNOTATIONS].type() != Json::arrayValue ||
+        serialized[KEY_UNITS].type() != Json::stringValue)
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "Cannot unserialize a set of annotations");
     }
 
+    const std::string& u = serialized[KEY_UNITS].asString();
+
+    if (u == VALUE_MILLIMETERS)
+    {
+      units_ = Units_Millimeters;
+    }
+    else if (u == VALUE_PIXELS)
+    {
+      units_ = Units_Pixels;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "Unknown units: " + u);
+    }
+
     const Json::Value& annotations = serialized[KEY_ANNOTATIONS];
 
     for (Json::Value::ArrayIndex i = 0; i < annotations.size(); i++)
@@ -1584,15 +1674,15 @@
 
       if (type == VALUE_ANGLE)
       {
-        AngleAnnotation::Unserialize(*this, annotations[i]);
+        AngleAnnotation::Unserialize(*this, units_, annotations[i]);
       }
       else if (type == VALUE_CIRCLE)
       {
-        CircleAnnotation::Unserialize(*this, annotations[i]);
+        CircleAnnotation::Unserialize(*this, units_, annotations[i]);
       }
       else if (type == VALUE_SEGMENT)
       {
-        SegmentAnnotation::Unserialize(*this, annotations[i]);
+        SegmentAnnotation::Unserialize(*this, units_, annotations[i]);
       }
       else
       {
--- a/OrthancStone/Sources/Scene2D/AnnotationsSceneLayer.h	Wed May 26 13:08:49 2021 +0200
+++ b/OrthancStone/Sources/Scene2D/AnnotationsSceneLayer.h	Wed May 26 14:02:12 2021 +0200
@@ -42,7 +42,7 @@
       Tool_Circle,
       Tool_Remove
     };
-    
+
   private:
     class GeometricPrimitive;    
     class Handle;    
@@ -71,6 +71,7 @@
     GeometricPrimitives  primitives_;
     Annotations          annotations_;
     SubLayers            subLayersToRemove_;
+    Units                units_;
 
     void AddAnnotation(Annotation* annotation);
     
@@ -100,6 +101,13 @@
       return activeTool_;
     }
 
+    void SetUnits(Units units);
+
+    Units GetUnits() const
+    {
+      return units_;
+    }
+
     void AddSegmentAnnotation(const ScenePoint2D& p1,
                               const ScenePoint2D& p2);
 
--- a/OrthancStone/Sources/StoneEnumerations.h	Wed May 26 13:08:49 2021 +0200
+++ b/OrthancStone/Sources/StoneEnumerations.h	Wed May 26 14:02:12 2021 +0200
@@ -157,6 +157,12 @@
     MouseAction_None
   };
 
+  enum Units
+  {
+    Units_Millimeters,
+    Units_Pixels
+  };
+    
   SopClassUid StringToSopClassUid(const std::string& source);
 
   void ComputeWindowing(float& targetCenter,
--- a/OrthancStone/Sources/Toolbox/DicomInstanceParameters.cpp	Wed May 26 13:08:49 2021 +0200
+++ b/OrthancStone/Sources/Toolbox/DicomInstanceParameters.cpp	Wed May 26 14:02:12 2021 +0200
@@ -123,7 +123,7 @@
       sliceThicknessPresent = false;
     }
 
-    GeometryToolbox::GetPixelSpacing(pixelSpacingX_, pixelSpacingY_, dicom);
+    hasPixelSpacing_ = GeometryToolbox::GetPixelSpacing(pixelSpacingX_, pixelSpacingY_, dicom);
 
     std::string position, orientation;
     if (dicom.LookupStringValue(position, Orthanc::DICOM_TAG_IMAGE_POSITION_PATIENT, false) &&
--- a/OrthancStone/Sources/Toolbox/DicomInstanceParameters.h	Wed May 26 13:08:49 2021 +0200
+++ b/OrthancStone/Sources/Toolbox/DicomInstanceParameters.h	Wed May 26 14:02:12 2021 +0200
@@ -62,6 +62,7 @@
       std::string         doseUnits_;
       double              doseGridScaling_;
       std::string         frameOfReferenceUid_;
+      bool                hasPixelSpacing_;
 
       explicit Data(const Orthanc::DicomMap& dicom);
     };
@@ -229,5 +230,10 @@
     {
       return data_.frameOfReferenceUid_;
     }
+
+    bool HasPixelSpacing() const
+    {
+      return data_.hasPixelSpacing_;
+    }
   };
 }
--- a/OrthancStone/Sources/Toolbox/GeometryToolbox.cpp	Wed May 26 13:08:49 2021 +0200
+++ b/OrthancStone/Sources/Toolbox/GeometryToolbox.cpp	Wed May 26 14:02:12 2021 +0200
@@ -304,7 +304,7 @@
     }
 
 
-    void GetPixelSpacing(double& spacingX, 
+    bool GetPixelSpacing(double& spacingX, 
                          double& spacingY,
                          const Orthanc::DicomMap& dicom)
     {
@@ -324,6 +324,7 @@
           // WARNING: X/Y are swapped (Y comes first)
           spacingX = v[1];
           spacingY = v[0];
+          return true;
         }
       }
       else
@@ -332,6 +333,7 @@
         // default value in such a case
         spacingX = 1;
         spacingY = 1;
+        return false;
       }
     }
 
--- a/OrthancStone/Sources/Toolbox/GeometryToolbox.h	Wed May 26 13:08:49 2021 +0200
+++ b/OrthancStone/Sources/Toolbox/GeometryToolbox.h	Wed May 26 14:02:12 2021 +0200
@@ -70,7 +70,7 @@
                              const double& xmax,
                              const double& ymax);
 
-    void GetPixelSpacing(double& spacingX, 
+    bool GetPixelSpacing(double& spacingX, 
                          double& spacingY,
                          const Orthanc::DicomMap& dicom);