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

NF: volume computation function for nifti masks #952

Merged
merged 9 commits into from
Oct 20, 2020

Conversation

JulianKlug
Copy link
Contributor

Adds a function to compute the volume of a mask image in mm3.
Simple vanilla python equivalent to "fslstats /path/file.nii -V".

Reason: Useful for pure python projects where one can not rely on fsl.

To Do:

  • Add CLI

Compute volume of mask image.
Equivalent to "fslstats /path/file.nii -V"
@JulianKlug
Copy link
Contributor Author

A few questions to help me guide this PR:

  1. Would this be the right location for this function? As I am still new to the source code of nibabel, I was unsure on where to add it.

  2. Where should the CLI for this function be?

@codecov
Copy link

codecov bot commented Sep 9, 2020

Codecov Report

Merging #952 into master will increase coverage by 0.04%.
The diff coverage is 85.29%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #952      +/-   ##
==========================================
+ Coverage   91.84%   91.89%   +0.04%     
==========================================
  Files          97      101       +4     
  Lines       12428    12548     +120     
  Branches     2191     2208      +17     
==========================================
+ Hits        11415    11531     +116     
- Misses        678      680       +2     
- Partials      335      337       +2     
Impacted Files Coverage Δ
nibabel/imagestats.py 83.33% <83.33%> (ø)
nibabel/cmdline/stats.py 86.36% <86.36%> (ø)
nibabel/testing/__init__.py 99.05% <0.00%> (-0.95%) ⬇️
nibabel/loadsave.py 92.59% <0.00%> (ø)
nibabel/deprecator.py 100.00% <0.00%> (ø)
nibabel/cmdline/roi.py 100.00% <0.00%> (ø)
nibabel/conftest.py 100.00% <0.00%> (ø)
nibabel/casting.py 86.32% <0.00%> (+0.17%) ⬆️
... and 1 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update e65b865...35beaba. Read the comment docs.

@0rC0
Copy link

0rC0 commented Sep 10, 2020

I think you've forgot a return :P, something like:

return mask_volume_vx * voxel_volume_mm3

or

return mask_volume_mm3

@effigies
Copy link
Member

If we want to implement something equivalent to fslstats, then I think it might be worth creating nibabel.imagestats (I think nibabel.stats might imply more analytical statistics, which are covered in nilearn) as a place to house the various functions.

The command line should be in nibabel.cmdline.<tool>, and expose the entrypoint nib-<tool>.

@JulianKlug
Copy link
Contributor Author

JulianKlug commented Sep 10, 2020

I think you've forgot a return :P, something like:

@0rC0 actually there is a return on line 248, it is just hidden by the codecov messages.

    return mask_volume_mm3

@JulianKlug
Copy link
Contributor Author

JulianKlug commented Sep 10, 2020

If we want to implement something equivalent to fslstats, then I think it might be worth creating nibabel.imagestats (I think nibabel.stats might imply more analytical statistics, which are covered in nilearn) as a place to house the various functions.

The command line should be in nibabel.cmdline.<tool>, and expose the entrypoint nib-<tool>.

@effigies : Sounds good to me as well. But do you think every element of fslstats should be implemented with a single release or should we implement them gradually?

@effigies
Copy link
Member

I think gradually is fine. Let's call it nib-stats and start with volume calculation.

Also, don't feel compelled to mimic the fslstats flags or output conventions exactly. Clarity is a better goal.

One thing to consider: fslstats produces both voxel and mm^3 results. Do we want to follow that lead, or permit users to say --units {vox,mm3}?

@effigies effigies mentioned this pull request Oct 16, 2020
12 tasks
@pep8speaks
Copy link

pep8speaks commented Oct 18, 2020

Hello @JulianKlug, Thank you for updating!

Cheers! There are no style issues detected in this Pull Request. 🍻 To test for issues locally, pip install flake8 and then run flake8 nibabel.

Comment last updated at 2020-10-19 21:36:20 UTC

- blank lines
- variable declaration in example
@JulianKlug JulianKlug marked this pull request as ready for review October 18, 2020 07:43
@JulianKlug
Copy link
Contributor Author

JulianKlug commented Oct 18, 2020

There seems to be an issue where the example part of the docstring is interpreted as code and not as a string which then lets the tests fail.

    >>> path = 'path/to/nifti/mask.nii'
    >>> img = nf.load(path) # path is contains a path to an example nifti mask
    >>> mask_volume(img)

Not quite sure how to resolve this...

@JulianKlug
Copy link
Contributor Author

would be ready for #960 if the aforementioned issue is resolved

Copy link
Member

@effigies effigies 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 pushing on this. I've left a batch of comments. Feel free to argue and ask questions. :-)

nibabel/cmdline/tests/test_stats.py Outdated Show resolved Hide resolved
nibabel/cmdline/tests/test_stats.py Outdated Show resolved Hide resolved
nibabel/imagestats.py Show resolved Hide resolved
nibabel/imagestats.py Outdated Show resolved Hide resolved
nibabel/imagestats.py Outdated Show resolved Hide resolved
Comment on lines 27 to 28
assert float(vol_mm3) == 2273328656.0
assert float(vol_vox) == 284166082.0
Copy link
Member

Choose a reason for hiding this comment

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

$ fslstats nibabel/tests/data/anatomical.nii -V
33825 270600.000000 

I'm guessing (but haven't checked) the discrepancy is from values > 1.

Copy link
Contributor Author

@JulianKlug JulianKlug Oct 18, 2020

Choose a reason for hiding this comment

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

I thought so too, but hadn't time to check.
The volume computation is only reliable for 0-1 masks, but as I didn't find any in the sample data, I didn’t use one for the test.

nibabel/cmdline/stats.py Outdated Show resolved Hide resolved
nibabel/cmdline/stats.py Outdated Show resolved Hide resolved
bin/nib-stats.py Outdated


if __name__ == '__main__':
main()
Copy link
Member

Choose a reason for hiding this comment

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

These files are actually out-of-date. Just add an entry to the entry points:

nibabel/setup.cfg

Lines 71 to 81 in 917afab

[options.entry_points]
console_scripts =
nib-conform=nibabel.cmdline.conform:main
nib-ls=nibabel.cmdline.ls:main
nib-dicomfs=nibabel.cmdline.dicomfs:main
nib-diff=nibabel.cmdline.diff:main
nib-nifti-dx=nibabel.cmdline.nifti_dx:main
nib-tck2trk=nibabel.cmdline.tck2trk:main
nib-trk2tck=nibabel.cmdline.trk2tck:main
nib-roi=nibabel.cmdline.roi:main
parrec2nii=nibabel.cmdline.parrec2nii:main

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great hadn't seen that!

Copy link
Member

Choose a reason for hiding this comment

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

Let's remove this file.

import numpy as np


def mask_volume(img, units='mm3'):
Copy link
Member

Choose a reason for hiding this comment

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

Thinking about this a bit, if units=="vox", then the output should be integer; there's no good reason to have a float for counting non-zero voxels. That makes the types a bit messy. What if we did (simplified):

def count_nonzero_voxels(img):
    return np.count_nonzero(img.dataobj)

def mask_volume(img):
    nz_vox = count_nonzero_voxels(img)
    return np.prod(img.header.get_zooms()[:3]) * nz_vox

The conditional logic inside this function basically evaporates, and is moved into cmdline.stats.main to select which function to call.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good to me. I have simply left the code a bit more verbose for better readability.

Copy link
Member

@effigies effigies left a comment

Choose a reason for hiding this comment

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

Mostly style suggestions. And please use flake8 to check for any additional issues.

bin/nib-stats.py Outdated


if __name__ == '__main__':
main()
Copy link
Member

Choose a reason for hiding this comment

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

Let's remove this file.

Comment on lines 20 to 28
class Capturing(list):
def __enter__(self):
self._stdout = sys.stdout
sys.stdout = self._stringio = StringIO()
return self
def __exit__(self, *args):
self.extend(self._stringio.getvalue().splitlines())
del self._stringio # free up some memory
sys.stdout = self._stdout
Copy link
Member

Choose a reason for hiding this comment

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

Use the capsys fixture..

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, didn't know about that one

nibabel/funcs.py Outdated
@@ -215,3 +215,4 @@ def _aff_is_diag(aff):
""" Utility function returning True if affine is nearly diagonal """
rzs_aff = aff[:3, :3]
return np.allclose(rzs_aff, np.diag(np.diag(rzs_aff)))

Copy link
Member

Choose a reason for hiding this comment

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

We don't need to touch this file.

nibabel/imagestats.py Show resolved Hide resolved
nibabel/imagestats.py Outdated Show resolved Hide resolved
nibabel/imagestats.py Outdated Show resolved Hide resolved
nibabel/imagestats.py Show resolved Hide resolved
nibabel/tests/test_imagestats.py Outdated Show resolved Hide resolved
@JulianKlug
Copy link
Contributor Author

Sorry for the style issues. Should be fixed now.

@effigies
Copy link
Member

Thanks a lot! This is a good start to a handy utility, and I look forward to seeing it grow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants