Mercurial > hg > orthanc-stone
comparison OrthancStone/Sources/Scene2D/AnnotationsSceneLayer.cpp @ 1987:a595776232bb
added ellipse probe
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Mon, 31 Oct 2022 17:43:44 +0100 |
parents | e29783c92419 |
children | f03a827f8b47 |
comparison
equal
deleted
inserted
replaced
1986:e29783c92419 | 1987:a595776232bb |
---|---|
24 #include "AnnotationsSceneLayer.h" | 24 #include "AnnotationsSceneLayer.h" |
25 | 25 |
26 #include "MacroSceneLayer.h" | 26 #include "MacroSceneLayer.h" |
27 #include "PolylineSceneLayer.h" | 27 #include "PolylineSceneLayer.h" |
28 #include "TextSceneLayer.h" | 28 #include "TextSceneLayer.h" |
29 #include "TextureBaseSceneLayer.h" // TODO REMOVE | 29 #include "TextureBaseSceneLayer.h" |
30 | 30 |
31 #include <Images/ImageTraits.h> | 31 #include <Images/ImageTraits.h> |
32 #include <OrthancException.h> | 32 #include <OrthancException.h> |
33 | 33 |
34 #include <boost/math/constants/constants.hpp> | 34 #include <boost/math/constants/constants.hpp> |
54 static const char* const VALUE_SEGMENT = "segment"; | 54 static const char* const VALUE_SEGMENT = "segment"; |
55 static const char* const VALUE_MILLIMETERS = "millimeters"; | 55 static const char* const VALUE_MILLIMETERS = "millimeters"; |
56 static const char* const VALUE_PIXELS = "pixels"; | 56 static const char* const VALUE_PIXELS = "pixels"; |
57 static const char* const VALUE_PIXEL_PROBE = "pixel-probe"; | 57 static const char* const VALUE_PIXEL_PROBE = "pixel-probe"; |
58 static const char* const VALUE_RECTANGLE_PROBE = "rectangle-probe"; | 58 static const char* const VALUE_RECTANGLE_PROBE = "rectangle-probe"; |
59 static const char* const VALUE_ELLIPSE_PROBE = "ellipse-probe"; | |
59 | 60 |
60 #if 0 | 61 #if 0 |
61 static OrthancStone::Color COLOR_PRIMITIVES(192, 192, 192); | 62 static OrthancStone::Color COLOR_PRIMITIVES(192, 192, 192); |
62 static OrthancStone::Color COLOR_HOVER(0, 255, 0); | 63 static OrthancStone::Color COLOR_HOVER(0, 255, 0); |
63 static OrthancStone::Color COLOR_TEXT(255, 0, 0); | 64 static OrthancStone::Color COLOR_TEXT(255, 0, 0); |
790 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); // No hit is possible | 791 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); // No hit is possible |
791 } | 792 } |
792 }; | 793 }; |
793 | 794 |
794 | 795 |
796 class AnnotationsSceneLayer::Ellipse : public GeometricPrimitive | |
797 { | |
798 private: | |
799 ScenePoint2D p1_; | |
800 ScenePoint2D p2_; | |
801 ScenePoint2D delta_; | |
802 | |
803 double GetCenterX() const | |
804 { | |
805 return (p1_.GetX() + p2_.GetX()) / 2.0 + delta_.GetX(); | |
806 } | |
807 | |
808 double GetCenterY() const | |
809 { | |
810 return (p1_.GetY() + p2_.GetY()) / 2.0 + delta_.GetY(); | |
811 } | |
812 | |
813 double GetRadiusX() const | |
814 { | |
815 return std::abs(p1_.GetX() - p2_.GetX()) / 2.0; | |
816 } | |
817 | |
818 double GetRadiusY() const | |
819 { | |
820 return std::abs(p1_.GetY() - p2_.GetY()) / 2.0; | |
821 } | |
822 | |
823 public: | |
824 Ellipse(Annotation& parentAnnotation, | |
825 const ScenePoint2D& p1, | |
826 const ScenePoint2D& p2) : | |
827 GeometricPrimitive(parentAnnotation, 2), | |
828 p1_(p1), | |
829 p2_(p2), | |
830 delta_(0, 0) | |
831 { | |
832 } | |
833 | |
834 void SetPosition(const ScenePoint2D& p1, | |
835 const ScenePoint2D& p2) | |
836 { | |
837 SetModified(true); | |
838 p1_ = p1; | |
839 p2_ = p2; | |
840 delta_ = ScenePoint2D(0, 0); | |
841 } | |
842 | |
843 ScenePoint2D GetPosition1() const | |
844 { | |
845 return p1_ + delta_; | |
846 } | |
847 | |
848 ScenePoint2D GetPosition2() const | |
849 { | |
850 return p2_ + delta_; | |
851 } | |
852 | |
853 double GetArea() const | |
854 { | |
855 return PI * GetRadiusX() * GetRadiusY(); | |
856 } | |
857 | |
858 bool IsPointInside(const ScenePoint2D& p) const | |
859 { | |
860 const double radiusX = GetRadiusX(); | |
861 const double radiusY = GetRadiusY(); | |
862 | |
863 double a, b, x, y; | |
864 | |
865 if (radiusX > radiusY) | |
866 { | |
867 // The ellipse is horizontal => we are in the case described | |
868 // on Wikipedia: | |
869 // https://en.wikipedia.org/wiki/Ellipse#Standard_equation | |
870 | |
871 a = radiusX; | |
872 b = radiusY; | |
873 x = p.GetX() - GetCenterX(); | |
874 y = p.GetY() - GetCenterY(); | |
875 } | |
876 else | |
877 { | |
878 a = radiusY; | |
879 b = radiusX; | |
880 x = p.GetY() - GetCenterY(); | |
881 y = p.GetX() - GetCenterX(); | |
882 } | |
883 | |
884 const double c = sqrt(a * a - b * b); | |
885 | |
886 return (sqrt((x - c) * (x - c) + y * y) + | |
887 sqrt((x + c) * (x + c) + y * y)) <= 2.0 * a; | |
888 } | |
889 | |
890 virtual bool IsHit(const ScenePoint2D& p, | |
891 const Scene2D& scene) const ORTHANC_OVERRIDE | |
892 { | |
893 const double zoom = scene.GetSceneToCanvasTransform().ComputeZoom(); | |
894 | |
895 const double radiusX = GetRadiusX(); | |
896 const double radiusY = GetRadiusY(); | |
897 | |
898 // Warning: This is only an approximation of the | |
899 // point-to-ellipse distance, as explained here: | |
900 // https://blog.chatfield.io/simple-method-for-distance-to-ellipse/ | |
901 | |
902 const double x = (p.GetX() - GetCenterX()) / radiusX; | |
903 const double y = (p.GetY() - GetCenterY()) / radiusY; | |
904 const double t = atan2(y, x); | |
905 const double xx = cos(t) - x; | |
906 const double yy = sin(t) - y; | |
907 | |
908 const double approximateDistance = sqrt(xx * xx + yy * yy) * (radiusX + radiusY) / 2.0; | |
909 return std::abs(approximateDistance) * zoom <= HANDLE_SIZE / 2.0; | |
910 } | |
911 | |
912 virtual void RenderPolylineLayer(PolylineSceneLayer& polyline, | |
913 const Scene2D& scene) ORTHANC_OVERRIDE | |
914 { | |
915 static unsigned int NUM_SEGMENTS = 128; | |
916 polyline.AddArc(GetCenterX(), GetCenterY(), GetRadiusX(), GetRadiusY(), 0, 2.0 * PI, GetActiveColor(), NUM_SEGMENTS); | |
917 } | |
918 | |
919 virtual void RenderOtherLayers(MacroSceneLayer& macro, | |
920 const Scene2D& scene) ORTHANC_OVERRIDE | |
921 { | |
922 } | |
923 | |
924 virtual void MovePreview(const ScenePoint2D& delta, | |
925 const Scene2D& scene) ORTHANC_OVERRIDE | |
926 { | |
927 SetModified(true); | |
928 delta_ = delta; | |
929 GetParentAnnotation().SignalMove(*this, scene); | |
930 } | |
931 | |
932 virtual void MoveDone(const ScenePoint2D& delta, | |
933 const Scene2D& scene) ORTHANC_OVERRIDE | |
934 { | |
935 SetModified(true); | |
936 p1_ = p1_ + delta; | |
937 p2_ = p2_ + delta; | |
938 delta_ = ScenePoint2D(0, 0); | |
939 GetParentAnnotation().SignalMove(*this, scene); | |
940 } | |
941 }; | |
942 | |
943 | |
795 class AnnotationsSceneLayer::EditPrimitiveTracker : public IFlexiblePointerTracker | 944 class AnnotationsSceneLayer::EditPrimitiveTracker : public IFlexiblePointerTracker |
796 { | 945 { |
797 private: | 946 private: |
798 AnnotationsSceneLayer& that_; | 947 AnnotationsSceneLayer& that_; |
799 GeometricPrimitive& primitive_; | 948 GeometricPrimitive& primitive_; |
1550 | 1699 |
1551 if (GetUnits() == Units_Millimeters) | 1700 if (GetUnits() == Units_Millimeters) |
1552 { | 1701 { |
1553 const double area = std::abs(x1 - x2) * std::abs(y1 - y2); | 1702 const double area = std::abs(x1 - x2) * std::abs(y1 - y2); |
1554 | 1703 |
1555 sprintf(buf, "%0.2f cm%c%c", | 1704 sprintf(buf, "Area: %0.2f cm%c%c", |
1556 area / 100.0, | 1705 area / 100.0, |
1557 0xc2, 0xb2 /* two bytes corresponding to two power in UTF-8 */); | 1706 0xc2, 0xb2 /* two bytes corresponding to two power in UTF-8 */); |
1558 text = buf; | 1707 text = buf; |
1559 } | 1708 } |
1560 | 1709 |
1766 } | 1915 } |
1767 } | 1916 } |
1768 }; | 1917 }; |
1769 | 1918 |
1770 | 1919 |
1920 class AnnotationsSceneLayer::EllipseProbeAnnotation : public ProbingAnnotation | |
1921 { | |
1922 private: | |
1923 Handle& handle1_; | |
1924 Handle& handle2_; | |
1925 Ellipse& ellipse_; | |
1926 Text& label_; | |
1927 | |
1928 protected: | |
1929 virtual void UpdateProbeForLayer(const ISceneLayer& layer) ORTHANC_OVERRIDE | |
1930 { | |
1931 double x1 = handle1_.GetCenter().GetX(); | |
1932 double y1 = handle1_.GetCenter().GetY(); | |
1933 double x2 = handle2_.GetCenter().GetX(); | |
1934 double y2 = handle2_.GetCenter().GetY(); | |
1935 | |
1936 // Put the label to the right of the right-most handle | |
1937 //const double y = std::min(y1, y2); | |
1938 const double y = (y1 + y2) / 2.0; | |
1939 if (x1 < x2) | |
1940 { | |
1941 label_.SetPosition(x2, y); | |
1942 } | |
1943 else | |
1944 { | |
1945 label_.SetPosition(x1, y); | |
1946 } | |
1947 | |
1948 std::string text; | |
1949 | |
1950 char buf[32]; | |
1951 | |
1952 if (GetUnits() == Units_Millimeters) | |
1953 { | |
1954 sprintf(buf, "Area: %0.2f cm%c%c", | |
1955 ellipse_.GetArea() / 100.0, | |
1956 0xc2, 0xb2 /* two bytes corresponding to two power in UTF-8 */); | |
1957 text = buf; | |
1958 } | |
1959 | |
1960 if (layer.GetType() == ISceneLayer::Type_FloatTexture) | |
1961 { | |
1962 const TextureBaseSceneLayer& texture = dynamic_cast<const TextureBaseSceneLayer&>(layer); | |
1963 const AffineTransform2D& textureToScene = texture.GetTransform(); | |
1964 const AffineTransform2D sceneToTexture = AffineTransform2D::Invert(textureToScene); | |
1965 | |
1966 const Orthanc::ImageAccessor& image = texture.GetTexture(); | |
1967 assert(image.GetFormat() == Orthanc::PixelFormat_Float32); | |
1968 | |
1969 sceneToTexture.Apply(x1, y1); | |
1970 sceneToTexture.Apply(x2, y2); | |
1971 int ix1 = static_cast<int>(std::floor(x1)); | |
1972 int iy1 = static_cast<int>(std::floor(y1)); | |
1973 int ix2 = static_cast<int>(std::floor(x2)); | |
1974 int iy2 = static_cast<int>(std::floor(y2)); | |
1975 | |
1976 if (ix1 > ix2) | |
1977 { | |
1978 std::swap(ix1, ix2); | |
1979 } | |
1980 | |
1981 if (iy1 > iy2) | |
1982 { | |
1983 std::swap(iy1, iy2); | |
1984 } | |
1985 | |
1986 LinearAlgebra::OnlineVarianceEstimator estimator; | |
1987 | |
1988 for (int y = std::max(0, iy1); y <= std::min(static_cast<int>(image.GetHeight()) - 1, iy2); y++) | |
1989 { | |
1990 int x = std::max(0, ix1); | |
1991 const float* p = reinterpret_cast<const float*>(image.GetConstRow(y)) + x; | |
1992 | |
1993 for (; x <= std::min(static_cast<int>(image.GetWidth()) - 1, ix2); x++, p++) | |
1994 { | |
1995 double yy = static_cast<double>(y) + 0.5; | |
1996 double xx = static_cast<double>(x) + 0.5; | |
1997 textureToScene.Apply(xx, yy); | |
1998 if (ellipse_.IsPointInside(ScenePoint2D(xx, yy))) | |
1999 { | |
2000 estimator.AddSample(*p); | |
2001 } | |
2002 } | |
2003 } | |
2004 | |
2005 if (estimator.GetCount() > 0) | |
2006 { | |
2007 if (!text.empty()) | |
2008 { | |
2009 text += "\n"; | |
2010 } | |
2011 sprintf(buf, "Mean: %0.1f\nStdDev: %0.1f", estimator.GetMean(), estimator.GetStandardDeviation()); | |
2012 text += buf; | |
2013 } | |
2014 } | |
2015 | |
2016 label_.SetText(text); | |
2017 } | |
2018 | |
2019 public: | |
2020 EllipseProbeAnnotation(AnnotationsSceneLayer& that, | |
2021 Units units, | |
2022 const ScenePoint2D& p1, | |
2023 const ScenePoint2D& p2) : | |
2024 ProbingAnnotation(that, units), | |
2025 handle1_(AddTypedPrimitive<Handle>(new Handle(*this, Handle::Shape_Square, p1))), | |
2026 handle2_(AddTypedPrimitive<Handle>(new Handle(*this, Handle::Shape_Square, p2))), | |
2027 ellipse_(AddTypedPrimitive<Ellipse>(new Ellipse(*this, p1, p2))), | |
2028 label_(AddTypedPrimitive<Text>(new Text(that, *this))) | |
2029 { | |
2030 TextSceneLayer content; | |
2031 content.SetAnchor(BitmapAnchor_CenterLeft); | |
2032 content.SetBorder(10); | |
2033 content.SetText("?"); | |
2034 | |
2035 label_.SetContent(content); | |
2036 label_.SetColor(COLOR_TEXT); | |
2037 } | |
2038 | |
2039 virtual unsigned int GetHandlesCount() const ORTHANC_OVERRIDE | |
2040 { | |
2041 return 2; | |
2042 } | |
2043 | |
2044 virtual Handle& GetHandle(unsigned int index) const ORTHANC_OVERRIDE | |
2045 { | |
2046 switch (index) | |
2047 { | |
2048 case 0: | |
2049 return handle1_; | |
2050 | |
2051 case 1: | |
2052 return handle2_; | |
2053 | |
2054 default: | |
2055 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); | |
2056 } | |
2057 } | |
2058 | |
2059 virtual void SignalMove(GeometricPrimitive& primitive, | |
2060 const Scene2D& scene) ORTHANC_OVERRIDE | |
2061 { | |
2062 if (&primitive == &handle1_ || | |
2063 &primitive == &handle2_) | |
2064 { | |
2065 ellipse_.SetPosition(handle1_.GetCenter(), handle2_.GetCenter()); | |
2066 } | |
2067 else if (&primitive == &ellipse_) | |
2068 { | |
2069 handle1_.SetCenter(ellipse_.GetPosition1()); | |
2070 handle2_.SetCenter(ellipse_.GetPosition2()); | |
2071 } | |
2072 else | |
2073 { | |
2074 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); | |
2075 } | |
2076 | |
2077 TagProbeAsChanged(); | |
2078 } | |
2079 | |
2080 virtual void Serialize(Json::Value& target) ORTHANC_OVERRIDE | |
2081 { | |
2082 target = Json::objectValue; | |
2083 target[KEY_TYPE] = VALUE_ELLIPSE_PROBE; | |
2084 target[KEY_X1] = handle1_.GetCenter().GetX(); | |
2085 target[KEY_Y1] = handle1_.GetCenter().GetY(); | |
2086 target[KEY_X2] = handle2_.GetCenter().GetX(); | |
2087 target[KEY_Y2] = handle2_.GetCenter().GetY(); | |
2088 } | |
2089 | |
2090 static void Unserialize(AnnotationsSceneLayer& target, | |
2091 Units units, | |
2092 const Json::Value& source) | |
2093 { | |
2094 if (source.isMember(KEY_X1) && | |
2095 source.isMember(KEY_Y1) && | |
2096 source.isMember(KEY_X2) && | |
2097 source.isMember(KEY_Y2) && | |
2098 source[KEY_X1].isNumeric() && | |
2099 source[KEY_Y1].isNumeric() && | |
2100 source[KEY_X2].isNumeric() && | |
2101 source[KEY_Y2].isNumeric()) | |
2102 { | |
2103 new EllipseProbeAnnotation(target, units, | |
2104 ScenePoint2D(source[KEY_X1].asDouble(), source[KEY_Y1].asDouble()), | |
2105 ScenePoint2D(source[KEY_X2].asDouble(), source[KEY_Y2].asDouble())); | |
2106 } | |
2107 else | |
2108 { | |
2109 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "Cannot unserialize an ellipse probe annotation"); | |
2110 } | |
2111 } | |
2112 }; | |
2113 | |
2114 | |
1771 class AnnotationsSceneLayer::CreateTwoHandlesTracker : public IFlexiblePointerTracker | 2115 class AnnotationsSceneLayer::CreateTwoHandlesTracker : public IFlexiblePointerTracker |
1772 { | 2116 { |
1773 private: | 2117 private: |
1774 AnnotationsSceneLayer& layer_; | 2118 AnnotationsSceneLayer& layer_; |
1775 Annotation* annotation_; | 2119 Annotation* annotation_; |
2252 { | 2596 { |
2253 Annotation* annotation = new RectangleProbeAnnotation(*this, units_, s, s); | 2597 Annotation* annotation = new RectangleProbeAnnotation(*this, units_, s, s); |
2254 return new CreateTwoHandlesTracker(*annotation, scene.GetCanvasToSceneTransform()); | 2598 return new CreateTwoHandlesTracker(*annotation, scene.GetCanvasToSceneTransform()); |
2255 } | 2599 } |
2256 | 2600 |
2601 case Tool_EllipseProbe: | |
2602 { | |
2603 Annotation* annotation = new EllipseProbeAnnotation(*this, units_, s, s); | |
2604 return new CreateTwoHandlesTracker(*annotation, scene.GetCanvasToSceneTransform()); | |
2605 } | |
2606 | |
2257 default: | 2607 default: |
2258 return NULL; | 2608 return NULL; |
2259 } | 2609 } |
2260 } | 2610 } |
2261 } | 2611 } |
2353 } | 2703 } |
2354 else if (type == VALUE_RECTANGLE_PROBE) | 2704 else if (type == VALUE_RECTANGLE_PROBE) |
2355 { | 2705 { |
2356 RectangleProbeAnnotation::Unserialize(*this, units_, annotations[i]); | 2706 RectangleProbeAnnotation::Unserialize(*this, units_, annotations[i]); |
2357 } | 2707 } |
2708 else if (type == VALUE_ELLIPSE_PROBE) | |
2709 { | |
2710 EllipseProbeAnnotation::Unserialize(*this, units_, annotations[i]); | |
2711 } | |
2358 else | 2712 else |
2359 { | 2713 { |
2360 LOG(ERROR) << "Cannot unserialize unknown type of annotation: " << type; | 2714 LOG(ERROR) << "Cannot unserialize unknown type of annotation: " << type; |
2361 } | 2715 } |
2362 } | 2716 } |