-
Notifications
You must be signed in to change notification settings - Fork 52
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
ENH: Add nibabel-based split and merge interfaces #489
ENH: Add nibabel-based split and merge interfaces #489
Conversation
Hello @dPys, Thank you for updating! Cheers! There are no style issues detected in this Pull Request. 🍻 To test for issues locally, Comment last updated at 2020-04-10 22:47:03 UTC |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good! Left a few comments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'd be great to get this pushed in sometime soon :)
BTW, in the interest of your time, I don't mind taking over the thrust in this PR, @dPys - let me know if you are fine with me editing your code |
Go for it @oesteban :-) |
@effigies, should # Test splitting ANTs warpfields
data = np.ones((20, 20, 20, 1, 3), dtype=float)
in_file = tmp_path / 'warpfield.nii.gz'
nb.Nifti1Image(data, np.eye(4), None).to_filename(str(in_file))
> split = SplitSeries(in_file=str(in_file)).run()
/home/travis/build/nipreps/niworkflows/niworkflows/interfaces/tests/test_nibabel.py:124:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/tmp/venv/lib/python3.6/site-packages/nipype/interfaces/base/core.py:397: in run
runtime = self._run_interface(runtime)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <niworkflows.interfaces.nibabel.SplitSeries object at 0x7fd69a6c6cc0>
runtime = Bunch(cwd='/tmp/pytest-of-travis/pytest-0/test_SplitSeries0', duration=0.003802, endTime='2020-04-08T20:10:06.709906',...s 5D (20x20x20x1x3).\nAn exception of type RuntimeError occurred while running interface SplitSeries.',), version=None)
def _run_interface(self, runtime):
filenii = nb.squeeze_image(nb.load(self.inputs.in_file))
ndim = filenii.dataobj.ndim
if ndim != 4:
if self.inputs.accept_3D and ndim == 3:
out_file = str(
Path(fname_presuffix(self.inputs.in_file, suffix=f"_idx-000")).absolute()
)
self._results['out_files'] = out_file
filenii.to_filename(out_file)
return runtime
raise RuntimeError(
> f"Input image image is {ndim}D ({'x'.join(['%d' % s for s in filenii.shape])}).")
E RuntimeError: Input image image is 5D (20x20x20x1x3).
/home/travis/build/nipreps/niworkflows/niworkflows/interfaces/nibabel.py:123: RuntimeError |
It removes final 1-dimensions. Check the docstring. |
Co-Authored-By: Chris Markiewicz <[email protected]>
Co-Authored-By: Chris Markiewicz <[email protected]>
Co-Authored-By: Chris Markiewicz <[email protected]>
Co-Authored-By: Chris Markiewicz <[email protected]>
Co-Authored-By: Chris Markiewicz <[email protected]>
Co-Authored-By: Chris Markiewicz <[email protected]>
Co-Authored-By: Oscar Esteban <[email protected]>
Co-Authored-By: Oscar Esteban <[email protected]>
Co-Authored-By: Oscar Esteban <[email protected]>
Co-Authored-By: Oscar Esteban <[email protected]>
Co-Authored-By: Oscar Esteban <[email protected]>
Co-Authored-By: Oscar Esteban <[email protected]>
acd9b67
to
0ae5351
Compare
Codecov Report
@@ Coverage Diff @@
## master #489 +/- ##
==========================================
+ Coverage 49.46% 58.89% +9.43%
==========================================
Files 42 43 +1
Lines 5093 5603 +510
Branches 741 883 +142
==========================================
+ Hits 2519 3300 +781
+ Misses 2473 2144 -329
- Partials 101 159 +58
Continue to review full report at Codecov.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, I think this is complete. I have beefed up the tests and addressed some minor things here and there.
niworkflows/interfaces/nibabel.py
Outdated
filenii = nb.squeeze_image(nb.load(self.inputs.in_file)) | ||
filenii = filenii.__class__( | ||
np.squeeze(filenii.dataobj), filenii.affine, filenii.header | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason that non-terminal 1-dimensions are left in place by squeeze_image
is to preserve the meaning of each dimension. For example, if I have a time series of a single slice (64, 64, 1, 48)
, squeeze_image
will preserve the meaning of i, j, k, n
, but np.squeeze
will recast n
as k
.
What's the use case that you're taking care of here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the use case I'm contemplating is separating out the three components of deformation fields and other model-based nonlinear transforms. There is one example of this in the tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Found it. I guess I would think splitting along the fifth dimension is a different task from splitting along the fourth, but I see that it's convenient to have a single interface. I'm not sure that risking dropping meaningful dimensions is a good idea.
What about forcing to 4D like:
extra_dims = tuple(dim for dim in img.shape[3:] if dim > 1) or (1,)
if len(extra_dims) != 1:
raise ValueError("Invalid shape")
img = img.__class__(img.dataobj.reshape(img.shape[:3] + extra_dims),
img.affine, img.header)
This coerces a 3D image to (x, y, z, 1) and a 4+D image to (x, y, z, n) assuming that dimensions 4-7 are all 1, n or absent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it guaranteed that the spatial dimensions will not be affected on that reshape? If so, I'm down with this solution.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Trivial dimensions have no effect on indexing, whether it's C- or Fortran ordered. If you don't change the order, it's fine.
Co-Authored-By: Chris Markiewicz <[email protected]>
niworkflows/interfaces/nibabel.py
Outdated
) | ||
ndim = filenii.dataobj.ndim | ||
if ndim != 4: | ||
if self.inputs.allow_3D and ndim == 3: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is odd, as the above will coerce a valid 4D (x, y, z, 1)
image to 3D (x, y, z)
, requiring you to then allow_3D
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is that odd? It is indeed a 3D volume, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for instance, there are a fair number of T1w images in OpenNeuro with (x, y, z, 1) dimensions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is a 4D series. You should be able to split it into one 3D volume without special casing it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess the above snippet you wrote would make this particular use-case a standard one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would say checking for 3D volumes should happen before reshaping the image.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A full enumeration of input shapes and expected outputs for each interface would be a good idea. I feel like this is going to have annoying edge cases and we're not testing them.
Some cases for split:
- Spatial dimensions of length 1
- Non-spatial dimensions of length 1 with and without
allow_3d
- Multiple non-spatial dimensions of length >1
- Various non-spatial dimensions of length >1 with other non-spatial dimensions of length 1
Merge I think should be simpler to come up with cases for (more invalid cases), but making sure we understand the dynamics is a good idea with so much squeezing going on.
I'm also not really sure that these are "nibabel-based" at this point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A full enumeration of input shapes and expected outputs for each interface would be a good idea. I feel like this is going to have annoying edge cases and we're not testing them.
Some cases for split:
Spatial dimensions of length 1
Non-spatial dimensions of length 1 with and without allow_3d
Multiple non-spatial dimensions of length >1
Various non-spatial dimensions of length >1 with other non-spatial dimensions of length 1
Merge I think should be simpler to come up with cases for (more invalid cases), but making sure we understand the dynamics is a good idea with so much squeezing going on.
How much of this you think would be left uncovered after I insert your snippet for the split?
I'm also not really sure that these are "nibabel-based" at this point.
Sure, is there any practical consequence of this? Happy to change the name of the module to something broader. However, the ApplyMask and Binarize will need to maintain some aliases in that case for a few releases.
niworkflows/interfaces/nibabel.py
Outdated
filenii = nb.squeeze_image(nb.load(self.inputs.in_file)) | ||
filenii = filenii.__class__( | ||
np.squeeze(filenii.dataobj), filenii.affine, filenii.header | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it guaranteed that the spatial dimensions will not be affected on that reshape? If so, I'm down with this solution.
niworkflows/interfaces/nibabel.py
Outdated
) | ||
ndim = filenii.dataobj.ndim | ||
if ndim != 4: | ||
if self.inputs.allow_3D and ndim == 3: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess the above snippet you wrote would make this particular use-case a standard one?
I think it should work fine, but the point was more that we should probably do this in a test-driven way.
No. I actually meant to delete it. I was going to say "If the idea is to be making nibabel functions available as interfaces, we've definitely strayed from that goal." But then I decided it didn't matter and failed to remove it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your patience. This LGTM.
Addresses an oversight in #489 .
per suggestion here