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 reading of displacement field vectors from NIFTI #3884

Conversation

lassoan
Copy link
Contributor

@lassoan lassoan commented Jan 21, 2023

NIFTI image containing displacement field (intent = NIFTI_INTENT_DISPVECT) was read as scalar volume (all 3 vector components were set to the same value). Fixed by reading it the same way as the general-purpose vector image (intent = NIFTI_INTENT_VECTOR).

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)

The changes already improve NIFTI compatibility, but we could potentially further improve correctness if we can answer these questions:

  1. Should the displacement vectors be converted from RAS (native NIFTI) to LPS coordinate system (ITK convention)? The same way as the reader changes the image origin, spacing, axis directions to be in LPS coordinate system instead RAS.
  2. NIFTI distinguishes between "vector" and "displacement vector" intent. Is there a way to keep this distinction in ITK? It would be nice if we read a displacement vector image and then we save it to file again to preserve the exact type.

@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 Jan 21, 2023
Comment on lines 1551 to 1555
// TODO: There is no way to tell if it is a displacement vector (NIFTI_INTENT_DISPVECT)
// or some other vector (NIFTI_INTENT_VECTOR), therefore we always use generic vector intent.
this->m_NiftiImage->intent_code = NIFTI_INTENT_VECTOR;
// TODO: consider reorienting vector field values (NIFTI_INTENT_DISPVECT or NIFTI_INTENT_VECTOR)
// from ITK's LPS coordinate system to NIFTI's native RAS coordinate system.
Copy link
Contributor

@N-Dekker N-Dekker Jan 23, 2023

Choose a reason for hiding this comment

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

Thanks @lassoan Would it be an idea to add such information to the MetaDataDictionary of the image? For example:

  // Assume LPS if unspecified.
  EncapsulateMetaData(
    metaDataDictionary, "CoordinateSystem", SpatialOrientationEnums::ValidCoordinateOrientations::ITK_COORDINATE_ORIENTATION_RAS);
  EncapsulateMetaData(metaDataDictionary, "Intent", "DisplacementField");

Copy link
Member

Choose a reason for hiding this comment

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

Of course, the writer then needs to make of the information in MetaDataDictionary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the suggestions!

MetaDataDictionary could work well for reading/writing the intent code. I'll add a commit to this PR to use these dictionary fields.

For the vector reorientation, I see two main options:

  • Option A: ITK always converts to/from RAS if intent is "displacement vector" (does not reorient if intent is just generic "vector"). This would make all ITK-based tools handle "displacement vector" data sets as expected. Helper functions for converting between RAS/LPS coordinate system may be made available to users to manually deal with data sets that use generic "vector" intent for storing spatial data.
  • Option B: Let the user reorient (regardless of "displacement vector" or "vector" intent), based on the MetadataDictionary field. This would be simpler for ITK but most likely users/applications would not be aware that flipping is necessary and they would just work incorrectly.

Would you have any preference?

Copy link
Member

Choose a reason for hiding this comment

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

Based on your description, I prefer A.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think I would prefer option A as well, unless some users would prefer to remain in RAS space...? If option A can be implemented, of course my suggested ("CoordinateSystem", ITK_COORDINATE_ORIENTATION_RAS) dictionary entry would not be necessary anymore. I don't know if an ("Intent", "DisplacementField") dictonary entry would then still be useful either. No problem.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was thinking about the automatic conversion to RAS, too. Slicer currently uses RAS internally, so after ITK reads a displacement field from a NRRD file, Slicer convert all the vectors to RAS. In case of NIFTI it would mean two conversions - RAS->LPS in ITK then LPS->RAS in Slicer. The time needed for conversion is usually not noticeable, so we could implement this simple, always-convert solution, and then if somebody runs into a performance issue then a bypass mechanism could be implemented.

Copy link
Contributor

Choose a reason for hiding this comment

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

The ITK MINC reader needs this LPS/RAS conversion as it currently blindly assumes LPS, which is wrong, see #147

@dzenanz
Copy link
Member

dzenanz commented Jan 23, 2023

@lassoan I didn't deal much with DisplacementFields, but I assume that converting it to LPS coordinate system would be preferable. Or maybe have a setting in NiftiIO which controls it, and defaults to ConvertToLPS?

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.

This PR looks good as is. The discussed improvements could be done in this PR, or a follow up PR.

@lassoan
Copy link
Contributor Author

lassoan commented Jan 23, 2023

I didn't deal much with DisplacementFields, but I assume that converting it to LPS coordinate system would be preferable. Or maybe have a setting in NiftiIO which controls it, and defaults to ConvertToLPS

Yes - see the two options that I described above.

Probably Option A would be more consistent with what ITK does already (automatic conversion of image geometry to LPS).

@lassoan
Copy link
Contributor Author

lassoan commented Jan 23, 2023

Could you recommend a simple and efficient way to rescale vector components with (-1,-1,1) factors to switch between LPS/RAS?

@dzenanz
Copy link
Member

dzenanz commented Jan 23, 2023

MultiplyImageFilter sounds as the right solution.

@lassoan
Copy link
Contributor Author

lassoan commented Jan 23, 2023

Is there an example of using it within a filter?

The class already can do some rescaling but only for scalars. Maybe it could be just modified to allow applying different factors for each vector component?

@N-Dekker
Copy link
Contributor

@lassoan Something like this?

using VectorType = itk::Vector<float, 3>;
using DisplacementFieldImageType = itk::Image<VectorType>;

void
ConvertCoordinates(DisplacementFieldImageType & displacementFieldImage)
{
  for (VectorType & displacementVector : itk::ImageBufferRange<DisplacementFieldImageType>(displacementFieldImage))
  {
    displacementVector[0] *= -1;
    displacementVector[1] *= -1;
  }
}

This range-based for-loop would iterate all the vectors in the displacement field image, and multiply the first and the second vector component "in place" by -1.

@dzenanz
Copy link
Member

dzenanz commented Jan 23, 2023

multiplyimagebyscalar name was given to differentiate it from multiplying pixels of two images. If you replace scalar types by vector types, it might work. If not, you might also need to provide definition for vector multiplication. But the filter should take care of parallelizing the operation. @N-Dekker's suggestion is not parallel.

@gdevenyi
Copy link
Contributor

gdevenyi commented Jan 23, 2023

The ITK MINC reader has similar issues of blindly reading in as LPS without conversion, see #147. I would love to see a generic solution to this as its needed for that IO system as well. @vfonov

@lassoan
Copy link
Contributor Author

lassoan commented Jan 23, 2023

Thanks for all the suggestions I'm still reviewing them to see which ones are the best applicable.

@gdevenyi This is indeed the same kind of issue as for MINC. It would be great to reuse the code that you implemented for MINC, but I find it hard (even if I copy-pasted the code) because the file formats are just too different and the IO classes are already way too complicated. Maybe by adding an adaptor layer outside could address this, but that would mean a much more complicated design, and I think it would only be needed for NIFTI and MINC, so I'm not sure if it is worth the effort.

@jhlegarreta
Copy link
Member

Would adding a test or modifying an existing one to account for this be relevant/doable?

@lassoan lassoan force-pushed the nifti-vector-field-reading-upstream branch from afb65df to 0bb83cc Compare January 25, 2023 14:51
@github-actions github-actions bot added the type:Testing Ensure that the purpose of a class is met/the results on a wide set of test cases are correct label Jan 25, 2023
@lassoan lassoan force-pushed the nifti-vector-field-reading-upstream branch 3 times, most recently from c6cf608 to 21055bd Compare January 25, 2023 15:16
@lassoan
Copy link
Contributor Author

lassoan commented Jan 25, 2023

I've added conversion of vectors between RAS/LPS coordinate systems. By default, conversion is enabled for displacement vector images and disabled for generic vector images. These defaults can be overridden by two member variables in the IO file.

I've also added the possibility of writing displacement vector images, by setting the "intent_code"="1006" in the metadata dictionary. I've kept the magic number stored as string in the metadata dictionary, as the NIFTI IO class already stored intent code like this and I wanted to preserve backward compatibility.

@dzenanz @N-Dekker it would be great if you could review.

@lassoan lassoan force-pushed the nifti-vector-field-reading-upstream branch from 21055bd to a07acb4 Compare January 25, 2023 19:15
@lassoan
Copy link
Contributor Author

lassoan commented Jan 25, 2023

@N-Dekker Thank you very much for the review, I've addressed your comments and force-pushed. Please have a look.

@N-Dekker
Copy link
Contributor

The library code looks fine to me, just two questions on the test!

Copy link
Member

@jhlegarreta jhlegarreta left a comment

Choose a reason for hiding this comment

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

Thanks for doing all this Andras 💯.

An in-line comment about printing m_ConvertRAS.

Modules/IO/NIFTI/include/itkNiftiImageIO.h Show resolved Hide resolved
@lassoan lassoan force-pushed the nifti-vector-field-reading-upstream branch from a07acb4 to cc88c15 Compare January 25, 2023 20:40
@lassoan
Copy link
Contributor Author

lassoan commented Jan 25, 2023

I've force-pushed the requested changes (except adding more tests).

@lassoan lassoan force-pushed the nifti-vector-field-reading-upstream branch from cc88c15 to edd33a9 Compare January 25, 2023 20:43
Copy link
Member

@jhlegarreta jhlegarreta left a comment

Choose a reason for hiding this comment

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

Sorry 🙏 for another review on the documentation Andras.

Modules/IO/NIFTI/src/itkNiftiImageIO.cxx Show resolved Hide resolved
Modules/IO/NIFTI/src/itkNiftiImageIO.cxx Show resolved Hide resolved
Modules/IO/NIFTI/include/itkNiftiImageIO.h Show resolved Hide resolved
@lassoan lassoan force-pushed the nifti-vector-field-reading-upstream branch 2 times, most recently from db14e60 to d52a682 Compare January 25, 2023 21:18
NIFTI image containing displacement field (intent = 1006) was incorrectly read as scalar volume (all 3 vector components were set to the same value).

Fixed by reading NIFTI displacement vector files similarly to general-purpose vector images. For displacement vector files,
vector components are converted between NIFTI file's RAS coordinate system and ITK's LPS coordinate system,
unless this conversion is explicitly disabled by setting ConvertRASDisplacementVectors to false.

An ITK vector image can be written as NIFTI displacement vector file by setting "intent_code"="1006" in the metadata dictionary of the image.
@lassoan lassoan force-pushed the nifti-vector-field-reading-upstream branch from d52a682 to 586da70 Compare January 25, 2023 21:31
Copy link
Contributor

@N-Dekker N-Dekker left a comment

Choose a reason for hiding this comment

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

Thanks for addressing my comments @lassoan. Your pull request looks fine to me now 👍

@dzenanz dzenanz merged commit d007ee8 into InsightSoftwareConsortium:master Jan 26, 2023
@jhlegarreta
Copy link
Member

Valgrind is showing some memory defects related to the NIFTI IO class. Not sure which is the change that triggered them, as they have only started appearing today (Valgrind builds have been green for 3 days after this branch was merged):
https://open.cdash.org/viewDynamicAnalysisFile.php?id=9949304

Valgrind points to
https://github.com/InsightSoftwareConsortium/ITK/blob/master/Modules/IO/NIFTI/src/itkNiftiImageIO.cxx#L708

Which was not modified in this PR in reality.

The commits on Jan 26-28 do not seem to introduce related changes (unsure if #3852 has side-effects).

@jhlegarreta
Copy link
Member

#3884 (comment) Looks like it was a hiccup from Valgrind: no defects have been notified since 3 days ago. The comment can thus be ignored. Sorry for the noise.

@lassoan
Copy link
Contributor Author

lassoan commented Feb 3, 2023

Thanks for the update. I reviewed NIFTI IO class and noticed that the implementation was kind of fragile, because memory allocation happened very far from the place where the buffer is used; but I did not see any specific issue. So, I'm glad it turned out that there was no real failure.

@lassoan lassoan deleted the nifti-vector-field-reading-upstream branch February 3, 2023 14:51
@jhlegarreta
Copy link
Member

#3884 (comment) Thanks for the effort Andras. Maybe something to watch then; if noticed again, maybe we can open an issue.

@blowekamp
Copy link
Member

@jhlegarreta The Valgrind run I saw had many tests failing. This can indicate some type of abnormal execution of the tests. In such a case, the processes may be terminated prematurely ( due to some OS resource limitation encountered) where the memory will not be cleaned up correctly. This can trigger false positives of valgrind defects.

Therefore, if there are abnormal or inconsistent number of tests failing during valgrind then defects should not be considered actionable, just monitored to see if they are consistent.

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 type:Testing Ensure that the purpose of a class is met/the results on a wide set of test cases are correct
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants