changeset 1987:a595776232bb

added ellipse probe
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 31 Oct 2022 17:43:44 +0100
parents e29783c92419
children 3c84c34322d7
files OrthancStone/Sources/Scene2D/AnnotationsSceneLayer.cpp OrthancStone/Sources/Scene2D/AnnotationsSceneLayer.h
diffstat 2 files changed, 358 insertions(+), 3 deletions(-) [+]
line wrap: on
line diff
--- a/OrthancStone/Sources/Scene2D/AnnotationsSceneLayer.cpp	Mon Oct 31 15:20:18 2022 +0100
+++ b/OrthancStone/Sources/Scene2D/AnnotationsSceneLayer.cpp	Mon Oct 31 17:43:44 2022 +0100
@@ -26,7 +26,7 @@
 #include "MacroSceneLayer.h"
 #include "PolylineSceneLayer.h"
 #include "TextSceneLayer.h"
-#include "TextureBaseSceneLayer.h"  // TODO REMOVE
+#include "TextureBaseSceneLayer.h"
 
 #include <Images/ImageTraits.h>
 #include <OrthancException.h>
@@ -56,6 +56,7 @@
 static const char* const VALUE_PIXELS = "pixels";
 static const char* const VALUE_PIXEL_PROBE = "pixel-probe";
 static const char* const VALUE_RECTANGLE_PROBE = "rectangle-probe";
+static const char* const VALUE_ELLIPSE_PROBE = "ellipse-probe";
 
 #if 0
 static OrthancStone::Color COLOR_PRIMITIVES(192, 192, 192);
@@ -792,6 +793,154 @@
   };
 
 
+  class AnnotationsSceneLayer::Ellipse : public GeometricPrimitive
+  {
+  private:
+    ScenePoint2D  p1_;
+    ScenePoint2D  p2_;
+    ScenePoint2D  delta_;
+
+    double GetCenterX() const
+    {
+      return (p1_.GetX() + p2_.GetX()) / 2.0 + delta_.GetX();
+    }
+
+    double GetCenterY() const
+    {
+      return (p1_.GetY() + p2_.GetY()) / 2.0 + delta_.GetY();
+    }
+
+    double GetRadiusX() const
+    {
+      return std::abs(p1_.GetX() - p2_.GetX()) / 2.0;
+    }
+
+    double GetRadiusY() const
+    {
+      return std::abs(p1_.GetY() - p2_.GetY()) / 2.0;
+    }
+    
+  public:
+    Ellipse(Annotation& parentAnnotation,
+            const ScenePoint2D& p1,
+            const ScenePoint2D& p2) :
+      GeometricPrimitive(parentAnnotation, 2),
+      p1_(p1),
+      p2_(p2),
+      delta_(0, 0)
+    {
+    }
+
+    void SetPosition(const ScenePoint2D& p1,
+                     const ScenePoint2D& p2)
+    {
+      SetModified(true);
+      p1_ = p1;
+      p2_ = p2;
+      delta_ = ScenePoint2D(0, 0);
+    }
+
+    ScenePoint2D GetPosition1() const
+    {
+      return p1_ + delta_;
+    }
+
+    ScenePoint2D GetPosition2() const
+    {
+      return p2_ + delta_;
+    }
+
+    double GetArea() const
+    {
+      return PI * GetRadiusX() * GetRadiusY();
+    }
+
+    bool IsPointInside(const ScenePoint2D& p) const
+    {
+      const double radiusX = GetRadiusX();
+      const double radiusY = GetRadiusY();
+
+      double a, b, x, y;
+      
+      if (radiusX > radiusY)
+      {
+        // The ellipse is horizontal => we are in the case described
+        // on Wikipedia:
+        // https://en.wikipedia.org/wiki/Ellipse#Standard_equation
+
+        a = radiusX;
+        b = radiusY;
+        x = p.GetX() - GetCenterX();
+        y = p.GetY() - GetCenterY();
+      }
+      else
+      {
+        a = radiusY;
+        b = radiusX;
+        x = p.GetY() - GetCenterY();
+        y = p.GetX() - GetCenterX();
+      }
+      
+      const double c = sqrt(a * a - b * b);
+
+      return (sqrt((x - c) * (x - c) + y * y) +
+              sqrt((x + c) * (x + c) + y * y)) <= 2.0 * a;
+    }
+    
+    virtual bool IsHit(const ScenePoint2D& p,
+                       const Scene2D& scene) const ORTHANC_OVERRIDE
+    {
+      const double zoom = scene.GetSceneToCanvasTransform().ComputeZoom();
+
+      const double radiusX = GetRadiusX();
+      const double radiusY = GetRadiusY();
+
+      // Warning: This is only an approximation of the
+      // point-to-ellipse distance, as explained here:
+      // https://blog.chatfield.io/simple-method-for-distance-to-ellipse/
+      
+      const double x = (p.GetX() - GetCenterX()) / radiusX;
+      const double y = (p.GetY() - GetCenterY()) / radiusY;
+      const double t = atan2(y, x);
+      const double xx = cos(t) - x;
+      const double yy = sin(t) - y;
+
+      const double approximateDistance = sqrt(xx * xx + yy * yy) * (radiusX + radiusY) / 2.0;
+      return std::abs(approximateDistance) * zoom <= HANDLE_SIZE / 2.0;
+    }
+
+    virtual void RenderPolylineLayer(PolylineSceneLayer& polyline,
+                                     const Scene2D& scene) ORTHANC_OVERRIDE
+    {
+      static unsigned int NUM_SEGMENTS = 128;
+      polyline.AddArc(GetCenterX(), GetCenterY(), GetRadiusX(), GetRadiusY(), 0, 2.0 * PI, GetActiveColor(), NUM_SEGMENTS);
+    }
+      
+    virtual void RenderOtherLayers(MacroSceneLayer& macro,
+                                   const Scene2D& scene) ORTHANC_OVERRIDE
+    {
+    }
+
+    virtual void MovePreview(const ScenePoint2D& delta,
+                             const Scene2D& scene) ORTHANC_OVERRIDE
+    {
+      SetModified(true);
+      delta_ = delta;
+      GetParentAnnotation().SignalMove(*this, scene);
+    }
+
+    virtual void MoveDone(const ScenePoint2D& delta,
+                          const Scene2D& scene) ORTHANC_OVERRIDE
+    {
+      SetModified(true);
+      p1_ = p1_ + delta;
+      p2_ = p2_ + delta;
+      delta_ = ScenePoint2D(0, 0);
+      GetParentAnnotation().SignalMove(*this, scene);
+    }
+  };
+
+    
   class AnnotationsSceneLayer::EditPrimitiveTracker : public IFlexiblePointerTracker
   {
   private:
@@ -1552,7 +1701,7 @@
       {
         const double area = std::abs(x1 - x2) * std::abs(y1 - y2);
 
-        sprintf(buf, "%0.2f cm%c%c",
+        sprintf(buf, "Area: %0.2f cm%c%c",
                 area / 100.0,
                 0xc2, 0xb2 /* two bytes corresponding to two power in UTF-8 */);
         text = buf;
@@ -1768,6 +1917,201 @@
   };
 
   
+  class AnnotationsSceneLayer::EllipseProbeAnnotation : public ProbingAnnotation
+  {
+  private:
+    Handle&   handle1_;
+    Handle&   handle2_;
+    Ellipse&  ellipse_;
+    Text&     label_;
+
+  protected:
+    virtual void UpdateProbeForLayer(const ISceneLayer& layer) ORTHANC_OVERRIDE
+    {
+      double x1 = handle1_.GetCenter().GetX();
+      double y1 = handle1_.GetCenter().GetY();
+      double x2 = handle2_.GetCenter().GetX();
+      double y2 = handle2_.GetCenter().GetY();
+        
+      // Put the label to the right of the right-most handle
+      //const double y = std::min(y1, y2);
+      const double y = (y1 + y2) / 2.0;
+      if (x1 < x2)
+      {
+        label_.SetPosition(x2, y);
+      }
+      else
+      {
+        label_.SetPosition(x1, y);
+      }
+
+      std::string text;
+      
+      char buf[32];
+
+      if (GetUnits() == Units_Millimeters)
+      {
+        sprintf(buf, "Area: %0.2f cm%c%c",
+                ellipse_.GetArea() / 100.0,
+                0xc2, 0xb2 /* two bytes corresponding to two power in UTF-8 */);
+        text = buf;
+      }
+
+      if (layer.GetType() == ISceneLayer::Type_FloatTexture)
+      {
+        const TextureBaseSceneLayer& texture = dynamic_cast<const TextureBaseSceneLayer&>(layer);
+        const AffineTransform2D& textureToScene = texture.GetTransform();
+        const AffineTransform2D sceneToTexture = AffineTransform2D::Invert(textureToScene);
+
+        const Orthanc::ImageAccessor& image = texture.GetTexture();
+        assert(image.GetFormat() == Orthanc::PixelFormat_Float32);
+
+        sceneToTexture.Apply(x1, y1);
+        sceneToTexture.Apply(x2, y2);
+        int ix1 = static_cast<int>(std::floor(x1));
+        int iy1 = static_cast<int>(std::floor(y1));
+        int ix2 = static_cast<int>(std::floor(x2));
+        int iy2 = static_cast<int>(std::floor(y2));
+
+        if (ix1 > ix2)
+        {
+          std::swap(ix1, ix2);
+        }
+
+        if (iy1 > iy2)
+        {
+          std::swap(iy1, iy2);
+        }
+
+        LinearAlgebra::OnlineVarianceEstimator estimator;
+
+        for (int y = std::max(0, iy1); y <= std::min(static_cast<int>(image.GetHeight()) - 1, iy2); y++)
+        {
+          int x = std::max(0, ix1);
+          const float* p = reinterpret_cast<const float*>(image.GetConstRow(y)) + x;
+
+          for (; x <= std::min(static_cast<int>(image.GetWidth()) - 1, ix2); x++, p++)
+          {
+            double yy = static_cast<double>(y) + 0.5;
+            double xx = static_cast<double>(x) + 0.5;
+            textureToScene.Apply(xx, yy);
+            if (ellipse_.IsPointInside(ScenePoint2D(xx, yy)))
+            {
+              estimator.AddSample(*p);
+            }
+          }
+        }
+
+        if (estimator.GetCount() > 0)
+        {
+          if (!text.empty())
+          {
+            text += "\n";
+          }
+          sprintf(buf, "Mean: %0.1f\nStdDev: %0.1f", estimator.GetMean(), estimator.GetStandardDeviation());
+          text += buf;
+        }
+      }
+      
+      label_.SetText(text);
+    }
+    
+  public:
+    EllipseProbeAnnotation(AnnotationsSceneLayer& that,
+                           Units units,
+                           const ScenePoint2D& p1,
+                           const ScenePoint2D& p2) :
+      ProbingAnnotation(that, units),
+      handle1_(AddTypedPrimitive<Handle>(new Handle(*this, Handle::Shape_Square, p1))),
+      handle2_(AddTypedPrimitive<Handle>(new Handle(*this, Handle::Shape_Square, p2))),
+      ellipse_(AddTypedPrimitive<Ellipse>(new Ellipse(*this, p1, p2))),
+      label_(AddTypedPrimitive<Text>(new Text(that, *this)))
+    {
+      TextSceneLayer content;
+      content.SetAnchor(BitmapAnchor_CenterLeft);
+      content.SetBorder(10);
+      content.SetText("?");
+
+      label_.SetContent(content);
+      label_.SetColor(COLOR_TEXT);
+    }
+
+    virtual unsigned int GetHandlesCount() const ORTHANC_OVERRIDE
+    {
+      return 2;
+    }
+
+    virtual Handle& GetHandle(unsigned int index) const ORTHANC_OVERRIDE
+    {
+      switch (index)
+      {
+        case 0:
+          return handle1_;
+
+        case 1:
+          return handle2_;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+    virtual void SignalMove(GeometricPrimitive& primitive,
+                            const Scene2D& scene) ORTHANC_OVERRIDE
+    {
+      if (&primitive == &handle1_ ||
+          &primitive == &handle2_)
+      {
+        ellipse_.SetPosition(handle1_.GetCenter(), handle2_.GetCenter());
+      }
+      else if (&primitive == &ellipse_)
+      {
+        handle1_.SetCenter(ellipse_.GetPosition1());
+        handle2_.SetCenter(ellipse_.GetPosition2());
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
+      
+      TagProbeAsChanged();
+    }
+
+    virtual void Serialize(Json::Value& target) ORTHANC_OVERRIDE
+    {
+      target = Json::objectValue;
+      target[KEY_TYPE] = VALUE_ELLIPSE_PROBE;
+      target[KEY_X1] = handle1_.GetCenter().GetX();
+      target[KEY_Y1] = handle1_.GetCenter().GetY();
+      target[KEY_X2] = handle2_.GetCenter().GetX();
+      target[KEY_Y2] = handle2_.GetCenter().GetY();
+    }
+
+    static void Unserialize(AnnotationsSceneLayer& target,
+                            Units units,
+                            const Json::Value& source)
+    {
+      if (source.isMember(KEY_X1) &&
+          source.isMember(KEY_Y1) &&
+          source.isMember(KEY_X2) &&
+          source.isMember(KEY_Y2) &&
+          source[KEY_X1].isNumeric() &&
+          source[KEY_Y1].isNumeric() &&
+          source[KEY_X2].isNumeric() &&
+          source[KEY_Y2].isNumeric())
+      {
+        new EllipseProbeAnnotation(target, units,
+                                     ScenePoint2D(source[KEY_X1].asDouble(), source[KEY_Y1].asDouble()),
+                                     ScenePoint2D(source[KEY_X2].asDouble(), source[KEY_Y2].asDouble()));
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "Cannot unserialize an ellipse probe annotation");
+      }
+    }
+  };
+
+  
   class AnnotationsSceneLayer::CreateTwoHandlesTracker : public IFlexiblePointerTracker
   {
   private:
@@ -2254,6 +2598,12 @@
             return new CreateTwoHandlesTracker(*annotation, scene.GetCanvasToSceneTransform());
           }
 
+          case Tool_EllipseProbe:
+          {
+            Annotation* annotation = new EllipseProbeAnnotation(*this, units_, s, s);
+            return new CreateTwoHandlesTracker(*annotation, scene.GetCanvasToSceneTransform());
+          }
+
           default:
             return NULL;
         }
@@ -2355,6 +2705,10 @@
       {
         RectangleProbeAnnotation::Unserialize(*this, units_, annotations[i]);
       }
+      else if (type == VALUE_ELLIPSE_PROBE)
+      {
+        EllipseProbeAnnotation::Unserialize(*this, units_, annotations[i]);
+      }
       else
       {
         LOG(ERROR) << "Cannot unserialize unknown type of annotation: " << type;
--- a/OrthancStone/Sources/Scene2D/AnnotationsSceneLayer.h	Mon Oct 31 15:20:18 2022 +0100
+++ b/OrthancStone/Sources/Scene2D/AnnotationsSceneLayer.h	Mon Oct 31 17:43:44 2022 +0100
@@ -55,6 +55,7 @@
     class Circle;    
     class Arc;
     class Text;
+    class Ellipse;
 
     class Annotation;
     class ProbingAnnotation;
@@ -63,12 +64,12 @@
     class AngleAnnotation;
     class CircleAnnotation;
     class RectangleProbeAnnotation;
+    class EllipseProbeAnnotation;
     
     class EditPrimitiveTracker;
     class CreateTwoHandlesTracker;
     class CreateAngleTracker;
     class CreatePixelProbeTracker;
-    class CreateRectangleProbeTracker;
     class RemoveTracker;
 
     typedef std::set<GeometricPrimitive*>  GeometricPrimitives;