Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BUG: Fix matrix check rejecting NIFTI sform for small voxels #2701

Merged

Conversation

cookpa
Copy link
Contributor

@cookpa cookpa commented Aug 19, 2021

Fixes #2674

NIFTI sform matrices were being called non-invertible if their determinant was <
10-5. Because the determinant scales with the product of the voxel spacings, any
image with voxels smaller than about 20 microns would not have its direction
cosines read from the sform. This caused errors when the header sform was valid,
but the qform transform was not available as a fallback.

Invertibility is now tested by checking the condition number of the singular
value decomposition. If this is above the float epsilon, the matrix will be
considered invertible.

An alternative solution would be to consider a matrix invertible whenever the
determinant is > 0, which is what itkMatrix does. The condition number test as
implemented is a bit more conservative, but should allow spacings down to a
nanometer or less.

PR Checklist

  • No API changes were made (or the changes have been approved)
  • No major design changes were made (or the changes have been approved)
  • Added test (or behavior not changed)
  • Updated API documentation (or API not changed)
  • Added license to new files (if any)
  • Added Python wrapping to new files (if any) as described in ITK Software Guide Section 9.5
  • Added ITK examples for all new major features (if any)

Refer to the ITK Software Guide for
further development details if necessary.

Fixes InsightSoftwareConsortium#2674

NIFTI sform matrices were being called non-invertible if their determinant was <
10-5. Because the determinant scales with the product of the voxel spacings, any
image with voxels smaller than about 20 microns would not have its direction
cosines read from the sform. This caused errors when the header sform was valid,
but the qform transform was not available as a fallback.

Invertibility is now tested by checking the condition number of the singular
value decomposition. If this is above the float epsilon, the matrix will be
considered invertible.

An alternative solution would be to consider a matrix invertible whenever the
determinant is > 0, which is what itkMatrix does. The condition number test as
implemented is a bit more conservative, but should allow spacings down to a
nanometer or less.
@github-actions github-actions bot added area:IO Issues affecting the IO module type:Bug Inconsistencies or issues which will cause an incorrect result under some or all circumstances labels Aug 19, 2021
Copy link
Member

@dzenanz dzenanz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, but it would be good if someone else took a look too. Even better if someone else tried 😄

Copy link
Member

@thewtex thewtex left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cookpa well done. Thanks for the contribution!

@cookpa
Copy link
Contributor Author

cookpa commented Aug 19, 2021

A simple test with some sample images here

https://github.com/cookpa/TestITKNIFTI

Copy link
Member

@hjmjohnson hjmjohnson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a much more comprehensive and well-thought-out condition. Thank you for your contribution!

@hjmjohnson hjmjohnson merged commit 6278c41 into InsightSoftwareConsortium:master Aug 20, 2021
@jhlegarreta
Copy link
Member

I have only loosely followed the issue/PR. Thanks for the effort all. @cookpa wondering whether the test you wrote in the separate repository should be put into ITK/you would be willing to do that.

@cookpa
Copy link
Contributor Author

cookpa commented Aug 21, 2021

I'd be willing to write the test @jhlegarreta , is there an up to date guide on how to do that? I think I would need to upload the data files from that repo, because I had to manually edit the headers to remove the qform.

@jhlegarreta
Copy link
Member

jhlegarreta commented Aug 21, 2021

💯 Thanks @cookpa. The ITK SW Guide contains guidelines (sections 9.4 C.23) to create ITK tests. Essentially:

  • Check if the test file you wrote is needed or else whether existing test files could do the job if the two NIfTI images were provided. If I read well through the downstream repository error reports, the previous ITK was refusing to read the files, so maybe reading the files is enough to test that the bug is and will stay fixed. Thus, maybe it should be decided whether other existing tests are enough. If we would like to check a number of things, e.g. if a header check is desirable, the test will need to compare the values read from the NIfTI file to those expected. Maybe other test files do also a similar thing, but at first sight they don't or they have some hard-coded expected values. So the main.cxx file you wrote should be placed in the Modules/IO/NIFTI/test folder and be renamed to e.g. itkNiftiImageIOReadHeaderTest.cxx, itkNiftiImageIOSmallVoxelSizeSformTest.cxx, itkNiftiImageIOQformFallBackTest.cxx or any other appropriate name. The main function should be named the same without the file extension. And at least three possibilities would exist for the comparison:
    • Adding an additional boolean flag to the test command in the CMakeLists.txt to know in which case a non-zero qform should be expected and in which case it should not.
    • Adding an additional array-like parameter with the expected qform values (given that the rest of the header is exactly the same).
    • Adding a filename parameter, and add two accompanying text files with the header information you printed in your repository so that they are read by the test and compared to what is read by the NIFTI IO object.
  • Use the ITK testing macros (see section 9.4 in the ITK SW Guide) as necessary.
  • Improve the input argument checking as stated in the pointed ITK SW Guide sections (if as mentioned above, the new test file is necessary).
  • Add the test entry/entries to the corresponding CMakeLists.txt. I'd say that two tests will need to be added: one for the image having the qform and the other one for the image whose qform is set to 0, e.g.:
itk_add_test(NAME itkNiftiImageIOSmallVoxelSizeSformTest1
      COMMAND (...)
itk_add_test(NAME itkNiftiImageIOSmallVoxelSizeSformTest2
      COMMAND (...)

each one reading the corresponding NIfTI image.

HTH.

@cookpa
Copy link
Contributor Author

cookpa commented Sep 2, 2021

Thanks @jhlegarreta - I have not forgotten this. I think this existing test code would work, if it was given files with the right headers:

https://github.com/cookpa/ITK/blob/master/Modules/IO/NIFTI/test/itkNiftiReadWriteDirectionTest.cxx

I'll look more into how to do that.

@cookpa cookpa deleted the nifti-sform-invertible branch September 25, 2021 02:25
@gdevenyi
Copy link
Contributor

Are there any plans to merge this into a 5.2.x bugfix? I'm running into this in with a high-resolution 0.3mm isotropic image I'm working with.


const float condition = vnl_matrix_inverse<float>(mat.as_matrix()).well_condition();
// Check matrix is invertible by testing condition number of inverse
if (!(condition > std::numeric_limits<float>::epsilon()))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why ! > instead of <= ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

! > and <= give different results for floating point NaNs. With the former, if the condition is small or is a NaN then ...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right! Pretty subtle though. Something like if (!isfinite(condition) || (condition < epsilon)) might make the intention clearer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@seanm you're right, that is a nicer way to phrase it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ! > approach dates back forever and its purpose is easily recognizable by us oldtimers. If we are nonetheless worried that it is opaque, that could be addressed in a comment.

If we instead go with the newer approach, instead of isfinite we might want to be checking isnan -- unless we are sure that a positive infinity should return false in our current context, or unless we are sure that positive infinity is impossible in our current context.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ! > approach dates back forever and its purpose is easily recognizable by us oldtimers

Maybe I'm an even older timer than you then, because my foggy old memory forgot about this trick. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The well_condition() function returns the ratio of the smallest to largest singular values. I think it was returning nan when both were 0. If there is an infinity something has gone seriously wrong, and the enclosing IsAffine function should also return false in that case.

@seanm
Copy link
Contributor

seanm commented Mar 22, 2022

The commit message is great, but alas my maths are too rusty... @vfonov maybe you'd care to take a look?

@gdevenyi
Copy link
Contributor

Looks like there's still some corner cases here, #3333

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area:IO Issues affecting the IO module type:Bug Inconsistencies or issues which will cause an incorrect result under some or all circumstances
Projects
None yet
Development

Successfully merging this pull request may close these issues.

PR switching from qform to sform causing many downstream support issues, particularly for small voxel sizes
8 participants