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.

39 changes: 39 additions & 0 deletions nibabel/cmdline/stats.py
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
33 changes: 33 additions & 0 deletions nibabel/cmdline/tests/test_stats.py
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


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.

58 changes: 58 additions & 0 deletions nibabel/imagestats.py
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'):
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.

""" 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
Copy link
Member

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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".')
29 changes: 29 additions & 0 deletions nibabel/tests/test_imagestats.py
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
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.