Skip to content
16 changes: 16 additions & 0 deletions Documentation/docs/migration_guides/itk_6_migration_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
--------------------------------------

Expand Down
4 changes: 2 additions & 2 deletions Modules/IO/MeshFreeSurfer/include/itkFreeSurferAsciiMeshIO.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include "itkMakeUniqueForOverwrite.h"

#include <fstream>
#include "itkNumberToString.h"

namespace itk
{
Expand Down Expand Up @@ -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';
}
Expand Down
3 changes: 2 additions & 1 deletion Modules/IO/Meta/include/itkMetaImageIO.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

#include <fstream>
#include "itkImageIOBase.h"
#include "itkNumberToString.h"
#include "itkSingletonMacro.h"
#include "itkMetaDataObject.h"
#include "metaObject.h"
Expand Down Expand Up @@ -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 << ' ';
Expand Down
28 changes: 11 additions & 17 deletions Modules/IO/Meta/src/itkMetaImageIO.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <set>

// Function to join strings with a delimiter similar to python's ' '.join([1, 2, 3 ])
template <typename ContainerType, typename DelimiterType, typename StreamType>
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
{
Expand Down Expand Up @@ -458,11 +445,11 @@ MetaImageIO::WriteImageInformation()
}
else if (ExposeMetaData<double>(metaDict, key, dval))
{
strs << dval;
strs << ConvertNumberToString(dval);
}
else if (ExposeMetaData<float>(metaDict, key, fval))
{
strs << fval;
strs << ConvertNumberToString(fval);
}
else if (ExposeMetaData<long>(metaDict, key, lval))
{
Expand Down Expand Up @@ -510,7 +497,14 @@ MetaImageIO::WriteImageInformation()
}
else if (ExposeMetaData<std::vector<double>>(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) ||
Expand Down
56 changes: 56 additions & 0 deletions Modules/IO/Meta/test/itkMetaImageIOMetaDataTest.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include "itkRandomImageSource.h"
#include "itkMetaDataObject.h"
#include "itkMetaImageIO.h"
#include "itkNumberToString.h"
#include "itkTestingMacros.h"


Expand Down Expand Up @@ -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<double>(precDict, std::string("high_precision_double"), hpDouble);
itk::EncapsulateMetaData<float>(precDict, std::string("high_precision_float"), hpFloat);

WriteImage<ImageType>(precImage, argv[1]);

const ImageType::Pointer precImage2 = ReadImage<ImageType>(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<std::string>(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<std::string>(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;
}
105 changes: 32 additions & 73 deletions Modules/IO/NIFTI/src/itkNiftiImageIO.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include "itkNiftiImageIO.h"
#include "itkIOCommon.h"
#include "itkMetaDataObject.h"
#include "itkNumberToString.h"
#include "itkAnatomicalOrientation.h"
#include <nifti1_io.h>
#include "itkNiftiImageIOConfigurePrivate.h"
Expand Down Expand Up @@ -645,17 +646,9 @@ NiftiImageIO::SetImageIOMetadataFromNIfTI()
EncapsulateMetaData<std::string>(thisDic, dimKey.str(), dim.str());
}

std::ostringstream intent_p1;
intent_p1 << m_Holder->ptr->intent_p1;
EncapsulateMetaData<std::string>(thisDic, "intent_p1", intent_p1.str());

std::ostringstream intent_p2;
intent_p2 << m_Holder->ptr->intent_p2;
EncapsulateMetaData<std::string>(thisDic, "intent_p2", intent_p2.str());

std::ostringstream intent_p3;
intent_p3 << m_Holder->ptr->intent_p3;
EncapsulateMetaData<std::string>(thisDic, "intent_p3", intent_p3.str());
EncapsulateMetaData<std::string>(thisDic, "intent_p1", ConvertNumberToString(m_Holder->ptr->intent_p1));
EncapsulateMetaData<std::string>(thisDic, "intent_p2", ConvertNumberToString(m_Holder->ptr->intent_p2));
EncapsulateMetaData<std::string>(thisDic, "intent_p3", ConvertNumberToString(m_Holder->ptr->intent_p3));

std::ostringstream intent_code;
intent_code << m_Holder->ptr->intent_code;
Expand All @@ -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<std::string>(thisDic, pixdimKey.str(), pixdim.str());
EncapsulateMetaData<std::string>(thisDic, pixdimKey.str(), ConvertNumberToString(m_Holder->ptr->pixdim[idx]));
}

std::ostringstream vox_offset;
vox_offset << m_Holder->ptr->iname_offset;
EncapsulateMetaData<std::string>(thisDic, "vox_offset", vox_offset.str());

std::ostringstream scl_slope;
scl_slope << m_Holder->ptr->scl_slope;
EncapsulateMetaData<std::string>(thisDic, "scl_slope", scl_slope.str());

std::ostringstream scl_inter;
scl_inter << m_Holder->ptr->scl_inter;
EncapsulateMetaData<std::string>(thisDic, "scl_inter", scl_inter.str());
EncapsulateMetaData<std::string>(thisDic, "scl_slope", ConvertNumberToString(m_Holder->ptr->scl_slope));
EncapsulateMetaData<std::string>(thisDic, "scl_inter", ConvertNumberToString(m_Holder->ptr->scl_inter));

std::ostringstream slice_end;
slice_end << m_Holder->ptr->slice_end;
Expand All @@ -706,21 +692,10 @@ NiftiImageIO::SetImageIOMetadataFromNIfTI()
xyzt_units << SPACE_TIME_TO_XYZT(m_Holder->ptr->xyz_units, m_Holder->ptr->time_units);
EncapsulateMetaData<std::string>(thisDic, "xyzt_units", xyzt_units.str());

std::ostringstream cal_max;
cal_max << m_Holder->ptr->cal_max;
EncapsulateMetaData<std::string>(thisDic, "cal_max", cal_max.str());

std::ostringstream cal_min;
cal_min << m_Holder->ptr->cal_min;
EncapsulateMetaData<std::string>(thisDic, "cal_min", cal_min.str());

std::ostringstream slice_duration;
slice_duration << m_Holder->ptr->slice_duration;
EncapsulateMetaData<std::string>(thisDic, "slice_duration", slice_duration.str());

std::ostringstream toffset;
toffset << m_Holder->ptr->toffset;
EncapsulateMetaData<std::string>(thisDic, "toffset", toffset.str());
EncapsulateMetaData<std::string>(thisDic, "cal_max", ConvertNumberToString(m_Holder->ptr->cal_max));
EncapsulateMetaData<std::string>(thisDic, "cal_min", ConvertNumberToString(m_Holder->ptr->cal_min));
EncapsulateMetaData<std::string>(thisDic, "slice_duration", ConvertNumberToString(m_Holder->ptr->slice_duration));
EncapsulateMetaData<std::string>(thisDic, "toffset", ConvertNumberToString(m_Holder->ptr->toffset));

std::ostringstream descrip;
descrip << m_Holder->ptr->descrip;
Expand All @@ -740,44 +715,28 @@ NiftiImageIO::SetImageIOMetadataFromNIfTI()
EncapsulateMetaData<std::string>(thisDic, "sform_code", sform_code.str());
EncapsulateMetaData<std::string>(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<std::string>(thisDic, "quatern_b", quatern_b.str());

std::ostringstream quatern_c;
quatern_c << m_Holder->ptr->quatern_c;
EncapsulateMetaData<std::string>(thisDic, "quatern_c", quatern_c.str());

std::ostringstream quatern_d;
quatern_d << m_Holder->ptr->quatern_d;
EncapsulateMetaData<std::string>(thisDic, "quatern_d", quatern_d.str());

std::ostringstream qoffset_x;
qoffset_x << m_Holder->ptr->qoffset_x;
EncapsulateMetaData<std::string>(thisDic, "qoffset_x", qoffset_x.str());

std::ostringstream qoffset_y;
qoffset_y << m_Holder->ptr->qoffset_y;
EncapsulateMetaData<std::string>(thisDic, "qoffset_y", qoffset_y.str());
EncapsulateMetaData<std::string>(thisDic, "quatern_b", ConvertNumberToString(m_Holder->ptr->quatern_b));
EncapsulateMetaData<std::string>(thisDic, "quatern_c", ConvertNumberToString(m_Holder->ptr->quatern_c));
EncapsulateMetaData<std::string>(thisDic, "quatern_d", ConvertNumberToString(m_Holder->ptr->quatern_d));
EncapsulateMetaData<std::string>(thisDic, "qoffset_x", ConvertNumberToString(m_Holder->ptr->qoffset_x));
EncapsulateMetaData<std::string>(thisDic, "qoffset_y", ConvertNumberToString(m_Holder->ptr->qoffset_y));
EncapsulateMetaData<std::string>(thisDic, "qoffset_z", ConvertNumberToString(m_Holder->ptr->qoffset_z));

std::ostringstream qoffset_z;
qoffset_z << m_Holder->ptr->qoffset_z;
EncapsulateMetaData<std::string>(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<std::string>(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<std::string>(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<std::string>(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<std::string>(thisDic, srowKey.str(), srowStr);
}

std::ostringstream intent_name;
intent_name << m_Holder->ptr->intent_name;
Expand Down
Loading
Loading