diff --git a/nibabel/cmdline/stats.py b/nibabel/cmdline/stats.py new file mode 100644 index 0000000000..91b9f7c104 --- /dev/null +++ b/nibabel/cmdline/stats.py @@ -0,0 +1,45 @@ +#!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, count_nonzero_voxels + + +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, + choices=("mm3", "vox"), help="Preferred output units") + 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: + if opts.units == 'mm3': + computed_volume = mask_volume(from_img) + elif opts.units == 'vox': + computed_volume = count_nonzero_voxels(from_img) + else: + raise ValueError(f'{opts.units} is not a valid unit. Choose "mm3" or "vox".') + print(computed_volume) + return 0 diff --git a/nibabel/cmdline/tests/test_stats.py b/nibabel/cmdline/tests/test_stats.py new file mode 100644 index 0000000000..1ceac90231 --- /dev/null +++ b/nibabel/cmdline/tests/test_stats.py @@ -0,0 +1,36 @@ +#!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. +# +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## + +from io import StringIO +import sys +import numpy as np + +from nibabel.loadsave import save +from nibabel.cmdline.stats import main +from nibabel import Nifti1Image + + +def test_volume(tmpdir, capsys): + mask_data = np.zeros((20, 20, 20), dtype='u1') + mask_data[5:15, 5:15, 5:15] = 1 + img = Nifti1Image(mask_data, np.eye(4)) + + infile = tmpdir / "input.nii" + save(img, infile) + + args = (f"{infile} --Volume") + main(args.split()) + vol_mm3 = capsys.readouterr() + args = (f"{infile} --Volume --units vox") + main(args.split()) + vol_vox = capsys.readouterr() + + assert float(vol_mm3[0]) == 1000.0 + assert int(vol_vox[0]) == 1000 \ No newline at end of file diff --git a/nibabel/imagestats.py b/nibabel/imagestats.py new file mode 100644 index 0000000000..4520d7f612 --- /dev/null +++ b/nibabel/imagestats.py @@ -0,0 +1,66 @@ +# 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 +from nibabel.imageclasses import spatial_axes_first + + +def count_nonzero_voxels(img): + """ + Count number of non-zero voxels + + Parameters + ---------- + img : ``SpatialImage`` + All voxels of the mask should be of value 1, background should have value 0. + + Returns + ------- + count : int + Number of non-zero voxels + + """ + return np.count_nonzero(img.dataobj) + + +def mask_volume(img): + """ Compute volume of mask image. + + 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. + + + Returns + ------- + volume : float + Volume of mask expressed in mm3. + + Examples + -------- + >>> import numpy as np + >>> import nibabel as nb + >>> mask_data = np.zeros((20, 20, 20), dtype='u1') + >>> mask_data[5:15, 5:15, 5:15] = 1 + >>> nb.imagestats.mask_volume(nb.Nifti1Image(mask_data, np.eye(4))) + 1000.0 + """ + if not spatial_axes_first(img): + raise ValueError("Cannot calculate voxel volume for image with unknown spatial axes") + voxel_volume_mm3 = np.prod(img.header.get_zooms()[:3]) + mask_volume_vx = count_nonzero_voxels(img) + mask_volume_mm3 = mask_volume_vx * voxel_volume_mm3 + + return mask_volume_mm3 diff --git a/nibabel/tests/test_imagestats.py b/nibabel/tests/test_imagestats.py new file mode 100644 index 0000000000..e104013ddd --- /dev/null +++ b/nibabel/tests/test_imagestats.py @@ -0,0 +1,28 @@ +# 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 .. import Nifti1Image + + +def test_mask_volume(): + # Test mask volume computation + + mask_data = np.zeros((20, 20, 20), dtype='u1') + mask_data[5:15, 5:15, 5:15] = 1 + img = Nifti1Image(mask_data, np.eye(4)) + + vol_mm3 = imagestats.mask_volume(img) + vol_vox = imagestats.count_nonzero_voxels(img) + + assert vol_mm3 == 1000.0 + assert vol_vox == 1000 diff --git a/setup.cfg b/setup.cfg index c2bf604f94..141a37caec 100644 --- a/setup.cfg +++ b/setup.cfg @@ -73,6 +73,7 @@ console_scripts = nib-ls=nibabel.cmdline.ls:main nib-dicomfs=nibabel.cmdline.dicomfs:main nib-diff=nibabel.cmdline.diff:main + nib-stats=nibabel.cmdline.stats:main nib-nifti-dx=nibabel.cmdline.nifti_dx:main nib-tck2trk=nibabel.cmdline.tck2trk:main nib-trk2tck=nibabel.cmdline.trk2tck:main