diff --git a/Documentation/docs/migration_guides/itk_6_migration_guide.md b/Documentation/docs/migration_guides/itk_6_migration_guide.md index 6a071b2427c..83be5a6df6b 100644 --- a/Documentation/docs/migration_guides/itk_6_migration_guide.md +++ b/Documentation/docs/migration_guides/itk_6_migration_guide.md @@ -213,6 +213,22 @@ The handful of manually wrapped long double functions were removed from python wrapping. +IO modules write floating-point metadata with lossless precision +----------------------------------------------------------------- + +`float` and `double` metadata values are now serialized using +`itk::ConvertNumberToString()`, which produces the shortest decimal string +that round-trips exactly — replacing the previous 6-significant-digit default. + +Affected modules: **NIfTI** (`scl_slope`, `scl_inter`, `pixdim`, spacing, +and offset fields), **MetaImage**, **NRRD**, **VoxBoCUB**, **SpatialObject**, +and **Mesh IO**. + +Tests that compare raw header text against golden baselines may fail because +values previously written as e.g. `"1"` are now written as `"1.0000001"`. +Replace exact string matches with numeric comparisons or regenerate baselines. +Files not written by ITK are unaffected. + Legacy GoogleTest Target Names Removed -------------------------------------- diff --git a/Modules/IO/MeshFreeSurfer/include/itkFreeSurferAsciiMeshIO.h b/Modules/IO/MeshFreeSurfer/include/itkFreeSurferAsciiMeshIO.h index 2703c1780bc..b1a7be85d88 100644 --- a/Modules/IO/MeshFreeSurfer/include/itkFreeSurferAsciiMeshIO.h +++ b/Modules/IO/MeshFreeSurfer/include/itkFreeSurferAsciiMeshIO.h @@ -24,6 +24,7 @@ #include "itkMakeUniqueForOverwrite.h" #include +#include "itkNumberToString.h" namespace itk { @@ -117,13 +118,12 @@ class ITKIOMeshFreeSurfer_EXPORT FreeSurferAsciiMeshIO : public MeshIOBase void WritePoints(T * buffer, std::ofstream & outputFile, T label = T{}) { - outputFile.precision(6); SizeValueType index = 0; for (SizeValueType ii = 0; ii < this->m_NumberOfPoints; ++ii) { for (unsigned int jj = 0; jj < this->m_PointDimension; ++jj) { - outputFile << std::fixed << buffer[index++] << " "; + outputFile << ConvertNumberToString(buffer[index++]) << " "; } outputFile << label << '\n'; } diff --git a/Modules/IO/Meta/include/itkMetaImageIO.h b/Modules/IO/Meta/include/itkMetaImageIO.h index 7bfea726a32..aab995adc58 100644 --- a/Modules/IO/Meta/include/itkMetaImageIO.h +++ b/Modules/IO/Meta/include/itkMetaImageIO.h @@ -22,6 +22,7 @@ #include #include "itkImageIOBase.h" +#include "itkNumberToString.h" #include "itkSingletonMacro.h" #include "itkMetaDataObject.h" #include "metaObject.h" @@ -220,7 +221,7 @@ MetaImageIO::WriteMatrixInMetaData(std::ostringstream & strs, { for (unsigned int j = 0; j < VNColumns; ++j) { - strs << mval[i][j]; + strs << ConvertNumberToString(mval[i][j]); if (i != VNRows - 1 || j != VNColumns - 1) { strs << ' '; diff --git a/Modules/IO/Meta/src/itkMetaImageIO.cxx b/Modules/IO/Meta/src/itkMetaImageIO.cxx index 24b24d91922..884a4c1d8f8 100644 --- a/Modules/IO/Meta/src/itkMetaImageIO.cxx +++ b/Modules/IO/Meta/src/itkMetaImageIO.cxx @@ -21,26 +21,13 @@ #include "itkIOCommon.h" #include "itksys/SystemTools.hxx" #include "itkMath.h" +#include "itkNumberToString.h" #include "itkSingleton.h" #include "itkMakeUniqueForOverwrite.h" #include "metaImageUtils.h" #include -// Function to join strings with a delimiter similar to python's ' '.join([1, 2, 3 ]) -template -static auto -joinElements(const ContainerType & elements, const DelimiterType & delimiter, StreamType & strs) -> void -{ - for (size_t i = 0; i < elements.size(); ++i) - { - strs << elements[i]; - if (i != elements.size() - 1) - { - strs << delimiter; - } - } -} namespace itk { @@ -458,11 +445,11 @@ MetaImageIO::WriteImageInformation() } else if (ExposeMetaData(metaDict, key, dval)) { - strs << dval; + strs << ConvertNumberToString(dval); } else if (ExposeMetaData(metaDict, key, fval)) { - strs << fval; + strs << ConvertNumberToString(fval); } else if (ExposeMetaData(metaDict, key, lval)) { @@ -510,7 +497,14 @@ MetaImageIO::WriteImageInformation() } else if (ExposeMetaData>(metaDict, key, vval)) { - joinElements(vval, ' ', strs); + for (size_t i = 0; i < vval.size(); ++i) + { + if (i > 0) + { + strs << ' '; + } + strs << ConvertNumberToString(vval[i]); + } } else if (WriteMatrixInMetaData<1>(strs, metaDict, key) || WriteMatrixInMetaData<2>(strs, metaDict, key) || WriteMatrixInMetaData<3>(strs, metaDict, key) || WriteMatrixInMetaData<4>(strs, metaDict, key) || diff --git a/Modules/IO/Meta/test/itkMetaImageIOMetaDataTest.cxx b/Modules/IO/Meta/test/itkMetaImageIOMetaDataTest.cxx index 58d34233ae8..0f37aa6b4b6 100644 --- a/Modules/IO/Meta/test/itkMetaImageIOMetaDataTest.cxx +++ b/Modules/IO/Meta/test/itkMetaImageIOMetaDataTest.cxx @@ -21,6 +21,7 @@ #include "itkRandomImageSource.h" #include "itkMetaDataObject.h" #include "itkMetaImageIO.h" +#include "itkNumberToString.h" #include "itkTestingMacros.h" @@ -341,5 +342,60 @@ itkMetaImageIOMetaDataTest(int argc, char * argv[]) return 1; // error } + // Precision test: verify that double and float metadata round-trips without + // the 6-digit truncation introduced by default stream precision. + // + // With default stream precision the MetaImageIO write path serialised a + // double like 1.2345678901234568 as "1.23457", which on read-back parses + // to a different double bit-pattern. itk::ConvertNumberToString() produces + // the shortest decimal string that round-trips exactly. + // + // These checks FAIL against the unfixed MetaImageIO and PASS with the fix. + { + // Values chosen to require more than 6 significant decimal digits. + constexpr double hpDouble = 1.2345678901234568; + constexpr float hpFloat = 1.2345679f; + + ImageType::Pointer precImage(source->GetOutput()); + itk::MetaDataDictionary & precDict = precImage->GetMetaDataDictionary(); + itk::EncapsulateMetaData(precDict, std::string("high_precision_double"), hpDouble); + itk::EncapsulateMetaData(precDict, std::string("high_precision_float"), hpFloat); + + WriteImage(precImage, argv[1]); + + const ImageType::Pointer precImage2 = ReadImage(argv[1]); + itk::MetaDataDictionary & precDict2 = precImage2->GetMetaDataDictionary(); + + // Extract raw string and parse back with exact equality to detect + // any bit-level precision loss from the string serialization. + std::string doubleStr; + if (!itk::ExposeMetaData(precDict2, std::string("high_precision_double"), doubleStr)) + { + std::cerr << "Key high_precision_double not found after round-trip\n"; + return 1; + } + const double parsedDouble = std::stod(doubleStr); + if (parsedDouble != hpDouble) + { + std::cerr << "Double precision loss: stored " << itk::ConvertNumberToString(hpDouble) << " but string '" + << doubleStr << "' parses to " << itk::ConvertNumberToString(parsedDouble) << '\n'; + return 1; + } + + std::string floatStr; + if (!itk::ExposeMetaData(precDict2, std::string("high_precision_float"), floatStr)) + { + std::cerr << "Key high_precision_float not found after round-trip\n"; + return 1; + } + const float parsedFloat = std::stof(floatStr); + if (parsedFloat != hpFloat) + { + std::cerr << "Float precision loss: stored " << itk::ConvertNumberToString(hpFloat) << " but string '" << floatStr + << "' parses to " << itk::ConvertNumberToString(parsedFloat) << '\n'; + return 1; + } + } + return 0; } diff --git a/Modules/IO/NIFTI/src/itkNiftiImageIO.cxx b/Modules/IO/NIFTI/src/itkNiftiImageIO.cxx index 4d6623b7b87..1dea0039017 100644 --- a/Modules/IO/NIFTI/src/itkNiftiImageIO.cxx +++ b/Modules/IO/NIFTI/src/itkNiftiImageIO.cxx @@ -19,6 +19,7 @@ #include "itkNiftiImageIO.h" #include "itkIOCommon.h" #include "itkMetaDataObject.h" +#include "itkNumberToString.h" #include "itkAnatomicalOrientation.h" #include #include "itkNiftiImageIOConfigurePrivate.h" @@ -645,17 +646,9 @@ NiftiImageIO::SetImageIOMetadataFromNIfTI() EncapsulateMetaData(thisDic, dimKey.str(), dim.str()); } - std::ostringstream intent_p1; - intent_p1 << m_Holder->ptr->intent_p1; - EncapsulateMetaData(thisDic, "intent_p1", intent_p1.str()); - - std::ostringstream intent_p2; - intent_p2 << m_Holder->ptr->intent_p2; - EncapsulateMetaData(thisDic, "intent_p2", intent_p2.str()); - - std::ostringstream intent_p3; - intent_p3 << m_Holder->ptr->intent_p3; - EncapsulateMetaData(thisDic, "intent_p3", intent_p3.str()); + EncapsulateMetaData(thisDic, "intent_p1", ConvertNumberToString(m_Holder->ptr->intent_p1)); + EncapsulateMetaData(thisDic, "intent_p2", ConvertNumberToString(m_Holder->ptr->intent_p2)); + EncapsulateMetaData(thisDic, "intent_p3", ConvertNumberToString(m_Holder->ptr->intent_p3)); std::ostringstream intent_code; intent_code << m_Holder->ptr->intent_code; @@ -675,24 +668,17 @@ NiftiImageIO::SetImageIOMetadataFromNIfTI() for (int idx = 0; idx < 8; ++idx) { - std::ostringstream pixdim; - pixdim << m_Holder->ptr->pixdim[idx]; std::ostringstream pixdimKey; pixdimKey << "pixdim[" << idx << ']'; - EncapsulateMetaData(thisDic, pixdimKey.str(), pixdim.str()); + EncapsulateMetaData(thisDic, pixdimKey.str(), ConvertNumberToString(m_Holder->ptr->pixdim[idx])); } std::ostringstream vox_offset; vox_offset << m_Holder->ptr->iname_offset; EncapsulateMetaData(thisDic, "vox_offset", vox_offset.str()); - std::ostringstream scl_slope; - scl_slope << m_Holder->ptr->scl_slope; - EncapsulateMetaData(thisDic, "scl_slope", scl_slope.str()); - - std::ostringstream scl_inter; - scl_inter << m_Holder->ptr->scl_inter; - EncapsulateMetaData(thisDic, "scl_inter", scl_inter.str()); + EncapsulateMetaData(thisDic, "scl_slope", ConvertNumberToString(m_Holder->ptr->scl_slope)); + EncapsulateMetaData(thisDic, "scl_inter", ConvertNumberToString(m_Holder->ptr->scl_inter)); std::ostringstream slice_end; slice_end << m_Holder->ptr->slice_end; @@ -706,21 +692,10 @@ NiftiImageIO::SetImageIOMetadataFromNIfTI() xyzt_units << SPACE_TIME_TO_XYZT(m_Holder->ptr->xyz_units, m_Holder->ptr->time_units); EncapsulateMetaData(thisDic, "xyzt_units", xyzt_units.str()); - std::ostringstream cal_max; - cal_max << m_Holder->ptr->cal_max; - EncapsulateMetaData(thisDic, "cal_max", cal_max.str()); - - std::ostringstream cal_min; - cal_min << m_Holder->ptr->cal_min; - EncapsulateMetaData(thisDic, "cal_min", cal_min.str()); - - std::ostringstream slice_duration; - slice_duration << m_Holder->ptr->slice_duration; - EncapsulateMetaData(thisDic, "slice_duration", slice_duration.str()); - - std::ostringstream toffset; - toffset << m_Holder->ptr->toffset; - EncapsulateMetaData(thisDic, "toffset", toffset.str()); + EncapsulateMetaData(thisDic, "cal_max", ConvertNumberToString(m_Holder->ptr->cal_max)); + EncapsulateMetaData(thisDic, "cal_min", ConvertNumberToString(m_Holder->ptr->cal_min)); + EncapsulateMetaData(thisDic, "slice_duration", ConvertNumberToString(m_Holder->ptr->slice_duration)); + EncapsulateMetaData(thisDic, "toffset", ConvertNumberToString(m_Holder->ptr->toffset)); std::ostringstream descrip; descrip << m_Holder->ptr->descrip; @@ -740,44 +715,28 @@ NiftiImageIO::SetImageIOMetadataFromNIfTI() EncapsulateMetaData(thisDic, "sform_code", sform_code.str()); EncapsulateMetaData(thisDic, "sform_code_name", std::string(str_xform(m_Holder->ptr->sform_code))); - std::ostringstream quatern_b; - quatern_b << m_Holder->ptr->quatern_b; - EncapsulateMetaData(thisDic, "quatern_b", quatern_b.str()); - - std::ostringstream quatern_c; - quatern_c << m_Holder->ptr->quatern_c; - EncapsulateMetaData(thisDic, "quatern_c", quatern_c.str()); - - std::ostringstream quatern_d; - quatern_d << m_Holder->ptr->quatern_d; - EncapsulateMetaData(thisDic, "quatern_d", quatern_d.str()); - - std::ostringstream qoffset_x; - qoffset_x << m_Holder->ptr->qoffset_x; - EncapsulateMetaData(thisDic, "qoffset_x", qoffset_x.str()); - - std::ostringstream qoffset_y; - qoffset_y << m_Holder->ptr->qoffset_y; - EncapsulateMetaData(thisDic, "qoffset_y", qoffset_y.str()); + EncapsulateMetaData(thisDic, "quatern_b", ConvertNumberToString(m_Holder->ptr->quatern_b)); + EncapsulateMetaData(thisDic, "quatern_c", ConvertNumberToString(m_Holder->ptr->quatern_c)); + EncapsulateMetaData(thisDic, "quatern_d", ConvertNumberToString(m_Holder->ptr->quatern_d)); + EncapsulateMetaData(thisDic, "qoffset_x", ConvertNumberToString(m_Holder->ptr->qoffset_x)); + EncapsulateMetaData(thisDic, "qoffset_y", ConvertNumberToString(m_Holder->ptr->qoffset_y)); + EncapsulateMetaData(thisDic, "qoffset_z", ConvertNumberToString(m_Holder->ptr->qoffset_z)); - std::ostringstream qoffset_z; - qoffset_z << m_Holder->ptr->qoffset_z; - EncapsulateMetaData(thisDic, "qoffset_z", qoffset_z.str()); - - std::ostringstream srow_x; - srow_x << m_Holder->ptr->sto_xyz.m[0][0] << ' ' << m_Holder->ptr->sto_xyz.m[0][1] << ' ' - << m_Holder->ptr->sto_xyz.m[0][2] << ' ' << m_Holder->ptr->sto_xyz.m[0][3]; - EncapsulateMetaData(thisDic, "srow_x", srow_x.str()); - - std::ostringstream srow_y; - srow_y << m_Holder->ptr->sto_xyz.m[1][0] << ' ' << m_Holder->ptr->sto_xyz.m[1][1] << ' ' - << m_Holder->ptr->sto_xyz.m[1][2] << ' ' << m_Holder->ptr->sto_xyz.m[1][3]; - EncapsulateMetaData(thisDic, "srow_y", srow_y.str()); - - std::ostringstream srow_z; - srow_z << m_Holder->ptr->sto_xyz.m[2][0] << ' ' << m_Holder->ptr->sto_xyz.m[2][1] << ' ' - << m_Holder->ptr->sto_xyz.m[2][2] << ' ' << m_Holder->ptr->sto_xyz.m[2][3]; - EncapsulateMetaData(thisDic, "srow_z", srow_z.str()); + for (int row = 0; row < 3; ++row) + { + std::string srowStr; + for (int col = 0; col < 4; ++col) + { + if (col > 0) + { + srowStr += ' '; + } + srowStr += ConvertNumberToString(m_Holder->ptr->sto_xyz.m[row][col]); + } + std::ostringstream srowKey; + srowKey << "srow_" << "xyz"[row]; + EncapsulateMetaData(thisDic, srowKey.str(), srowStr); + } std::ostringstream intent_name; intent_name << m_Holder->ptr->intent_name; diff --git a/Modules/IO/NIFTI/test/itkNiftiImageIOTest3.cxx b/Modules/IO/NIFTI/test/itkNiftiImageIOTest3.cxx index 2d626e7afc5..758171a4849 100644 --- a/Modules/IO/NIFTI/test/itkNiftiImageIOTest3.cxx +++ b/Modules/IO/NIFTI/test/itkNiftiImageIOTest3.cxx @@ -17,6 +17,7 @@ *=========================================================================*/ #include "itkNiftiImageIOTest.h" +#include "itkNumberToString.h" #include // for enable_if #include @@ -284,6 +285,94 @@ TestImageOfVectors(const std::string & fname, const std::string & intentCode = " return same ? 0 : EXIT_FAILURE; } +/** Verify that NIfTI pixdim[] float header fields round-trip without + * precision loss through the string metadata dictionary. + * + * NIfTI stores spatial information (pixdim[]) as binary float32. When ITK + * reads a NIfTI file it converts these values to strings in the + * MetaDataDictionary. With default stream precision (6 sig-digits) a value + * like 0.12345679f would be serialised as "0.123457", which parses back to a + * different float. With itk::ConvertNumberToString() the round-trip is exact. + * + * This test FAILS without the ConvertNumberToString fix in itkNiftiImageIO.cxx. + */ +int +TestNiftiFloatMetadataPrecision(const std::string & fname) +{ + using ImageType = itk::Image; + auto image = ImageType::New(); + ImageType::SizeType size; + size.Fill(2); + image->SetRegions(size); + image->Allocate(true); + + // Choose a spacing value that needs more than 6 significant decimal digits. + // NIfTI stores spacing as float32 in pixdim[]. + // The write path casts GetSpacing(0) to float; the read path converts that + // float32 back to a string stored in MetaDataDictionary["pixdim[1]"]. + // stof("0.123457") != 0.12345679f, so the default-precision string loses + // information. ConvertNumberToString produces the shortest exact string. + constexpr double spacingValue = 0.123456789; + // The float32 that will actually be stored in the NIfTI binary header: + const float spacingAsFloat = static_cast(spacingValue); + + ImageType::SpacingType spacing; + spacing[0] = spacingValue; + spacing[1] = 1.0; + spacing[2] = 1.0; + image->SetSpacing(spacing); + + try + { + itk::IOTestHelper::WriteImage(image, fname); + } + catch (const itk::ExceptionObject & ex) + { + std::cerr << "TestNiftiFloatMetadataPrecision: write failed: " << ex << '\n'; + return EXIT_FAILURE; + } + + ImageType::Pointer readback; + try + { + readback = itk::IOTestHelper::ReadImage(fname); + } + catch (const itk::ExceptionObject & ex) + { + std::cerr << "TestNiftiFloatMetadataPrecision: read failed: " << ex << '\n'; + itk::IOTestHelper::Remove(fname.c_str()); + return EXIT_FAILURE; + } + + // The MetaDataDictionary key for pixdim[1] (spacing along dimension 0). + const itk::MetaDataDictionary & rdict = readback->GetMetaDataDictionary(); + bool pass = true; + + std::string pixdim1Str; + if (itk::ExposeMetaData(rdict, "pixdim[1]", pixdim1Str)) + { + const float parsedSpacing = std::stof(pixdim1Str); + if (parsedSpacing != spacingAsFloat) + { + std::cerr << "pixdim[1] precision loss: stored float32 " << itk::ConvertNumberToString(spacingAsFloat) + << " but MetaData string '" << pixdim1Str << "' parses to " << itk::ConvertNumberToString(parsedSpacing) + << '\n'; + pass = false; + } + } + else + { + std::cerr << "pixdim[1] key not found after round-trip\n"; + pass = false; + } + + if (pass) + { + itk::IOTestHelper::Remove(fname.c_str()); + } + return pass ? EXIT_SUCCESS : EXIT_FAILURE; +} + /** Test writing and reading a Vector Image */ int @@ -323,5 +412,7 @@ itkNiftiImageIOTest3(int argc, char * argv[]) success |= TestImageOfVectors(std::string("testDispacementImage_double.nii.gz"), std::string("1006")); success |= TestImageOfVectors(std::string("testDisplacementImage_float.nii.gz"), std::string("1006")); + success |= TestNiftiFloatMetadataPrecision(std::string("testFloatMetadataPrecision.nii.gz")); + return success; } diff --git a/Modules/IO/NRRD/src/itkNrrdImageIO.cxx b/Modules/IO/NRRD/src/itkNrrdImageIO.cxx index c41156fe515..c6eb7b3fd8a 100644 --- a/Modules/IO/NRRD/src/itkNrrdImageIO.cxx +++ b/Modules/IO/NRRD/src/itkNrrdImageIO.cxx @@ -23,8 +23,10 @@ #include "itkIOCommon.h" #include "itkFloatingPointExceptions.h" #include "itkNumericLocale.h" +#include "itkNumberToString.h" #include +#include namespace { @@ -1053,7 +1055,14 @@ _dump_metadata_to_stream(MetaDataDictionary & thisDic, const std::string & key, T value; if (ExposeMetaData(thisDic, key, value)) { - buffer << value; + if constexpr (std::is_same_v || std::is_same_v) + { + buffer << ConvertNumberToString(value); + } + else + { + buffer << value; + } return true; } return false; diff --git a/Modules/IO/NRRD/test/itkNrrdMetaDataTest.cxx b/Modules/IO/NRRD/test/itkNrrdMetaDataTest.cxx index ae173fa2118..56282f92d21 100644 --- a/Modules/IO/NRRD/test/itkNrrdMetaDataTest.cxx +++ b/Modules/IO/NRRD/test/itkNrrdMetaDataTest.cxx @@ -18,6 +18,7 @@ #include "itkImageFileReader.h" #include "itkImageFileWriter.h" #include "itkMetaDataObject.h" +#include "itkNumberToString.h" #include "itksys/SystemTools.hxx" #include "itkNrrdImageIO.h" @@ -90,10 +91,86 @@ itkNrrdMetaDataTest(int argc, char * argv[]) std::string NrrdTest; // if it exists and the string matches what we put in on the image // to write, AOK. - if (itk::ExposeMetaData(dict, metaDataObjectName, NrrdTest) != false && NrrdTest == metaDataObjectValue) + if (itk::ExposeMetaData(dict, metaDataObjectName, NrrdTest) == false || NrrdTest != metaDataObjectValue) { - return EXIT_SUCCESS; + return EXIT_FAILURE; // oops! } - // oops! - return EXIT_FAILURE; + + // Precision test: verify that native double and float metadata round-trips + // without the 6-digit truncation from default stream precision. + // + // The NrrdImageIO write path serialises native double/float metadata via + // _dump_metadata_to_stream. Without the ConvertNumberToString fix the + // output is truncated to 6 significant digits. The read-back string then + // parses to a different bit-pattern. + // + // These checks FAIL against the unfixed NrrdImageIO and PASS with the fix. + { + constexpr double hpDouble = 1.2345678901234568; + constexpr float hpFloat = 1.2345679f; + const char * const hpDoubleKey = "high_precision_double"; + const char * const hpFloatKey = "high_precision_float"; + + // Write a fresh image with high-precision double and float metadata. + itk::MetaDataDictionary & dict2 = image1->GetMetaDataDictionary(); + itk::EncapsulateMetaData(dict2, hpDoubleKey, hpDouble); + itk::EncapsulateMetaData(dict2, hpFloatKey, hpFloat); + + std::string precFname = argv[1]; + precFname += "/metadatatest_precision.nrrd"; + + auto precWriter = ImageWriterType::New(); + precWriter->SetImageIO(itk::NrrdImageIO::New()); + precWriter->SetFileName(precFname.c_str()); + precWriter->SetInput(image1); + + auto precReader = ImageReaderType::New(); + precReader->SetFileName(precFname.c_str()); + precReader->SetImageIO(itk::NrrdImageIO::New()); + + try + { + precWriter->Update(); + precReader->Update(); + } + catch (const itk::ExceptionObject & ex) + { + std::cerr << "Precision test write/read failed: " << ex << '\n'; + return EXIT_FAILURE; + } + + const itk::MetaDataDictionary & rdict = precReader->GetOutput()->GetMetaDataDictionary(); + + std::string doubleStr; + if (!itk::ExposeMetaData(rdict, hpDoubleKey, doubleStr)) + { + std::cerr << "Key " << hpDoubleKey << " not found after round-trip\n"; + return EXIT_FAILURE; + } + const double parsedDouble = std::stod(doubleStr); + if (parsedDouble != hpDouble) + { + std::cerr << "Double precision loss: stored " << itk::ConvertNumberToString(hpDouble) << " but NRRD string '" + << doubleStr << "' parses to " << itk::ConvertNumberToString(parsedDouble) << '\n'; + return EXIT_FAILURE; + } + + std::string floatStr; + if (!itk::ExposeMetaData(rdict, hpFloatKey, floatStr)) + { + std::cerr << "Key " << hpFloatKey << " not found after round-trip\n"; + return EXIT_FAILURE; + } + const float parsedFloat = std::stof(floatStr); + if (parsedFloat != hpFloat) + { + std::cerr << "Float precision loss: stored " << itk::ConvertNumberToString(hpFloat) << " but NRRD string '" + << floatStr << "' parses to " << itk::ConvertNumberToString(parsedFloat) << '\n'; + return EXIT_FAILURE; + } + + itksys::SystemTools::RemoveFile(precFname); + } + + return EXIT_SUCCESS; } diff --git a/Modules/IO/SpatialObjects/src/itkPolygonGroupSpatialObjectXMLFile.cxx b/Modules/IO/SpatialObjects/src/itkPolygonGroupSpatialObjectXMLFile.cxx index cc5a08d275f..956f4792b24 100644 --- a/Modules/IO/SpatialObjects/src/itkPolygonGroupSpatialObjectXMLFile.cxx +++ b/Modules/IO/SpatialObjects/src/itkPolygonGroupSpatialObjectXMLFile.cxx @@ -20,6 +20,7 @@ #include "itksys/SystemTools.hxx" #include "itkMetaDataObject.h" #include "itkIOCommon.h" +#include "itkNumberToString.h" #define RAISE_EXCEPTION(s) \ { \ ExceptionObject exception(__FILE__, __LINE__); \ @@ -253,7 +254,8 @@ PolygonGroupSpatialObjectXMLFileWriter::WriteFile() { PolygonSpatialObjectType::PointType curpoint = pointIt->GetPositionInObjectSpace(); WriteStartElement("POINT", output); - output << curpoint[0] << ' ' << curpoint[1] << ' ' << curpoint[2]; + output << ConvertNumberToString(curpoint[0]) << ' ' << ConvertNumberToString(curpoint[1]) << ' ' + << ConvertNumberToString(curpoint[2]); WriteEndElement("POINT", output); output << std::endl; ++pointIt; diff --git a/Modules/Nonunit/Review/src/itkVoxBoCUBImageIO.cxx b/Modules/Nonunit/Review/src/itkVoxBoCUBImageIO.cxx index 6ec857f6ef9..aa43f231711 100644 --- a/Modules/Nonunit/Review/src/itkVoxBoCUBImageIO.cxx +++ b/Modules/Nonunit/Review/src/itkVoxBoCUBImageIO.cxx @@ -19,6 +19,7 @@ #include "itkIOCommon.h" #include "itkMetaDataObject.h" #include "itkByteSwapper.h" +#include "itkNumberToString.h" #include "itksys/SystemTools.hxx" #include #include @@ -668,7 +669,8 @@ VoxBoCUBImageIO::WriteImageInformation() << std::endl; // Write the spacing - header << m_VB_SPACING << ":\t" << m_Spacing[0] << '\t' << m_Spacing[1] << '\t' << m_Spacing[2] << std::endl; + header << m_VB_SPACING << ":\t" << ConvertNumberToString(m_Spacing[0]) << '\t' << ConvertNumberToString(m_Spacing[1]) + << '\t' << ConvertNumberToString(m_Spacing[2]) << std::endl; // Write the origin (have to convert to bytes) diff --git a/Modules/Nonunit/Review/test/itkVoxBoCUBImageIOTest.cxx b/Modules/Nonunit/Review/test/itkVoxBoCUBImageIOTest.cxx index 590fe5acabd..9818ffd949f 100644 --- a/Modules/Nonunit/Review/test/itkVoxBoCUBImageIOTest.cxx +++ b/Modules/Nonunit/Review/test/itkVoxBoCUBImageIOTest.cxx @@ -20,7 +20,10 @@ #include "itkImageFileWriter.h" #include "itkVoxBoCUBImageIOFactory.h" +#include "itkVoxBoCUBImageIO.h" +#include "itkNumberToString.h" #include "itkTestingMacros.h" +#include "itksys/SystemTools.hxx" int itkVoxBoCUBImageIOTest(int argc, char * argv[]) @@ -51,6 +54,53 @@ itkVoxBoCUBImageIOTest(int argc, char * argv[]) ITK_TRY_EXPECT_NO_EXCEPTION(writer->Update()); + // Precision test: verify that voxel spacing round-trips without the + // 6-digit truncation introduced by default stream precision. + // + // The VoxBoCUBImageIO write path serialises m_Spacing[] as text in the CUB + // header. Without the ConvertNumberToString fix the string has only 6 + // significant digits, so reading back produces a different double value. + // + // This sub-test FAILS against the unfixed VoxBoCUBImageIO. + { + // Choose a spacing value that needs more than 6 significant decimal digits. + constexpr double hpSpacing = 0.123456789012345; + + auto precImage = ImageType::New(); + ImageType::SizeType precSize; + precSize.Fill(2); + precImage->SetRegions(precSize); + precImage->Allocate(true); + + ImageType::SpacingType spacing; + spacing[0] = hpSpacing; + spacing[1] = hpSpacing; + spacing[2] = hpSpacing; + precImage->SetSpacing(spacing); + + const std::string precFname = std::string(argv[2]) + "_precision.cub"; + + auto precWriter = WriterType::New(); + precWriter->SetFileName(precFname.c_str()); + precWriter->SetImageIO(itk::VoxBoCUBImageIO::New()); + precWriter->SetInput(precImage); + + auto precReader = ReaderType::New(); + precReader->SetFileName(precFname.c_str()); + precReader->SetImageIO(itk::VoxBoCUBImageIO::New()); + + ITK_TRY_EXPECT_NO_EXCEPTION(precWriter->Update()); + ITK_TRY_EXPECT_NO_EXCEPTION(precReader->Update()); + + const double readSpacing = precReader->GetOutput()->GetSpacing()[0]; + itksys::SystemTools::RemoveFile(precFname); + if (readSpacing != hpSpacing) + { + std::cerr << "VoxBoCUB spacing precision loss: wrote " << itk::ConvertNumberToString(hpSpacing) + << " but read back " << itk::ConvertNumberToString(readSpacing) << '\n'; + return EXIT_FAILURE; + } + } std::cout << "Test finished." << std::endl; return EXIT_SUCCESS;