From e4d80361da55b6f2da21e96788057ae8e25ebb75 Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauldmccarthy@gmail.com>
Date: Tue, 28 Nov 2023 13:10:40 +0000
Subject: [PATCH] MNT: Add all options to fslmaths, and minor clean-up

---
 fsl/wrappers/fslmaths.py | 519 +++++++++++++++++++++++++++++----------
 1 file changed, 385 insertions(+), 134 deletions(-)

diff --git a/fsl/wrappers/fslmaths.py b/fsl/wrappers/fslmaths.py
index b6f160107..7e345bc1c 100644
--- a/fsl/wrappers/fslmaths.py
+++ b/fsl/wrappers/fslmaths.py
@@ -44,185 +44,436 @@ class fslmaths:
         if dt is not None:
             self.__args.extend(('-dt',  dt))
 
-    def abs(self):
-        """Absolute value."""
-        self.__args.append("-abs")
-        return self
+    def addargs(func):
+        """Decorator used by fslmaths methods. Allows them to just return
+        a list of arguments to add to the command invocation.
+        """
+        def wrapper(self, *args, **kwargs):
+            args = func(self, *args, **kwargs)
+            self.__args.extend(args)
+            return self
+        return wrapper
 
-    def bin(self):
-        """Use (current image>0) to binarise."""
-        self.__args.append("-bin")
-        return self
+    # Binary operations
 
-    def binv(self):
-        """Binarise and invert (binarisation and logical inversion)."""
-        self.__args.append("-binv")
-        return self
+    @addargs
+    def add(self, image):
+        """Add input to current image."""
+        return ['-add', image]
+
+    @addargs
+    def sub(self, image):
+        """Subtract image from current image."""
+        return ["-sub", image]
+
+    @addargs
+    def mul(self, image):
+        """Multiply current image by image."""
+        return ["-mul", image]
+
+    @addargs
+    def div(self, image):
+        """Divide current image by image."""
+        return ["-div", image]
+
+    @addargs
+    def rem(self, image):
+        """Divide current image by following image and take remainder."""
+        return ["-rem", image]
+
+    @addargs
+    def mas(self, image):
+        """Use image (>0) to mask current image."""
+        return ["-mas", image]
+
+    @addargs
+    def thr(self, image):
+        """threshold below the following number (zero anything below the
+        number)"""
+        return ["-thr", image]
+
+    @addargs
+    def thrp(self, perc):
+        """threshold below the following percentage (0-100) of ROBUST RANGE"""
+        return ["-thrp", perc]
+
+    @addargs
+    def thrP(self, perc):
+        """threshold below the following percentage (0-100) of the positive
+        voxels' ROBUST RANGE"""
+        return ["-thrP", perc]
+
+    @addargs
+    def uthr(self, image):
+        """use image number to upper-threshold current image (zero
+        anything above the number)."""
+        return ["-uthr", image]
+
+    @addargs
+    def uthrp(self, perc):
+        """upper-threshold above the following percentage (0-100) of the
+        ROBUST RANGE"""
+        return ["-uthrp", perc]
+
+    @addargs
+    def uthrP(self, perc):
+        """upper-threshold above the following percentage (0-100) of the
+        positive voxels' ROBUST RANGE"""
+        return ["-uthrP", perc]
+
+    @addargs
+    def max(self, image):
+        """take maximum of following input and current image."""
+        return ["-max", image]
 
+    @addargs
+    def min(self, image):
+        """take minimum of following input and current image."""
+        return ["-min", image]
+
+    @addargs
+    def seed(self, seed):
+        """seed random number generator with following number"""
+        return ['-seed', seed]
+
+    @addargs
+    def restart(self, image):
+        """replace the current image with input for future processing
+        operations"""
+        return ['-restart', image]
+
+    @addargs
+    def save(self, filename):
+        """save the current working image to the input filename"""
+        return ['-save', filename]
+
+    # Basic unary operations
+
+    @addargs
+    def exp(self):
+        """exponential"""
+        return ["-exp"]
+
+    @addargs
+    def log(self):
+        """Natural logarithm."""
+        return ["-log"]
+
+    @addargs
+    def sin(self):
+        """sine function"""
+        return ["-sin"]
+
+    @addargs
+    def cos(self):
+        """cosine function"""
+        return ["-cos"]
+
+    @addargs
+    def tan(self):
+        """tangent function"""
+        return ["-tan"]
+
+    @addargs
+    def asin(self):
+        """arc sine function"""
+        return ["-asin"]
+
+    @addargs
+    def acos(self):
+        """arc cosine function"""
+        return ["-acos"]
+
+    @addargs
+    def atan(self):
+        """arc tangent function"""
+        return ["-atan"]
+
+    @addargs
     def sqr(self):
         """Square."""
-        self.__args.append("-sqr")
-        return self
+        return ["-sqr"]
 
+    @addargs
     def sqrt(self):
         """Square root."""
-        self.__args.append("-sqrt")
-        return self
-
-    def log(self):
-        """Natural logarithm."""
-        self.__args.append("-log")
-        return self
+        return ["-sqrt"]
 
+    @addargs
     def recip(self):
         """Reciprocal (1/current image)."""
-        self.__args.append("-recip")
-        return self
-
-    def range(self):
-        """Set the output calmin/max to full data range."""
-        self.__args.append("-range")
-        return self
-
-    def Tmean(self):
-        """Mean across time."""
-        self.__args.append("-Tmean")
-        return self
+        return ["-recip"]
 
-    def Tstd(self):
-        """Standard deviation across time."""
-        self.__args.append("-Tstd")
-        return self
+    @addargs
+    def abs(self):
+        """Absolute value."""
+        return ["-abs"]
 
-    def Tmin(self):
-        """Min across time."""
-        self.__args.append("-Tmin")
-        return self
+    @addargs
+    def bin(self):
+        """Use (current image>0) to binarise."""
+        return ["-bin"]
 
-    def Tmax(self):
-        """Max across time."""
-        self.__args.append("-Tmax")
-        return self
+    @addargs
+    def binv(self):
+        """Binarise and invert (binarisation and logical inversion)."""
+        return ["-binv"]
 
+    @addargs
     def fillh(self):
         """fill holes in a binary mask (holes are internal - i.e. do not touch
         the edge of the FOV)."""
-        self.__args.append("-fillh")
-        return self
+        return ["-fillh"]
+
+    @addargs
+    def fillh26(self):
+        """fill holes using 26 connectivity"""
+        return ["-fillh26"]
+
+    @addargs
+    def index(self):
+        """replace each nonzero voxel with a unique (subject to wrapping) index
+        number"""
+        return ["-index"]
+
+    @addargs
+    def grid(self, value, spacing):
+        """add a 3D grid of intensity <value> with grid spacing <spacing>"""
+        return ['-grid', value, spacing]
+
+    @addargs
+    def edge(self):
+        """edge strength"""
+        return ["-edge"]
+
+    @addargs
+    def dog_edge(self, sigma1, sigma2):
+        """difference of gaussians edge filter. Typical sigma1 is 1.0 and
+        sigma2 is 1.6
+        """
+        return ['-dog_edge', sigma1, sigma2]
 
-    def ero(self, repeat=1):
-        """Erode by zeroing non-zero voxels when zero voxels in kernel."""
-        for i in range(repeat):
-            self.__args.append("-ero")
-        return self
+    @addargs
+    def tfce(self, h, e, connectivity):
+        """enhance with TFCE, e.g. -tfce 2 0.5 6 (maybe change 6 to 26 for
+        skeletons)
+        """
+        return ['-tfce', h, e, connectivity]
+
+    @addargs
+    def tfceS(self, h, e, connectivity, x, y, z, tfce_thresh):
+        """show support area for voxel (X,Y,Z)"""
+        return ['-tfceS', h, e, connectivity, x, y, z, tfce_thresh]
+
+    @addargs
+    def nan(self):
+        """replace NaNs (improper numbers) with 0"""
+        return ["-nan"]
+
+    @addargs
+    def nanm(self):
+        """make NaN (improper number) mask with 1 for NaN voxels, 0 otherwise"""
+        return ["-nanm"]
+
+    @addargs
+    def rand(self):
+        """add uniform noise (range 0:1)"""
+        return ["-rand"]
+
+    @addargs
+    def randn(self):
+        """add Gaussian noise (mean=0 sigma=1)"""
+        return ["-randn"]
+
+    @addargs
+    def inm(self, image):
+        """Intensity normalisation (per 3D volume mean)"""
+        return ["-inm", image]
 
+    @addargs
+    def ing(self, image):
+        """intensity normalisation, global 4D mean)"""
+        return ["-ing", image]
+
+    @addargs
+    def range(self):
+        """Set the output calmin/max to full data range."""
+        return ["-range"]
+
+    # Matrix operations
+
+    @addargs
+    def tensor_decomp(self):
+        """convert a 4D (6-timepoint )tensor image into L1,2,3,FA,MD,MO,V1,2,3
+        (remaining image in pipeline is FA)
+        """
+        return ["-tensor_decomp"]
+
+    @addargs
+    def kernel(self, *args):
+        """Perform a kernel operation"""
+        return ["-kernel"] + list(args)
+
+    # Spatial filtering
+
+    @addargs
     def dilM(self, repeat=1):
         """Mean Dilation of non-zero voxels."""
-        for i in range(repeat):
-            self.__args.append("-dilM")
-        return self
+        return ['-dilM'] * repeat
 
+    @addargs
     def dilD(self, repeat=1):
         """Modal Dilation of non-zero voxels."""
-        for i in range(repeat):
-            self.__args.append("-dilD")
-        return self
+        return ["-dilD"] * repeat
 
+    @addargs
     def dilF(self, repeat=1):
         """Maximum filtering of all voxels."""
-        for i in range(repeat):
-            self.__args.append("-dilF")
-        return self
-
-    def smooth(self, sigma):
-        """Spatial smoothing - mean filtering using a gauss kernel of sigma mm"""
-        self.__args.extend(("-s", sigma))
-        return self
-
-    def add(self, image):
-        """Add input to current image."""
-        self.__args.extend(("-add", image))
-        return self
+        return ["-dilF"] * repeat
 
-    def sub(self, image):
-        """Subtract image from current image."""
-        self.__args.extend(("-sub", image))
-        return self
+    @addargs
+    def dilall(self):
+        """Apply -dilM repeatedly until the entire FOV is covered"""
+        return ["-dilall"]
 
-    def mul(self, image):
-        """Multiply current image by image."""
-        self.__args.extend(("-mul", image))
-        return self
+    @addargs
+    def ero(self, repeat=1):
+        """Erode by zeroing non-zero voxels when zero voxels in kernel."""
+        return ["-ero"] * repeat
 
-    def div(self, image):
-        """Divide current image by image."""
-        self.__args.extend(("-div", image))
-        return self
+    @addargs
+    def eroF(self, repeat=1):
+        """Minimum filtering of all voxels"""
+        return ["-eroF"] * repeat
 
-    def mas(self, image):
-        """Use image (>0) to mask current image."""
-        self.__args.extend(("-mas", image))
-        return self
+    @addargs
+    def fmedian(self):
+        """Median filtering"""
+        return ["-fmedian"]
 
-    def rem(self, image):
-        """Divide current image by following image and take remainder."""
-        self.__args.extend(("-rem", image))
-        return self
+    @addargs
+    def fmean(self):
+        """Mean filtering, kernel weighted, (conventionally used with gauss kernel)"""
+        return ["-fmean"]
 
-    def thr(self, image):
-        """use image number to threshold current image (zero < image)."""
-        self.__args.extend(("-thr", image))
-        return self
+    @addargs
+    def fmeanu(self):
+        """Mean filtering, kernel weighted, un-normalised (gives edge effects)"""
+        return ["-fmeanu"]
 
-    def uthr(self, image):
-        """use image number to upper-threshold current image (zero
-        anything above the number)."""
-        self.__args.extend(("-uthr", image))
-        return self
+    @addargs
+    def s(self, sigma):
+        """Create a gauss kernel of sigma mm and perform mean filtering"""
+        return ["-s", sigma]
 
-    def max(self, image):
-        """take maximum of following input and current image."""
-        self.__args.extend(("-max", image))
-        return self
+    # alias for -s
+    smooth = s
 
-    def min(self, image):
-        """take minimum of following input and current image."""
-        self.__args.extend(("-min", image))
-        return self
+    @addargs
+    def subsamp2(self):
+        """downsamples image by a factor of 2 (keeping new voxels centred on
+        old)"""
+        return ["-subsamp2"]
 
-    def inm(self, image):
-        """Intensity normalisation (per 3D volume mean)"""
-        self.__args.extend(("-inm", image))
-        return self
+    @addargs
+    def subsamp2offc(self):
+        """downsamples image by a factor of 2 (non-centred)"""
+        return ["-subsamp2offc"]
 
-    def bptf(self, hp_sigma, lp_sigma):
-        """Bandpass temporal filtering; nonlinear highpass and Gaussian linear
-        lowpass (with sigmas in volumes, not seconds); set either sigma<0 to
-        skip that filter."""
-        self.__args.extend(("-bptf", hp_sigma, lp_sigma))
-        return self
+    # Dimensionality reduction operations
 
-    def kernel(self, *args):
-        """Perform a kernel operation"""
-        self.__args.extend(["-kernel"] + list(args))
-        return self
+    @addargs
+    def Tmean(self):
+        """Mean across time."""
+        return ["-Tmean"]
 
-    def fmedian(self):
-        """Median filtering"""
-        self.__args.append("-fmedian")
-        return self
+    @addargs
+    def Tstd(self):
+        """Standard deviation across time."""
+        return ["-Tstd"]
 
-    def fmeanu(self):
-        """Mean filtering, kernel weighted, un-normalised (gives edge effects)"""
-        self.__args.append("-fmeanu")
-        return self
+    @addargs
+    def Tmin(self):
+        """Min across time."""
+        return ["-Tmin"]
 
+    @addargs
+    def Tmax(self):
+        """Max across time."""
+        return ["-Tmax"]
+
+    @addargs
+    def Tmaxn(self):
+        """time index of max across time."""
+        return ["-Tmaxn"]
+
+    @addargs
+    def Tmedian(self):
+        """median across time."""
+        return ["-Tmedian"]
+
+    @addargs
+    def Tperc(self, percentage):
+        """nth percentile (0-100) of FULL RANGE across time"""
+        return ["-Tperc", percentage]
+
+    @addargs
+    def Tar1(self):
+        """temporal AR(1) coefficient (use -odt float and probably demean
+        first)"""
+        return ["-Tar1"]
+
+    # Basic statistical operations
+
+    @addargs
+    def pval(self):
+        """Nonparametric uncorrected P-value"""
+        return ['-pval']
+
+    @addargs
+    def pval0(self):
+        """Same as -pval, but treat zeros as missing data"""
+        return ['-pval0']
+
+    @addargs
+    def cpval(self):
+        """Same as -pval, but gives FWE corrected P-values"""
+        return ['-cpval']
+
+    @addargs
+    def ztop(self):
+        """Convert Z-stat to (uncorrected) P"""
+        return ['-ztop']
+
+    @addargs
+    def ptoz(self):
+        """Convert (uncorrected) P to Z"""
+        return ['-ptoz']
+
+    @addargs
+    def rank(self):
+        """Convert (uncorrected) P to Z"""
+        return ['-rank']
+
+    @addargs
+    def ranknorm(self):
+        """Transform to Normal dist via ranks"""
+        return ['-ranknorm']
+
+    # Multi-argument operations
+
+    @addargs
     def roi(self, xmin, xsize, ymin, ysize, zmin, zsize, tmin=0, tsize=-1):
         """Zero outside ROI (using voxel coordinates). """
-        self.__args.extend(('-roi',
-                            xmin, xsize, ymin, ysize,
-                            zmin, zsize, tmin, tsize))
-        return self
+        return ['-roi', xmin, xsize, ymin, ysize,
+                zmin, zsize, tmin, tsize]
+
+    @addargs
+    def bptf(self, hp_sigma, lp_sigma):
+        """Bandpass temporal filtering; nonlinear highpass and Gaussian linear
+        lowpass (with sigmas in volumes, not seconds); set either sigma<0 to
+        skip that filter."""
+        return ["-bptf", hp_sigma, lp_sigma]
 
     def run(self, output=None, odt=None, **kwargs):
         """Save output of operations to image. Set ``output`` to a filename to have
-- 
GitLab