-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathnifti.py
executable file
·239 lines (196 loc) · 7.88 KB
/
nifti.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Tools for dealing with Nifti volumes
"""
import os
import numpy as np
import nibabel as nib
# Workaround for pickling error affecting nibabel, caused by bug in older
# versions of indexed_gzip
# https://github.com/pauldmccarthy/indexed_gzip/issues/28
# https://github.com/nipy/nibabel/issues/969#issuecomment-729206375
try:
import indexed_gzip
except ImportError:
pass
else:
from packaging.version import parse as parse_version
if parse_version(indexed_gzip.__version__) < parse_version('1.1.0'):
import gzip
nib.openers.HAVE_INDEXED_GZIP = False
nib.openers.IndexedGzipFile = gzip.GzipFile
class QuickMasker(object):
"""
Simple class for loading a mask and (inverse) transforming data through it.
Loosely based on nilearn's MultiNiftiMasker, but without all the bells and
whistles so it runs faster.
Arguments
---------
mask : str or Nifti1Image object
Mask image. Can be a path to a NIFTI file or a nibabel Nifti1Image
object.
mask2 : str, NiftiImage object, or None
Only used if .[inverse_]transform() methods are called with invert_mask
set to True. If supplied, will cause this to take only voxels from
this secondary mask that don't overlap with the primary mask. If
omitted, will take voxels from whole volume outside the primary mask.
Methods
-------
* ``transform`` : Load data and apply mask
* ``inverse_transform`` : Create new NIFTI image from masked data
Example useage
--------------
>>> masker = QuickMasker('/path/to/mask.nii.gz')
>>> ROI_data = masker.transform('/path/to/data.nii.gz')
>>> masker.inverse_trasnform(ROI_data) \\
... .to_filename('/path/to/masked_data.nii.gz')
"""
def __init__(self, mask, mask2=None):
# Assign args to class
self.mask = mask
self.mask2 = mask2
# Load primary mask
self.mask_img, self.mask_array = self._load_mask(self.mask)
# Load secondary mask?
if self.mask2 is not None:
_, self.mask_array2 = self._load_mask(self.mask2)
else:
self.mask_array2 = None
@staticmethod
def _load_mask(mask):
if isinstance(mask, str) and os.path.isfile(mask):
mask_img = nib.load(mask)
elif isinstance(mask, nib.Nifti1Image):
mask_img = mask
else:
raise TypeError('Mask must be valid filepath or Nifti1Image object')
mask_array = mask_img.get_fdata().astype(int)
mask_img.uncache()
return mask_img, mask_array
@staticmethod
def _load_data(img, dtype):
if isinstance(img, str) and os.path.isfile(img):
data = nib.load(img).get_fdata(dtype=dtype)
elif isinstance(img, nib.Nifti1Image):
data = img.get_fdata(dtype=dtype)
elif isinstance(img, np.ndarray):
data = img.astype(dtype)
else:
raise TypeError('img must be valid filepath, Nifti1Image object, '
'or numpy array')
return data
def transform(self, imgs, labelID=None, invert_mask=False, vstack=False,
dtype=np.float64):
"""
Load data from imgfile and apply mask.
Arguments
---------
imgs : str, Nifti1Image object, ndarray, or list thereof
Input data. Can be path(s) to a NIFTI file, nibabel Nifti1Image
object(s), or 3/4D numpy array(s) containing data values.
labelID : int or None
Numeric value of label within mask to use, in case multiple labels
contained within mask. If None (default), use all non-zero labels.
invert_mask : bool
If True, load from voxels OUTSIDE of mask instead
(default = False)
vstack : bool
If True, concatenate data arrays over input files (default = False)
dtype : valid data type
Type to cast data to (default is float64).
Returns
-------
data : 1D or 2D ndarray
Masked data, provided as an [nVoxels] 1D array if input is 3D,
or an [nSamples x nVoxels] 2D array if input is 4D. If multiple
inputs are provided, will be a list of each result if vstack is
False, or an array concatenating the results over the samples
axis if vstack is True.
"""
# Setup
if not isinstance(imgs, (tuple, list)):
imgs = [imgs]
if labelID is None:
mask = self.mask_array.astype(bool)
else:
mask = self.mask_array == labelID
if invert_mask:
mask = ~mask
if self.mask_array2 is not None:
mask = mask & self.mask_array2.astype(bool)
# Load data for each image and apply mask
data = [self._load_data(img, dtype)[mask].T for img in imgs]
# Stack data over images?
if vstack:
data = np.vstack(data)
elif len(data) == 1:
data = data[0]
# Return
return data
def inverse_transform(self, data, labelID=None, invert_mask=False,
dtype=np.float32, return_as_nii=True, header=None,
affine=None, extra=None):
"""
"Unmask" data array
Arguments
---------
data : 1D or 2D ndarray
[nVoxels] 1D or [nSamples x nVoxels] 2D nNumpy array containing
data. Unmasked array will be 3D if data is 1D, or 4D if data is 2D.
labelID : int or None
Numeric value of label within mask to use, in case multiple labels
contained within mask. If None (default), use all non-zero labels.
Should match value supplied to forward transformation.
invert_mask : boolean
Use inverted version of primary mask (default = False). Should
match value supplied to forward transformation.
dtype : valid data type
Type to cast data to (default is float32).
return_as_nii : bool
If True (default), return as Nifti1Image object. If False, return
numpy array.
header : Nifti1Header or None
Header information for NIFTI. If None, will take from mask. Note
that header datatype will always be updated to match specified
dtype. Ignored if return_as_nii is False.
affine : 2D array or None
Affine information for NIFTI. If None, will take from mask. Ignored
if return_as_nii is False.
extra : dict or None
Extra metadata for NIFTI. If None, will take from mask. Ignored if
return_as_nii is False.
Returns
-------
new_img : Nifti1Image or ndarray
Unmasked data in requested format.
"""
# Setup
if labelID is None:
mask = self.mask_array.astype(bool)
else:
mask = self.mask_array == labelID
if invert_mask:
mask = ~mask
if self.mask_array2 is not None:
mask = mask & self.mask_array2.astype(bool)
# Allocate new array and populate mask region with data
dims = list(self.mask_img.shape)
if data.ndim == 2:
dims.append(data.shape[0])
inv_data = np.zeros(dims, dtype=dtype)
inv_data[mask] = data.T
# Convert to NiftiImage if requested, and return
if return_as_nii:
if header is None:
header = self.mask_img.header.copy()
if affine is None:
affine = self.mask_img.affine.copy()
if extra is None:
extra = self.mask_img.extra.copy()
header.set_data_dtype(dtype)
new_img = nib.Nifti1Image(inv_data, affine, header, extra)
new_img.update_header()
return new_img
else:
return inv_data