changeset 4515:8734caa12448

Improved precision of floating-point numbers in DICOM-as-JSON and DICOM summary
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 16 Feb 2021 14:42:04 +0100
parents 5b929e6b3c36
children 671ee7c1fd46
files NEWS OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp
diffstat 3 files changed, 111 insertions(+), 10 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Tue Feb 16 12:18:41 2021 +0100
+++ b/NEWS	Tue Feb 16 14:42:04 2021 +0100
@@ -23,6 +23,7 @@
 Maintenance
 -----------
 
+* Improved precision of floating-point numbers in DICOM-as-JSON and DICOM summary
 * Optimization in C-STORE SCP by avoiding an unnecessary DICOM parsing
 * Fix build on big-endian architectures
 * Possibility to generate a static library containing the Orthanc Framework
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Tue Feb 16 12:18:41 2021 +0100
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Tue Feb 16 14:42:04 2021 +0100
@@ -191,26 +191,68 @@
     };
 
     
-#define DCMTK_TO_CTYPE_CONVERTER(converter, cType, dcmtkType, getter)   \
+    ORTHANC_FORCE_INLINE
+    static std::string FloatToString(float v)
+    {
+      /**
+       * From "boost::lexical_cast" documentation: "For more involved
+       * conversions, such as where precision or formatting need tighter
+       * control than is offered by the default behavior of
+       * lexical_cast, the conventional stringstream approach is
+       * recommended."
+       * https://www.boost.org/doc/libs/1_65_0/doc/html/boost_lexical_cast.html
+       * http://www.gotw.ca/publications/mill19.htm
+       *
+       * The precision of 17 corresponds to "defaultRealPrecision" in JsonCpp:
+       * https://github.com/open-source-parsers/jsoncpp/blob/master/include/json/value.h
+       **/
+
+      //return boost::lexical_cast<std::string>(v);  // This was used in Orthanc <= 1.9.0
+      
+      std::ostringstream ss;
+      ss << std::setprecision(17) << v;
+      return ss.str();
+    }
+
+
+    ORTHANC_FORCE_INLINE
+    static std::string DoubleToString(double v)
+    {
+      //return boost::lexical_cast<std::string>(v);  // This was used in Orthanc <= 1.9.0
+      
+      std::ostringstream ss;
+      ss << std::setprecision(17) << v;
+      return ss.str();
+    }
+
+    
+#define DCMTK_TO_CTYPE_CONVERTER(converter, cType, dcmtkType, getter, toStringFunction)  \
                                                                         \
     struct converter                                                    \
     {                                                                   \
       typedef cType CType;                                              \
                                                                         \
+      ORTHANC_FORCE_INLINE                                              \
       static bool Apply(CType& result,                                  \
                         DcmElement& element,                            \
                         size_t i)                                       \
       {                                                                 \
         return dynamic_cast<dcmtkType&>(element).getter(result, i).good(); \
       }                                                                 \
+                                                                        \
+      ORTHANC_FORCE_INLINE                                              \
+      static std::string ToString(CType value)                          \
+      {                                                                 \
+        return toStringFunction(value);                                 \
+      }                                                                 \
     };
 
-DCMTK_TO_CTYPE_CONVERTER(DcmtkToSint32Converter, Sint32, DcmSignedLong, getSint32)
-DCMTK_TO_CTYPE_CONVERTER(DcmtkToSint16Converter, Sint16, DcmSignedShort, getSint16)
-DCMTK_TO_CTYPE_CONVERTER(DcmtkToUint32Converter, Uint32, DcmUnsignedLong, getUint32)
-DCMTK_TO_CTYPE_CONVERTER(DcmtkToUint16Converter, Uint16, DcmUnsignedShort, getUint16)
-DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat32Converter, Float32, DcmFloatingPointSingle, getFloat32)
-DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64Converter, Float64, DcmFloatingPointDouble, getFloat64)
+    DCMTK_TO_CTYPE_CONVERTER(DcmtkToSint32Converter, Sint32, DcmSignedLong, getSint32, boost::lexical_cast<std::string>)
+    DCMTK_TO_CTYPE_CONVERTER(DcmtkToSint16Converter, Sint16, DcmSignedShort, getSint16, boost::lexical_cast<std::string>)
+    DCMTK_TO_CTYPE_CONVERTER(DcmtkToUint32Converter, Uint32, DcmUnsignedLong, getUint32, boost::lexical_cast<std::string>)
+    DCMTK_TO_CTYPE_CONVERTER(DcmtkToUint16Converter, Uint16, DcmUnsignedShort, getUint16, boost::lexical_cast<std::string>)
+    DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat32Converter, Float32, DcmFloatingPointSingle, getFloat32, FloatToString)
+    DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64Converter, Float64, DcmFloatingPointDouble, getFloat64, DoubleToString)
 
 
     template <typename F>
@@ -226,19 +268,18 @@
         std::vector<std::string> strings;
         for (size_t i = 0; i < count; i++) {
           if (f.Apply(value, element, i)) {
-            strings.push_back(boost::lexical_cast<std::string>(value));
+            strings.push_back(F::ToString(value));
           }
         }
         return new DicomValue(boost::algorithm::join(strings, "\\"), false);
       }
       else if (f.Apply(value, element, 0)) {
-        return new DicomValue(boost::lexical_cast<std::string>(value), false);
+        return new DicomValue(F::ToString(value), false);
       }
       else {
         return new DicomValue;
       }
     }
-
   }
 
 
--- a/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Tue Feb 16 12:18:41 2021 +0100
+++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Tue Feb 16 14:42:04 2021 +0100
@@ -1265,6 +1265,65 @@
 
 
 
+TEST(ParsedDicomFile, FloatPrecision)
+{
+  Float32 v;
+
+  switch (Toolbox::DetectEndianness())
+  {
+    case Endianness_Little:
+      reinterpret_cast<uint8_t*>(&v)[3] = 0x4E;
+      reinterpret_cast<uint8_t*>(&v)[2] = 0x9C;
+      reinterpret_cast<uint8_t*>(&v)[1] = 0xAD;
+      reinterpret_cast<uint8_t*>(&v)[0] = 0x8F;
+      break;
+
+    case Endianness_Big:
+      reinterpret_cast<uint8_t*>(&v)[0] = 0x4E;
+      reinterpret_cast<uint8_t*>(&v)[1] = 0x9C;
+      reinterpret_cast<uint8_t*>(&v)[2] = 0xAD;
+      reinterpret_cast<uint8_t*>(&v)[3] = 0x8F;
+      break;
+
+    default:
+      throw OrthancException(ErrorCode_InternalError);
+  }
+
+  ParsedDicomFile f(false);
+  ASSERT_TRUE(f.GetDcmtkObject().getDataset()->putAndInsertFloat32(DCM_ExaminedBodyThickness /* VR: FL */, v).good());
+
+  {
+    Float32 u;
+    ASSERT_TRUE(f.GetDcmtkObject().getDataset()->findAndGetFloat32(DCM_ExaminedBodyThickness, u).good());
+    ASSERT_FLOAT_EQ(u, v);
+    ASSERT_TRUE(memcmp(&u, &v, 4) == 0);
+  }
+
+  {
+    Json::Value json;
+    f.DatasetToJson(json, DicomToJsonFormat_Short, DicomToJsonFlags_None, 256);
+    ASSERT_EQ("1314310016", json["0010,9431"].asString());
+  }
+
+  {
+    DicomMap summary;
+    f.ExtractDicomSummary(summary, 256);
+    std::string s;
+    ASSERT_EQ("1314310016", summary.GetStringValue(DicomTag(0x0010, 0x9431), "nope", false));
+  }
+
+  {
+    // This flavor uses "Json::Value" serialization
+    DicomWebJsonVisitor visitor;
+    f.Apply(visitor);
+    Float32 u = visitor.GetResult() ["00109431"]["Value"][0].asFloat();
+    ASSERT_FLOAT_EQ(u, v);
+    ASSERT_TRUE(memcmp(&u, &v, 4) == 0);
+  }
+}
+
+
+
 TEST(Toolbox, RemoveIso2022EscapeSequences)
 {
   // +----------------------------------+