Skip to content

Commit 1e73a2b

Browse files
authored
Merge pull request #6003 from hjmjohnson/fix-nifti-lossless-metadata
ENH: Use NumberToString for lossless float metadata in NIfTI reader
2 parents c4a34f0 + cd9ec88 commit 1e73a2b

File tree

12 files changed

+357
-100
lines changed

12 files changed

+357
-100
lines changed

Documentation/docs/migration_guides/itk_6_migration_guide.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,22 @@ The handful of manually wrapped long double functions were
213213
removed from python wrapping.
214214

215215

216+
IO modules write floating-point metadata with lossless precision
217+
-----------------------------------------------------------------
218+
219+
`float` and `double` metadata values are now serialized using
220+
`itk::ConvertNumberToString()`, which produces the shortest decimal string
221+
that round-trips exactly — replacing the previous 6-significant-digit default.
222+
223+
Affected modules: **NIfTI** (`scl_slope`, `scl_inter`, `pixdim`, spacing,
224+
and offset fields), **MetaImage**, **NRRD**, **VoxBoCUB**, **SpatialObject**,
225+
and **Mesh IO**.
226+
227+
Tests that compare raw header text against golden baselines may fail because
228+
values previously written as e.g. `"1"` are now written as `"1.0000001"`.
229+
Replace exact string matches with numeric comparisons or regenerate baselines.
230+
Files not written by ITK are unaffected.
231+
216232
Legacy GoogleTest Target Names Removed
217233
--------------------------------------
218234

Modules/IO/MeshFreeSurfer/include/itkFreeSurferAsciiMeshIO.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include "itkMakeUniqueForOverwrite.h"
2525

2626
#include <fstream>
27+
#include "itkNumberToString.h"
2728

2829
namespace itk
2930
{
@@ -117,13 +118,12 @@ class ITKIOMeshFreeSurfer_EXPORT FreeSurferAsciiMeshIO : public MeshIOBase
117118
void
118119
WritePoints(T * buffer, std::ofstream & outputFile, T label = T{})
119120
{
120-
outputFile.precision(6);
121121
SizeValueType index = 0;
122122
for (SizeValueType ii = 0; ii < this->m_NumberOfPoints; ++ii)
123123
{
124124
for (unsigned int jj = 0; jj < this->m_PointDimension; ++jj)
125125
{
126-
outputFile << std::fixed << buffer[index++] << " ";
126+
outputFile << ConvertNumberToString(buffer[index++]) << " ";
127127
}
128128
outputFile << label << '\n';
129129
}

Modules/IO/Meta/include/itkMetaImageIO.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
#include <fstream>
2424
#include "itkImageIOBase.h"
25+
#include "itkNumberToString.h"
2526
#include "itkSingletonMacro.h"
2627
#include "itkMetaDataObject.h"
2728
#include "metaObject.h"
@@ -220,7 +221,7 @@ MetaImageIO::WriteMatrixInMetaData(std::ostringstream & strs,
220221
{
221222
for (unsigned int j = 0; j < VNColumns; ++j)
222223
{
223-
strs << mval[i][j];
224+
strs << ConvertNumberToString(mval[i][j]);
224225
if (i != VNRows - 1 || j != VNColumns - 1)
225226
{
226227
strs << ' ';

Modules/IO/Meta/src/itkMetaImageIO.cxx

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,26 +21,13 @@
2121
#include "itkIOCommon.h"
2222
#include "itksys/SystemTools.hxx"
2323
#include "itkMath.h"
24+
#include "itkNumberToString.h"
2425
#include "itkSingleton.h"
2526
#include "itkMakeUniqueForOverwrite.h"
2627
#include "metaImageUtils.h"
2728

2829
#include <set>
2930

30-
// Function to join strings with a delimiter similar to python's ' '.join([1, 2, 3 ])
31-
template <typename ContainerType, typename DelimiterType, typename StreamType>
32-
static auto
33-
joinElements(const ContainerType & elements, const DelimiterType & delimiter, StreamType & strs) -> void
34-
{
35-
for (size_t i = 0; i < elements.size(); ++i)
36-
{
37-
strs << elements[i];
38-
if (i != elements.size() - 1)
39-
{
40-
strs << delimiter;
41-
}
42-
}
43-
}
4431

4532
namespace itk
4633
{
@@ -458,11 +445,11 @@ MetaImageIO::WriteImageInformation()
458445
}
459446
else if (ExposeMetaData<double>(metaDict, key, dval))
460447
{
461-
strs << dval;
448+
strs << ConvertNumberToString(dval);
462449
}
463450
else if (ExposeMetaData<float>(metaDict, key, fval))
464451
{
465-
strs << fval;
452+
strs << ConvertNumberToString(fval);
466453
}
467454
else if (ExposeMetaData<long>(metaDict, key, lval))
468455
{
@@ -510,7 +497,14 @@ MetaImageIO::WriteImageInformation()
510497
}
511498
else if (ExposeMetaData<std::vector<double>>(metaDict, key, vval))
512499
{
513-
joinElements(vval, ' ', strs);
500+
for (size_t i = 0; i < vval.size(); ++i)
501+
{
502+
if (i > 0)
503+
{
504+
strs << ' ';
505+
}
506+
strs << ConvertNumberToString(vval[i]);
507+
}
514508
}
515509
else if (WriteMatrixInMetaData<1>(strs, metaDict, key) || WriteMatrixInMetaData<2>(strs, metaDict, key) ||
516510
WriteMatrixInMetaData<3>(strs, metaDict, key) || WriteMatrixInMetaData<4>(strs, metaDict, key) ||

Modules/IO/Meta/test/itkMetaImageIOMetaDataTest.cxx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include "itkRandomImageSource.h"
2222
#include "itkMetaDataObject.h"
2323
#include "itkMetaImageIO.h"
24+
#include "itkNumberToString.h"
2425
#include "itkTestingMacros.h"
2526

2627

@@ -341,5 +342,60 @@ itkMetaImageIOMetaDataTest(int argc, char * argv[])
341342
return 1; // error
342343
}
343344

345+
// Precision test: verify that double and float metadata round-trips without
346+
// the 6-digit truncation introduced by default stream precision.
347+
//
348+
// With default stream precision the MetaImageIO write path serialised a
349+
// double like 1.2345678901234568 as "1.23457", which on read-back parses
350+
// to a different double bit-pattern. itk::ConvertNumberToString() produces
351+
// the shortest decimal string that round-trips exactly.
352+
//
353+
// These checks FAIL against the unfixed MetaImageIO and PASS with the fix.
354+
{
355+
// Values chosen to require more than 6 significant decimal digits.
356+
constexpr double hpDouble = 1.2345678901234568;
357+
constexpr float hpFloat = 1.2345679f;
358+
359+
ImageType::Pointer precImage(source->GetOutput());
360+
itk::MetaDataDictionary & precDict = precImage->GetMetaDataDictionary();
361+
itk::EncapsulateMetaData<double>(precDict, std::string("high_precision_double"), hpDouble);
362+
itk::EncapsulateMetaData<float>(precDict, std::string("high_precision_float"), hpFloat);
363+
364+
WriteImage<ImageType>(precImage, argv[1]);
365+
366+
const ImageType::Pointer precImage2 = ReadImage<ImageType>(argv[1]);
367+
itk::MetaDataDictionary & precDict2 = precImage2->GetMetaDataDictionary();
368+
369+
// Extract raw string and parse back with exact equality to detect
370+
// any bit-level precision loss from the string serialization.
371+
std::string doubleStr;
372+
if (!itk::ExposeMetaData<std::string>(precDict2, std::string("high_precision_double"), doubleStr))
373+
{
374+
std::cerr << "Key high_precision_double not found after round-trip\n";
375+
return 1;
376+
}
377+
const double parsedDouble = std::stod(doubleStr);
378+
if (parsedDouble != hpDouble)
379+
{
380+
std::cerr << "Double precision loss: stored " << itk::ConvertNumberToString(hpDouble) << " but string '"
381+
<< doubleStr << "' parses to " << itk::ConvertNumberToString(parsedDouble) << '\n';
382+
return 1;
383+
}
384+
385+
std::string floatStr;
386+
if (!itk::ExposeMetaData<std::string>(precDict2, std::string("high_precision_float"), floatStr))
387+
{
388+
std::cerr << "Key high_precision_float not found after round-trip\n";
389+
return 1;
390+
}
391+
const float parsedFloat = std::stof(floatStr);
392+
if (parsedFloat != hpFloat)
393+
{
394+
std::cerr << "Float precision loss: stored " << itk::ConvertNumberToString(hpFloat) << " but string '" << floatStr
395+
<< "' parses to " << itk::ConvertNumberToString(parsedFloat) << '\n';
396+
return 1;
397+
}
398+
}
399+
344400
return 0;
345401
}

Modules/IO/NIFTI/src/itkNiftiImageIO.cxx

Lines changed: 32 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "itkNiftiImageIO.h"
2020
#include "itkIOCommon.h"
2121
#include "itkMetaDataObject.h"
22+
#include "itkNumberToString.h"
2223
#include "itkAnatomicalOrientation.h"
2324
#include <nifti1_io.h>
2425
#include "itkNiftiImageIOConfigurePrivate.h"
@@ -645,17 +646,9 @@ NiftiImageIO::SetImageIOMetadataFromNIfTI()
645646
EncapsulateMetaData<std::string>(thisDic, dimKey.str(), dim.str());
646647
}
647648

648-
std::ostringstream intent_p1;
649-
intent_p1 << m_Holder->ptr->intent_p1;
650-
EncapsulateMetaData<std::string>(thisDic, "intent_p1", intent_p1.str());
651-
652-
std::ostringstream intent_p2;
653-
intent_p2 << m_Holder->ptr->intent_p2;
654-
EncapsulateMetaData<std::string>(thisDic, "intent_p2", intent_p2.str());
655-
656-
std::ostringstream intent_p3;
657-
intent_p3 << m_Holder->ptr->intent_p3;
658-
EncapsulateMetaData<std::string>(thisDic, "intent_p3", intent_p3.str());
649+
EncapsulateMetaData<std::string>(thisDic, "intent_p1", ConvertNumberToString(m_Holder->ptr->intent_p1));
650+
EncapsulateMetaData<std::string>(thisDic, "intent_p2", ConvertNumberToString(m_Holder->ptr->intent_p2));
651+
EncapsulateMetaData<std::string>(thisDic, "intent_p3", ConvertNumberToString(m_Holder->ptr->intent_p3));
659652

660653
std::ostringstream intent_code;
661654
intent_code << m_Holder->ptr->intent_code;
@@ -675,24 +668,17 @@ NiftiImageIO::SetImageIOMetadataFromNIfTI()
675668

676669
for (int idx = 0; idx < 8; ++idx)
677670
{
678-
std::ostringstream pixdim;
679-
pixdim << m_Holder->ptr->pixdim[idx];
680671
std::ostringstream pixdimKey;
681672
pixdimKey << "pixdim[" << idx << ']';
682-
EncapsulateMetaData<std::string>(thisDic, pixdimKey.str(), pixdim.str());
673+
EncapsulateMetaData<std::string>(thisDic, pixdimKey.str(), ConvertNumberToString(m_Holder->ptr->pixdim[idx]));
683674
}
684675

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

689-
std::ostringstream scl_slope;
690-
scl_slope << m_Holder->ptr->scl_slope;
691-
EncapsulateMetaData<std::string>(thisDic, "scl_slope", scl_slope.str());
692-
693-
std::ostringstream scl_inter;
694-
scl_inter << m_Holder->ptr->scl_inter;
695-
EncapsulateMetaData<std::string>(thisDic, "scl_inter", scl_inter.str());
680+
EncapsulateMetaData<std::string>(thisDic, "scl_slope", ConvertNumberToString(m_Holder->ptr->scl_slope));
681+
EncapsulateMetaData<std::string>(thisDic, "scl_inter", ConvertNumberToString(m_Holder->ptr->scl_inter));
696682

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

709-
std::ostringstream cal_max;
710-
cal_max << m_Holder->ptr->cal_max;
711-
EncapsulateMetaData<std::string>(thisDic, "cal_max", cal_max.str());
712-
713-
std::ostringstream cal_min;
714-
cal_min << m_Holder->ptr->cal_min;
715-
EncapsulateMetaData<std::string>(thisDic, "cal_min", cal_min.str());
716-
717-
std::ostringstream slice_duration;
718-
slice_duration << m_Holder->ptr->slice_duration;
719-
EncapsulateMetaData<std::string>(thisDic, "slice_duration", slice_duration.str());
720-
721-
std::ostringstream toffset;
722-
toffset << m_Holder->ptr->toffset;
723-
EncapsulateMetaData<std::string>(thisDic, "toffset", toffset.str());
695+
EncapsulateMetaData<std::string>(thisDic, "cal_max", ConvertNumberToString(m_Holder->ptr->cal_max));
696+
EncapsulateMetaData<std::string>(thisDic, "cal_min", ConvertNumberToString(m_Holder->ptr->cal_min));
697+
EncapsulateMetaData<std::string>(thisDic, "slice_duration", ConvertNumberToString(m_Holder->ptr->slice_duration));
698+
EncapsulateMetaData<std::string>(thisDic, "toffset", ConvertNumberToString(m_Holder->ptr->toffset));
724699

725700
std::ostringstream descrip;
726701
descrip << m_Holder->ptr->descrip;
@@ -740,44 +715,28 @@ NiftiImageIO::SetImageIOMetadataFromNIfTI()
740715
EncapsulateMetaData<std::string>(thisDic, "sform_code", sform_code.str());
741716
EncapsulateMetaData<std::string>(thisDic, "sform_code_name", std::string(str_xform(m_Holder->ptr->sform_code)));
742717

743-
std::ostringstream quatern_b;
744-
quatern_b << m_Holder->ptr->quatern_b;
745-
EncapsulateMetaData<std::string>(thisDic, "quatern_b", quatern_b.str());
746-
747-
std::ostringstream quatern_c;
748-
quatern_c << m_Holder->ptr->quatern_c;
749-
EncapsulateMetaData<std::string>(thisDic, "quatern_c", quatern_c.str());
750-
751-
std::ostringstream quatern_d;
752-
quatern_d << m_Holder->ptr->quatern_d;
753-
EncapsulateMetaData<std::string>(thisDic, "quatern_d", quatern_d.str());
754-
755-
std::ostringstream qoffset_x;
756-
qoffset_x << m_Holder->ptr->qoffset_x;
757-
EncapsulateMetaData<std::string>(thisDic, "qoffset_x", qoffset_x.str());
758-
759-
std::ostringstream qoffset_y;
760-
qoffset_y << m_Holder->ptr->qoffset_y;
761-
EncapsulateMetaData<std::string>(thisDic, "qoffset_y", qoffset_y.str());
718+
EncapsulateMetaData<std::string>(thisDic, "quatern_b", ConvertNumberToString(m_Holder->ptr->quatern_b));
719+
EncapsulateMetaData<std::string>(thisDic, "quatern_c", ConvertNumberToString(m_Holder->ptr->quatern_c));
720+
EncapsulateMetaData<std::string>(thisDic, "quatern_d", ConvertNumberToString(m_Holder->ptr->quatern_d));
721+
EncapsulateMetaData<std::string>(thisDic, "qoffset_x", ConvertNumberToString(m_Holder->ptr->qoffset_x));
722+
EncapsulateMetaData<std::string>(thisDic, "qoffset_y", ConvertNumberToString(m_Holder->ptr->qoffset_y));
723+
EncapsulateMetaData<std::string>(thisDic, "qoffset_z", ConvertNumberToString(m_Holder->ptr->qoffset_z));
762724

763-
std::ostringstream qoffset_z;
764-
qoffset_z << m_Holder->ptr->qoffset_z;
765-
EncapsulateMetaData<std::string>(thisDic, "qoffset_z", qoffset_z.str());
766-
767-
std::ostringstream srow_x;
768-
srow_x << m_Holder->ptr->sto_xyz.m[0][0] << ' ' << m_Holder->ptr->sto_xyz.m[0][1] << ' '
769-
<< m_Holder->ptr->sto_xyz.m[0][2] << ' ' << m_Holder->ptr->sto_xyz.m[0][3];
770-
EncapsulateMetaData<std::string>(thisDic, "srow_x", srow_x.str());
771-
772-
std::ostringstream srow_y;
773-
srow_y << m_Holder->ptr->sto_xyz.m[1][0] << ' ' << m_Holder->ptr->sto_xyz.m[1][1] << ' '
774-
<< m_Holder->ptr->sto_xyz.m[1][2] << ' ' << m_Holder->ptr->sto_xyz.m[1][3];
775-
EncapsulateMetaData<std::string>(thisDic, "srow_y", srow_y.str());
776-
777-
std::ostringstream srow_z;
778-
srow_z << m_Holder->ptr->sto_xyz.m[2][0] << ' ' << m_Holder->ptr->sto_xyz.m[2][1] << ' '
779-
<< m_Holder->ptr->sto_xyz.m[2][2] << ' ' << m_Holder->ptr->sto_xyz.m[2][3];
780-
EncapsulateMetaData<std::string>(thisDic, "srow_z", srow_z.str());
725+
for (int row = 0; row < 3; ++row)
726+
{
727+
std::string srowStr;
728+
for (int col = 0; col < 4; ++col)
729+
{
730+
if (col > 0)
731+
{
732+
srowStr += ' ';
733+
}
734+
srowStr += ConvertNumberToString(m_Holder->ptr->sto_xyz.m[row][col]);
735+
}
736+
std::ostringstream srowKey;
737+
srowKey << "srow_" << "xyz"[row];
738+
EncapsulateMetaData<std::string>(thisDic, srowKey.str(), srowStr);
739+
}
781740

782741
std::ostringstream intent_name;
783742
intent_name << m_Holder->ptr->intent_name;

0 commit comments

Comments
 (0)