From 9f7e26e18edf1201aa4b416ceb5427b14b84e827 Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 9 Aug 2022 16:49:10 -0400 Subject: [PATCH 01/15] remove c-extension: minimal gain and memory leaks --- gradunwarp/core/unwarp_resample.py | 39 ++++++++++++++++-------------- gradunwarp/core/utils.py | 14 ++++++++--- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/gradunwarp/core/unwarp_resample.py b/gradunwarp/core/unwarp_resample.py index 89dbd99..7176e85 100644 --- a/gradunwarp/core/unwarp_resample.py +++ b/gradunwarp/core/unwarp_resample.py @@ -19,6 +19,7 @@ from .globals import siemens_max_det import nibabel as nib import subprocess +import scipy.special #np.seterr(all='raise') @@ -49,7 +50,7 @@ def __init__(self, vol, m_rcs2ras, vendor, coeffs, fileName): self.order = 1 def eval_spharm_grid(self, vendor, coeffs): - ''' + ''' We evaluate the spherical harmonics on a less sampled grid. This is a spacetime vs accuracy tradeoff. ''' @@ -75,7 +76,7 @@ def eval_spharm_grid(self, vendor, coeffs): gvx, gvy, gvz = utils.meshgrid(vec, vec, vec) # mm cf = (fovmax - fovmin) / numpoints - + # deduce the transformation from rcs to grid g_rcs2xyz = np.array( [[0, cf, 0, fovmin], [cf, 0, 0, fovmin], @@ -94,7 +95,7 @@ def eval_spharm_grid(self, vendor, coeffs): log.info('with extents ' + str(fovmin) + 'mm to ' + str(fovmax) + 'mm') gvxyz = CV(gvx, gvy, gvz) _dv, _dxyz = eval_spherical_harmonics(coeffs, vendor, gvxyz) - + return CV(_dv.x, _dv.y, _dv.z), g_xyz2rcs @@ -107,7 +108,7 @@ def run(self): if self.warp: self.polarity = -1. - # Evaluate spherical harmonics on a smaller grid + # Evaluate spherical harmonics on a smaller grid dv, g_xyz2rcs = self.eval_spharm_grid(self.vendor, self.coeffs) # transform RAS-coordinates into LAI-coordinates @@ -146,12 +147,13 @@ def run(self): # through rotation and scaling, where any mirror # will impose a negation ones = CV(1., 1., 1.) - dxyz = utils.transform_coordinates_old(ones, r_rcs2lai) + dxyz = utils.transform_coordinates(ones, r_rcs2lai) # do the nonlinear unwarp if self.vendor == 'siemens': self.out, self.vjacout = self.non_linear_unwarp_siemens(self.vol.shape, dv, dxyz, m_rcs2lai, m_rcs2lai_nohalf, g_xyz2rcs) + del dv, g_xyz2rcs, self.vjacout def non_linear_unwarp_siemens(self, volshape, dv, dxyz, m_rcs2lai, m_rcs2lai_nohalf, g_xyz2rcs): ''' Performs the crux of the unwarping. @@ -185,7 +187,7 @@ def non_linear_unwarp_siemens(self, volshape, dv, dxyz, m_rcs2lai, m_rcs2lai_noh else: vjacdet_lps = eval_siemens_jacobian_mult(dv, dxyz) - # essentially pre-allocating everything + # essentially pre-allocating everything out = np.zeros((nr, nc, ns), dtype=np.float32) fullWarp = np.zeros((nr, nc, ns, 3), dtype=np.float32) @@ -204,7 +206,7 @@ def non_linear_unwarp_siemens(self, volshape, dv, dxyz, m_rcs2lai, m_rcs2lai_noh except ValueError: log.error('Failure during fslval call. Make sure fslval, fslhd, fslorient are in your PATH, and that FSLOUTPUTTYPE is set.') sys.exit(1) - + pixdim2=float((subprocess.Popen(['fslval', self.name,'pixdim2'], stdout=subprocess.PIPE).communicate()[0]).strip()) pixdim3=float((subprocess.Popen(['fslval', self.name,'pixdim3'], stdout=subprocess.PIPE).communicate()[0]).strip()) dim1=float((subprocess.Popen(['fslval', self.name,'dim1'], stdout=subprocess.PIPE).communicate()[0]).strip()) @@ -221,7 +223,7 @@ def non_linear_unwarp_siemens(self, volshape, dv, dxyz, m_rcs2lai, m_rcs2lai_noh [0.0, pixdim2, 0.0, 0.0], [0.0, 0.0, pixdim3, 0.0], [0.0, 0.0, 0.0, 1.0]], dtype=np.float) - + log.info('Unwarping slice by slice') # for every slice for s in range(ns): @@ -231,7 +233,7 @@ def non_linear_unwarp_siemens(self, volshape, dv, dxyz, m_rcs2lai, m_rcs2lai_noh print(s+1, end=' ') else: print('.', end=' ') - + # hopefully, free memory gc.collect() # init to 0 @@ -239,7 +241,7 @@ def non_linear_unwarp_siemens(self, volshape, dv, dxyz, m_rcs2lai, m_rcs2lai_noh dvy.fill(0.) dvz.fill(0.) im_.fill(0.) - + vs = np.ones(vr.shape) * s vrcs = CV(vr, vc, vs) vxyz = utils.transform_coordinates(vrcs, m_rcs2lai_nohalf) @@ -261,7 +263,7 @@ def non_linear_unwarp_siemens(self, volshape, dv, dxyz, m_rcs2lai, m_rcs2lai_noh #dvx.fill(0.) #dvy.fill(0.) #dvz.fill(0.) - + vxyzw = CV(x=vxyz.x + self.polarity * dvx, y=vxyz.y + self.polarity * dvy, z=vxyz.z + self.polarity * dvz) @@ -314,10 +316,11 @@ def non_linear_unwarp_siemens(self, volshape, dv, dxyz, m_rcs2lai, m_rcs2lai_noh out[..., s] = im2 print() - + img=nib.Nifti1Image(fullWarp,self.m_rcs2ras) nib.save(img,"fullWarp_abs.nii.gz") # return image and the jacobian + del vrcsw, vfsl, vxyzw, vrcs, vxyz, vrcsg, fullWarp return out, vjacout def write(self, outfile): @@ -417,16 +420,16 @@ def siemens_B(alpha, beta, x1, y1, z1, R0): f = np.power(r / R0, n) for m in range(0, n + 1): f2 = alpha[n, m] * np.cos(m * phi) + beta[n, m] * np.sin(m * phi) - _ptemp = utils.legendre(n, m, np.cos(theta)) - #_ptemp = scipy.special.lpmv(m, n, np.cos(theta)) + #_ptemp = utils.legendre(n, m, np.cos(theta)) + _p = scipy.special.lpmv(m, n, np.cos(theta)) normfact = 1 # this is Siemens normalization if m > 0: normfact = math.pow(-1, m) * \ math.sqrt(float((2 * n + 1) * factorial(n - m)) \ / float(2 * factorial(n + m))) - _p = normfact * _ptemp - b = b + f * _p * f2 + _p *= normfact + b += f * _p * f2 return b @@ -450,7 +453,7 @@ def ge_D(alpha, beta, x1, y1, z1): for m in range(0, n + 1): f2 = alpha[n, m] * np.cos(m * theta) + beta[n, m] \ * np.sin(m * theta) - _p = utils.legendre(n, m, np.cos(phi)) - d = d + f * _p * f2 + _p = scipy.special.lpmv(m, n, np.cos(phi)) + d += f * _p * f2 d = d / 100.0 # cm back to meters return d diff --git a/gradunwarp/core/utils.py b/gradunwarp/core/utils.py index 76006fd..c1d3312 100644 --- a/gradunwarp/core/utils.py +++ b/gradunwarp/core/utils.py @@ -20,7 +20,7 @@ # this method is deprecated because it's slow and my suspicion that -# the matrix expressions create unnecessary temp matrices which +# the matrix expressions create unnecessary temp matrices which # are costly for huge matrices def transform_coordinates_old(A, M): ''' 4x4 matrix M operates on orthogonal coordinates arrays @@ -34,7 +34,7 @@ def transform_coordinates_old(A, M): B3 = A1 * M[2, 0] + A2 * M[2, 1] + A3 * M[2, 2] + M[2, 3] return CoordsVector(B1, B2, B3) -def transform_coordinates(A, M): +def transform_coordinates_old2(A, M): ''' 4x4 matrix M operates on orthogonal coordinates arrays A1, A2, A3 to give B1, B2, B3 ''' @@ -54,6 +54,12 @@ def transform_coordinates(A, M): B1, B2, B3 = _transform_coordinates(A1, A2, A3, M) return CoordsVector(B1, B2, B3) +from nibabel.affines import apply_affine + +def transform_coordinates(A, M): + vecs = np.asanyarray([A.x,A.y,A.z]).T + vecs_trans = apply_affine(M, vecs) + return CoordsVector(vecs_trans[...,0].T, vecs_trans[...,1].T, vecs_trans[...,2].T) def get_vol_affine(infile): try: @@ -62,7 +68,7 @@ def get_vol_affine(infile): raise ImportError('gradunwarp needs nibabel for I/O of mgz/nifti files.' ' Please install') nibimage = nib.load(infile) - return nibimage.get_data(), nibimage.get_affine() + return nibimage.get_data(), nibimage.affine # memoized factorial @@ -276,7 +282,7 @@ def legendre(nu, mu, x): x = x.astype(np.float32) b = _legendre(nu, mu, x) return b - + def interp3(vol, R, C, S): ''' From 1d6e14196cfc672a24daef4188cc187f0a1966b5 Mon Sep 17 00:00:00 2001 From: cneuromod_bot Date: Wed, 2 Aug 2023 09:03:53 -0400 Subject: [PATCH 02/15] wip:cleanup, changes for numpy upgrade --- gradunwarp/core/unwarp_resample.py | 8 +++++--- gradunwarp/core/utils.py | 2 +- setup.py | 11 +++++------ 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/gradunwarp/core/unwarp_resample.py b/gradunwarp/core/unwarp_resample.py index 7176e85..e747e46 100644 --- a/gradunwarp/core/unwarp_resample.py +++ b/gradunwarp/core/unwarp_resample.py @@ -115,7 +115,7 @@ def run(self): m_ras2lai = np.array([[-1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, -1.0, 0.0], - [0.0, 0.0, 0.0, 1.0]], dtype=np.float) + [0.0, 0.0, 0.0, 1.0]], dtype=np.float64) m_rcs2lai = np.dot(m_ras2lai, self.m_rcs2ras) m_rcs2lai_nohalf = m_rcs2lai[:, :] @@ -217,12 +217,14 @@ def non_linear_unwarp_siemens(self, volshape, dv, dxyz, m_rcs2lai, m_rcs2lai_noh m_vox2fsl = np.array([[-1.0*pixdim1, 0.0, 0.0, pixdim1*(dim1-1)], [0.0, pixdim2, 0.0, 0.0], [0.0, 0.0, pixdim3, 0.0], - [0.0, 0.0, 0.0, 1.0]], dtype=np.float) + [0.0, 0.0, 0.0, 1.0]], dtype=np.float64) else: m_vox2fsl = np.array([[pixdim1, 0.0, 0.0, 0.0], [0.0, pixdim2, 0.0, 0.0], [0.0, 0.0, pixdim3, 0.0], - [0.0, 0.0, 0.0, 1.0]], dtype=np.float) + [0.0, 0.0, 0.0, 1.0]], dtype=np.float64) + print(m_vox2fsl) + print(self.m_rcs2ras) log.info('Unwarping slice by slice') # for every slice diff --git a/gradunwarp/core/utils.py b/gradunwarp/core/utils.py index c1d3312..d1cf1c4 100644 --- a/gradunwarp/core/utils.py +++ b/gradunwarp/core/utils.py @@ -68,7 +68,7 @@ def get_vol_affine(infile): raise ImportError('gradunwarp needs nibabel for I/O of mgz/nifti files.' ' Please install') nibimage = nib.load(infile) - return nibimage.get_data(), nibimage.affine + return nibimage.get_fdata(), nibimage.affine # memoized factorial diff --git a/setup.py b/setup.py index 3631111..c440eda 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,8 @@ from numpy.distutils.core import setup, Extension -from numpy.distutils.misc_util import get_numpy_include_dirs +from numpy.distutils.misc_util import get_numpy_include_dirs import os, sys -mods = ['gradunwarp.core.coeffs', 'gradunwarp.core.globals', +mods = ['gradunwarp.core.coeffs', 'gradunwarp.core.globals', 'gradunwarp.core.__init__', 'gradunwarp.__init__', 'gradunwarp.core.utils', 'gradunwarp.core.unwarp_resample', @@ -32,7 +32,7 @@ extra_compile_args=['-O3']) scripts_cmd = ['gradunwarp/core/gradient_unwarp.py',] - + def configuration(parent_package='', top_path=None): from numpy.distutils.misc_util import Configuration @@ -41,12 +41,11 @@ def configuration(parent_package='', top_path=None): return config setup(name='gradunwarp', - version = 'HCP-1.2.0', + version = '1.3.0', description = 'HCP version of Gradient Unwarping Package for Python/Numpy', author = 'Human Connectome Project', py_modules = mods, - ext_modules = [ext1, ext2, ext3], + #ext_modules = [ext1, ext2, ext3], scripts = scripts_cmd, configuration=configuration, ) - From 2e29dc4db615ea1d79c46d44b2fc32c3045eb471 Mon Sep 17 00:00:00 2001 From: bpinsard Date: Mon, 7 Aug 2023 14:55:38 -0400 Subject: [PATCH 03/15] cleanup wip --- gradunwarp/core/interp3_ext.c | 198 -------------------- gradunwarp/core/legendre_ext.c | 169 ----------------- gradunwarp/core/tests/test_utils.py | 46 ----- gradunwarp/core/transform_coordinates_ext.c | 153 --------------- gradunwarp/core/unwarp_resample.py | 10 +- 5 files changed, 1 insertion(+), 575 deletions(-) delete mode 100644 gradunwarp/core/interp3_ext.c delete mode 100644 gradunwarp/core/legendre_ext.c delete mode 100644 gradunwarp/core/tests/test_utils.py delete mode 100644 gradunwarp/core/transform_coordinates_ext.c diff --git a/gradunwarp/core/interp3_ext.c b/gradunwarp/core/interp3_ext.c deleted file mode 100644 index 9990e3c..0000000 --- a/gradunwarp/core/interp3_ext.c +++ /dev/null @@ -1,198 +0,0 @@ -/* -### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## -# -# See COPYING file distributed along with the gradunwarp package for the -# copyright and license terms. -# -### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## -*/ -#include "Python.h" -#include "numpy/arrayobject.h" -#include - -# define CUBE(x) ((x) * (x) * (x)) -# define SQR(x) ((x) * (x)) - -/* compile command - gcc -shared -pthread -fPIC -fwrapv -O2 -Wall -fno-strict-aliasing -I/usr/include/python2.7 -o interp3_ext.so interp3_ext.c - */ -static PyObject *interp3(PyObject *self, PyObject *args); -float TriCubic (float px, float py, float pz, float *volume, int xDim, int yDim, int zDim); - -// what function are exported -static PyMethodDef tricubicmethods[] = { - {"_interp3", interp3, METH_VARARGS}, - {NULL, NULL} -}; - -// This function is essential for an extension for Numpy created in C -#if PY_MAJOR_VERSION >= 3 - -static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - "interp3_ext", - NULL, - -1, - tricubicmethods, - NULL, - NULL, - NULL, - NULL -}; -#define INITERROR return NULL - -PyObject * -PyInit_interp3_ext(void) - -#else -#define INITERROR return - -void -initinterp3_ext(void) -#endif -{ -#if PY_MAJOR_VERSION >= 3 - PyObject *module = PyModule_Create(&moduledef); -#else - PyObject *module = Py_InitModule("interp3_ext", tricubicmethods); -#endif - - if (module == NULL) - INITERROR; - - import_array(); - -#if PY_MAJOR_VERSION >= 3 - return module; -#endif -} - -// the data should be FLOAT32 and should be ensured in the wrapper -static PyObject *interp3(PyObject *self, PyObject *args) -{ - PyArrayObject *volume, *result, *C, *R, *S; - float *pr, *pc, *ps; - float *pvol, *pvc; - int xdim, ydim, zdim; - - // We expect 4 arguments of the PyArray_Type - if(!PyArg_ParseTuple(args, "O!O!O!O!", - &PyArray_Type, &volume, - &PyArray_Type, &R, - &PyArray_Type, &C, - &PyArray_Type, &S)) return NULL; - - if ( NULL == volume ) return NULL; - if ( NULL == C ) return NULL; - if ( NULL == R ) return NULL; - if ( NULL == S ) return NULL; - - // result matrix is the same size as C and is float - result = (PyArrayObject*) PyArray_ZEROS(PyArray_NDIM(C), C->dimensions, NPY_FLOAT, 0); - // This is for reference counting ( I think ) - PyArray_FLAGS(result) |= NPY_OWNDATA; - - // massive use of iterators to progress through the data - PyArrayIterObject *itr_v, *itr_r, *itr_c, *itr_s; - itr_v = (PyArrayIterObject *) PyArray_IterNew(result); - itr_r = (PyArrayIterObject *) PyArray_IterNew(R); - itr_c = (PyArrayIterObject *) PyArray_IterNew(C); - itr_s = (PyArrayIterObject *) PyArray_IterNew(S); - pvol = (float *)PyArray_DATA(volume); - xdim = PyArray_DIM(volume, 0); - ydim = PyArray_DIM(volume, 1); - zdim = PyArray_DIM(volume, 2); - //printf("%f\n", pvol[4*20*30 + 11*30 + 15]); - while(PyArray_ITER_NOTDONE(itr_v)) - { - pvc = (float *) PyArray_ITER_DATA(itr_v); - pr = (float *) PyArray_ITER_DATA(itr_r); - pc = (float *) PyArray_ITER_DATA(itr_c); - ps = (float *) PyArray_ITER_DATA(itr_s); - // The order is weird because the tricubic code below is - // for Fortran ordering. Note that the xdim changes fast in - // the code, whereas the rightmost dim should change fast - // in C multidimensional arrays. - *pvc = TriCubic(*ps, *pc, *pr, pvol, zdim, ydim, xdim); - PyArray_ITER_NEXT(itr_v); - PyArray_ITER_NEXT(itr_r); - PyArray_ITER_NEXT(itr_c); - PyArray_ITER_NEXT(itr_s); - } - - return result; -} - -/* - * TriCubic - tri-cubic interpolation at point, p. - * inputs: - * px, py, pz - the interpolation point. - * volume - a pointer to the float volume data, stored in x, - * y, then z order (x index increasing fastest). - * xDim, yDim, zDim - dimensions of the array of volume data. - * returns: - * the interpolated value at p. - * note: - * rudimentary range checking is done in this function. - */ - -float TriCubic (float px, float py, float pz, float *volume, int xDim, int yDim, int zDim) -{ - int x, y, z; - int i, j, k; - float dx, dy, dz; - float *pv; - float u[4], v[4], w[4]; - float r[4], q[4]; - float vox = 0; - int xyDim; - - xyDim = xDim * yDim; - - x = (int) px, y = (int) py, z = (int) pz; - // necessary evil truncating at dim-2 because tricubic needs 2 more values - // which is criminal near edges - // future work includes doing trilinear for edge cases - // range checking is extremely important here - if (x < 2 || x > xDim-3 || y < 2 || y > yDim-3 || z < 2 || z > zDim-3) - return (0); - - dx = px - (float) x, dy = py - (float) y, dz = pz - (float) z; - pv = volume + (x - 1) + (y - 1) * xDim + (z - 1) * xyDim; - - /* factors for Catmull-Rom interpolation */ - - u[0] = -0.5 * CUBE (dx) + SQR (dx) - 0.5 * dx; - u[1] = 1.5 * CUBE (dx) - 2.5 * SQR (dx) + 1; - u[2] = -1.5 * CUBE (dx) + 2 * SQR (dx) + 0.5 * dx; - u[3] = 0.5 * CUBE (dx) - 0.5 * SQR (dx); - - v[0] = -0.5 * CUBE (dy) + SQR (dy) - 0.5 * dy; - v[1] = 1.5 * CUBE (dy) - 2.5 * SQR (dy) + 1; - v[2] = -1.5 * CUBE (dy) + 2 * SQR (dy) + 0.5 * dy; - v[3] = 0.5 * CUBE (dy) - 0.5 * SQR (dy); - - w[0] = -0.5 * CUBE (dz) + SQR (dz) - 0.5 * dz; - w[1] = 1.5 * CUBE (dz) - 2.5 * SQR (dz) + 1; - w[2] = -1.5 * CUBE (dz) + 2 * SQR (dz) + 0.5 * dz; - w[3] = 0.5 * CUBE (dz) - 0.5 * SQR (dz); - - for (k = 0; k < 4; k++) - { - q[k] = 0; - for (j = 0; j < 4; j++) - { - r[j] = 0; - for (i = 0; i < 4; i++) - { - r[j] += u[i] * *pv; - pv++; - } - q[k] += v[j] * r[j]; - pv += xDim - 4; - } - vox += w[k] * q[k]; - pv += xyDim - 4 * xDim; - } - return vox; -} diff --git a/gradunwarp/core/legendre_ext.c b/gradunwarp/core/legendre_ext.c deleted file mode 100644 index 372c629..0000000 --- a/gradunwarp/core/legendre_ext.c +++ /dev/null @@ -1,169 +0,0 @@ -/* -### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## -# -# See COPYING file distributed along with the gradunwarp package for the -# copyright and license terms. -# -### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## -*/ -#include "Python.h" -#include "numpy/arrayobject.h" -#include - -/* compile command - gcc -shared -pthread -fPIC -fwrapv -O2 -Wall -fno-strict-aliasing -I/usr/include/python2.7 -o legendre_ext.so legendre_ext.c - */ -static PyObject *legendre(PyObject *self, PyObject *args); -long odd_factorial(int k); - -// what function are exported -static PyMethodDef legendremethods[] = { - {"_legendre", legendre, METH_VARARGS}, - {NULL, NULL} -}; - -// This function is essential for an extension for Numpy created in C -#if PY_MAJOR_VERSION >= 3 - -static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - "legendre_ext", - NULL, - -1, - legendremethods, - NULL, - NULL, - NULL, - NULL -}; - -#define INITERROR return NULL - -PyObject * -PyInit_legendre_ext(void) - -#else -#define INITERROR return - -void -initlegendre_ext(void) -#endif -{ -#if PY_MAJOR_VERSION >= 3 - PyObject *module = PyModule_Create(&moduledef); -#else - PyObject *module = Py_InitModule("legendre_ext", legendremethods); -#endif - - if (module == NULL) - INITERROR; - - import_array(); - -#if PY_MAJOR_VERSION >= 3 - return module; -#endif -} - -// the data should be FLOAT32 and should be ensured in the wrapper -static PyObject *legendre(PyObject *self, PyObject *args) -{ - PyArrayObject *x, *result; - long nu, mu; - - // We expect 1 argument of the PyArray_Type - // and 2 of Python float32 - if(!PyArg_ParseTuple(args, "llO!", - &nu, &mu, - &PyArray_Type, &x)) return NULL; - - if ( NULL == x ) return NULL; - - // Error check - if ( mu < 0.0 || mu > nu ) - { - printf("Error!: require legendre computation to have 0 <= mu <=nu,\n"); - printf("but mu=%d and nu=%d\n", mu, nu); - return NULL; - } - - //result matrix is the same size as x and is float - result = (PyArrayObject*) PyArray_ZEROS(PyArray_NDIM(x), x->dimensions, NPY_FLOAT, 0); - // This is for reference counting ( I think ) - PyArray_FLAGS(result) |= NPY_OWNDATA; - - PyArrayIterObject *itr_x, *itr_r; - int s, n; - float *px, *pr, p_nu, p_nu_prev; - - itr_x = (PyArrayIterObject *) PyArray_IterNew(x); - itr_r = (PyArrayIterObject *) PyArray_IterNew(result); - while(PyArray_ITER_NOTDONE(itr_x)) - { - px = (float *) PyArray_ITER_DATA(itr_x); - // unfortunate error check in the main loop - if ( abs(*px) > 1.0 ) - { - printf("Error! require -1 <= x <= 1 in the legendre computation.\n"); - printf("but got x=%f\n", x); - return NULL; - } - pr = (float *) PyArray_ITER_DATA(itr_r); - - // Compute the initial term in the recursion - if ( mu ) - { - s = 1; - if ( mu & 1) - s = -1; - p_nu = s * odd_factorial(2 * mu - 1) * pow(sqrt( 1.0 - *px * *px), mu); - } - else - p_nu = 1.0; - - // special case.. clear up and return - if ( mu == nu ) - { - *pr = p_nu; - PyArray_ITER_NEXT(itr_x); - PyArray_ITER_NEXT(itr_r); - continue; - } - - // compute the next term in recursion - p_nu_prev = p_nu; - p_nu = *px * (2 * mu + 1) * p_nu; - - // special case.. clear up and return - if ( nu == mu + 1) - { - *pr = p_nu; - PyArray_ITER_NEXT(itr_x); - PyArray_ITER_NEXT(itr_r); - continue; - } - - for(n=mu+2; n= 3) - { - k -= 2; - f *= k; - } - return f; -} diff --git a/gradunwarp/core/tests/test_utils.py b/gradunwarp/core/tests/test_utils.py deleted file mode 100644 index 46161b1..0000000 --- a/gradunwarp/core/tests/test_utils.py +++ /dev/null @@ -1,46 +0,0 @@ -### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## -# -# See COPYING file distributed along with the NiBabel package for the -# copyright and license terms. -# -### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## -'''Test multiple utils''' -import os -import numpy as np -from numpy.testing import assert_equal, assert_array_equal, \ - assert_array_almost_equal, assert_almost_equal -from gradunwarp.core.utils import interp3 -from gradunwarp.core.utils import legendre - - -def test_interp3(): - arr = np.linspace(-4, 4, 6000) - arr = np.sin(arr) - arr = arr.reshape(10, 20, 30).astype('float32') - - # sanity check - ex1 = arr[4, 11, 15] - ac1 = interp3(arr, np.array([4.]), np.array([11.]), np.array([15.])) - assert_almost_equal(ex1, ac1[0], 5) - - ex2 = arr[5, 12, 16] - ac2 = interp3(arr, np.array([5.]), np.array([12.]), np.array([16.])) - assert_almost_equal(ex2, ac2[0], 5) - - ex3 = np.array([-0.33291185, -0.24946867, -0.1595035, - -0.06506295, 0.03180848, 0.12906794, - 0.22467692, 0.31659847, 0.40280011, - 0.48125309]) - gridn = 10 - R1 = np.linspace(4., 5., gridn).astype('float32') - C1 = np.linspace(11., 12., gridn).astype('float32') - S1 = np.linspace(15., 16., gridn).astype('float32') - ac3 = interp3(arr, R1, C1, S1) - assert_array_almost_equal(ex3, ac3) - - -def test_legendre(): - arr = np.array([0.1, 0.2, 0.3]) - ex1 = np.array([44.83644, 75.85031, 82.44417]) - ac1 = legendre(6, 3, arr) - assert_array_almost_equal(ex1, ac1, 5) diff --git a/gradunwarp/core/transform_coordinates_ext.c b/gradunwarp/core/transform_coordinates_ext.c deleted file mode 100644 index 8323f0b..0000000 --- a/gradunwarp/core/transform_coordinates_ext.c +++ /dev/null @@ -1,153 +0,0 @@ -/* -### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## -# -# See COPYING file distributed along with the gradunwarp package for the -# copyright and license terms. -# -### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## -*/ -#include "Python.h" -#include "numpy/arrayobject.h" -#include - -/* compile command - gcc -shared -pthread -fPIC -fwrapv -O2 -Wall -fno-strict-aliasing -I/usr/include/python2.7 -o transform_coordinates_ext.so transform_coordinates_ext.c - */ -static PyObject *transform_coordinates(PyObject *self, PyObject *args); - - -// what function are exported -static PyMethodDef transform_coordinatesmethods[] = { - {"_transform_coordinates", transform_coordinates, METH_VARARGS}, - {NULL, NULL} -}; - - -// This function is essential for an extension for Numpy created in C -#if PY_MAJOR_VERSION >= 3 - -static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - "transform_coordinates_ext", - NULL, - -1, - transform_coordinatesmethods, - NULL, - NULL, - NULL, - NULL -}; - -#define INITERROR return NULL - -PyObject * -PyInit_transform_coordinates_ext(void) - -#else -#define INITERROR return - -void -inittransform_coordinates_ext(void) -#endif -{ -#if PY_MAJOR_VERSION >= 3 - PyObject *module = PyModule_Create(&moduledef); -#else - PyObject *module = Py_InitModule("transform_coordinates_ext", transform_coordinatesmethods); -#endif - - if (module == NULL) - INITERROR; - - import_array(); - -#if PY_MAJOR_VERSION >= 3 - return module; -#endif -} - - -// the data should be FLOAT32 and should be ensured in the wrapper -static PyObject *transform_coordinates(PyObject *self, PyObject *args) -{ - float *m; - float *x, *y, *z, *xm, *ym, *zm; - PyArrayObject *X, *Y, *Z, *mat; - PyArrayObject *Xm, *Ym, *Zm; - // We expect 4 arguments of the PyArray_Type - if(!PyArg_ParseTuple(args, "O!O!O!O!", - &PyArray_Type, &X, - &PyArray_Type, &Y, - &PyArray_Type, &Z, - &PyArray_Type, &mat)) return NULL; - - if ( NULL == X ) return NULL; - if ( NULL == Y ) return NULL; - if ( NULL == Z ) return NULL; - if ( NULL == mat ) return NULL; - - // result matrices are the same size and float - Xm = (PyArrayObject*) PyArray_ZEROS(PyArray_NDIM(X), X->dimensions, NPY_FLOAT, 0); - Ym = (PyArrayObject*) PyArray_ZEROS(PyArray_NDIM(X), X->dimensions, NPY_FLOAT, 0); - Zm = (PyArrayObject*) PyArray_ZEROS(PyArray_NDIM(X), X->dimensions, NPY_FLOAT, 0); - - // This is for reference counting ( I think ) - PyArray_FLAGS(Xm) |= NPY_OWNDATA; - PyArray_FLAGS(Ym) |= NPY_OWNDATA; - PyArray_FLAGS(Zm) |= NPY_OWNDATA; - - // massive use of iterators to progress through the data - PyArrayIterObject *itr_x, *itr_y, *itr_z; - PyArrayIterObject *itr_xm, *itr_ym, *itr_zm; - itr_x = (PyArrayIterObject *) PyArray_IterNew(X); - itr_y = (PyArrayIterObject *) PyArray_IterNew(Y); - itr_z = (PyArrayIterObject *) PyArray_IterNew(Z); - itr_xm = (PyArrayIterObject *) PyArray_IterNew(Xm); - itr_ym = (PyArrayIterObject *) PyArray_IterNew(Ym); - itr_zm = (PyArrayIterObject *) PyArray_IterNew(Zm); - /*m = (float *)PyArray_DATA(mat); - printf("%f %f %f %f\n", m[0], m[1], m[2], m[3]); - printf("%f %f %f %f\n", m[4], m[5], m[6], m[7]); - printf("%f %f %f %f\n", m[8], m[9], m[10], m[11]); - */ - float *m00, *m01, *m02, *m03; - float *m10, *m11, *m12, *m13; - float *m20, *m21, *m22, *m23; - m00 = (float *)PyArray_GETPTR2(mat, 0, 0); - m01 = (float *)PyArray_GETPTR2(mat, 0, 1); - m02 = (float *)PyArray_GETPTR2(mat, 0, 2); - m03 = (float *)PyArray_GETPTR2(mat, 0, 3); - m10 = (float *)PyArray_GETPTR2(mat, 1, 0); - m11 = (float *)PyArray_GETPTR2(mat, 1, 1); - m12 = (float *)PyArray_GETPTR2(mat, 1, 2); - m13 = (float *)PyArray_GETPTR2(mat, 1, 3); - m20 = (float *)PyArray_GETPTR2(mat, 2, 0); - m21 = (float *)PyArray_GETPTR2(mat, 2, 1); - m22 = (float *)PyArray_GETPTR2(mat, 2, 2); - m23 = (float *)PyArray_GETPTR2(mat, 2, 3); - - // start the iteration - while(PyArray_ITER_NOTDONE(itr_x)) - { - x = (float *) PyArray_ITER_DATA(itr_x); - y = (float *) PyArray_ITER_DATA(itr_y); - z = (float *) PyArray_ITER_DATA(itr_z); - xm = (float *) PyArray_ITER_DATA(itr_xm); - ym = (float *) PyArray_ITER_DATA(itr_ym); - zm = (float *) PyArray_ITER_DATA(itr_zm); - - // transform coordinates - *xm = *x * *m00 + *y * *m01 + *z * *m02 + *m03; - *ym = *x * *m10 + *y * *m11 + *z * *m12 + *m13; - *zm = *x * *m20 + *y * *m21 + *z * *m22 + *m23; - - PyArray_ITER_NEXT(itr_x); - PyArray_ITER_NEXT(itr_y); - PyArray_ITER_NEXT(itr_z); - PyArray_ITER_NEXT(itr_xm); - PyArray_ITER_NEXT(itr_ym); - PyArray_ITER_NEXT(itr_zm); - } - - return Py_BuildValue("OOO", Xm, Ym, Zm); -} diff --git a/gradunwarp/core/unwarp_resample.py b/gradunwarp/core/unwarp_resample.py index e747e46..a687153 100644 --- a/gradunwarp/core/unwarp_resample.py +++ b/gradunwarp/core/unwarp_resample.py @@ -119,14 +119,6 @@ def run(self): m_rcs2lai = np.dot(m_ras2lai, self.m_rcs2ras) m_rcs2lai_nohalf = m_rcs2lai[:, :] - # indices of image volume - ''' - nr, nc, ns = self.vol.shape[:3] - vc3, vr3, vs3 = utils.meshgrid(np.arange(nr), np.arange(nc), np.arange(ns), dtype=np.float32) - vrcs = CV(x=vr3, y=vc3, z=vs3) - vxyz = utils.transform_coordinates(vrcs, m_rcs2lai) - ''' - # account for half-voxel shift in R and C directions halfvox = np.zeros((4, 4)) halfvox[0, 3] = m_rcs2lai[0, 0] / 2.0 @@ -319,7 +311,7 @@ def non_linear_unwarp_siemens(self, volshape, dv, dxyz, m_rcs2lai, m_rcs2lai_noh print() - img=nib.Nifti1Image(fullWarp,self.m_rcs2ras) + img=nib.Nifti1Image(fullWarp, self.m_rcs2ras) nib.save(img,"fullWarp_abs.nii.gz") # return image and the jacobian del vrcsw, vfsl, vxyzw, vrcs, vxyz, vrcsg, fullWarp From f9204180531a10496aebd9a3c33e5e0e6b13399e Mon Sep 17 00:00:00 2001 From: bpinsard Date: Mon, 7 Aug 2023 15:36:56 -0400 Subject: [PATCH 04/15] cleanup:wip --- setup.py | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/setup.py b/setup.py index c440eda..d4bde9c 100644 --- a/setup.py +++ b/setup.py @@ -6,38 +6,13 @@ 'gradunwarp.core.__init__', 'gradunwarp.__init__', 'gradunwarp.core.utils', 'gradunwarp.core.unwarp_resample', - 'gradunwarp.core.gradient_unwarp', - 'gradunwarp.core.tests.test_utils', - ] - -dats = [('gradunwarp/core/', ['gradunwarp/core/interp3_ext.c']), - ('gradunwarp/core/', ['gradunwarp/core/legendre_ext.c']), - ('gradunwarp/core/', ['gradunwarp/core/transform_coordinates_ext.c']), - ] - -# to build the C extension interp3_ext.c -ext1 = Extension('gradunwarp.core.interp3_ext', - include_dirs = get_numpy_include_dirs(), - sources = ['gradunwarp/core/interp3_ext.c'], - extra_compile_args=['-O3']) -# to build the C extension legendre_ext.c -ext2 = Extension('gradunwarp.core.legendre_ext', - include_dirs = get_numpy_include_dirs(), - sources = ['gradunwarp/core/legendre_ext.c'], - extra_compile_args=['-O3']) -# to build the C extension transform_coordinates_ext.c -ext3 = Extension('gradunwarp.core.transform_coordinates_ext', - include_dirs = get_numpy_include_dirs(), - sources = ['gradunwarp/core/transform_coordinates_ext.c'], - extra_compile_args=['-O3']) + 'gradunwarp.core.gradient_unwarp', ] scripts_cmd = ['gradunwarp/core/gradient_unwarp.py',] - def configuration(parent_package='', top_path=None): from numpy.distutils.misc_util import Configuration config = Configuration('',parent_package,top_path) - config.add_data_files ( *dats ) return config setup(name='gradunwarp', @@ -45,7 +20,6 @@ def configuration(parent_package='', top_path=None): description = 'HCP version of Gradient Unwarping Package for Python/Numpy', author = 'Human Connectome Project', py_modules = mods, - #ext_modules = [ext1, ext2, ext3], scripts = scripts_cmd, configuration=configuration, ) From 6b46400535c00c7e48cba0383aa05b7133cd1bcb Mon Sep 17 00:00:00 2001 From: bpinsard Date: Mon, 7 Aug 2023 16:23:49 -0400 Subject: [PATCH 05/15] black --- gradunwarp/core/coeffs.py | 163 +++++++------- gradunwarp/core/globals.py | 16 +- gradunwarp/core/gradient_unwarp.py | 134 ++++++----- gradunwarp/core/unwarp_resample.py | 345 ++++++++++++++++------------- gradunwarp/core/utils.py | 106 +++++---- setup.py | 42 ++-- 6 files changed, 459 insertions(+), 347 deletions(-) diff --git a/gradunwarp/core/coeffs.py b/gradunwarp/core/coeffs.py index efa9a77..9e1f8ca 100644 --- a/gradunwarp/core/coeffs.py +++ b/gradunwarp/core/coeffs.py @@ -13,46 +13,52 @@ from .globals import siemens_cas, ge_cas -log = logging.getLogger('gradunwarp') +log = logging.getLogger("gradunwarp") -Coeffs = namedtuple('Coeffs', 'alpha_x, alpha_y, alpha_z, \ - beta_x, beta_y, beta_z, R0_m') +Coeffs = namedtuple( + "Coeffs", + "alpha_x, alpha_y, alpha_z, \ + beta_x, beta_y, beta_z, R0_m", +) try: advance_iterator = next except NameError: + def advance_iterator(it): return it.next() + + next = advance_iterator def get_coefficients(vendor, cfile): - ''' depending on the vendor and the coefficient file, + """depending on the vendor and the coefficient file, return the spherical harmonics coefficients as a named tuple. - ''' - log.info('Parsing ' + cfile + ' for harmonics coeffs') - if vendor == 'siemens' and cfile.endswith('.coef'): + """ + log.info("Parsing " + cfile + " for harmonics coeffs") + if vendor == "siemens" and cfile.endswith(".coef"): return get_siemens_coef(cfile) - if vendor == 'siemens' and cfile.endswith('.grad'): + if vendor == "siemens" and cfile.endswith(".grad"): return get_siemens_grad(cfile) def coef_file_parse(cfile, txt_var_map): - ''' a separate function because GE and Siemens .coef files + """a separate function because GE and Siemens .coef files have similar structure modifies txt_var_map in place - ''' + """ # parse .coef file. Strip unneeded characters. a valid line in that file is # broken into validline_list - coef_re = re.compile('^[^\#]') # regex for first character not a '#' - coef_file = open(cfile, 'r') + coef_re = re.compile("^[^\#]") # regex for first character not a '#' + coef_file = open(cfile, "r") for line in coef_file.readlines(): if coef_re.match(line): - validline_list = line.lstrip(' \t').rstrip(';\n').split() + validline_list = line.lstrip(" \t").rstrip(";\n").split() if validline_list: - log.info('Parsed : %s' % validline_list) + log.info("Parsed : %s" % validline_list) l = validline_list x = int(l[1]) y = int(l[2]) @@ -60,23 +66,25 @@ def coef_file_parse(cfile, txt_var_map): def get_siemens_coef(cfile): - ''' Parse the Siemens .coef file. + """Parse the Siemens .coef file. Note that R0_m is not explicitly contained in the file - ''' - R0m_map = {'sonata': 0.25, - 'avanto': 0.25, - 'quantum': 0.25, - 'allegra': 0.14, - 'as39s': 0.25, - 'as39st': 0.25, - 'as39t': 0.25} + """ + R0m_map = { + "sonata": 0.25, + "avanto": 0.25, + "quantum": 0.25, + "allegra": 0.14, + "as39s": 0.25, + "as39st": 0.25, + "as39t": 0.25, + } for rad in R0m_map.keys(): if cfile.startswith(rad): R0_m = R0m_map[rad] coef_array_sz = siemens_cas # allegra is slightly different - if cfile.startswith('allegra'): + if cfile.startswith("allegra"): coef_array_sz = 15 ax = np.zeros((coef_array_sz, coef_array_sz)) ay = np.zeros((coef_array_sz, coef_array_sz)) @@ -84,12 +92,14 @@ def get_siemens_coef(cfile): bx = np.zeros((coef_array_sz, coef_array_sz)) by = np.zeros((coef_array_sz, coef_array_sz)) bz = np.zeros((coef_array_sz, coef_array_sz)) - txt_var_map = {'Alpha_x': ax, - 'Alpha_y': ay, - 'Alpha_z': az, - 'Beta_x': bx, - 'Beta_y': by, - 'Beta_z': bz} + txt_var_map = { + "Alpha_x": ax, + "Alpha_y": ay, + "Alpha_z": az, + "Beta_x": bx, + "Beta_y": by, + "Beta_z": bz, + } coef_file_parse(cfile, txt_var_map) @@ -97,35 +107,37 @@ def get_siemens_coef(cfile): def get_ge_coef(cfile): - ''' Parse the GE .coef file. - ''' + """Parse the GE .coef file.""" ax = np.zeros((ge_cas, ge_cas)) ay = np.zeros((ge_cas, ge_cas)) az = np.zeros((ge_cas, ge_cas)) bx = np.zeros((ge_cas, ge_cas)) by = np.zeros((ge_cas, ge_cas)) bz = np.zeros((ge_cas, ge_cas)) - txt_var_map = {'Alpha_x': ax, - 'Alpha_y': ay, - 'Alpha_z': az, - 'Beta_x': bx, - 'Beta_y': by, - 'Beta_z': bz} + txt_var_map = { + "Alpha_x": ax, + "Alpha_y": ay, + "Alpha_z": az, + "Beta_x": bx, + "Beta_y": by, + "Beta_z": bz, + } coef_file_parse(cfile, txt_var_map) return Coeffs(ax, ay, az, bx, by, bz, R0_m) + def grad_file_parse(gfile, txt_var_map): - ''' a separate function because GE and Siemens .coef files + """a separate function because GE and Siemens .coef files have similar structure modifies txt_var_map in place - ''' - gf = open(gfile, 'r') + """ + gf = open(gfile, "r") line = next(gf) # skip the comments - while not line.startswith('#*] END:'): + while not line.startswith("#*] END:"): line = next(gf) # get R0 @@ -147,30 +159,30 @@ def grad_file_parse(gfile, txt_var_map): ymax = 0 while 1: - lindex = line.find('(') - rindex = line.find(')') + lindex = line.find("(") + rindex = line.find(")") if lindex == -1 and rindex == -1: break - arrindex = line[lindex+1:rindex] - xs, ys = arrindex.split(',') - x = int(xs) + arrindex = line[lindex + 1 : rindex] + xs, ys = arrindex.split(",") + x = int(xs) y = int(ys) if x > xmax: xmax = x if y > ymax: ymax = y - if line.find('A') != -1 and line.find('x') != -1: - txt_var_map['Alpha_x'][x,y] = float(line.split()[-2]) - if line.find('A') != -1 and line.find('y') != -1: - txt_var_map['Alpha_y'][x,y] = float(line.split()[-2]) - if line.find('A') != -1 and line.find('z') != -1: - txt_var_map['Alpha_z'][x,y] = float(line.split()[-2]) - if line.find('B') != -1 and line.find('x') != -1: - txt_var_map['Beta_x'][x,y] = float(line.split()[-2]) - if line.find('B') != -1 and line.find('y') != -1: - txt_var_map['Beta_y'][x,y] = float(line.split()[-2]) - if line.find('B') != -1 and line.find('z') != -1: - txt_var_map['Beta_z'][x,y] = float(line.split()[-2]) + if line.find("A") != -1 and line.find("x") != -1: + txt_var_map["Alpha_x"][x, y] = float(line.split()[-2]) + if line.find("A") != -1 and line.find("y") != -1: + txt_var_map["Alpha_y"][x, y] = float(line.split()[-2]) + if line.find("A") != -1 and line.find("z") != -1: + txt_var_map["Alpha_z"][x, y] = float(line.split()[-2]) + if line.find("B") != -1 and line.find("x") != -1: + txt_var_map["Beta_x"][x, y] = float(line.split()[-2]) + if line.find("B") != -1 and line.find("y") != -1: + txt_var_map["Beta_y"][x, y] = float(line.split()[-2]) + if line.find("B") != -1 and line.find("z") != -1: + txt_var_map["Beta_z"][x, y] = float(line.split()[-2]) try: line = next(gf) except StopIteration: @@ -179,12 +191,12 @@ def grad_file_parse(gfile, txt_var_map): # just return R0_m but also txt_var_map is returned return R0_m, (xmax, ymax) + def get_siemens_grad(gfile): - ''' Parse the siemens .grad file - ''' + """Parse the siemens .grad file""" coef_array_sz = siemens_cas # allegra is slightly different - if gfile.startswith('coef_AC44'): + if gfile.startswith("coef_AC44"): coef_array_sz = 15 ax = np.zeros((coef_array_sz, coef_array_sz)) ay = np.zeros((coef_array_sz, coef_array_sz)) @@ -192,23 +204,24 @@ def get_siemens_grad(gfile): bx = np.zeros((coef_array_sz, coef_array_sz)) by = np.zeros((coef_array_sz, coef_array_sz)) bz = np.zeros((coef_array_sz, coef_array_sz)) - txt_var_map = {'Alpha_x': ax, - 'Alpha_y': ay, - 'Alpha_z': az, - 'Beta_x': bx, - 'Beta_y': by, - 'Beta_z': bz} + txt_var_map = { + "Alpha_x": ax, + "Alpha_y": ay, + "Alpha_z": az, + "Beta_x": bx, + "Beta_y": by, + "Beta_z": bz, + } R0_m, max_ind = grad_file_parse(gfile, txt_var_map) ind = max(max_ind) # pruned alphas and betas - ax = ax[:ind+1, :ind+1] - ay = ay[:ind+1, :ind+1] - az = az[:ind+1, :ind+1] - bx = bx[:ind+1, :ind+1] - by = by[:ind+1, :ind+1] - bz = bz[:ind+1, :ind+1] + ax = ax[: ind + 1, : ind + 1] + ay = ay[: ind + 1, : ind + 1] + az = az[: ind + 1, : ind + 1] + bx = bx[: ind + 1, : ind + 1] + by = by[: ind + 1, : ind + 1] + bz = bz[: ind + 1, : ind + 1] return Coeffs(ax, ay, az, bx, by, bz, R0_m) - diff --git a/gradunwarp/core/globals.py b/gradunwarp/core/globals.py index a07a4e0..68d4061 100644 --- a/gradunwarp/core/globals.py +++ b/gradunwarp/core/globals.py @@ -6,20 +6,20 @@ ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## import logging -VERSION = '1.2.1+HCP' +VERSION = "1.2.1+HCP" -usage = ''' +usage = """ gradient_unwarp infile outfile manufacturer -g [optional arguments] -''' +""" # SIEMENS stuff siemens_cas = 100 # coefficient array size -siemens_fovmin = -.30 # fov min in meters -siemens_fovmax = .30 # fov max in meters -siemens_numpoints = 60 # number of grid points in each direction +siemens_fovmin = -0.30 # fov min in meters +siemens_fovmax = 0.30 # fov max in meters +siemens_numpoints = 60 # number of grid points in each direction # max jacobian determinant for siemens -siemens_max_det = 10. +siemens_max_det = 10.0 # GE stuff @@ -30,7 +30,7 @@ def get_logger(): - log = logging.getLogger('gradunwarp') + log = logging.getLogger("gradunwarp") log.setLevel(logging.INFO) ch = logging.StreamHandler() ch.setLevel(logging.INFO) diff --git a/gradunwarp/core/gradient_unwarp.py b/gradunwarp/core/gradient_unwarp.py index 49aee18..3920ac9 100755 --- a/gradunwarp/core/gradient_unwarp.py +++ b/gradunwarp/core/gradient_unwarp.py @@ -8,110 +8,140 @@ import argparse as arg import os import logging -from gradunwarp.core import (globals, coeffs, utils) +from gradunwarp.core import globals, coeffs, utils from gradunwarp.core.unwarp_resample import Unwarper log = globals.get_logger() def argument_parse_gradunwarp(): - '''Arguments parser from the command line - ''' + """Arguments parser from the command line""" # initiate try: p = arg.ArgumentParser(usage=globals.usage) - p.add_argument('--version', '-v', action='version', version=globals.VERSION) + p.add_argument("--version", "-v", action="version", version=globals.VERSION) except: - #maintain compatibility with pre py2.7 argparse (deprecated in py2.7 but fails in py3) + # maintain compatibility with pre py2.7 argparse (deprecated in py2.7 but fails in py3) p = arg.ArgumentParser(version=globals.VERSION, usage=globals.usage) # required arguments - p.add_argument('infile', action='store', - help='The input warped file (nifti or mgh)') - p.add_argument('outfile', action='store', - help='The output unwarped file (extension should be .nii/.nii.gz/.mgh/.mgz)') - p.add_argument('vendor', action='store', choices=['siemens', 'ge'], - help='vendor (either "ge" or "siemens" for now)') + p.add_argument( + "infile", action="store", help="The input warped file (nifti or mgh)" + ) + p.add_argument( + "outfile", + action="store", + help="The output unwarped file (extension should be .nii/.nii.gz/.mgh/.mgz)", + ) + p.add_argument( + "vendor", + action="store", + choices=["siemens", "ge"], + help='vendor (either "ge" or "siemens" for now)', + ) coef_grp = p.add_mutually_exclusive_group(required=True) - coef_grp.add_argument('-g', '--gradfile', dest='gradfile', - help='The .grad coefficient file') - coef_grp.add_argument('-c', '--coeffile', dest='coeffile', - help='The .coef coefficient file') + coef_grp.add_argument( + "-g", "--gradfile", dest="gradfile", help="The .grad coefficient file" + ) + coef_grp.add_argument( + "-c", "--coeffile", dest="coeffile", help="The .coef coefficient file" + ) # optional arguments - p.add_argument('-w', '--warp', action='store_true', default=False, - help='warp a volume (as opposed to unwarping)') - p.add_argument('-n', '--nojacobian', dest='nojac', action='store_true', - default=False, help='Do not perform Jacobian intensity correction') - p.add_argument('--fovmin', dest='fovmin', - help='the minimum extent of harmonics evaluation grid in meters') - p.add_argument('--fovmax', dest='fovmax', - help='the maximum extent of harmonics evaluation grid in meters') - p.add_argument('--numpoints', dest='numpoints', - help='number of grid points in each direction') - p.add_argument('--interp_order', dest='order', - help='the order of interpolation(1..4) where 1 is linear - default') - - p.add_argument('--verbose', action='store_true', default=False) + p.add_argument( + "-w", + "--warp", + action="store_true", + default=False, + help="warp a volume (as opposed to unwarping)", + ) + p.add_argument( + "-n", + "--nojacobian", + dest="nojac", + action="store_true", + default=False, + help="Do not perform Jacobian intensity correction", + ) + p.add_argument( + "--fovmin", + dest="fovmin", + help="the minimum extent of harmonics evaluation grid in meters", + ) + p.add_argument( + "--fovmax", + dest="fovmax", + help="the maximum extent of harmonics evaluation grid in meters", + ) + p.add_argument( + "--numpoints", dest="numpoints", help="number of grid points in each direction" + ) + p.add_argument( + "--interp_order", + dest="order", + help="the order of interpolation(1..4) where 1 is linear - default", + ) + + p.add_argument("--verbose", action="store_true", default=False) args = p.parse_args() # do some validation if not os.path.exists(args.infile): - raise IOError(args.infile + ' not found') + raise IOError(args.infile + " not found") if args.gradfile: if not os.path.exists(args.gradfile): - raise IOError(args.gradfile + ' not found') + raise IOError(args.gradfile + " not found") if args.coeffile: if not os.path.exists(args.coeffile): - raise IOError(args.coeffile + ' not found') + raise IOError(args.coeffile + " not found") return args class GradientUnwarpRunner(object): - ''' Takes the option datastructure after parsing the commandline. + """Takes the option datastructure after parsing the commandline. run() method performs the actual unwarping write() method performs the writing of the unwarped volume - ''' + """ + def __init__(self, args): - ''' constructor takes the option datastructure which is the + """constructor takes the option datastructure which is the result of (options, args) = parser.parse_args() - ''' + """ self.args = args self.unwarper = None log.setLevel(logging.INFO) - if hasattr(self.args, 'verbose'): + if hasattr(self.args, "verbose"): log.setLevel(logging.DEBUG) def run(self): - ''' run the unwarp resample - ''' + """run the unwarp resample""" # get the spherical harmonics coefficients from parsing # the given .coeff file xor .grad file - if hasattr(self.args, 'gradfile') and self.args.gradfile: - self.coeffs = coeffs.get_coefficients(self.args.vendor, - self.args.gradfile) + if hasattr(self.args, "gradfile") and self.args.gradfile: + self.coeffs = coeffs.get_coefficients(self.args.vendor, self.args.gradfile) else: - self.coeffs = coeffs.get_coefficients(self.args.vendor, - self.args.coeffile) + self.coeffs = coeffs.get_coefficients(self.args.vendor, self.args.coeffile) self.vol, self.m_rcs2ras = utils.get_vol_affine(self.args.infile) - self.unwarper = Unwarper(self.vol, self.m_rcs2ras, self.args.vendor, self.coeffs, self.args.infile ) - if hasattr(self.args, 'fovmin') and self.args.fovmin: + self.unwarper = Unwarper( + self.vol, self.m_rcs2ras, self.args.vendor, self.coeffs, self.args.infile + ) + if hasattr(self.args, "fovmin") and self.args.fovmin: self.unwarper.fovmin = float(self.args.fovmin) - if hasattr(self.args, 'fovmax') and self.args.fovmax: + if hasattr(self.args, "fovmax") and self.args.fovmax: self.unwarper.fovmax = float(self.args.fovmax) - if hasattr(self.args, 'numpoints') and self.args.numpoints: + if hasattr(self.args, "numpoints") and self.args.numpoints: self.unwarper.numpoints = int(self.args.numpoints) - if hasattr(self.args, 'warp') and self.args.warp: + if hasattr(self.args, "warp") and self.args.warp: self.unwarper.warp = True - if hasattr(self.args, 'nojac') and self.args.nojac: + if hasattr(self.args, "nojac") and self.args.nojac: self.unwarper.nojac = True - if hasattr(self.args, 'order') and self.args.order: + if hasattr(self.args, "order") and self.args.order: self.unwarper.order = int(self.args.order) self.unwarper.run() @@ -119,7 +149,7 @@ def write(self): self.unwarper.write(self.args.outfile) -if __name__ == '__main__': +if __name__ == "__main__": args = argument_parse_gradunwarp() grad_unwarp = GradientUnwarpRunner(args) diff --git a/gradunwarp/core/unwarp_resample.py b/gradunwarp/core/unwarp_resample.py index 174d3ee..6ef9355 100644 --- a/gradunwarp/core/unwarp_resample.py +++ b/gradunwarp/core/unwarp_resample.py @@ -21,22 +21,21 @@ import subprocess import scipy.special -#np.seterr(all='raise') +# np.seterr(all='raise') -log = logging.getLogger('gradunwarp') +log = logging.getLogger("gradunwarp") class Unwarper(object): - ''' - ''' + """ """ + def __init__(self, vol, m_rcs2ras, vendor, coeffs, fileName): - ''' - ''' + """ """ self.vol = vol self.m_rcs2ras = m_rcs2ras self.vendor = vendor self.coeffs = coeffs - self.name=fileName + self.name = fileName self.warp = False self.nojac = False self.m_rcs2lai = None @@ -50,10 +49,10 @@ def __init__(self, vol, m_rcs2ras, vendor, coeffs, fileName): self.order = 1 def eval_spharm_grid(self, vendor, coeffs): - ''' + """ We evaluate the spherical harmonics on a less sampled grid. This is a spacetime vs accuracy tradeoff. - ''' + """ # init the grid first if not self.fovmin: fovmin = globals.siemens_fovmin @@ -69,8 +68,8 @@ def eval_spharm_grid(self, vendor, coeffs): numpoints = self.numpoints # convert to mm - fovmin = fovmin * 1000. - fovmax = fovmax * 1000. + fovmin = fovmin * 1000.0 + fovmax = fovmax * 1000.0 # the grid in meters. this is needed for spherical harmonics vec = np.linspace(fovmin, fovmax, numpoints) gvx, gvy, gvz = utils.meshgrid(vec, vec, vec) @@ -78,44 +77,51 @@ def eval_spharm_grid(self, vendor, coeffs): cf = (fovmax - fovmin) / numpoints # deduce the transformation from rcs to grid - g_rcs2xyz = np.array( [[0, cf, 0, fovmin], - [cf, 0, 0, fovmin], - [0, 0, cf, fovmin], - [0, 0, 0, 1]], dtype=np.float32 ) + g_rcs2xyz = np.array( + [[0, cf, 0, fovmin], [cf, 0, 0, fovmin], [0, 0, cf, fovmin], [0, 0, 0, 1]], + dtype=np.float32, + ) # get the grid to rcs transformation also g_xyz2rcs = np.linalg.inv(g_rcs2xyz) # indices into the gradient displacement vol - gr, gc, gs = utils.meshgrid(np.arange(numpoints), np.arange(numpoints), - np.arange(numpoints), dtype=np.float32) - - log.info('Evaluating spherical harmonics') - log.info('on a ' + str(numpoints) + '^3 grid') - log.info('with extents ' + str(fovmin) + 'mm to ' + str(fovmax) + 'mm') + gr, gc, gs = utils.meshgrid( + np.arange(numpoints), + np.arange(numpoints), + np.arange(numpoints), + dtype=np.float32, + ) + + log.info("Evaluating spherical harmonics") + log.info("on a " + str(numpoints) + "^3 grid") + log.info("with extents " + str(fovmin) + "mm to " + str(fovmax) + "mm") gvxyz = CV(gvx, gvy, gvz) _dv, _dxyz = eval_spherical_harmonics(coeffs, vendor, gvxyz) return CV(_dv.x, _dv.y, _dv.z), g_xyz2rcs - def run(self): - ''' - ''' - #pdb.set_trace() + """ """ + # pdb.set_trace() # define polarity based on the warp requested - self.polarity = 1. + self.polarity = 1.0 if self.warp: - self.polarity = -1. + self.polarity = -1.0 # Evaluate spherical harmonics on a smaller grid dv, g_xyz2rcs = self.eval_spharm_grid(self.vendor, self.coeffs) # transform RAS-coordinates into LAI-coordinates - m_ras2lai = np.array([[-1.0, 0.0, 0.0, 0.0], - [0.0, 1.0, 0.0, 0.0], - [0.0, 0.0, -1.0, 0.0], - [0.0, 0.0, 0.0, 1.0]], dtype=np.float64) + m_ras2lai = np.array( + [ + [-1.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, -1.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + ], + dtype=np.float64, + ) m_rcs2lai = np.dot(m_ras2lai, self.m_rcs2ras) m_rcs2lai_nohalf = m_rcs2lai[:, :] @@ -123,7 +129,7 @@ def run(self): halfvox = np.zeros((4, 4)) halfvox[0, 3] = m_rcs2lai[0, 0] / 2.0 halfvox[1, 3] = m_rcs2lai[1, 1] / 2.0 - #m_rcs2lai = m_rcs2lai + halfvox + # m_rcs2lai = m_rcs2lai + halfvox # extract rotational and scaling parts of the transformation matrix # ignore the translation part @@ -138,17 +144,20 @@ def run(self): # The differentials can be determined by mapping a vector of 1s # through rotation and scaling, where any mirror # will impose a negation - ones = CV(1., 1., 1.) + ones = CV(1.0, 1.0, 1.0) dxyz = utils.transform_coordinates(ones, r_rcs2lai) # do the nonlinear unwarp - if self.vendor == 'siemens': - self.out, self.vjacout = self.non_linear_unwarp_siemens(self.vol.shape, dv, dxyz, - m_rcs2lai, m_rcs2lai_nohalf, g_xyz2rcs) + if self.vendor == "siemens": + self.out, self.vjacout = self.non_linear_unwarp_siemens( + self.vol.shape, dv, dxyz, m_rcs2lai, m_rcs2lai_nohalf, g_xyz2rcs + ) del dv, g_xyz2rcs, self.vjacout - def non_linear_unwarp_siemens(self, volshape, dv, dxyz, m_rcs2lai, m_rcs2lai_nohalf, g_xyz2rcs): - ''' Performs the crux of the unwarping. + def non_linear_unwarp_siemens( + self, volshape, dv, dxyz, m_rcs2lai, m_rcs2lai_nohalf, g_xyz2rcs + ): + """Performs the crux of the unwarping. It's agnostic to Siemens or GE and uses more functions to do the processing separately. @@ -168,8 +177,8 @@ def non_linear_unwarp_siemens(self, volshape, dv, dxyz, m_rcs2lai, m_rcs2lai_noh x,y and z coordinates of the unwarped coordinates vjacmult_lps : np.array the jacobian multiplier (determinant) - ''' - log.info('Evaluating the jacobian multiplier') + """ + log.info("Evaluating the jacobian multiplier") nr, nc, ns = self.vol.shape[:3] if not self.nojac: jim2 = np.zeros((nr, nc), dtype=np.float32) @@ -194,159 +203,197 @@ def non_linear_unwarp_siemens(self, volshape, dv, dxyz, m_rcs2lai, m_rcs2lai_noh # Compute transform to map the internal voxel coordinates to FSL scaled mm coordinates try: - pixdim1=float((subprocess.Popen(['fslval', self.name,'pixdim1'], stdout=subprocess.PIPE).communicate()[0]).strip()) + pixdim1 = float( + ( + subprocess.Popen( + ["fslval", self.name, "pixdim1"], stdout=subprocess.PIPE + ).communicate()[0] + ).strip() + ) except ValueError: - log.error('Failure during fslval call. Make sure fslval, fslhd, fslorient are in your PATH, and that FSLOUTPUTTYPE is set.') + log.error( + "Failure during fslval call. Make sure fslval, fslhd, fslorient are in your PATH, and that FSLOUTPUTTYPE is set." + ) sys.exit(1) - pixdim2=float((subprocess.Popen(['fslval', self.name,'pixdim2'], stdout=subprocess.PIPE).communicate()[0]).strip()) - pixdim3=float((subprocess.Popen(['fslval', self.name,'pixdim3'], stdout=subprocess.PIPE).communicate()[0]).strip()) - dim1=float((subprocess.Popen(['fslval', self.name,'dim1'], stdout=subprocess.PIPE).communicate()[0]).strip()) - outputOrient=subprocess.Popen(['fslorient', self.name], stdout=subprocess.PIPE).communicate()[0].strip() - if outputOrient == b'NEUROLOGICAL': - log.info('Input volume is NEUROLOGICAL orientation. Flipping x-axis in output fullWarp_abs.nii.gz') + pixdim2 = float( + ( + subprocess.Popen( + ["fslval", self.name, "pixdim2"], stdout=subprocess.PIPE + ).communicate()[0] + ).strip() + ) + pixdim3 = float( + ( + subprocess.Popen( + ["fslval", self.name, "pixdim3"], stdout=subprocess.PIPE + ).communicate()[0] + ).strip() + ) + dim1 = float( + ( + subprocess.Popen( + ["fslval", self.name, "dim1"], stdout=subprocess.PIPE + ).communicate()[0] + ).strip() + ) + outputOrient = ( + subprocess.Popen(["fslorient", self.name], stdout=subprocess.PIPE) + .communicate()[0] + .strip() + ) + if outputOrient == b"NEUROLOGICAL": + log.info( + "Input volume is NEUROLOGICAL orientation. Flipping x-axis in output fullWarp_abs.nii.gz" + ) # if neurological then flip x coordinate (both here in premat and later in postmat) - m_vox2fsl = np.array([[-1.0*pixdim1, 0.0, 0.0, pixdim1*(dim1-1)], - [0.0, pixdim2, 0.0, 0.0], - [0.0, 0.0, pixdim3, 0.0], - [0.0, 0.0, 0.0, 1.0]], dtype=np.float64) + m_vox2fsl = np.array( + [ + [-1.0 * pixdim1, 0.0, 0.0, pixdim1 * (dim1 - 1)], + [0.0, pixdim2, 0.0, 0.0], + [0.0, 0.0, pixdim3, 0.0], + [0.0, 0.0, 0.0, 1.0], + ], + dtype=np.float64, + ) else: - m_vox2fsl = np.array([[pixdim1, 0.0, 0.0, 0.0], - [0.0, pixdim2, 0.0, 0.0], - [0.0, 0.0, pixdim3, 0.0], - [0.0, 0.0, 0.0, 1.0]], dtype=np.float64) - - log.info('Unwarping slice by slice') + m_vox2fsl = np.array( + [ + [pixdim1, 0.0, 0.0, 0.0], + [0.0, pixdim2, 0.0, 0.0], + [0.0, 0.0, pixdim3, 0.0], + [0.0, 0.0, 0.0, 1.0], + ], + dtype=np.float64, + ) + + log.info("Unwarping slice by slice") # for every slice for s in range(ns): # pretty print sys.stdout.flush() - if (s+1) % 10 == 0: - print(s+1, end=' ') + if (s + 1) % 10 == 0: + print(s + 1, end=" ") else: - print('.', end=' ') + print(".", end=" ") # hopefully, free memory gc.collect() # init to 0 - dvx.fill(0.) - dvy.fill(0.) - dvz.fill(0.) - im_.fill(0.) + dvx.fill(0.0) + dvy.fill(0.0) + dvz.fill(0.0) + im_.fill(0.0) vs = np.ones(vr.shape) * s vrcs = CV(vr, vc, vs) vxyz = utils.transform_coordinates(vrcs, m_rcs2lai_nohalf) vrcsg = utils.transform_coordinates(vxyz, g_xyz2rcs) - ndimage.interpolation.map_coordinates(dv.x, - vrcsg, - output=dvx, - order=self.order) - ndimage.interpolation.map_coordinates(dv.y, - vrcsg, - output=dvy, - order=self.order) - ndimage.interpolation.map_coordinates(dv.z, - vrcsg, - output=dvz, - order=self.order) + ndimage.interpolation.map_coordinates( + dv.x, vrcsg, output=dvx, order=self.order + ) + ndimage.interpolation.map_coordinates( + dv.y, vrcsg, output=dvy, order=self.order + ) + ndimage.interpolation.map_coordinates( + dv.z, vrcsg, output=dvz, order=self.order + ) # new locations of the image voxels in XYZ ( LAI ) coords - #dvx.fill(0.) - #dvy.fill(0.) - #dvz.fill(0.) + # dvx.fill(0.) + # dvy.fill(0.) + # dvz.fill(0.) - vxyzw = CV(x=vxyz.x + self.polarity * dvx, - y=vxyz.y + self.polarity * dvy, - z=vxyz.z + self.polarity * dvz) + vxyzw = CV( + x=vxyz.x + self.polarity * dvx, + y=vxyz.y + self.polarity * dvy, + z=vxyz.z + self.polarity * dvz, + ) # convert the locations got into RCS indices - vrcsw = utils.transform_coordinates(vxyzw, - np.linalg.inv(m_rcs2lai)) + vrcsw = utils.transform_coordinates(vxyzw, np.linalg.inv(m_rcs2lai)) # map the internal voxel coordinates to FSL scaled mm coordinates vfsl = utils.transform_coordinates(vrcsw, m_vox2fsl) - - #im_ = utils.interp3(self.vol, vrcsw.x, vrcsw.y, vrcsw.z) - ndimage.interpolation.map_coordinates(self.vol, - vrcsw, - output=im_, - order=self.order) + # im_ = utils.interp3(self.vol, vrcsw.x, vrcsw.y, vrcsw.z) + ndimage.interpolation.map_coordinates( + self.vol, vrcsw, output=im_, order=self.order + ) # find NaN voxels, and set them to 0 - im_[np.where(np.isnan(im_))] = 0. - im_[np.where(np.isinf(im_))] = 0. + im_[np.where(np.isnan(im_))] = 0.0 + im_[np.where(np.isinf(im_))] = 0.0 im2[vr, vc] = im_ - #img = nib.Nifti1Image(dvx,np.eye(4)) - #nib.save(img,"x"+str(s).zfill(3)+".nii.gz") - #img = nib.Nifti1Image(dvy,np.eye(4)) - #nib.save(img,"y"+str(s).zfill(3)+".nii.gz") - #img = nib.Nifti1Image(dvz,np.eye(4)) - #nib.save(img,"z"+str(s).zfill(3)+".nii.gz") + # img = nib.Nifti1Image(dvx,np.eye(4)) + # nib.save(img,"x"+str(s).zfill(3)+".nii.gz") + # img = nib.Nifti1Image(dvy,np.eye(4)) + # nib.save(img,"y"+str(s).zfill(3)+".nii.gz") + # img = nib.Nifti1Image(dvz,np.eye(4)) + # nib.save(img,"z"+str(s).zfill(3)+".nii.gz") # Multiply the intensity with the Jacobian det, if needed if not self.nojac: - vjacdet_lpsw.fill(0.) - jim2.fill(0.) + vjacdet_lpsw.fill(0.0) + jim2.fill(0.0) # if polarity is negative, the jacobian is also inversed if self.polarity == -1: - vjacdet_lps = 1. / vjacdet_lps - - ndimage.interpolation.map_coordinates(vjacdet_lps, - vrcsg, - output=vjacdet_lpsw, - order=self.order) - vjacdet_lpsw[np.where(np.isnan(vjacdet_lpsw))] = 0. - vjacdet_lpsw[np.where(np.isinf(vjacdet_lpsw))] = 0. + vjacdet_lps = 1.0 / vjacdet_lps + + ndimage.interpolation.map_coordinates( + vjacdet_lps, vrcsg, output=vjacdet_lpsw, order=self.order + ) + vjacdet_lpsw[np.where(np.isnan(vjacdet_lpsw))] = 0.0 + vjacdet_lpsw[np.where(np.isinf(vjacdet_lpsw))] = 0.0 jim2[vr, vc] = vjacdet_lpsw im2 = im2 * jim2 vjacout[..., s] = jim2 - fullWarp[...,s,0]=vfsl.x - fullWarp[...,s,1]=vfsl.y - fullWarp[...,s,2]=vfsl.z + fullWarp[..., s, 0] = vfsl.x + fullWarp[..., s, 1] = vfsl.y + fullWarp[..., s, 2] = vfsl.z out[..., s] = im2 print() - img=nib.Nifti1Image(fullWarp, self.m_rcs2ras) - nib.save(img,"fullWarp_abs.nii.gz") + img = nib.Nifti1Image(fullWarp, self.m_rcs2ras) + nib.save(img, "fullWarp_abs.nii.gz") # return image and the jacobian del vrcsw, vfsl, vxyzw, vrcs, vxyz, vrcsg, fullWarp return out, vjacout def write(self, outfile): - log.info('Writing output to ' + outfile) + log.info("Writing output to " + outfile) # if out datatype is float64 make it float32 if self.out.dtype == np.float64: self.out = self.out.astype(np.float32) - if outfile.endswith('.nii') or outfile.endswith('.nii.gz'): + if outfile.endswith(".nii") or outfile.endswith(".nii.gz"): img = nib.Nifti1Image(self.out, self.m_rcs2ras) - if outfile.endswith('.mgh') or outfile.endswith('.mgz'): - #self.out = self.out.astype(self.vol.dtype) + if outfile.endswith(".mgh") or outfile.endswith(".mgz"): + # self.out = self.out.astype(self.vol.dtype) img = nib.MGHImage(self.out, self.m_rcs2ras) nib.save(img, outfile) def eval_siemens_jacobian_mult(F, dxyz): - ''' - ''' + """ """ d0, d1, d2 = dxyz.x, dxyz.y, dxyz.z - #print F.x.shape, d0, d1, d2 + # print F.x.shape, d0, d1, d2 if d0 == 0 or d1 == 0 or d2 == 0: - raise ValueError('weirdness found in Jacobian calculation') + raise ValueError("weirdness found in Jacobian calculation") dFxdx, dFxdy, dFxdz = np.gradient(F.x, d0, d1, d2) dFydx, dFydy, dFydz = np.gradient(F.y, d0, d1, d2) dFzdx, dFzdy, dFzdz = np.gradient(F.z, d0, d1, d2) - jacdet = (1. + dFxdx) * (1. + dFydy) * (1. + dFzdz) \ - - (1. + dFxdx) * dFydz * dFzdy \ - - dFxdy * dFydx * (1. + dFzdz) \ - + dFxdy * dFydz * dFzdx \ - + dFxdz * dFydx * dFzdy \ - - dFxdz * (1. + dFydy) * dFzdx + jacdet = ( + (1.0 + dFxdx) * (1.0 + dFydy) * (1.0 + dFzdz) + - (1.0 + dFxdx) * dFydz * dFzdy + - dFxdy * dFydx * (1.0 + dFzdz) + + dFxdy * dFydz * dFzdx + + dFxdz * dFydx * dFzdy + - dFxdz * (1.0 + dFydy) * dFzdx + ) jacdet = np.abs(jacdet) jacdet[np.where(jacdet > siemens_max_det)] = siemens_max_det @@ -354,7 +401,7 @@ def eval_siemens_jacobian_mult(F, dxyz): def eval_spherical_harmonics(coeffs, vendor, vxyz): - ''' Evaluate spherical harmonics + """Evaluate spherical harmonics Parameters ---------- @@ -366,39 +413,38 @@ def eval_spherical_harmonics(coeffs, vendor, vxyz): in the function resolution : float (optional) useful in case vxyz is scalar - ''' + """ # convert radius into mm - R0 = coeffs.R0_m * 1000 + R0 = coeffs.R0_m * 1000 x, y, z = vxyz - #pdb.set_trace() + # pdb.set_trace() # log.info('calculating displacements (mm) ' # 'using spherical harmonics coeffcients...') - if vendor == 'siemens': - log.info('along x...') + if vendor == "siemens": + log.info("along x...") bx = siemens_B(coeffs.alpha_x, coeffs.beta_x, x, y, z, R0) - log.info('along y...') + log.info("along y...") by = siemens_B(coeffs.alpha_y, coeffs.beta_y, x, y, z, R0) - log.info('along z...') + log.info("along z...") bz = siemens_B(coeffs.alpha_z, coeffs.beta_z, x, y, z, R0) else: # GE - log.info('along x...') + log.info("along x...") bx = ge_D(coeffs.alpha_x, coeffs.beta_x, x, y, z) - log.info('along y...') + log.info("along y...") by = ge_D(coeffs.alpha_y, coeffs.beta_y, x, y, z) - log.info('along z...') + log.info("along z...") bz = siemens_B(coeffs.alpha_z, coeffs.beta_z, x, y, z, R0) bz = ge_D(coeffs.alpha_z, coeffs.beta_z, x, y, z) return CV(bx * R0, by * R0, bz * R0), CV(x, y, z) -#@profile +# @profile def siemens_B(alpha, beta, x1, y1, z1, R0): - ''' Calculate displacement field from Siemens coefficients - ''' + """Calculate displacement field from Siemens coefficients""" nmax = alpha.shape[0] - 1 x1 = x1 + 0.0001 # hack to avoid singularities at R=0 @@ -412,22 +458,22 @@ def siemens_B(alpha, beta, x1, y1, z1, R0): f = np.power(r / R0, n) for m in range(0, n + 1): f2 = alpha[n, m] * np.cos(m * phi) + beta[n, m] * np.sin(m * phi) - #_ptemp = utils.legendre(n, m, np.cos(theta)) + # _ptemp = utils.legendre(n, m, np.cos(theta)) _p = scipy.special.lpmv(m, n, np.cos(theta)) normfact = 1 # this is Siemens normalization if m > 0: - normfact = math.pow(-1, m) * \ - math.sqrt(float((2 * n + 1) * factorial(n - m)) \ - / float(2 * factorial(n + m))) + normfact = math.pow(-1, m) * math.sqrt( + float((2 * n + 1) * factorial(n - m)) / float(2 * factorial(n + m)) + ) _p *= normfact b += f * _p * f2 return b def ge_D(alpha, beta, x1, y1, z1): - ''' GE Gradwarp coeffs define the error rather than the total - gradient field''' + """GE Gradwarp coeffs define the error rather than the total + gradient field""" nmax = alpha.shape[0] - 1 x1 = x1 + 0.0001 # hack to avoid singularities @@ -443,8 +489,7 @@ def ge_D(alpha, beta, x1, y1, z1): # So GE uses the usual unnormalized legendre polys. f = np.power(r, n) for m in range(0, n + 1): - f2 = alpha[n, m] * np.cos(m * theta) + beta[n, m] \ - * np.sin(m * theta) + f2 = alpha[n, m] * np.cos(m * theta) + beta[n, m] * np.sin(m * theta) _p = scipy.special.lpmv(m, n, np.cos(phi)) d += f * _p * f2 d = d / 100.0 # cm back to meters diff --git a/gradunwarp/core/utils.py b/gradunwarp/core/utils.py index d1cf1c4..58905d2 100644 --- a/gradunwarp/core/utils.py +++ b/gradunwarp/core/utils.py @@ -16,16 +16,16 @@ # of a meshgrid belongs to this # x, y, z = meshgrid(np.arange(5), np.arange(6), np.arange(7)) # cv = CoordsVector(x=x, y=y, z=z) -CoordsVector = namedtuple('CoordsVector', 'x, y, z') +CoordsVector = namedtuple("CoordsVector", "x, y, z") # this method is deprecated because it's slow and my suspicion that # the matrix expressions create unnecessary temp matrices which # are costly for huge matrices def transform_coordinates_old(A, M): - ''' 4x4 matrix M operates on orthogonal coordinates arrays + """4x4 matrix M operates on orthogonal coordinates arrays A1, A2, A3 to give B1, B2, B3 - ''' + """ A1 = A.x A2 = A.y A3 = A.z @@ -34,10 +34,11 @@ def transform_coordinates_old(A, M): B3 = A1 * M[2, 0] + A2 * M[2, 1] + A3 * M[2, 2] + M[2, 3] return CoordsVector(B1, B2, B3) + def transform_coordinates_old2(A, M): - ''' 4x4 matrix M operates on orthogonal coordinates arrays + """4x4 matrix M operates on orthogonal coordinates arrays A1, A2, A3 to give B1, B2, B3 - ''' + """ A1 = A.x A2 = A.y A3 = A.z @@ -48,25 +49,33 @@ def transform_coordinates_old2(A, M): try: from .transform_coordinates_ext import _transform_coordinates except ImportError: - raise ImportError('The transform_coordinates C extension module is missing.' \ - ' Fallback code not yet implemented.') + raise ImportError( + "The transform_coordinates C extension module is missing." + " Fallback code not yet implemented." + ) B1, B2, B3 = _transform_coordinates(A1, A2, A3, M) return CoordsVector(B1, B2, B3) + from nibabel.affines import apply_affine + def transform_coordinates(A, M): - vecs = np.asanyarray([A.x,A.y,A.z]).T + vecs = np.asanyarray([A.x, A.y, A.z]).T vecs_trans = apply_affine(M, vecs) - return CoordsVector(vecs_trans[...,0].T, vecs_trans[...,1].T, vecs_trans[...,2].T) + return CoordsVector( + vecs_trans[..., 0].T, vecs_trans[..., 1].T, vecs_trans[..., 2].T + ) + def get_vol_affine(infile): try: import nibabel as nib except ImportError: - raise ImportError('gradunwarp needs nibabel for I/O of mgz/nifti files.' - ' Please install') + raise ImportError( + "gradunwarp needs nibabel for I/O of mgz/nifti files." " Please install" + ) nibimage = nib.load(infile) return nibimage.get_fdata(), nibimage.affine @@ -82,7 +91,8 @@ def __call__(self, *args): self.memo[args] = self.f(*args) return self.memo[args] -#factorial = Memoize(math.factorial) + +# factorial = Memoize(math.factorial) factorial = math.factorial @@ -174,28 +184,27 @@ def meshgrid(*xi, **kwargs): >>> xx, yy = meshgrid(x, y, sparse=True) >>> z = np.sin(xx**2+yy**2)/(xx**2+yy**2) """ - copy = kwargs.get('copy', True) + copy = kwargs.get("copy", True) args = np.atleast_1d(*xi) if not isinstance(args, list): if args.size > 0: return args.copy() if copy else args else: - raise TypeError('meshgrid() take 1 or more arguments (0 given)') + raise TypeError("meshgrid() take 1 or more arguments (0 given)") - sparse = kwargs.get('sparse', False) - indexing = kwargs.get('indexing', 'xy') # 'ij' + sparse = kwargs.get("sparse", False) + indexing = kwargs.get("indexing", "xy") # 'ij' ndim = len(args) s0 = (1,) * ndim - output = [x.reshape(s0[:i] + (-1, ) + s0[i + 1::]) \ - for i, x in enumerate(args)] + output = [x.reshape(s0[:i] + (-1,) + s0[i + 1 : :]) for i, x in enumerate(args)] shape = [x.size for x in output] - if indexing == 'xy': + if indexing == "xy": # switch first and second axis - output[0].shape = (1, -1) + (1, ) * (ndim - 2) - output[1].shape = (-1, 1) + (1, ) * (ndim - 2) + output[0].shape = (1, -1) + (1,) * (ndim - 2) + output[1].shape = (-1, 1) + (1,) * (ndim - 2) shape[0], shape[1] = shape[1], shape[0] if sparse: @@ -217,7 +226,7 @@ def ndgrid(*args, **kwargs): Same as calling meshgrid with indexing='ij' (see meshgrid for documentation). """ - kwargs['indexing'] = 'ij' + kwargs["indexing"] = "ij" return meshgrid(*args, **kwargs) @@ -237,8 +246,7 @@ def legendre_old(nu, mu, x): (Abramowitz & Stegun, Section 8.5.) """ if mu < 0 or mu > nu: - raise ValueError('require 0 <= mu <= nu, but mu=%d and nu=%d' \ - % (nu, mu)) + raise ValueError("require 0 <= mu <= nu, but mu=%d and nu=%d" % (nu, mu)) # if abs(x) > 1: # raise ValueError('require -1 <= x <= 1, but x=%f', x) @@ -249,8 +257,8 @@ def legendre_old(nu, mu, x): s = 1 if mu & 1: s = -1 - z = sqrt(1 - x ** 2) - p_nu = s * odd_factorial(2 * mu - 1) * z ** mu + z = sqrt(1 - x**2) + p_nu = s * odd_factorial(2 * mu - 1) * z**mu if mu == nu: return p_nu @@ -275,8 +283,10 @@ def legendre(nu, mu, x): try: from .legendre_ext import _legendre except ImportError: - raise ImportError('The legendre C extension module is missing.' \ - ' Fallback legendre code not yet implemented.') + raise ImportError( + "The legendre C extension module is missing." + " Fallback legendre code not yet implemented." + ) nu = int(nu) mu = int(mu) x = x.astype(np.float32) @@ -285,14 +295,16 @@ def legendre(nu, mu, x): def interp3(vol, R, C, S): - ''' + """ TODO - ''' + """ try: from .interp3_ext import _interp3 except ImportError: - raise ImportError('The interp3 C extension module is missing.' \ - ' Fallback interp3 code not yet implemented.') + raise ImportError( + "The interp3 C extension module is missing." + " Fallback interp3 code not yet implemented." + ) vol = vol.astype(np.float32) vol = np.ascontiguousarray(vol) R = R.astype(np.float32) @@ -305,29 +317,31 @@ def interp3(vol, R, C, S): return V -if __name__ == '__main__': - print('Testing meshgrid...') - #import doctest - #doctest.testmod() +if __name__ == "__main__": + print("Testing meshgrid...") + # import doctest + # doctest.testmod() - print('Profiling interp3...') - print('Interpolation for a million coordinates should be' \ - ' in the order of seconds. Anything significantly less ' \ - 'is considered slow') + print("Profiling interp3...") + print( + "Interpolation for a million coordinates should be" + " in the order of seconds. Anything significantly less " + "is considered slow" + ) import time + arr = np.linspace(-4, 4, 6000) arr = np.sin(arr) - arr = arr.reshape(10, 20, 30).astype('float32') + arr = arr.reshape(10, 20, 30).astype("float32") gridn = 1 for c in range(8): - R1 = np.linspace(4., 5., gridn).astype('float32') - C1 = np.linspace(11., 12., gridn).astype('float32') - S1 = np.linspace(15., 16., gridn).astype('float32') + R1 = np.linspace(4.0, 5.0, gridn).astype("float32") + C1 = np.linspace(11.0, 12.0, gridn).astype("float32") + S1 = np.linspace(15.0, 16.0, gridn).astype("float32") tic = time.time() v1 = interp3(arr, R1, C1, S1) if gridn == 10 or gridn == 1: print(v1) toc = time.time() - print("1 followed by %d zeros" % c, "|", gridn, "|", \ - toc - tic, "seconds") + print("1 followed by %d zeros" % c, "|", gridn, "|", toc - tic, "seconds") gridn = gridn * 10 diff --git a/setup.py b/setup.py index 118421d..0b930f3 100644 --- a/setup.py +++ b/setup.py @@ -2,24 +2,34 @@ from numpy.distutils.misc_util import get_numpy_include_dirs import os, sys -mods = ['gradunwarp.core.coeffs', 'gradunwarp.core.globals', - 'gradunwarp.core.__init__', 'gradunwarp.__init__', - 'gradunwarp.core.utils', - 'gradunwarp.core.unwarp_resample', - 'gradunwarp.core.gradient_unwarp', ] +mods = [ + "gradunwarp.core.coeffs", + "gradunwarp.core.globals", + "gradunwarp.core.__init__", + "gradunwarp.__init__", + "gradunwarp.core.utils", + "gradunwarp.core.unwarp_resample", + "gradunwarp.core.gradient_unwarp", +] -scripts_cmd = ['gradunwarp/core/gradient_unwarp.py',] +scripts_cmd = [ + "gradunwarp/core/gradient_unwarp.py", +] -def configuration(parent_package='', top_path=None): + +def configuration(parent_package="", top_path=None): from numpy.distutils.misc_util import Configuration - config = Configuration('',parent_package,top_path) + + config = Configuration("", parent_package, top_path) return config -setup(name='gradunwarp', - version = '1.3.0+HCP', - description = 'HCP version of Gradient Unwarping Package for Python/Numpy', - author = 'Human Connectome Project', - py_modules = mods, - scripts = scripts_cmd, - configuration=configuration, - ) + +setup( + name="gradunwarp", + version="1.3.0+HCP", + description="HCP version of Gradient Unwarping Package for Python/Numpy", + author="Human Connectome Project", + py_modules=mods, + scripts=scripts_cmd, + configuration=configuration, +) From afca423275ba13902b08dd68c92ccba4606426e6 Mon Sep 17 00:00:00 2001 From: bpinsard Date: Tue, 15 Aug 2023 14:21:16 -0400 Subject: [PATCH 06/15] remove distutils --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 0b930f3..3e3a768 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,4 @@ -from numpy.distutils.core import setup, Extension -from numpy.distutils.misc_util import get_numpy_include_dirs +from setuptools import setup import os, sys mods = [ From 67d77a1e9e5b7fe47dc6165eea55b04ffb2142e2 Mon Sep 17 00:00:00 2001 From: bpinsard Date: Tue, 22 Aug 2023 15:46:36 -0400 Subject: [PATCH 07/15] remove deps to fsl, orientation part to be reviewed --- gradunwarp/core/gradient_unwarp.py | 6 ++-- gradunwarp/core/unwarp_resample.py | 52 +++++++----------------------- 2 files changed, 14 insertions(+), 44 deletions(-) diff --git a/gradunwarp/core/gradient_unwarp.py b/gradunwarp/core/gradient_unwarp.py index 3920ac9..d73f720 100755 --- a/gradunwarp/core/gradient_unwarp.py +++ b/gradunwarp/core/gradient_unwarp.py @@ -126,10 +126,10 @@ def run(self): else: self.coeffs = coeffs.get_coefficients(self.args.vendor, self.args.coeffile) - self.vol, self.m_rcs2ras = utils.get_vol_affine(self.args.infile) - + self.input_nii = nib.load(self.args.infile) + self.unwarper = Unwarper( - self.vol, self.m_rcs2ras, self.args.vendor, self.coeffs, self.args.infile + self.input_nii, self.args.vendor, self.coeffs, self.args.infile ) if hasattr(self.args, "fovmin") and self.args.fovmin: self.unwarper.fovmin = float(self.args.fovmin) diff --git a/gradunwarp/core/unwarp_resample.py b/gradunwarp/core/unwarp_resample.py index 6ef9355..372eaa0 100644 --- a/gradunwarp/core/unwarp_resample.py +++ b/gradunwarp/core/unwarp_resample.py @@ -20,6 +20,7 @@ import nibabel as nib import subprocess import scipy.special +import nibabel as nb # np.seterr(all='raise') @@ -29,8 +30,10 @@ class Unwarper(object): """ """ - def __init__(self, vol, m_rcs2ras, vendor, coeffs, fileName): + def __init__(self, input_nii, vendor, coeffs, fileName): """ """ + self.input_nii = input_nii + self.vol, self.m_rcs2ras = self.input_nii.get_fdata(), self.input_nii.affine self.vol = vol self.m_rcs2ras = m_rcs2ras self.vendor = vendor @@ -202,46 +205,13 @@ def non_linear_unwarp_siemens( vc, vr = utils.meshgrid(np.arange(nc), np.arange(nr)) # Compute transform to map the internal voxel coordinates to FSL scaled mm coordinates - try: - pixdim1 = float( - ( - subprocess.Popen( - ["fslval", self.name, "pixdim1"], stdout=subprocess.PIPE - ).communicate()[0] - ).strip() - ) - except ValueError: - log.error( - "Failure during fslval call. Make sure fslval, fslhd, fslorient are in your PATH, and that FSLOUTPUTTYPE is set." - ) - sys.exit(1) - - pixdim2 = float( - ( - subprocess.Popen( - ["fslval", self.name, "pixdim2"], stdout=subprocess.PIPE - ).communicate()[0] - ).strip() - ) - pixdim3 = float( - ( - subprocess.Popen( - ["fslval", self.name, "pixdim3"], stdout=subprocess.PIPE - ).communicate()[0] - ).strip() - ) - dim1 = float( - ( - subprocess.Popen( - ["fslval", self.name, "dim1"], stdout=subprocess.PIPE - ).communicate()[0] - ).strip() - ) - outputOrient = ( - subprocess.Popen(["fslorient", self.name], stdout=subprocess.PIPE) - .communicate()[0] - .strip() - ) + pixdim1, pixdim2, pixdim3 = self.input_nii.header.get('pixdim')[1:4] + + dim1 = self.input_nii.header.get('dim')[1] + axcodes = ''.join(nb.orientations.aff2axcodes(nii.affine)) + + # TODO: figure out if that logic works + outputOrient = 'NEUROLOGICAL' if axcodes in ['RAS', 'RPI', 'LPS', 'LAI'] else 'RADIOLOGICAL' if outputOrient == b"NEUROLOGICAL": log.info( "Input volume is NEUROLOGICAL orientation. Flipping x-axis in output fullWarp_abs.nii.gz" From ea644cf325f66961050ed669409fcb67a3333c5a Mon Sep 17 00:00:00 2001 From: bpinsard Date: Tue, 22 Aug 2023 15:56:05 -0400 Subject: [PATCH 08/15] add dockerfile and run script --- Dockerfile | 10 + docker/procGradCorrect | 190 +++++++++++++++++++ docker/run.sh | 420 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 620 insertions(+) create mode 100644 Dockerfile create mode 100644 docker/procGradCorrect create mode 100644 docker/run.sh diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5a6d0e1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.8-slim + +COPY . /gradunwarp + +RUN cd /gradunwarp \ + && python setup.py install + +ENV PATH /gradunwarp/docker:$PATH + +ENTRYPOINT ["/bin/bash", "/gradunwarp/docker/run.sh"] diff --git a/docker/procGradCorrect b/docker/procGradCorrect new file mode 100644 index 0000000..bfec52f --- /dev/null +++ b/docker/procGradCorrect @@ -0,0 +1,190 @@ +#!/bin/bash + + +#function for unwarping and generating warp files +function usage { + echo "Performs gradient unwarping and saves relevant output" + echo "" + echo "Required args:" + echo " -i input_nii" + echo " -g input_grad_coeff" + echo "" + echo "Optional args:" + echo " -u output_unwarped_nii" + echo " -c output_unwarped_intcorr_nii" + echo " -w output_warp_nii (apply with FSL applywarp --abs)" + echo " -j output_jacobian_determinant_nii (multiply for intensity correction)" + echo " -d output_grad_dev_nii (for DWI)" + echo " -s scratch_dir (default `pwd`/gradcorr)" + echo " -k (keep scratch directory , default removed afterwards)" + echo "" + echo "Grad-unwarp options" + echo " -I interporder" + echo " -N numpoints" + echo " -F fovmin (from -fovmin to +fovmin)" + + +} + +if [ "$#" -lt 3 ] +then + usage + exit 1 +fi + +in_nii= +grad_coeff= +scratch_dir=`pwd`/gradcorr +unwarped_nii= +unwarped_intcorr_nii= +warp_nii= +detjac_nii= +grad_dev_nii= +keep_scratch=0 +fovmin=0.2 #in metres +numpoints=150 +interporder=3 #cubic + + +while getopts "i:g:s:kw:u:c:j:d:I:N:F:" options; do + case $options in + i ) echo " Input nii $OPTARG" + in_nii=$OPTARG;; + g ) echo " Input grad_coeff $OPTARG" + grad_coeff=$OPTARG;; + s ) echo " Using scratch dir $OPTARG" + scratch_dir=$OPTARG;; + k ) echo " Keeping scratch dir" + keep_scratch=1;; + w ) echo " Saving warp nii to $OPTARG " + warp_nii=$OPTARG;; + u ) echo " Saving unwarped nii to $OPTARG" + unwarped_nii=$OPTARG;; + c ) echo " Saving unwarped, intensity-corrected nii to $OPTARG" + unwarped_intcorr_nii=$OPTARG;; + j ) echo " Saving warp detjac nii to $OPTARG" + detjac_nii=$OPTARG;; + d ) echo " Saving warp grad_dev nii to $OPTARG" + grad_dev_nii=$OPTARG;; + I ) echo " Interp order $OPTARG" + interporder=$OPTARG;; + N ) echo " Numpoints $OPTARG" + numpoints=$OPTARG;; + F ) echo " fovmin $OPTARG" + fovmin=$OPTARG;; + + * ) usage + exit 1;; + esac +done + +if [ ! -n "$in_nii" -o ! -n "$grad_coeff" ] +then + usage + exit 1 +fi + +#CAVEAT -- works only for nii.gz, not nii + +if [ ! -e $in_nii ] +then + echo "Input image $in_nii does not exist!" + exit 1 +fi + +if [ ! -e $grad_coeff ] +then + echo "Input grad coeff $grad_coeff does not exist!" + exit 1 +fi + + + +if [ ! -e $scratch_dir ] +then + mkdir -p $scratch_dir +fi + +in=input.nii.gz +coeff=coeff.grad +unwarped=input_unwarped.nii.gz +unwarped_intcorr=input_unwarped_intcorr.nii.gz +detjac=detjac.nii.gz +warp=fullWarp_abs.nii.gz +grad_dev=grad_dev.nii.gz +grad_dev_prefix=grad_dev + +ext=${in_nii##*.} +if [ "$ext" = "nii" ] +then + cp -v --no-preserve mode $in_nii $scratch_dir/input.nii + gzip $scratch_dir/input.nii + +elif [ "$ext" = "gz" ] +then + cp -v --no-preserve mode $in_nii $scratch_dir/$in +else + echo "Unknown file extension, $ext, for $in_nii" + exit 1 +fi + +cp -v --no-preserve mode $grad_coeff $scratch_dir/$coeff + +pushd $scratch_dir + + + #run gradunwarp + gradient_unwarp.py $in $unwarped siemens -g $coeff -n --fovmin -$fovmin --fovmax $fovmin --numpoints $numpoints --interp_order $interporder --verbose + + #get jacobian determinant + reg_jacobian -ref $unwarped -def $warp -jac $detjac + fslmaths $detjac -mul -1 -abs $detjac + + #modulate by det jac (intensity correction) + fslmaths $unwarped -mul $detjac $unwarped_intcorr + + if [ -n "$grad_dev_nii" ] + then + #gen grad_dev (for dwi) + calc_grad_perc_dev --fullwarp=$warp --out=$grad_dev_prefix + fslmerge -t $grad_dev ${grad_dev_prefix}_x ${grad_dev_prefix}_y ${grad_dev_prefix}_z + fslmaths $grad_dev -div 100 $grad_dev +fi +popd + +#copy files to output paths +if [ -n "$unwarped_nii" ] +then +cp -v $scratch_dir/$unwarped $unwarped_nii +fi + +if [ -n "$unwarped_intcorr_nii" ] +then +cp -v $scratch_dir/$unwarped_intcorr $unwarped_intcorr_nii +fi + +if [ -n "$warp_nii" ] +then +cp -v $scratch_dir/$warp $warp_nii +fi + +if [ -n "$detjac_nii" ] +then +cp -v $scratch_dir/$detjac $detjac_nii +fi + +if [ -n "$grad_dev_nii" ] +then +cp -v $scratch_dir/$grad_dev $grad_dev_nii +fi + +if [ -n "$unwarped_nii" ] +then +cp -v $scratch_dir/$unwarped $unwarped_nii +fi + +if [ "$keep_scratch" = 0 ] +then + rm -f $scratch_dir/* + rmdir $scratch_dir +fi diff --git a/docker/run.sh b/docker/run.sh new file mode 100644 index 0000000..5328657 --- /dev/null +++ b/docker/run.sh @@ -0,0 +1,420 @@ +#!/bin/bash + + +#bids app to gradunwarp, creating mirror of input bids structure + + +#for all nifti files that are not already corrected: +# save unwarped identically-named file in output folder +# copy json,bvec,bval,tsv files from anat/func/fmap/dwi +# copy json,tsv from main folder + +#create sourcedata/gradcorrect folder for: +# save warpfiles as {prefix}_target-nativeGC_warp.nii.gz +# save detjac as {prefix}_target-nativeGC_detjac.nii.gz + + + +function die { + echo $1 >&2 + exit 1 +} + +participant_label= + +if [ "$#" -lt 3 ] +then + echo "Usage: gradcorrect bids_dir output_dir {participant,group} " + echo " Required arguments:" + echo " [--grad_coeff_file GRAD_COEFF_FILE] (required)" + echo "" + echo " Optional arguments:" + echo " [--participant_label PARTICIPANT_LABEL [PARTICIPANT_LABEL...]]" + echo " [--only_matching SEARCHSTRING ] ( e.g.: use 2RAGE to only convert *2RAGE* images, e.g. MP2RAGE and SA2RAGE)" + echo "" + exit 1 +fi + + +in_bids=$1 +out_folder=$2 +analysis_level=$3 +grad_coeff_file= + +searchstring=* + + +fovmin=0.2 +numpoints=150 +interporder=3 + +shift 3 + + + +while :; do + case $1 in + -h|-\?|--help) + usage + exit + ;; + --participant_label ) # takes an option argument; ensure it has been specified. + if [ "$2" ]; then + participant_label=$2 + shift + else + die 'error: "--participant" requires a non-empty option argument.' + fi + ;; + --participant_label=?*) + participant_label=${1#*=} # delete everything up to "=" and assign the remainder. + ;; + --participant_label=) # handle the case of an empty --participant= + die 'error: "--participant_label" requires a non-empty option argument.' + ;; + + --grad_coeff_file ) # takes an option argument; ensure it has been specified. + if [ "$2" ]; then + grad_coeff_file=$2 + shift + else + die 'error: "--grad_coeff_file" requires a non-empty option argument.' + fi + ;; + --grad_coeff_file=?*) + grad_coeff_file=${1#*=} # delete everything up to "=" and assign the remainder. + ;; + --grad_coeff_file=) # handle the case of an empty --participant= + die 'error: "--grad_coeff_file" requires a non-empty option argument.' + ;; + + + + --only_matching ) # takes an option argument; ensure it has been specified. + if [ "$2" ]; then + only_matching=$2 + shift + else + die 'error: "--only_matching" requires a non-empty option argument.' + fi + ;; + --only_matching=?*) + only_matching=${1#*=} # delete everything up to "=" and assign the remainder. + ;; + --only_matching=) # handle the case of an empty --participant= + die 'error: "--only_matching" requires a non-empty option argument.' + ;; + + + + -?*) + printf 'WARN: Unknown option (ignored): %s\n' "$1" >&2 + ;; + *) # Default case: No more options, so break out of the loop. + break + esac + + shift + done + + +shift $((OPTIND-1)) + +if [ -n "$only_matching" ] +then + searchstring=*${only_matching}* +fi + +if [ -e $in_bids ] +then + in_bids=`realpath $in_bids` +else + echo "ERROR: bids_dir $in_bids does not exist!" + exit 1 +fi + + +if [ "$analysis_level" = "participant" ] +then + echo " running participant level analysis" + else + echo "only participant level analysis is enabled" + exit 0 +fi + +if [ -n "$grad_coeff_file" ] +then + if [ -e $grad_coeff_file ] + then + grad_coeff_file=`realpath $grad_coeff_file` + else + echo "ERROR: --grad_coeff_file $grad_coeff_file does not exist!" + exit 1 + fi + +else + echo "ERROR: --grad_coeff_file is a required argument" + exit 1 +fi + + +participants=$in_bids/participants.tsv + +scratch_dir=$out_folder/sourcedata/scratch +sourcedata=$out_folder/sourcedata/gradcorrect + +mkdir -p $scratch_dir $sourcedata + +scratch_dir=`realpath $scratch_dir` +sourcedata=`realpath $sourcedata` +out_folder=`realpath $out_folder` + +if [ ! -e $participants ] +then + #participants tsv not required by bids, so if it doesn't exist, create one for temporary use + participants=$scratch_dir/participants.tsv + echo participant_id > $participants + pushd $in_bids + ls -d sub-* >> $participants + popd +fi + +echo $participants + + +if [ -n "$participant_label" ] +then +subjlist=`echo $participant_label | sed 's/,/\ /g'` +else +subjlist=`tail -n +2 $participants | awk '{print $1}'` +fi + +for subj in $subjlist +do + +#add on sub- if not exists +if [ ! "${subj:0:4}" = "sub-" ] +then + subj="sub-$subj" +fi + +pushd $in_bids + +#for every nifti: +for nii in `ls $subj/{anat,func,fmap,dwi,asl}/${searchstring}.nii.gz $subj/*/{anat,func,fmap,dwi,asl}/${searchstring}.nii.gz` +do + + folder=${nii%/*} + file=${nii##*/} + file_noext=${file%.nii*} + filetype=${file_noext##*_} + fileprefix=${file_noext%_*} + + if echo $file | grep -q DIS + then + echo "$file already gradient distortion corrected from scanner, skipping..." + continue + fi + + mkdir -p $out_folder/$folder $sourcedata/$folder + + #keep best unwarped in the main folder (to mirror input bids structure) + out_unwarped=$out_folder/$folder/${file} + + #intermediate files + intermediate_3d=$sourcedata/$folder/${fileprefix}_${filetype}_3dvol.nii.gz + + #extra files (keep in sourcedata) + out_warp=$sourcedata/$folder/${fileprefix}_${filetype}_target-nativeGC_warp.nii.gz + out_nointcorr=$sourcedata/$folder/${fileprefix}_${filetype}_nodetjac.nii.gz + out_detjac=$sourcedata/$folder/${fileprefix}_${filetype}_target-nativeGC_warpdetjac.nii.gz + out_graddev=$sourcedata/$folder/${fileprefix}_${filetype}_target-nativeGC_graddev.nii.gz + out_inpaintmask=$sourcedata/$folder/${fileprefix}_${filetype}_inpaintMask.nii.gz + + if [ -e $out_unwarped ] + then + echo "$file already gradient distortion corrected by this app, skipping..." + echo " to force re-processing: rm $out_unwarped" + continue + fi + + + if [ "`fslval $nii dim4`" -lt 2 ] + then + echo "3D volume, using as is" + dimension=3 + in_vol=$nii + else + dimension=4 + echo "4D volume, extracting 1st 3D vol" + #extract 3d vol for procGradCorrect + echo fslroi $nii $intermediate_3d 0 1 + fslroi $nii $intermediate_3d 0 1 + in_vol=$intermediate_3d + fi + + + #now, to avoid re-computing warps that are the same for different images, use a hash of the nii sform to store a link to the out_warp + echo "sform for $in_vol is `fslorient -getsform $in_vol`" + hash=`fslorient -getsform $in_vol | cksum | cut -f 1 -d ' '` + + existing_warp=$scratch_dir/$subj.$hash.warp.nii.gz + existing_detjac=$scratch_dir/$subj.$hash.detjac.nii.gz + existing_graddev=$scratch_dir/$subj.$hash.graddev.nii.gz + + echo "hashed file for out_warp is: $existing_warp" + if [ ! -e $existing_warp ] + then + echo "existing_warp doesn't exist, so going to run procGradCorrect" + + #generic command to generate output warp + cmd="procGradCorrect -i $in_vol -g $grad_coeff_file -s $scratch_dir/$subj -w $out_warp -j $out_detjac -F $fovmin -N $numpoints -I $interporder" + + #add graddev for dwi + if [ "$filetype" = "dwi" ] + then + cmd="$cmd -d $out_graddev" + fi + + if [ ! -e $out_warp ] + then + echo $cmd + $cmd + fi + + #now that warp is computed, keep a reference to it + echo linking $out_warp as $existing_warp + ln -s $out_warp $existing_warp + ln -s $out_detjac $existing_detjac + if [ "$filetype" = "dwi" ] + then + ln -s $out_graddev $existing_graddev + fi + else + #existing_warp exists, copy it to output + echo copying to $out_warp from $existing_warp + cp -Lv $existing_warp $out_warp + cp -Lv $existing_detjac $out_detjac + if [ "$filetype" = "dwi" ] + then + cp -Lv $existing_graddev $out_graddev + fi + + fi + + #now, at this point, out_warp exists, so apply as required: + + if echo $file | grep -qE 'part-phase|part-comb|phasediff' + then + #phase image, skip detjac normalization, and use nearest neighbout (interporder=0) + echo phase image, using nn + applyinterp=nn + isphase=1 + else + echo non-phase image, using spline + applyinterp=spline + isphase=0 + fi + + if echo $file | grep -qE 'T1map|MP2RAGE|SA2RAGE' + then + echo mp2rage/sa2rage/t1map image, using spline, but skipping jac modulation + applyinterp=spline + isqmap=1 + else + isqmap=0 + fi + + #remove extra file + rm -vf $intermediate_3d + + #this applies warp to input nifti if 4d + echo "applying warp to $nii using $applyinterp interpolation with $out_warp" + echo applywarp -i $nii -o $out_nointcorr -w $out_warp --abs --interp=$applyinterp -r $nii + applywarp -i $nii -o $out_nointcorr -w $out_warp --abs --interp=$applyinterp -r $nii + + + if [ "$isphase" = "0" ] + then + if [ "$isqmap" = 1 ] + then + cp -v $out_nointcorr $out_unwarped + else + echo "non-phase image, doing detjac modulation and cubic spline overshoot correction" + #detjac modulation + echo fslmaths $out_nointcorr -mul $out_detjac $out_unwarped + fslmaths $out_nointcorr -mul $out_detjac $out_unwarped + + fi + #perform correction of cubic spline overshoot + inpaint_iters=3 + echo fslmaths $out_unwarped -thr 0 $out_inpaintmask + fslmaths $out_unwarped -thr 0 $out_inpaintmask + echo "starting inpainting at `date`" + echo ImageMath $dimension $out_unwarped InPaint $out_inpaintmask $inpaint_iters + ImageMath $dimension $out_unwarped InPaint $out_inpaintmask $inpaint_iters + echo "done inpainting at `date`" + + else + echo "phase image, so skipping detjac modulation and cubic spline correction (since nn interp)" + cp -v $out_nointcorr $out_unwarped + fi + + echo "settings datatype to short and resetting header from original image" + + #ensure final unwarped (out_unwarped) is same datatype and geom as input (assuming mr images are input type short) + echo fslmaths $out_unwarped $out_unwarped -odt short + fslmaths $out_unwarped $out_unwarped -odt short + echo fslcpgeom $nii $out_unwarped + fslcpgeom $nii $out_unwarped + + + #copy extra files + for ext in json bvec bval tsv + do + if [ -e $folder/${file_noext}.$ext ] + then + cp -v --no-preserve mode $folder/${file_noext}.$ext $out_folder/$folder + fi + done + + +done #nii + +#TODO: add check if existing first to avoid errors in log + +for otherfile in `ls ./*.{tsv,json} ./.bidsignore $subj/${searchstring}.{tsv,json} $subj/*/${searchstring}.{tsv,json}` +do + folder=${otherfile%/*} + file=${otherfile##*/} + + if echo $file | grep -q DIS + then + continue + fi + + cp -v --no-preserve mode $otherfile $out_folder/$folder/$file + +done + +popd + + +#TODO: add check if existing first to avoid errors in log + +for otherfile in `ls ./${searchstring}.{tsv,json}` +do + folder=${otherfile%/*} + file=${otherfile##*/} + if echo $file | grep -q DIS + then + continue + fi + + cp -v --no-preserve mode $otherfile $out_folder/$folder/$file + +done + +#TO DO: remove *DIS* scans from the _scans.tsv file + +done #subj From b87a29227057e19bb1021e4facd2e231854cc740 Mon Sep 17 00:00:00 2001 From: bpinsard Date: Wed, 23 Aug 2023 10:17:14 -0400 Subject: [PATCH 09/15] fixes --- gradunwarp/core/gradient_unwarp.py | 5 +++-- gradunwarp/core/unwarp_resample.py | 10 +++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/gradunwarp/core/gradient_unwarp.py b/gradunwarp/core/gradient_unwarp.py index d73f720..2f225fd 100755 --- a/gradunwarp/core/gradient_unwarp.py +++ b/gradunwarp/core/gradient_unwarp.py @@ -10,6 +10,7 @@ import logging from gradunwarp.core import globals, coeffs, utils from gradunwarp.core.unwarp_resample import Unwarper +import nibabel as nb log = globals.get_logger() @@ -126,8 +127,8 @@ def run(self): else: self.coeffs = coeffs.get_coefficients(self.args.vendor, self.args.coeffile) - self.input_nii = nib.load(self.args.infile) - + self.input_nii = nb.load(self.args.infile) + self.unwarper = Unwarper( self.input_nii, self.args.vendor, self.coeffs, self.args.infile ) diff --git a/gradunwarp/core/unwarp_resample.py b/gradunwarp/core/unwarp_resample.py index 372eaa0..6085979 100644 --- a/gradunwarp/core/unwarp_resample.py +++ b/gradunwarp/core/unwarp_resample.py @@ -34,8 +34,6 @@ def __init__(self, input_nii, vendor, coeffs, fileName): """ """ self.input_nii = input_nii self.vol, self.m_rcs2ras = self.input_nii.get_fdata(), self.input_nii.affine - self.vol = vol - self.m_rcs2ras = m_rcs2ras self.vendor = vendor self.coeffs = coeffs self.name = fileName @@ -208,7 +206,7 @@ def non_linear_unwarp_siemens( pixdim1, pixdim2, pixdim3 = self.input_nii.header.get('pixdim')[1:4] dim1 = self.input_nii.header.get('dim')[1] - axcodes = ''.join(nb.orientations.aff2axcodes(nii.affine)) + axcodes = ''.join(nb.orientations.aff2axcodes(self.input_nii.affine)) # TODO: figure out if that logic works outputOrient = 'NEUROLOGICAL' if axcodes in ['RAS', 'RPI', 'LPS', 'LAI'] else 'RADIOLOGICAL' @@ -325,8 +323,7 @@ def non_linear_unwarp_siemens( print() - img = nib.Nifti1Image(fullWarp, self.m_rcs2ras) - nib.save(img, "fullWarp_abs.nii.gz") + self.fullWarp = nib.Nifti1Image(fullWarp, self.m_rcs2ras) # return image and the jacobian del vrcsw, vfsl, vxyzw, vrcs, vxyz, vrcsg, fullWarp return out, vjacout @@ -343,6 +340,9 @@ def write(self, outfile): img = nib.MGHImage(self.out, self.m_rcs2ras) nib.save(img, outfile) + suffix = outfile.split('_')[-1] + nib.save(self.fullWarp, outfile.replace(suffix, f"desc-{suffix.replace('.nii.gz','')}_warp.nii.gz")) + def eval_siemens_jacobian_mult(F, dxyz): """ """ From feb577cb2c5938626edf5d74e6edc62a23733475 Mon Sep 17 00:00:00 2001 From: bpinsard Date: Wed, 23 Aug 2023 10:19:30 -0400 Subject: [PATCH 10/15] fix Dockerfile --- Dockerfile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5a6d0e1..c049e15 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,8 +3,9 @@ FROM python:3.8-slim COPY . /gradunwarp RUN cd /gradunwarp \ - && python setup.py install + && pip install -r requirements.txt \ + && pip install . ENV PATH /gradunwarp/docker:$PATH -ENTRYPOINT ["/bin/bash", "/gradunwarp/docker/run.sh"] +ENTRYPOINT ["/usr/local/bin/gradient_unwarp.py"] From 9f9c41ac40663aed5b3b1c15be3747f4b9a547ea Mon Sep 17 00:00:00 2001 From: bpinsard Date: Wed, 23 Aug 2023 10:21:20 -0400 Subject: [PATCH 11/15] remove gradcorrect files --- docker/procGradCorrect | 190 ------------------- docker/run.sh | 420 ----------------------------------------- 2 files changed, 610 deletions(-) delete mode 100644 docker/procGradCorrect delete mode 100644 docker/run.sh diff --git a/docker/procGradCorrect b/docker/procGradCorrect deleted file mode 100644 index bfec52f..0000000 --- a/docker/procGradCorrect +++ /dev/null @@ -1,190 +0,0 @@ -#!/bin/bash - - -#function for unwarping and generating warp files -function usage { - echo "Performs gradient unwarping and saves relevant output" - echo "" - echo "Required args:" - echo " -i input_nii" - echo " -g input_grad_coeff" - echo "" - echo "Optional args:" - echo " -u output_unwarped_nii" - echo " -c output_unwarped_intcorr_nii" - echo " -w output_warp_nii (apply with FSL applywarp --abs)" - echo " -j output_jacobian_determinant_nii (multiply for intensity correction)" - echo " -d output_grad_dev_nii (for DWI)" - echo " -s scratch_dir (default `pwd`/gradcorr)" - echo " -k (keep scratch directory , default removed afterwards)" - echo "" - echo "Grad-unwarp options" - echo " -I interporder" - echo " -N numpoints" - echo " -F fovmin (from -fovmin to +fovmin)" - - -} - -if [ "$#" -lt 3 ] -then - usage - exit 1 -fi - -in_nii= -grad_coeff= -scratch_dir=`pwd`/gradcorr -unwarped_nii= -unwarped_intcorr_nii= -warp_nii= -detjac_nii= -grad_dev_nii= -keep_scratch=0 -fovmin=0.2 #in metres -numpoints=150 -interporder=3 #cubic - - -while getopts "i:g:s:kw:u:c:j:d:I:N:F:" options; do - case $options in - i ) echo " Input nii $OPTARG" - in_nii=$OPTARG;; - g ) echo " Input grad_coeff $OPTARG" - grad_coeff=$OPTARG;; - s ) echo " Using scratch dir $OPTARG" - scratch_dir=$OPTARG;; - k ) echo " Keeping scratch dir" - keep_scratch=1;; - w ) echo " Saving warp nii to $OPTARG " - warp_nii=$OPTARG;; - u ) echo " Saving unwarped nii to $OPTARG" - unwarped_nii=$OPTARG;; - c ) echo " Saving unwarped, intensity-corrected nii to $OPTARG" - unwarped_intcorr_nii=$OPTARG;; - j ) echo " Saving warp detjac nii to $OPTARG" - detjac_nii=$OPTARG;; - d ) echo " Saving warp grad_dev nii to $OPTARG" - grad_dev_nii=$OPTARG;; - I ) echo " Interp order $OPTARG" - interporder=$OPTARG;; - N ) echo " Numpoints $OPTARG" - numpoints=$OPTARG;; - F ) echo " fovmin $OPTARG" - fovmin=$OPTARG;; - - * ) usage - exit 1;; - esac -done - -if [ ! -n "$in_nii" -o ! -n "$grad_coeff" ] -then - usage - exit 1 -fi - -#CAVEAT -- works only for nii.gz, not nii - -if [ ! -e $in_nii ] -then - echo "Input image $in_nii does not exist!" - exit 1 -fi - -if [ ! -e $grad_coeff ] -then - echo "Input grad coeff $grad_coeff does not exist!" - exit 1 -fi - - - -if [ ! -e $scratch_dir ] -then - mkdir -p $scratch_dir -fi - -in=input.nii.gz -coeff=coeff.grad -unwarped=input_unwarped.nii.gz -unwarped_intcorr=input_unwarped_intcorr.nii.gz -detjac=detjac.nii.gz -warp=fullWarp_abs.nii.gz -grad_dev=grad_dev.nii.gz -grad_dev_prefix=grad_dev - -ext=${in_nii##*.} -if [ "$ext" = "nii" ] -then - cp -v --no-preserve mode $in_nii $scratch_dir/input.nii - gzip $scratch_dir/input.nii - -elif [ "$ext" = "gz" ] -then - cp -v --no-preserve mode $in_nii $scratch_dir/$in -else - echo "Unknown file extension, $ext, for $in_nii" - exit 1 -fi - -cp -v --no-preserve mode $grad_coeff $scratch_dir/$coeff - -pushd $scratch_dir - - - #run gradunwarp - gradient_unwarp.py $in $unwarped siemens -g $coeff -n --fovmin -$fovmin --fovmax $fovmin --numpoints $numpoints --interp_order $interporder --verbose - - #get jacobian determinant - reg_jacobian -ref $unwarped -def $warp -jac $detjac - fslmaths $detjac -mul -1 -abs $detjac - - #modulate by det jac (intensity correction) - fslmaths $unwarped -mul $detjac $unwarped_intcorr - - if [ -n "$grad_dev_nii" ] - then - #gen grad_dev (for dwi) - calc_grad_perc_dev --fullwarp=$warp --out=$grad_dev_prefix - fslmerge -t $grad_dev ${grad_dev_prefix}_x ${grad_dev_prefix}_y ${grad_dev_prefix}_z - fslmaths $grad_dev -div 100 $grad_dev -fi -popd - -#copy files to output paths -if [ -n "$unwarped_nii" ] -then -cp -v $scratch_dir/$unwarped $unwarped_nii -fi - -if [ -n "$unwarped_intcorr_nii" ] -then -cp -v $scratch_dir/$unwarped_intcorr $unwarped_intcorr_nii -fi - -if [ -n "$warp_nii" ] -then -cp -v $scratch_dir/$warp $warp_nii -fi - -if [ -n "$detjac_nii" ] -then -cp -v $scratch_dir/$detjac $detjac_nii -fi - -if [ -n "$grad_dev_nii" ] -then -cp -v $scratch_dir/$grad_dev $grad_dev_nii -fi - -if [ -n "$unwarped_nii" ] -then -cp -v $scratch_dir/$unwarped $unwarped_nii -fi - -if [ "$keep_scratch" = 0 ] -then - rm -f $scratch_dir/* - rmdir $scratch_dir -fi diff --git a/docker/run.sh b/docker/run.sh deleted file mode 100644 index 5328657..0000000 --- a/docker/run.sh +++ /dev/null @@ -1,420 +0,0 @@ -#!/bin/bash - - -#bids app to gradunwarp, creating mirror of input bids structure - - -#for all nifti files that are not already corrected: -# save unwarped identically-named file in output folder -# copy json,bvec,bval,tsv files from anat/func/fmap/dwi -# copy json,tsv from main folder - -#create sourcedata/gradcorrect folder for: -# save warpfiles as {prefix}_target-nativeGC_warp.nii.gz -# save detjac as {prefix}_target-nativeGC_detjac.nii.gz - - - -function die { - echo $1 >&2 - exit 1 -} - -participant_label= - -if [ "$#" -lt 3 ] -then - echo "Usage: gradcorrect bids_dir output_dir {participant,group} " - echo " Required arguments:" - echo " [--grad_coeff_file GRAD_COEFF_FILE] (required)" - echo "" - echo " Optional arguments:" - echo " [--participant_label PARTICIPANT_LABEL [PARTICIPANT_LABEL...]]" - echo " [--only_matching SEARCHSTRING ] ( e.g.: use 2RAGE to only convert *2RAGE* images, e.g. MP2RAGE and SA2RAGE)" - echo "" - exit 1 -fi - - -in_bids=$1 -out_folder=$2 -analysis_level=$3 -grad_coeff_file= - -searchstring=* - - -fovmin=0.2 -numpoints=150 -interporder=3 - -shift 3 - - - -while :; do - case $1 in - -h|-\?|--help) - usage - exit - ;; - --participant_label ) # takes an option argument; ensure it has been specified. - if [ "$2" ]; then - participant_label=$2 - shift - else - die 'error: "--participant" requires a non-empty option argument.' - fi - ;; - --participant_label=?*) - participant_label=${1#*=} # delete everything up to "=" and assign the remainder. - ;; - --participant_label=) # handle the case of an empty --participant= - die 'error: "--participant_label" requires a non-empty option argument.' - ;; - - --grad_coeff_file ) # takes an option argument; ensure it has been specified. - if [ "$2" ]; then - grad_coeff_file=$2 - shift - else - die 'error: "--grad_coeff_file" requires a non-empty option argument.' - fi - ;; - --grad_coeff_file=?*) - grad_coeff_file=${1#*=} # delete everything up to "=" and assign the remainder. - ;; - --grad_coeff_file=) # handle the case of an empty --participant= - die 'error: "--grad_coeff_file" requires a non-empty option argument.' - ;; - - - - --only_matching ) # takes an option argument; ensure it has been specified. - if [ "$2" ]; then - only_matching=$2 - shift - else - die 'error: "--only_matching" requires a non-empty option argument.' - fi - ;; - --only_matching=?*) - only_matching=${1#*=} # delete everything up to "=" and assign the remainder. - ;; - --only_matching=) # handle the case of an empty --participant= - die 'error: "--only_matching" requires a non-empty option argument.' - ;; - - - - -?*) - printf 'WARN: Unknown option (ignored): %s\n' "$1" >&2 - ;; - *) # Default case: No more options, so break out of the loop. - break - esac - - shift - done - - -shift $((OPTIND-1)) - -if [ -n "$only_matching" ] -then - searchstring=*${only_matching}* -fi - -if [ -e $in_bids ] -then - in_bids=`realpath $in_bids` -else - echo "ERROR: bids_dir $in_bids does not exist!" - exit 1 -fi - - -if [ "$analysis_level" = "participant" ] -then - echo " running participant level analysis" - else - echo "only participant level analysis is enabled" - exit 0 -fi - -if [ -n "$grad_coeff_file" ] -then - if [ -e $grad_coeff_file ] - then - grad_coeff_file=`realpath $grad_coeff_file` - else - echo "ERROR: --grad_coeff_file $grad_coeff_file does not exist!" - exit 1 - fi - -else - echo "ERROR: --grad_coeff_file is a required argument" - exit 1 -fi - - -participants=$in_bids/participants.tsv - -scratch_dir=$out_folder/sourcedata/scratch -sourcedata=$out_folder/sourcedata/gradcorrect - -mkdir -p $scratch_dir $sourcedata - -scratch_dir=`realpath $scratch_dir` -sourcedata=`realpath $sourcedata` -out_folder=`realpath $out_folder` - -if [ ! -e $participants ] -then - #participants tsv not required by bids, so if it doesn't exist, create one for temporary use - participants=$scratch_dir/participants.tsv - echo participant_id > $participants - pushd $in_bids - ls -d sub-* >> $participants - popd -fi - -echo $participants - - -if [ -n "$participant_label" ] -then -subjlist=`echo $participant_label | sed 's/,/\ /g'` -else -subjlist=`tail -n +2 $participants | awk '{print $1}'` -fi - -for subj in $subjlist -do - -#add on sub- if not exists -if [ ! "${subj:0:4}" = "sub-" ] -then - subj="sub-$subj" -fi - -pushd $in_bids - -#for every nifti: -for nii in `ls $subj/{anat,func,fmap,dwi,asl}/${searchstring}.nii.gz $subj/*/{anat,func,fmap,dwi,asl}/${searchstring}.nii.gz` -do - - folder=${nii%/*} - file=${nii##*/} - file_noext=${file%.nii*} - filetype=${file_noext##*_} - fileprefix=${file_noext%_*} - - if echo $file | grep -q DIS - then - echo "$file already gradient distortion corrected from scanner, skipping..." - continue - fi - - mkdir -p $out_folder/$folder $sourcedata/$folder - - #keep best unwarped in the main folder (to mirror input bids structure) - out_unwarped=$out_folder/$folder/${file} - - #intermediate files - intermediate_3d=$sourcedata/$folder/${fileprefix}_${filetype}_3dvol.nii.gz - - #extra files (keep in sourcedata) - out_warp=$sourcedata/$folder/${fileprefix}_${filetype}_target-nativeGC_warp.nii.gz - out_nointcorr=$sourcedata/$folder/${fileprefix}_${filetype}_nodetjac.nii.gz - out_detjac=$sourcedata/$folder/${fileprefix}_${filetype}_target-nativeGC_warpdetjac.nii.gz - out_graddev=$sourcedata/$folder/${fileprefix}_${filetype}_target-nativeGC_graddev.nii.gz - out_inpaintmask=$sourcedata/$folder/${fileprefix}_${filetype}_inpaintMask.nii.gz - - if [ -e $out_unwarped ] - then - echo "$file already gradient distortion corrected by this app, skipping..." - echo " to force re-processing: rm $out_unwarped" - continue - fi - - - if [ "`fslval $nii dim4`" -lt 2 ] - then - echo "3D volume, using as is" - dimension=3 - in_vol=$nii - else - dimension=4 - echo "4D volume, extracting 1st 3D vol" - #extract 3d vol for procGradCorrect - echo fslroi $nii $intermediate_3d 0 1 - fslroi $nii $intermediate_3d 0 1 - in_vol=$intermediate_3d - fi - - - #now, to avoid re-computing warps that are the same for different images, use a hash of the nii sform to store a link to the out_warp - echo "sform for $in_vol is `fslorient -getsform $in_vol`" - hash=`fslorient -getsform $in_vol | cksum | cut -f 1 -d ' '` - - existing_warp=$scratch_dir/$subj.$hash.warp.nii.gz - existing_detjac=$scratch_dir/$subj.$hash.detjac.nii.gz - existing_graddev=$scratch_dir/$subj.$hash.graddev.nii.gz - - echo "hashed file for out_warp is: $existing_warp" - if [ ! -e $existing_warp ] - then - echo "existing_warp doesn't exist, so going to run procGradCorrect" - - #generic command to generate output warp - cmd="procGradCorrect -i $in_vol -g $grad_coeff_file -s $scratch_dir/$subj -w $out_warp -j $out_detjac -F $fovmin -N $numpoints -I $interporder" - - #add graddev for dwi - if [ "$filetype" = "dwi" ] - then - cmd="$cmd -d $out_graddev" - fi - - if [ ! -e $out_warp ] - then - echo $cmd - $cmd - fi - - #now that warp is computed, keep a reference to it - echo linking $out_warp as $existing_warp - ln -s $out_warp $existing_warp - ln -s $out_detjac $existing_detjac - if [ "$filetype" = "dwi" ] - then - ln -s $out_graddev $existing_graddev - fi - else - #existing_warp exists, copy it to output - echo copying to $out_warp from $existing_warp - cp -Lv $existing_warp $out_warp - cp -Lv $existing_detjac $out_detjac - if [ "$filetype" = "dwi" ] - then - cp -Lv $existing_graddev $out_graddev - fi - - fi - - #now, at this point, out_warp exists, so apply as required: - - if echo $file | grep -qE 'part-phase|part-comb|phasediff' - then - #phase image, skip detjac normalization, and use nearest neighbout (interporder=0) - echo phase image, using nn - applyinterp=nn - isphase=1 - else - echo non-phase image, using spline - applyinterp=spline - isphase=0 - fi - - if echo $file | grep -qE 'T1map|MP2RAGE|SA2RAGE' - then - echo mp2rage/sa2rage/t1map image, using spline, but skipping jac modulation - applyinterp=spline - isqmap=1 - else - isqmap=0 - fi - - #remove extra file - rm -vf $intermediate_3d - - #this applies warp to input nifti if 4d - echo "applying warp to $nii using $applyinterp interpolation with $out_warp" - echo applywarp -i $nii -o $out_nointcorr -w $out_warp --abs --interp=$applyinterp -r $nii - applywarp -i $nii -o $out_nointcorr -w $out_warp --abs --interp=$applyinterp -r $nii - - - if [ "$isphase" = "0" ] - then - if [ "$isqmap" = 1 ] - then - cp -v $out_nointcorr $out_unwarped - else - echo "non-phase image, doing detjac modulation and cubic spline overshoot correction" - #detjac modulation - echo fslmaths $out_nointcorr -mul $out_detjac $out_unwarped - fslmaths $out_nointcorr -mul $out_detjac $out_unwarped - - fi - #perform correction of cubic spline overshoot - inpaint_iters=3 - echo fslmaths $out_unwarped -thr 0 $out_inpaintmask - fslmaths $out_unwarped -thr 0 $out_inpaintmask - echo "starting inpainting at `date`" - echo ImageMath $dimension $out_unwarped InPaint $out_inpaintmask $inpaint_iters - ImageMath $dimension $out_unwarped InPaint $out_inpaintmask $inpaint_iters - echo "done inpainting at `date`" - - else - echo "phase image, so skipping detjac modulation and cubic spline correction (since nn interp)" - cp -v $out_nointcorr $out_unwarped - fi - - echo "settings datatype to short and resetting header from original image" - - #ensure final unwarped (out_unwarped) is same datatype and geom as input (assuming mr images are input type short) - echo fslmaths $out_unwarped $out_unwarped -odt short - fslmaths $out_unwarped $out_unwarped -odt short - echo fslcpgeom $nii $out_unwarped - fslcpgeom $nii $out_unwarped - - - #copy extra files - for ext in json bvec bval tsv - do - if [ -e $folder/${file_noext}.$ext ] - then - cp -v --no-preserve mode $folder/${file_noext}.$ext $out_folder/$folder - fi - done - - -done #nii - -#TODO: add check if existing first to avoid errors in log - -for otherfile in `ls ./*.{tsv,json} ./.bidsignore $subj/${searchstring}.{tsv,json} $subj/*/${searchstring}.{tsv,json}` -do - folder=${otherfile%/*} - file=${otherfile##*/} - - if echo $file | grep -q DIS - then - continue - fi - - cp -v --no-preserve mode $otherfile $out_folder/$folder/$file - -done - -popd - - -#TODO: add check if existing first to avoid errors in log - -for otherfile in `ls ./${searchstring}.{tsv,json}` -do - folder=${otherfile%/*} - file=${otherfile##*/} - if echo $file | grep -q DIS - then - continue - fi - - cp -v --no-preserve mode $otherfile $out_folder/$folder/$file - -done - -#TO DO: remove *DIS* scans from the _scans.tsv file - -done #subj From 730251e46c1103c80bf85680307191898d8fd45e Mon Sep 17 00:00:00 2001 From: bpinsard Date: Wed, 23 Aug 2023 10:34:30 -0400 Subject: [PATCH 12/15] cleanup --- Dockerfile | 2 -- gradunwarp/core/unwarp_resample.py | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index c049e15..2e0aa4b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,4 @@ RUN cd /gradunwarp \ && pip install -r requirements.txt \ && pip install . -ENV PATH /gradunwarp/docker:$PATH - ENTRYPOINT ["/usr/local/bin/gradient_unwarp.py"] diff --git a/gradunwarp/core/unwarp_resample.py b/gradunwarp/core/unwarp_resample.py index 6085979..fff3ffa 100644 --- a/gradunwarp/core/unwarp_resample.py +++ b/gradunwarp/core/unwarp_resample.py @@ -20,7 +20,6 @@ import nibabel as nib import subprocess import scipy.special -import nibabel as nb # np.seterr(all='raise') @@ -206,7 +205,7 @@ def non_linear_unwarp_siemens( pixdim1, pixdim2, pixdim3 = self.input_nii.header.get('pixdim')[1:4] dim1 = self.input_nii.header.get('dim')[1] - axcodes = ''.join(nb.orientations.aff2axcodes(self.input_nii.affine)) + axcodes = ''.join(nib.orientations.aff2axcodes(self.input_nii.affine)) # TODO: figure out if that logic works outputOrient = 'NEUROLOGICAL' if axcodes in ['RAS', 'RPI', 'LPS', 'LAI'] else 'RADIOLOGICAL' From e699128a9b1cedd37cc1bbf2fa19c57d5b60c97b Mon Sep 17 00:00:00 2001 From: bpinsard Date: Wed, 30 Aug 2023 10:57:26 -0400 Subject: [PATCH 13/15] pythonify fsl logic to get fsl orient --- gradunwarp/core/unwarp_resample.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/gradunwarp/core/unwarp_resample.py b/gradunwarp/core/unwarp_resample.py index fff3ffa..90c7ad5 100644 --- a/gradunwarp/core/unwarp_resample.py +++ b/gradunwarp/core/unwarp_resample.py @@ -205,10 +205,9 @@ def non_linear_unwarp_siemens( pixdim1, pixdim2, pixdim3 = self.input_nii.header.get('pixdim')[1:4] dim1 = self.input_nii.header.get('dim')[1] - axcodes = ''.join(nib.orientations.aff2axcodes(self.input_nii.affine)) # TODO: figure out if that logic works - outputOrient = 'NEUROLOGICAL' if axcodes in ['RAS', 'RPI', 'LPS', 'LAI'] else 'RADIOLOGICAL' + outputOrient = get_fslorient(self.input_nii) if outputOrient == b"NEUROLOGICAL": log.info( "Input volume is NEUROLOGICAL orientation. Flipping x-axis in output fullWarp_abs.nii.gz" @@ -463,3 +462,22 @@ def ge_D(alpha, beta, x1, y1, z1): d += f * _p * f2 d = d / 100.0 # cm back to meters return d + + + +def get_fslorient(nii): + h = nii.header + sform = h.get_sform() + sform_det = np.linalg.det(sform[:3,:3]) if sform is not None else None + + qform = h.get_qform() + qform_det = np.linalg.det(qform[:3,:3]) if qform is not None else None + + det = sform_det or qform_det + + if not sform_det and not qform_det: + return 'UNKNOWN' + elif sform_det and qform_det and (sform_det * qform_det) < 0 : + return 'INCONSISTENT' + else: + return 'RADIOLOGICAL' if det < 0 else 'NEUROLOGICAL' From c54239ebd764a6d6f629f0f61bb6c2d2d8b0ef23 Mon Sep 17 00:00:00 2001 From: Basile Date: Wed, 30 Aug 2023 17:11:47 -0400 Subject: [PATCH 14/15] Update gradunwarp/core/unwarp_resample.py Co-authored-by: Chris Markiewicz --- gradunwarp/core/unwarp_resample.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gradunwarp/core/unwarp_resample.py b/gradunwarp/core/unwarp_resample.py index 90c7ad5..1da3234 100644 --- a/gradunwarp/core/unwarp_resample.py +++ b/gradunwarp/core/unwarp_resample.py @@ -207,8 +207,7 @@ def non_linear_unwarp_siemens( dim1 = self.input_nii.header.get('dim')[1] # TODO: figure out if that logic works - outputOrient = get_fslorient(self.input_nii) - if outputOrient == b"NEUROLOGICAL": + if np.linalg.det(self.input_nii.affine) > 0: log.info( "Input volume is NEUROLOGICAL orientation. Flipping x-axis in output fullWarp_abs.nii.gz" ) From 60863a90a3f17ca5cdcbe9bdd942507bf70e333d Mon Sep 17 00:00:00 2001 From: Basile Date: Fri, 15 Sep 2023 16:10:43 -0400 Subject: [PATCH 15/15] Update gradunwarp/core/unwarp_resample.py Co-authored-by: Chris Markiewicz --- gradunwarp/core/unwarp_resample.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/gradunwarp/core/unwarp_resample.py b/gradunwarp/core/unwarp_resample.py index 1da3234..46f3bc6 100644 --- a/gradunwarp/core/unwarp_resample.py +++ b/gradunwarp/core/unwarp_resample.py @@ -202,9 +202,8 @@ def non_linear_unwarp_siemens( vc, vr = utils.meshgrid(np.arange(nc), np.arange(nr)) # Compute transform to map the internal voxel coordinates to FSL scaled mm coordinates - pixdim1, pixdim2, pixdim3 = self.input_nii.header.get('pixdim')[1:4] - - dim1 = self.input_nii.header.get('dim')[1] + pixdim1, pixdim2, pixdim3 = self.input_nii.header.get_zooms()[:3] + dim1 = self.input_nii.shape[0] # TODO: figure out if that logic works if np.linalg.det(self.input_nii.affine) > 0: