From d1ed8be76a663f8801512c81da29270706aa4f91 Mon Sep 17 00:00:00 2001 From: Paul McCarthy <pauldmccarthy@gmail.com> Date: Thu, 26 Mar 2020 14:43:48 +0000 Subject: [PATCH] ENH: fslstats wrapper. Needs tweaking/testing --- fsl/wrappers/__init__.py | 5 +- fsl/wrappers/fslstats.py | 256 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 259 insertions(+), 2 deletions(-) create mode 100644 fsl/wrappers/fslstats.py diff --git a/fsl/wrappers/__init__.py b/fsl/wrappers/__init__.py index 5e1f61e89..44812bce9 100644 --- a/fsl/wrappers/__init__.py +++ b/fsl/wrappers/__init__.py @@ -37,8 +37,8 @@ instead. Aliases may also be used to provide a more readable interface (e.g. the :func:`.bet` function uses ``mask`` instead of ``m``). -One exception to the above is :class:`.fslmaths`, which provides a more -object-oriented interface:: +Two exceptions to the above are :class:`.fslmaths` and :class:`.fslstats`, +which provide a more object-oriented interface:: from fsl.wrappers import fslmaths fslmaths('image.nii').mas('mask.nii').bin().run('output.nii') @@ -95,6 +95,7 @@ from .fnirt import (fnirt, # noqa invwarp, convertwarp) from .fslmaths import (fslmaths,) # noqa +from .fslstats import (fslstats,) # noqa from .fugue import (fugue, # noqa prelude, sigloss) diff --git a/fsl/wrappers/fslstats.py b/fsl/wrappers/fslstats.py new file mode 100644 index 000000000..77479a2ab --- /dev/null +++ b/fsl/wrappers/fslstats.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python +# +# fslstats.py - Wrapper for fslstats +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# +"""This module provides the :class:`fslstats` class, which acts as a wrapper +for the ``fslstats`` command-line tool. + + +.. warning:: This wrapper function will only work with FSL 6.0.2 or newer. +""" + + +import six +import functools as ft + +import fsl.data.image as fslimage +from . import wrapperutils as wutils + + +OPTIONS = { + 'robust_minmax' : 'r', + 'minmax' : 'R', + 'mean_entropy' : 'e', + 'mean_entropy_nz' : 'E', + 'volume' : 'v', + 'volume_nz' : 'V', + 'mean' : 'm', + 'mean_nz' : 'M', + 'stddev' : 's', + 'stddev_nz' : 'S', + 'smallest_roi' : 'w', + 'max_vox' : 'x', + 'min_vox' : 'X', + 'cog_mm' : 'c', + 'cog_vox' : 'C', + 'abs' : 'a', + 'zero_naninf' : 'n', +} +"""This dict contains options which do not require any additional arguments. +They are set via attribute access on the ``fslstats`` object. +""" + + +ARG_OPTIONS = { + 'lower_threshold' : 'l', + 'upper_threshold' : 'u', + 'percentile' : 'p', + 'percentile_nz' : 'P', + 'mask' : 'k', + 'diff' : 'd', + 'hist' : 'h', + 'hist_bounded' : 'H', +} +"""This dict contains options which require additional arguments. +They are set via method calls on the ``fslstats`` object (with the +additional arguments passed into the method call). +""" + + +# add {shortopt : shortopt} mappings +# for all options to simplify code in +# the fslstats class +OPTIONS .update({v : v for v in OPTIONS .values()}) +ARG_OPTIONS.update({v : v for v in ARG_OPTIONS.values()}) + + +class fslstats(object): + """The ``fslstats`` class is a wrapper around the ``fslstats`` command-line + tool. It provides an object-oriented interface - options are specified by + chaining method calls and attribute accesses together. + + + .. warning:: This wrapper function will only work with FSL 6.0.2 or newer, + due to bugs in ``fslstats`` output formatting that are + present in older versions. + + + Any ``fslstats`` command-line option which does not require any arguments + (e.g. ``-r``) can be set by accessing an attribute on a ``fslstats`` + object, e.g.:: + + stats = fslstats('image.nii.gz') + stats.r + + + ``fslstats`` command-line options which do require additional arguments + (e.g. ``-k``) can be set by calling a method on an ``fslstats`` object, + e.g.:: + + stats = fslstats('image.nii.gz') + stats.k('mask.nii.gz') + + + The ``fslstats`` command can be executed via the :meth:`run` method. + Normally, the results will be returned as a list of floating point + numbers. Pre-options will affect the structure of the return value - see + :meth:`__init__` for details. + + + Attribute and method calls can be chained together, so a complete + ``fslstats`` call can be performed in a single line, e.g.:: + + imgmin, imgmax = fslstats('image.nii.gz').k('mask.nii.gz').r.run() + """ + + + def __init__(self, + input, + t=False, + K=None, + sep_volumes=False, + index_mask=None): + """Create a ``fslstats`` object. + + If one of the ``t`` or ``K`` pre-options is set, e.g.:: + + fslstats('image_4d.nii.gz', t=True) + + or:: + + fslstats('image_4d.nii.gz', K='mask.nii.gz') + + then the value returned by :meth:`run` will contain a list-of-lists, + with each child list containing the results: + + - for each 3D volume contained within the input image (if ``t`` + is set), or + - for each sub-mask contained within the mask image (if ``K`` + is set) + + + If both of the ``t`` and ``K`` pre-options are set, e.g.:: + + fslstats('image_4d.nii.gz', t=True, K='mask.nii.gz') + + then the result will be a list-of-lists-of-lists, where each child + list corresponds to each 3D volume (``t``), and each grand-child list + corresponds to each sub-mask (``K``). + + + :arg input: Input image - either a file name, or an + :class:`.Image` object, or a ``nibabel.Nifti1Image`` + object. + :arg t: Produce separate results for each 3D volume in the + input image. + :arg K: Produce separate results for each sub-mask within + the provided mask image. + :arg sep_volumes: Alias for ``t``. + :arg index_mask: Alias for ``K``. + """ + + if t is None: t = sep_volumes + if K is None: K = index_mask + + self.__input = input + self.__options = [] + + # pre-options must be supplied + # before input image + if t: self.__options.append( '-t') + if K is not None: self.__options.extend(('-K', K)) + + self.__options.append(input) + + + def __getattr__(self, name): + """Intercepts attribute accesses,... + """ + + # options which take no args + # are called as attributes + if name in OPTIONS: + flag = OPTIONS[name] + args = False + + # options which take args + # are called as methods + elif name in ARG_OPTIONS: + flag = ARG_OPTIONS[name] + args = True + else: + raise AttributeError() + + addFlag = ft.partial(self.__addFlag, flag) + + if args: return addFlag + else: return addFlag() + + + def __addFlag(self, flag, *args): + """Used by :meth:`__getattr__`. Add the given flag and any arguments to + the accumulated list of command-line options. + """ + self.__options.extend(('-' + flag,) + args) + return self + + + def run(self): + """Run the ``fslstats`` command-line tool. The results are returned as + floating point numbers. + """ + + result = self.__run('fslstats', *self.__options, log=None) + result = result.stdout[0].strip() + result = [line.split() for line in result.split('\n')] + + sepvols = '-t' in self.__options + lblmask = '-K' in self.__options + + # This parsing logic will not work with + # versions of fslstats prior to fsl 6.0.2, + # due to a quirk in the output formatting + # of older versions. + + # One line of output for each volume and + # for each label (with volume the slowest + # changing). + if sepvols and lblmask: + + # One line of output for + # each volume and label + result = [[float(v) for v in line] for line in result] + + # We need know the number of volumes + # (or the number of labels) in order + # to know how to nest the results. + img = fslimage.Image(self.__input, loadData=False) + + if img.ndim >= 4: nvols = img.shape[3] + else: nvols = 1 + + # split the result up into + # nlbls lines for each volume + nlbls = len(result) / nvols + offsets = range(0, nvols * nlbls, nlbls) + result = [result[off:off + nlbls] for off in offsets] + + # One line of output + # for each volume/label + elif sepvols or lblmask: + result = [[float(v) for v in line] for line in result] + + # One line of output + else: + result = [float(v) for v in result[0]] + + return result + + + @wutils.fileOrImage() + @wutils.fslwrapper + def __run(self, *cmd): + """Run the given ``fslmaths`` command. """ + return [str(c) for c in cmd] -- GitLab