# HG changeset patch # User Sebastien Jodogne # Date 1622030532 -7200 # Node ID 36430d73e36c842fef8918861ee3f5fa6bea17b9 # Parent 5baaad557d58b2067f7384c5f08fc69bc459d4de introducing measure units in AnnotationsSceneLayer diff -r 5baaad557d58 -r 36430d73e36c Applications/Samples/Sdl/SingleFrameViewer/SdlSimpleViewer.cpp --- 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)) diff -r 5baaad557d58 -r 36430d73e36c Applications/Samples/Sdl/SingleFrameViewer/SdlSimpleViewerApplication.h --- 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 viewport_; boost::shared_ptr dicomLoader_; boost::shared_ptr framesLoader_; + OrthancStone::Units units_; SdlSimpleViewerApplication(ILoadersContext& context, boost::shared_ptr 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); { diff -r 5baaad557d58 -r 36430d73e36c OrthancStone/Sources/Scene2D/AnnotationsSceneLayer.cpp --- 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(new Handle(*this, p1))), handle2_(AddTypedPrimitive(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(new Handle(*this, start))), middleHandle_(AddTypedPrimitive(new Handle(*this, middle))), endHandle_(AddTypedPrimitive(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(new Handle(*this, p1))), handle2_(AddTypedPrimitive(new Handle(*this, p2))), segment_(AddTypedPrimitive(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(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(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 { diff -r 5baaad557d58 -r 36430d73e36c OrthancStone/Sources/Scene2D/AnnotationsSceneLayer.h --- 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); diff -r 5baaad557d58 -r 36430d73e36c OrthancStone/Sources/StoneEnumerations.h --- 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, diff -r 5baaad557d58 -r 36430d73e36c OrthancStone/Sources/Toolbox/DicomInstanceParameters.cpp --- 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) && diff -r 5baaad557d58 -r 36430d73e36c OrthancStone/Sources/Toolbox/DicomInstanceParameters.h --- 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_; + } }; } diff -r 5baaad557d58 -r 36430d73e36c OrthancStone/Sources/Toolbox/GeometryToolbox.cpp --- 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; } } diff -r 5baaad557d58 -r 36430d73e36c OrthancStone/Sources/Toolbox/GeometryToolbox.h --- 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);