Skip to content

Commit

Permalink
ENH: Test NIFTI I/O with small voxels
Browse files Browse the repository at this point in the history
Use new test data to ensure transforms are correctly handled when
voxels are small. This covers a previous bug where the sform would not
be read if the determinant of the sform matrix was < 1E-5.

Update the test program used for the new test, and
itkNiftiReadWriteDirectionTest, to conform to ITK coding style.
Specifically,
    - Use modern ITK operations for I/O
    - Use the `itkNameOfTestExecutableMacro` macro to print the test name
      when the input argument count is insufficient.
    - Make the input argument checking message to comply with ITK
      convention.
   - Remove duplicate image type alias definition.
   - Remove unnecessary input argument duplicate in reader lambda function.
   - Use case change for variable names, start with lowercase, and avoid
     underscores.
   - Start method names with capitals.
   - Use the `ITK_TRY_EXPECT_NO_EXCEPTION` for statements that might
     trigger exceptions.
   - Print a message to mark the end of the test.
   - Descriptive error messages

Co-authored-by: Philip Cook <cookpa@pennmedicine.upenn.edu>
Co-authored-by: Jon Haitz Legarreta Gorroño <jon.haitz.legarreta@gmail.com>
  • Loading branch information
2 people authored and dzenanz committed Sep 28, 2021
1 parent 9def6b7 commit 6fc6164
Show file tree
Hide file tree
Showing 5 changed files with 53 additions and 39 deletions.
8 changes: 8 additions & 0 deletions Modules/IO/NIFTI/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,11 @@ itk_add_test(NAME itkNiftiReadWriteDirectionTest
DATA{${ITK_DATA_ROOT}/Input/LPSLabels_nosform.nii.gz}
${ITK_TEST_OUTPUT_DIR}
)

itk_add_test(NAME itkNiftiReadWriteDirectionSmallVoxelTest
COMMAND ITKIONIFTITestDriver itkNiftiReadWriteDirectionTest
DATA{Input/SmallVoxels.nii.gz}
DATA{Input/SmallVoxels_noqform.nii.gz}
DATA{Input/SmallVoxels_nosform.nii.gz}
${ITK_TEST_OUTPUT_DIR}
)
1 change: 1 addition & 0 deletions Modules/IO/NIFTI/test/Input/SmallVoxels.nii.gz.sha512
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
4bb35b1cd7fe4e8919f2fd200d70fb1a35fc4d2becf58e3092677bb6832292602dd6bf5b73cd8c2d85c1afabc4fb39d403d789ddd01b14539dc507c5a0e63e67
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
4c31c4ae8f7ca016996ca59f77d7b6b3db9c8518ffaa56ec64c3a7725a4d444442e78442827e949e2158f50457c6dec35e1061e4d5cd17ac4fc15fbd485575ae
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
d7446020071253470c450a725e9756d29eee3d7b19080275a322a0aa6b394cce106935de6a61e94cbcb4aa30dce046e502801c45d564b81dc195cebe77feceb3
81 changes: 42 additions & 39 deletions Modules/IO/NIFTI/test/itkNiftiReadWriteDirectionTest.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "itkNiftiImageIO.h"
#include "itkImageFileReader.h"
#include "itkImageFileWriter.h"
#include "itkTestingMacros.h"

// debug
#include <map>
Expand All @@ -29,9 +30,10 @@ itkNiftiReadWriteDirectionTest(int ac, char * av[])
{
if (ac < 5)
{
std::cerr << "itkNiftiReadWriteDirectionTest: <ImageWithBoth Q and S forms> <no qform> <no sform> <TestOutputDir>"
<< std::endl;
std::cerr << "5 arguments required, recieved " << ac << std::endl;
std::cerr << "Missing Parameters." << std::endl;
std::cerr << "Usage: " << itkNameOfTestExecutableMacro(av)
<< "imageWithBothQAndSForms imageWithNoQform imageWithNoSform testOutputDir" << std::endl;
std::cerr << "5 arguments required, received " << ac << std::endl;
for (int i = 0; i < ac; ++i)
{
std::cerr << "\t" << i << " : " << av[i] << std::endl;
Expand All @@ -40,75 +42,76 @@ itkNiftiReadWriteDirectionTest(int ac, char * av[])
}

using TestImageType = itk::Image<float, 3>;
auto reader_lambda = [](std::string filename) -> TestImageType::Pointer {
itk::ImageFileReader<TestImageType>::Pointer reader = itk::ImageFileReader<TestImageType>::New();
std::string test(filename);
reader->SetFileName(test);
reader->Update();
return reader->GetOutput();
};
TestImageType::Pointer inputImage = itk::ReadImage<TestImageType>(av[1]);
TestImageType::Pointer inputImageNoQform = itk::ReadImage<TestImageType>(av[2]);
TestImageType::Pointer inputImageNoSform = itk::ReadImage<TestImageType>(av[3]);

using TestImageType = itk::Image<float, 3>;
TestImageType::Pointer LPSLabel = reader_lambda(av[1]);
TestImageType::Pointer LPSLabel_noqform = reader_lambda(av[2]);
TestImageType::Pointer LPSLabel_nosform = reader_lambda(av[3]);

const auto LPSLabel_direction = LPSLabel->GetDirection();
const auto LPSLabel_noqform_direction = LPSLabel_noqform->GetDirection();
const auto LPSLabel_nosform_direction = LPSLabel_nosform->GetDirection();
const auto inputImageDirection = inputImage->GetDirection();
const auto inputImageNoQformDirection = inputImageNoQform->GetDirection();
const auto inputImageNoSformDirection = inputImageNoSform->GetDirection();

// Verify that the sform from LPSLabel being used when reading an image with both sform and qform
// Verify that the sform from InputImage being used when reading an image with both sform and qform
// The sforms should be identical in both cases.
if (LPSLabel_direction != LPSLabel_noqform_direction)
if (inputImageDirection != inputImageNoQformDirection)
{
std::cerr << "Error: direction matrix from the image with sform and qform should be identical to ";
std::cerr << "the matrix from the image with sform only" << std::endl;
std::cerr << "sform and qform image direction:" << std::endl << inputImageDirection << std::endl;
std::cerr << "sform-only image direction:" << std::endl << inputImageNoQformDirection << std::endl;
return EXIT_FAILURE;
}
// Verify that the qform alone is not identical to the sform (due to lossy storage capacity)
// This difference is small, but periodically causes numerical precision errors,
// VERIFY THAT THE DIRECTIONS ARE ACTUALLY DIFFERENT, They are expected to be different due to lossy
// representaiton of the qform.
if (LPSLabel_direction == LPSLabel_nosform_direction)
// representation of the qform.
if (inputImageDirection == inputImageNoSformDirection)
{
std::cerr << "Error: direction matrices from sform and qform should differ because of lossy ";
std::cerr << "storage methods." << std::endl;
std::cerr << "sform and qform image direction:" << std::endl << inputImageDirection << std::endl;
std::cerr << "qform-only image direction:" << std::endl << inputImageNoSformDirection << std::endl;
return EXIT_FAILURE;
}

// Write image that originally had no sform direction representation into a file with both sform and qform
const std::string test_output_dir = av[4];
const std::string test_filename = test_output_dir + "/test_filled_sform.nii.gz";
const std::string testOutputDir = av[4];
const std::string testFilename = testOutputDir + "/test_filled_sform.nii.gz";
itk::ImageFileWriter<TestImageType>::Pointer writer = itk::ImageFileWriter<TestImageType>::New();
writer->SetFileName(test_filename);
writer->SetInput(LPSLabel_nosform);
writer->Update();
ITK_TRY_EXPECT_NO_EXCEPTION(itk::WriteImage(inputImageNoSform, testFilename));


// This time it should read from the newly written "sform" code in the image, which should
// be the same as reading from qform of the original iamge
TestImageType::Pointer reread = reader_lambda(test_filename);
const auto reread_direction = reread->GetDirection();
const auto mdd = reread->GetMetaDataDictionary();
std::string sform_code_from_nifti;
const bool expose_success = itk::ExposeMetaData<std::string>(mdd, "sform_code_name", sform_code_from_nifti);
if (!expose_success || sform_code_from_nifti != "NIFTI_XFORM_SCANNER_ANAT")
// be the same as reading from qform of the original image
TestImageType::Pointer reReadImage = itk::ReadImage<TestImageType>(testFilename);
const auto reReadImageDirection = reReadImage->GetDirection();
const auto mdd = reReadImage->GetMetaDataDictionary();
std::string sformCodeFromNifti;
const bool exposeSuccess = itk::ExposeMetaData<std::string>(mdd, "sform_code_name", sformCodeFromNifti);
if (!exposeSuccess || sformCodeFromNifti != "NIFTI_XFORM_SCANNER_ANAT")
{
std::cerr << "Error: sform not set during writing" << std::endl;
return EXIT_FAILURE;
}
bool is_close_qform_converted = true;
bool isCloseQformConverted = true;
for (int r = 0; r < 3; ++r)
{
for (int c = 0; c < 3; ++c)
{
const double diff = std::fabs(LPSLabel_nosform_direction[r][c] - reread_direction[r][c]);
const double diff = std::fabs(inputImageNoSformDirection[r][c] - reReadImageDirection[r][c]);
if (diff > 1e-8)
{
is_close_qform_converted = false;
isCloseQformConverted = false;
}
}
}
if (!is_close_qform_converted)
if (!isCloseQformConverted)
{
std::cerr << LPSLabel_nosform_direction << " \n\n" << reread_direction << std::endl;
std::cerr << "Error: sform reconstructed from qform is not sufficiently similar to qform" << std::endl;
std::cerr << "qform-only image direction:" << std::endl << inputImageNoSformDirection << std::endl;
std::cerr << "Reconstructed sform image direction:" << std::endl << reReadImageDirection << std::endl;
return EXIT_FAILURE;
}

std::cout << "Test finished." << std::endl;
return EXIT_SUCCESS;
}

0 comments on commit 6fc6164

Please sign in to comment.