-
Notifications
You must be signed in to change notification settings - Fork 258
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
Changes from 3 commits
d7c7069
73bff0f
57f7723
e15bb8d
7412ba9
d357eae
93532de
2331e9d
35beaba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
#!python | ||
# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- | ||
# vi: set ft=python sts=4 ts=4 sw=4 et: | ||
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## | ||
# | ||
# See COPYING file distributed along with the NiBabel package for the | ||
# copyright and license terms. | ||
# | ||
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## | ||
""" | ||
Compute image statistics | ||
""" | ||
|
||
from nibabel.cmdline.stats import main | ||
|
||
|
||
if __name__ == '__main__': | ||
main() | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
#!python | ||
# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- | ||
# vi: set ft=python sts=4 ts=4 sw=4 et: | ||
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## | ||
# | ||
# See COPYING file distributed along with the NiBabel package for the | ||
# copyright and license terms. | ||
# | ||
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## | ||
""" | ||
Compute image statistics | ||
""" | ||
|
||
import argparse | ||
from nibabel.loadsave import load | ||
from nibabel.imagestats import mask_volume | ||
|
||
|
||
def _get_parser(): | ||
"""Return command-line argument parser.""" | ||
p = argparse.ArgumentParser(description=__doc__) | ||
p.add_argument("infile", | ||
help="Neuroimaging volume to compute statistics on.") | ||
p.add_argument("-V", "--Volume", action="store_true", required=False, | ||
help="Compute mask volume of a given mask image.") | ||
p.add_argument("--units", default="mm3", required=False, | ||
help="Preferred output units of {mm3, vox}. Defaults to mm3") | ||
JulianKlug marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return p | ||
|
||
def main(args=None): | ||
"""Main program function.""" | ||
parser = _get_parser() | ||
opts = parser.parse_args(args) | ||
from_img = load(opts.infile) | ||
|
||
if opts.Volume: | ||
computed_volume = mask_volume(from_img, opts.units) | ||
print(computed_volume) | ||
return computed_volume | ||
JulianKlug marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
#!python | ||
# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- | ||
# vi: set ft=python sts=4 ts=4 sw=4 et: | ||
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## | ||
# | ||
# See COPYING file distributed along with the NiBabel package for the | ||
# copyright and license terms. | ||
# | ||
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## | ||
|
||
import unittest | ||
|
||
import pytest | ||
|
||
JulianKlug marked this conversation as resolved.
Show resolved
Hide resolved
|
||
from nibabel.testing import test_data | ||
from nibabel.cmdline.stats import main | ||
from nibabel.optpkg import optional_package | ||
|
||
_, have_scipy, _ = optional_package('scipy.ndimage') | ||
needs_scipy = unittest.skipUnless(have_scipy, 'These tests need scipy') | ||
JulianKlug marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
|
||
def test_volume(): | ||
infile = test_data(fname="anatomical.nii") | ||
args = (f"{infile} --Volume") | ||
vol_mm3 = main(args.split()) | ||
args = (f"{infile} --Volume --units vox") | ||
vol_vox = main(args.split()) | ||
|
||
assert float(vol_mm3) == 2273328656.0 | ||
assert float(vol_vox) == 284166082.0 | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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))) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't need to touch this file. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- | ||
# vi: set ft=python sts=4 ts=4 sw=4 et: | ||
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## | ||
# | ||
# See COPYING file distributed along with the NiBabel package for the | ||
# copyright and license terms. | ||
# | ||
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## | ||
""" | ||
Functions for computing image statistics | ||
""" | ||
|
||
import numpy as np | ||
JulianKlug marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
|
||
def mask_volume(img, units='mm3'): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thinking about this a bit, if 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
""" Compute volume of mask image. | ||
JulianKlug marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Equivalent to "fslstats /path/file.nii -V" | ||
|
||
Parameters | ||
---------- | ||
img : ``SpatialImage`` | ||
All voxels of the mask should be of value 1, background should have value 0. | ||
|
||
units : string {"mm3", "vox"}, optional | ||
Unit of the returned mask volume. Defaults to "mm3". | ||
|
||
Returns | ||
------- | ||
mask_volume_vx: float | ||
Volume of mask expressed in voxels. | ||
|
||
or | ||
|
||
mask_volume_mm3: float | ||
JulianKlug marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Volume of mask expressed in mm3. | ||
|
||
Examples | ||
-------- | ||
>>> import nibabel as nf | ||
>>> path = 'path/to/nifti/mask.nii' | ||
>>> img = nf.load(path) # path is contains a path to an example nifti mask | ||
>>> mask_volume(img) | ||
50.3021 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Examples are tests. This should use an actual image. You can use test data or create one, e.g., >>> mask_data = np.zeros((20, 20, 20), dtype='u1')
>>> mask_data[5:15, 5:15, 5:15] = 1
>>> mask_volume(nb.Nifti1Image(mask_data, np.eye(4))
1000.0 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
""" | ||
header = img.header | ||
_, vx, vy, vz, _, _, _, _ = header['pixdim'] | ||
voxel_volume_mm3 = vx * vy * vz | ||
JulianKlug marked this conversation as resolved.
Show resolved
Hide resolved
|
||
mask = img.get_fdata() | ||
mask_volume_vx = np.sum(mask) | ||
JulianKlug marked this conversation as resolved.
Show resolved
Hide resolved
|
||
mask_volume_mm3 = mask_volume_vx * voxel_volume_mm3 | ||
|
||
if units == 'vox': | ||
return mask_volume_vx | ||
elif units == 'mm3': | ||
return mask_volume_mm3 | ||
else: | ||
raise ValueError(f'{units} is not a valid unit. Choose "mm3" or "vox".') |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- | ||
# vi: set ft=python sts=4 ts=4 sw=4 et: | ||
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## | ||
# | ||
# See COPYING file distributed along with the NiBabel package for the | ||
# copyright and license terms. | ||
# | ||
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## | ||
""" Tests for image statistics """ | ||
|
||
import numpy as np | ||
|
||
from .. import imagestats | ||
from nibabel.testing import test_data | ||
from nibabel.loadsave import load | ||
|
||
import pytest | ||
|
||
JulianKlug marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
def test_mask_volume(): | ||
# Test mask volume computation | ||
infile = test_data(fname="anatomical.nii") | ||
img = load(infile) | ||
vol_mm3 = imagestats.mask_volume(img) | ||
vol_vox = imagestats.mask_volume(img, units='vox') | ||
|
||
assert float(vol_mm3) == 2273328656.0 | ||
assert float(vol_vox) == 284166082.0 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I'm guessing (but haven't checked) the discrepancy is from values > 1. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought so too, but hadn't time to check. |
||
|
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.
These files are actually out-of-date. Just add an entry to the entry points:
nibabel/setup.cfg
Lines 71 to 81 in 917afab
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.
Great hadn't seen that!
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.
Let's remove this file.