|
17 | 17 | *=========================================================================*/ |
18 | 18 |
|
19 | 19 | #include "itkNiftiImageIOTest.h" |
| 20 | +#include "itkNumberToString.h" |
20 | 21 | #include <type_traits> // for enable_if |
21 | 22 | #include <limits> |
22 | 23 |
|
@@ -284,6 +285,94 @@ TestImageOfVectors(const std::string & fname, const std::string & intentCode = " |
284 | 285 | return same ? 0 : EXIT_FAILURE; |
285 | 286 | } |
286 | 287 |
|
| 288 | +/** Verify that NIfTI pixdim[] float header fields round-trip without |
| 289 | + * precision loss through the string metadata dictionary. |
| 290 | + * |
| 291 | + * NIfTI stores spatial information (pixdim[]) as binary float32. When ITK |
| 292 | + * reads a NIfTI file it converts these values to strings in the |
| 293 | + * MetaDataDictionary. With default stream precision (6 sig-digits) a value |
| 294 | + * like 0.12345679f would be serialised as "0.123457", which parses back to a |
| 295 | + * different float. With itk::ConvertNumberToString() the round-trip is exact. |
| 296 | + * |
| 297 | + * This test FAILS without the ConvertNumberToString fix in itkNiftiImageIO.cxx. |
| 298 | + */ |
| 299 | +int |
| 300 | +TestNiftiFloatMetadataPrecision(const std::string & fname) |
| 301 | +{ |
| 302 | + using ImageType = itk::Image<float, 3>; |
| 303 | + auto image = ImageType::New(); |
| 304 | + ImageType::SizeType size; |
| 305 | + size.Fill(2); |
| 306 | + image->SetRegions(size); |
| 307 | + image->Allocate(true); |
| 308 | + |
| 309 | + // Choose a spacing value that needs more than 6 significant decimal digits. |
| 310 | + // NIfTI stores spacing as float32 in pixdim[]. |
| 311 | + // The write path casts GetSpacing(0) to float; the read path converts that |
| 312 | + // float32 back to a string stored in MetaDataDictionary["pixdim[1]"]. |
| 313 | + // stof("0.123457") != 0.12345679f, so the default-precision string loses |
| 314 | + // information. ConvertNumberToString produces the shortest exact string. |
| 315 | + constexpr double spacingValue = 0.123456789; |
| 316 | + // The float32 that will actually be stored in the NIfTI binary header: |
| 317 | + const float spacingAsFloat = static_cast<float>(spacingValue); |
| 318 | + |
| 319 | + ImageType::SpacingType spacing; |
| 320 | + spacing[0] = spacingValue; |
| 321 | + spacing[1] = 1.0; |
| 322 | + spacing[2] = 1.0; |
| 323 | + image->SetSpacing(spacing); |
| 324 | + |
| 325 | + try |
| 326 | + { |
| 327 | + itk::IOTestHelper::WriteImage<ImageType, itk::NiftiImageIO>(image, fname); |
| 328 | + } |
| 329 | + catch (const itk::ExceptionObject & ex) |
| 330 | + { |
| 331 | + std::cerr << "TestNiftiFloatMetadataPrecision: write failed: " << ex << '\n'; |
| 332 | + return EXIT_FAILURE; |
| 333 | + } |
| 334 | + |
| 335 | + ImageType::Pointer readback; |
| 336 | + try |
| 337 | + { |
| 338 | + readback = itk::IOTestHelper::ReadImage<ImageType>(fname); |
| 339 | + } |
| 340 | + catch (const itk::ExceptionObject & ex) |
| 341 | + { |
| 342 | + std::cerr << "TestNiftiFloatMetadataPrecision: read failed: " << ex << '\n'; |
| 343 | + itk::IOTestHelper::Remove(fname.c_str()); |
| 344 | + return EXIT_FAILURE; |
| 345 | + } |
| 346 | + |
| 347 | + // The MetaDataDictionary key for pixdim[1] (spacing along dimension 0). |
| 348 | + const itk::MetaDataDictionary & rdict = readback->GetMetaDataDictionary(); |
| 349 | + bool pass = true; |
| 350 | + |
| 351 | + std::string pixdim1Str; |
| 352 | + if (itk::ExposeMetaData<std::string>(rdict, "pixdim[1]", pixdim1Str)) |
| 353 | + { |
| 354 | + const float parsedSpacing = std::stof(pixdim1Str); |
| 355 | + if (parsedSpacing != spacingAsFloat) |
| 356 | + { |
| 357 | + std::cerr << "pixdim[1] precision loss: stored float32 " << itk::ConvertNumberToString(spacingAsFloat) |
| 358 | + << " but MetaData string '" << pixdim1Str << "' parses to " << itk::ConvertNumberToString(parsedSpacing) |
| 359 | + << '\n'; |
| 360 | + pass = false; |
| 361 | + } |
| 362 | + } |
| 363 | + else |
| 364 | + { |
| 365 | + std::cerr << "pixdim[1] key not found after round-trip\n"; |
| 366 | + pass = false; |
| 367 | + } |
| 368 | + |
| 369 | + if (pass) |
| 370 | + { |
| 371 | + itk::IOTestHelper::Remove(fname.c_str()); |
| 372 | + } |
| 373 | + return pass ? EXIT_SUCCESS : EXIT_FAILURE; |
| 374 | +} |
| 375 | + |
287 | 376 | /** Test writing and reading a Vector Image |
288 | 377 | */ |
289 | 378 | int |
@@ -323,5 +412,7 @@ itkNiftiImageIOTest3(int argc, char * argv[]) |
323 | 412 | success |= TestImageOfVectors<double, 3, 1>(std::string("testDispacementImage_double.nii.gz"), std::string("1006")); |
324 | 413 | success |= TestImageOfVectors<float, 3, 1>(std::string("testDisplacementImage_float.nii.gz"), std::string("1006")); |
325 | 414 |
|
| 415 | + success |= TestNiftiFloatMetadataPrecision(std::string("testFloatMetadataPrecision.nii.gz")); |
| 416 | + |
326 | 417 | return success; |
327 | 418 | } |
0 commit comments