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
18 changes: 18 additions & 0 deletions bin/nib-stats.py
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()
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.

44 changes: 44 additions & 0 deletions nibabel/cmdline/stats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!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
47 changes: 47 additions & 0 deletions nibabel/cmdline/tests/test_stats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!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


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



def test_volume(tmpdir):
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")
with Capturing() as vol_mm3:
main(args.split())
args = (f"{infile} --Volume --units vox")
with Capturing() as vol_vox:
main(args.split())

assert float(vol_mm3[0]) == 1000.0
assert int(vol_vox[0]) == 1000
1 change: 1 addition & 0 deletions nibabel/funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

61 changes: 61 additions & 0 deletions nibabel/imagestats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# 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
from nibabel.imageclasses import spatial_axes_first

def count_nonzero_voxels(img):
"""
Count number of non-zero voxels
JulianKlug marked this conversation as resolved.
Show resolved Hide resolved
Parameters
----------
img : ``SpatialImage``
All voxels of the mask should be of value 1, background should have value 0.

Returns
-------
non zero voxel volume: int
JulianKlug marked this conversation as resolved.
Show resolved Hide resolved
Number of non-zero voxels

"""
return np.count_nonzero(img.dataobj)

def mask_volume(img):
""" 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.


Returns
-------
mask_volume_mm3: float
JulianKlug marked this conversation as resolved.
Show resolved Hide resolved
Volume of mask expressed in mm3.

Examples
--------
>>> 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
31 changes: 31 additions & 0 deletions nibabel/tests/test_imagestats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# 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

import pytest

JulianKlug marked this conversation as resolved.
Show resolved Hide resolved

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

1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down