nimare 0.4.2rc4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- benchmarks/__init__.py +0 -0
- benchmarks/bench_cbma.py +57 -0
- nimare/__init__.py +45 -0
- nimare/_version.py +21 -0
- nimare/annotate/__init__.py +21 -0
- nimare/annotate/cogat.py +213 -0
- nimare/annotate/gclda.py +924 -0
- nimare/annotate/lda.py +147 -0
- nimare/annotate/text.py +75 -0
- nimare/annotate/utils.py +87 -0
- nimare/base.py +217 -0
- nimare/cli.py +124 -0
- nimare/correct.py +462 -0
- nimare/dataset.py +685 -0
- nimare/decode/__init__.py +33 -0
- nimare/decode/base.py +115 -0
- nimare/decode/continuous.py +462 -0
- nimare/decode/discrete.py +753 -0
- nimare/decode/encode.py +110 -0
- nimare/decode/utils.py +44 -0
- nimare/diagnostics.py +510 -0
- nimare/estimator.py +139 -0
- nimare/extract/__init__.py +19 -0
- nimare/extract/extract.py +466 -0
- nimare/extract/utils.py +295 -0
- nimare/generate.py +331 -0
- nimare/io.py +635 -0
- nimare/meta/__init__.py +39 -0
- nimare/meta/cbma/__init__.py +6 -0
- nimare/meta/cbma/ale.py +951 -0
- nimare/meta/cbma/base.py +947 -0
- nimare/meta/cbma/mkda.py +1361 -0
- nimare/meta/cbmr.py +970 -0
- nimare/meta/ibma.py +1683 -0
- nimare/meta/kernel.py +501 -0
- nimare/meta/models.py +1199 -0
- nimare/meta/utils.py +494 -0
- nimare/nimads.py +492 -0
- nimare/reports/__init__.py +24 -0
- nimare/reports/base.py +664 -0
- nimare/reports/default.yml +123 -0
- nimare/reports/figures.py +651 -0
- nimare/reports/report.tpl +160 -0
- nimare/resources/__init__.py +1 -0
- nimare/resources/atlases/Harvard-Oxford-LICENSE +93 -0
- nimare/resources/atlases/HarvardOxford-cort-maxprob-thr25-2mm.nii.gz +0 -0
- nimare/resources/database_file_manifest.json +142 -0
- nimare/resources/english_spellings.csv +1738 -0
- nimare/resources/filenames.json +32 -0
- nimare/resources/neurosynth_laird_studies.json +58773 -0
- nimare/resources/neurosynth_stoplist.txt +396 -0
- nimare/resources/nidm_pain_dset.json +1349 -0
- nimare/resources/references.bib +541 -0
- nimare/resources/semantic_knowledge_children.txt +325 -0
- nimare/resources/semantic_relatedness_children.txt +249 -0
- nimare/resources/templates/MNI152_2x2x2_brainmask.nii.gz +0 -0
- nimare/resources/templates/tpl-MNI152NLin6Asym_res-01_T1w.nii.gz +0 -0
- nimare/resources/templates/tpl-MNI152NLin6Asym_res-01_desc-brain_mask.nii.gz +0 -0
- nimare/resources/templates/tpl-MNI152NLin6Asym_res-02_T1w.nii.gz +0 -0
- nimare/resources/templates/tpl-MNI152NLin6Asym_res-02_desc-brain_mask.nii.gz +0 -0
- nimare/results.py +225 -0
- nimare/stats.py +276 -0
- nimare/tests/__init__.py +1 -0
- nimare/tests/conftest.py +229 -0
- nimare/tests/data/amygdala_roi.nii.gz +0 -0
- nimare/tests/data/data-neurosynth_version-7_coordinates.tsv.gz +0 -0
- nimare/tests/data/data-neurosynth_version-7_metadata.tsv.gz +0 -0
- nimare/tests/data/data-neurosynth_version-7_vocab-terms_source-abstract_type-tfidf_features.npz +0 -0
- nimare/tests/data/data-neurosynth_version-7_vocab-terms_vocabulary.txt +100 -0
- nimare/tests/data/neurosynth_dset.json +2868 -0
- nimare/tests/data/neurosynth_laird_studies.json +58773 -0
- nimare/tests/data/nidm_pain_dset.json +1349 -0
- nimare/tests/data/nimads_annotation.json +1 -0
- nimare/tests/data/nimads_studyset.json +1 -0
- nimare/tests/data/test_baseline.txt +2 -0
- nimare/tests/data/test_pain_dataset.json +1278 -0
- nimare/tests/data/test_pain_dataset_multiple_contrasts.json +1242 -0
- nimare/tests/data/test_sleuth_file.txt +18 -0
- nimare/tests/data/test_sleuth_file2.txt +10 -0
- nimare/tests/data/test_sleuth_file3.txt +5 -0
- nimare/tests/data/test_sleuth_file4.txt +5 -0
- nimare/tests/data/test_sleuth_file5.txt +5 -0
- nimare/tests/test_annotate_cogat.py +32 -0
- nimare/tests/test_annotate_gclda.py +86 -0
- nimare/tests/test_annotate_lda.py +27 -0
- nimare/tests/test_dataset.py +99 -0
- nimare/tests/test_decode_continuous.py +132 -0
- nimare/tests/test_decode_discrete.py +92 -0
- nimare/tests/test_diagnostics.py +168 -0
- nimare/tests/test_estimator_performance.py +385 -0
- nimare/tests/test_extract.py +46 -0
- nimare/tests/test_generate.py +247 -0
- nimare/tests/test_io.py +240 -0
- nimare/tests/test_meta_ale.py +298 -0
- nimare/tests/test_meta_cbmr.py +295 -0
- nimare/tests/test_meta_ibma.py +240 -0
- nimare/tests/test_meta_kernel.py +209 -0
- nimare/tests/test_meta_mkda.py +234 -0
- nimare/tests/test_nimads.py +21 -0
- nimare/tests/test_reports.py +110 -0
- nimare/tests/test_stats.py +101 -0
- nimare/tests/test_transforms.py +272 -0
- nimare/tests/test_utils.py +200 -0
- nimare/tests/test_workflows.py +221 -0
- nimare/tests/utils.py +126 -0
- nimare/transforms.py +907 -0
- nimare/utils.py +1367 -0
- nimare/workflows/__init__.py +14 -0
- nimare/workflows/base.py +189 -0
- nimare/workflows/cbma.py +165 -0
- nimare/workflows/ibma.py +108 -0
- nimare/workflows/macm.py +77 -0
- nimare/workflows/misc.py +65 -0
- nimare-0.4.2rc4.dist-info/LICENSE +21 -0
- nimare-0.4.2rc4.dist-info/METADATA +124 -0
- nimare-0.4.2rc4.dist-info/RECORD +119 -0
- nimare-0.4.2rc4.dist-info/WHEEL +5 -0
- nimare-0.4.2rc4.dist-info/entry_points.txt +2 -0
- nimare-0.4.2rc4.dist-info/top_level.txt +2 -0
nimare/meta/utils.py
ADDED
@@ -0,0 +1,494 @@
|
|
1
|
+
"""Utilities for coordinate-based meta-analysis estimators."""
|
2
|
+
|
3
|
+
import warnings
|
4
|
+
|
5
|
+
import numpy as np
|
6
|
+
import sparse
|
7
|
+
from numba import jit
|
8
|
+
from scipy import ndimage
|
9
|
+
|
10
|
+
from nimare.utils import unique_rows
|
11
|
+
|
12
|
+
|
13
|
+
@jit(nopython=True, cache=True)
|
14
|
+
def _convolve_sphere(kernel, ijks, index, max_shape):
|
15
|
+
"""Convolve peaks with a spherical kernel.
|
16
|
+
|
17
|
+
Parameters
|
18
|
+
----------
|
19
|
+
kernel : 2D numpy.ndarray
|
20
|
+
IJK coordinates of a sphere, relative to a central point
|
21
|
+
(not the brain template).
|
22
|
+
peaks : 2D numpy.ndarray
|
23
|
+
The IJK coordinates of peaks to convolve with the kernel.
|
24
|
+
max_shape: 1D numpy.ndarray
|
25
|
+
The maximum shape of the image volume.
|
26
|
+
|
27
|
+
Returns
|
28
|
+
-------
|
29
|
+
sphere_coords : 2D numpy.ndarray
|
30
|
+
All coordinates that fall within any sphere.ß∑
|
31
|
+
Coordinates from overlapping spheres will appear twice.
|
32
|
+
"""
|
33
|
+
|
34
|
+
def np_all_axis1(x):
|
35
|
+
"""Numba compatible version of np.all(x, axis=1)."""
|
36
|
+
out = np.ones(x.shape[0], dtype=np.bool8)
|
37
|
+
for i in range(x.shape[1]):
|
38
|
+
out = np.logical_and(out, x[:, i])
|
39
|
+
return out
|
40
|
+
|
41
|
+
peaks = ijks[index]
|
42
|
+
sphere_coords = np.zeros((kernel.shape[1] * len(peaks), 3), dtype=np.int32)
|
43
|
+
chunk_idx = np.arange(0, (kernel.shape[1]), dtype=np.int64)
|
44
|
+
for peak in peaks:
|
45
|
+
sphere_coords[chunk_idx, :] = kernel.T + peak
|
46
|
+
chunk_idx = chunk_idx + kernel.shape[1]
|
47
|
+
|
48
|
+
# Mask coordinates beyond space
|
49
|
+
idx = np_all_axis1(np.logical_and(sphere_coords >= 0, np.less(sphere_coords, max_shape)))
|
50
|
+
|
51
|
+
return sphere_coords[idx, :]
|
52
|
+
|
53
|
+
|
54
|
+
def compute_kda_ma(
|
55
|
+
mask,
|
56
|
+
ijks,
|
57
|
+
r,
|
58
|
+
value=1.0,
|
59
|
+
exp_idx=None,
|
60
|
+
sum_overlap=False,
|
61
|
+
sum_across_studies=False,
|
62
|
+
):
|
63
|
+
"""Compute (M)KDA modeled activation (MA) map.
|
64
|
+
|
65
|
+
.. versionchanged:: 0.0.12
|
66
|
+
|
67
|
+
* Remove low-memory option in favor of sparse arrays.
|
68
|
+
* Return 4D sparse array.
|
69
|
+
* `shape` and `vox_dims` parameters have been removed. That information is now extracted
|
70
|
+
from the new parameter `mask`.
|
71
|
+
|
72
|
+
.. versionadded:: 0.0.4
|
73
|
+
|
74
|
+
Replaces the values around each focus in ijk with binary sphere.
|
75
|
+
|
76
|
+
Parameters
|
77
|
+
----------
|
78
|
+
mask : img_like
|
79
|
+
Mask to extract the MA maps shape (typically (91, 109, 91)) and voxel dimension.
|
80
|
+
The mask is applied the data coordinated before creating the kernel_data.
|
81
|
+
ijks : array-like
|
82
|
+
Indices of foci. Each row is a coordinate, with the three columns
|
83
|
+
corresponding to index in each of three dimensions.
|
84
|
+
r : :obj:`int`
|
85
|
+
Sphere radius, in mm.
|
86
|
+
value : :obj:`int`
|
87
|
+
Value for sphere.
|
88
|
+
exp_idx : array_like
|
89
|
+
Optional indices of experiments. If passed, must be of same length as
|
90
|
+
ijks. Each unique value identifies all coordinates in ijk that come from
|
91
|
+
the same experiment. If None passed, it is assumed that all coordinates
|
92
|
+
come from the same experiment.
|
93
|
+
sum_overlap : :obj:`bool`
|
94
|
+
Whether to sum voxel values in overlapping spheres.
|
95
|
+
sum_across_studies : :obj:`bool`
|
96
|
+
Whether to sum voxel values across studies.
|
97
|
+
|
98
|
+
Returns
|
99
|
+
-------
|
100
|
+
kernel_data : :obj:`sparse._coo.core.COO`
|
101
|
+
4D sparse array. If `exp_idx` is none, a 3d array in the same
|
102
|
+
shape as the `shape` argument is returned. If `exp_idx` is passed, a 4d array
|
103
|
+
is returned, where the first dimension has size equal to the number of
|
104
|
+
unique experiments, and the remaining 3 dimensions are equal to `shape`.
|
105
|
+
"""
|
106
|
+
if sum_overlap and sum_across_studies:
|
107
|
+
raise NotImplementedError("sum_overlap and sum_across_studies cannot both be True.")
|
108
|
+
|
109
|
+
# recast ijks to int32 to reduce memory footprint
|
110
|
+
ijks = ijks.astype(np.int32)
|
111
|
+
shape = mask.shape
|
112
|
+
vox_dims = mask.header.get_zooms()
|
113
|
+
|
114
|
+
mask_data = mask.get_fdata().astype(bool)
|
115
|
+
|
116
|
+
if exp_idx is None:
|
117
|
+
exp_idx = np.ones(len(ijks))
|
118
|
+
|
119
|
+
exp_idx_uniq, exp_idx = np.unique(exp_idx, return_inverse=True)
|
120
|
+
n_studies = len(exp_idx_uniq)
|
121
|
+
|
122
|
+
kernel_shape = (n_studies,) + shape
|
123
|
+
|
124
|
+
n_dim = ijks.shape[1]
|
125
|
+
xx, yy, zz = [slice(-r // vox_dims[i], r // vox_dims[i] + 0.01, 1) for i in range(n_dim)]
|
126
|
+
cube = np.vstack([row.ravel() for row in (np.mgrid[xx, yy, zz]).astype(np.int32)])
|
127
|
+
kernel = cube[:, np.sum(np.dot(np.diag(vox_dims), cube) ** 2, 0) ** 0.5 <= r]
|
128
|
+
|
129
|
+
if sum_across_studies:
|
130
|
+
all_values = np.zeros(shape, dtype=np.int32)
|
131
|
+
|
132
|
+
# Loop over experiments
|
133
|
+
for i_exp, _ in enumerate(exp_idx_uniq):
|
134
|
+
# Index peaks by experiment
|
135
|
+
curr_exp_idx = exp_idx == i_exp
|
136
|
+
sphere_coords = _convolve_sphere(kernel, ijks, curr_exp_idx, np.array(shape))
|
137
|
+
|
138
|
+
# preallocate array for current study
|
139
|
+
study_values = np.zeros(shape, dtype=np.int32)
|
140
|
+
study_values[sphere_coords[:, 0], sphere_coords[:, 1], sphere_coords[:, 2]] = value
|
141
|
+
|
142
|
+
# Sum across studies
|
143
|
+
all_values += study_values
|
144
|
+
|
145
|
+
# Only return values within the mask
|
146
|
+
all_values = all_values.reshape(-1)
|
147
|
+
kernel_data = all_values[mask_data.reshape(-1)]
|
148
|
+
|
149
|
+
else:
|
150
|
+
all_coords = []
|
151
|
+
# Loop over experiments
|
152
|
+
for i_exp, _ in enumerate(exp_idx_uniq):
|
153
|
+
curr_exp_idx = exp_idx == i_exp
|
154
|
+
# Convolve with sphere
|
155
|
+
all_spheres = _convolve_sphere(kernel, ijks, curr_exp_idx, np.array(shape))
|
156
|
+
|
157
|
+
if not sum_overlap:
|
158
|
+
all_spheres = unique_rows(all_spheres)
|
159
|
+
|
160
|
+
# Apply mask
|
161
|
+
sphere_idx_inside_mask = np.where(mask_data[tuple(all_spheres.T)])[0]
|
162
|
+
all_spheres = all_spheres[sphere_idx_inside_mask, :]
|
163
|
+
|
164
|
+
# Combine experiment id with coordinates
|
165
|
+
all_coords.append(all_spheres)
|
166
|
+
|
167
|
+
# Add exp_idx to coordinates
|
168
|
+
exp_shapes = [coords.shape[0] for coords in all_coords]
|
169
|
+
exp_indicator = np.repeat(np.arange(len(exp_shapes)), exp_shapes)
|
170
|
+
|
171
|
+
all_coords = np.vstack(all_coords).T
|
172
|
+
all_coords = np.insert(all_coords, 0, exp_indicator, axis=0)
|
173
|
+
|
174
|
+
kernel_data = sparse.COO(
|
175
|
+
all_coords, data=value, has_duplicates=sum_overlap, shape=kernel_shape
|
176
|
+
)
|
177
|
+
|
178
|
+
return kernel_data
|
179
|
+
|
180
|
+
|
181
|
+
def compute_ale_ma(mask, ijks, kernel=None, exp_idx=None, sample_sizes=None, use_dict=False):
|
182
|
+
"""Generate ALE modeled activation (MA) maps.
|
183
|
+
|
184
|
+
Replaces the values around each focus in ijk with the contrast-specific
|
185
|
+
kernel. Takes the element-wise maximum when looping through foci, which
|
186
|
+
accounts for foci which are near to one another and may have overlapping
|
187
|
+
kernels.
|
188
|
+
|
189
|
+
.. versionchanged:: 0.0.12
|
190
|
+
|
191
|
+
* This function now returns a 4D sparse array.
|
192
|
+
* `shape` parameter has been removed. That information is now extracted
|
193
|
+
from the new parameter `mask`.
|
194
|
+
* Replace `ijk` with `ijks`.
|
195
|
+
* New parameters: `exp_idx`, `sample_sizes`, and `use_dict`.
|
196
|
+
|
197
|
+
Parameters
|
198
|
+
----------
|
199
|
+
mask : img_like
|
200
|
+
Mask to extract the MA maps shape (typically (91, 109, 91)) and voxel dimension.
|
201
|
+
The mask is applied to the coordinates before creating the kernel_data.
|
202
|
+
ijks : array-like
|
203
|
+
Indices of foci. Each row is a coordinate, with the three columns
|
204
|
+
corresponding to index in each of three dimensions.
|
205
|
+
kernel : array-like, or None, optional
|
206
|
+
3D array of smoothing kernel. Typically of shape (30, 30, 30).
|
207
|
+
exp_idx : array_like
|
208
|
+
Optional indices of experiments. If passed, must be of same length as
|
209
|
+
ijks. Each unique value identifies all coordinates in ijk that come from
|
210
|
+
the same experiment. If None passed, it is assumed that all coordinates
|
211
|
+
come from the same experiment.
|
212
|
+
sample_sizes : array_like, :obj:`int` or None, optional
|
213
|
+
Array of smaple sizes or sample size, used to derive FWHM for Gaussian kernel.
|
214
|
+
use_dict : :obj:`bool`, optional
|
215
|
+
If True, empty kernels dictionary is used to retain the kernel for each element of
|
216
|
+
sample_sizes. If False and sample_sizes is int, the ale kernel is calculated for
|
217
|
+
sample_sizes. If False and sample_sizes is None, the unique kernels is used.
|
218
|
+
|
219
|
+
Returns
|
220
|
+
-------
|
221
|
+
kernel_data : :obj:`sparse._coo.core.COO`
|
222
|
+
4D sparse array. If `exp_idx` is none, a 3d array in the same
|
223
|
+
shape as the `shape` argument is returned. If `exp_idx` is passed, a 4d array
|
224
|
+
is returned, where the first dimension has size equal to the number of
|
225
|
+
unique experiments, and the remaining 3 dimensions are equal to `shape`.
|
226
|
+
"""
|
227
|
+
if use_dict:
|
228
|
+
if kernel is not None:
|
229
|
+
warnings.warn("The kernel provided will be replace by an empty dictionary.")
|
230
|
+
kernels = {} # retain kernels in dictionary to speed things up
|
231
|
+
if not isinstance(sample_sizes, np.ndarray):
|
232
|
+
raise ValueError("To use a kernel dictionary sample_sizes must be a list.")
|
233
|
+
elif sample_sizes is not None:
|
234
|
+
if not isinstance(sample_sizes, int):
|
235
|
+
raise ValueError("If use_dict is False, sample_sizes provided must be integer.")
|
236
|
+
else:
|
237
|
+
if kernel is None:
|
238
|
+
raise ValueError("3D array of smoothing kernel must be provided.")
|
239
|
+
|
240
|
+
if exp_idx is None:
|
241
|
+
exp_idx = np.ones(len(ijks))
|
242
|
+
|
243
|
+
shape = mask.shape
|
244
|
+
mask_data = mask.get_fdata().astype(bool)
|
245
|
+
|
246
|
+
exp_idx_uniq, exp_idx = np.unique(exp_idx, return_inverse=True)
|
247
|
+
n_studies = len(exp_idx_uniq)
|
248
|
+
|
249
|
+
kernel_shape = (n_studies,) + shape
|
250
|
+
all_exp = []
|
251
|
+
all_coords = []
|
252
|
+
all_data = []
|
253
|
+
for i_exp, _ in enumerate(exp_idx_uniq):
|
254
|
+
# Index peaks by experiment
|
255
|
+
curr_exp_idx = exp_idx == i_exp
|
256
|
+
ijk = ijks[curr_exp_idx]
|
257
|
+
|
258
|
+
if use_dict:
|
259
|
+
# Get sample_size from input
|
260
|
+
sample_size = sample_sizes[curr_exp_idx][0]
|
261
|
+
if sample_size not in kernels.keys():
|
262
|
+
_, kernel = get_ale_kernel(mask, sample_size=sample_size)
|
263
|
+
kernels[sample_size] = kernel
|
264
|
+
else:
|
265
|
+
kernel = kernels[sample_size]
|
266
|
+
elif sample_sizes is not None:
|
267
|
+
_, kernel = get_ale_kernel(mask, sample_size=sample_sizes)
|
268
|
+
|
269
|
+
mid = int(np.floor(kernel.shape[0] / 2.0))
|
270
|
+
mid1 = mid + 1
|
271
|
+
ma_values = np.zeros(shape)
|
272
|
+
for j_peak in range(ijk.shape[0]):
|
273
|
+
i, j, k = ijk[j_peak, :]
|
274
|
+
xl = max(i - mid, 0)
|
275
|
+
xh = min(i + mid1, ma_values.shape[0])
|
276
|
+
yl = max(j - mid, 0)
|
277
|
+
yh = min(j + mid1, ma_values.shape[1])
|
278
|
+
zl = max(k - mid, 0)
|
279
|
+
zh = min(k + mid1, ma_values.shape[2])
|
280
|
+
xlk = mid - (i - xl)
|
281
|
+
xhk = mid - (i - xh)
|
282
|
+
ylk = mid - (j - yl)
|
283
|
+
yhk = mid - (j - yh)
|
284
|
+
zlk = mid - (k - zl)
|
285
|
+
zhk = mid - (k - zh)
|
286
|
+
|
287
|
+
if (
|
288
|
+
(xl >= 0)
|
289
|
+
& (xh >= 0)
|
290
|
+
& (yl >= 0)
|
291
|
+
& (yh >= 0)
|
292
|
+
& (zl >= 0)
|
293
|
+
& (zh >= 0)
|
294
|
+
& (xlk >= 0)
|
295
|
+
& (xhk >= 0)
|
296
|
+
& (ylk >= 0)
|
297
|
+
& (yhk >= 0)
|
298
|
+
& (zlk >= 0)
|
299
|
+
& (zhk >= 0)
|
300
|
+
):
|
301
|
+
ma_values[xl:xh, yl:yh, zl:zh] = np.maximum(
|
302
|
+
ma_values[xl:xh, yl:yh, zl:zh], kernel[xlk:xhk, ylk:yhk, zlk:zhk]
|
303
|
+
)
|
304
|
+
# Set voxel outside the mask to zero.
|
305
|
+
ma_values[~mask_data] = 0
|
306
|
+
nonzero_idx = np.where(ma_values > 0)
|
307
|
+
|
308
|
+
all_exp.append(np.full(nonzero_idx[0].shape[0], i_exp))
|
309
|
+
all_coords.append(np.vstack(nonzero_idx))
|
310
|
+
all_data.append(ma_values[nonzero_idx])
|
311
|
+
|
312
|
+
exp = np.hstack(all_exp)
|
313
|
+
coords = np.vstack((exp.flatten(), np.hstack(all_coords)))
|
314
|
+
data = np.hstack(all_data).flatten()
|
315
|
+
|
316
|
+
kernel_data = sparse.COO(coords, data, shape=kernel_shape)
|
317
|
+
|
318
|
+
return kernel_data
|
319
|
+
|
320
|
+
|
321
|
+
def get_ale_kernel(img, sample_size=None, fwhm=None):
|
322
|
+
"""Estimate 3D Gaussian and sigma (in voxels) for ALE kernel given sample size or fwhm."""
|
323
|
+
if sample_size is not None and fwhm is not None:
|
324
|
+
raise ValueError('Only one of "sample_size" and "fwhm" may be specified')
|
325
|
+
elif sample_size is None and fwhm is None:
|
326
|
+
raise ValueError('Either "sample_size" or "fwhm" must be provided')
|
327
|
+
elif sample_size is not None:
|
328
|
+
uncertain_templates = (
|
329
|
+
5.7 / (2.0 * np.sqrt(2.0 / np.pi)) * np.sqrt(8.0 * np.log(2.0))
|
330
|
+
) # pylint: disable=no-member
|
331
|
+
# Assuming 11.6 mm ED between matching points
|
332
|
+
uncertain_subjects = (11.6 / (2 * np.sqrt(2 / np.pi)) * np.sqrt(8 * np.log(2))) / np.sqrt(
|
333
|
+
sample_size
|
334
|
+
) # pylint: disable=no-member
|
335
|
+
fwhm = np.sqrt(uncertain_subjects**2 + uncertain_templates**2)
|
336
|
+
|
337
|
+
fwhm_vox = fwhm / np.sqrt(np.prod(img.header.get_zooms()))
|
338
|
+
sigma_vox = (
|
339
|
+
fwhm_vox * np.sqrt(2.0) / (np.sqrt(2.0 * np.log(2.0)) * 2.0)
|
340
|
+
) # pylint: disable=no-member
|
341
|
+
|
342
|
+
data = np.zeros((31, 31, 31))
|
343
|
+
mid = int(np.floor(data.shape[0] / 2.0))
|
344
|
+
data[mid, mid, mid] = 1.0
|
345
|
+
kernel = ndimage.gaussian_filter(data, sigma_vox, mode="constant")
|
346
|
+
|
347
|
+
# Crop kernel to drop surrounding zeros
|
348
|
+
mn = np.min(np.where(kernel > np.spacing(1))[0])
|
349
|
+
mx = np.max(np.where(kernel > np.spacing(1))[0])
|
350
|
+
kernel = kernel[mn : mx + 1, mn : mx + 1, mn : mx + 1]
|
351
|
+
mid = int(np.floor(data.shape[0] / 2.0))
|
352
|
+
return sigma_vox, kernel
|
353
|
+
|
354
|
+
|
355
|
+
def _get_last_bin(arr1d):
|
356
|
+
"""Index the last location in a 1D array with a non-zero value."""
|
357
|
+
if np.any(arr1d):
|
358
|
+
last_bin = np.where(arr1d)[0][-1]
|
359
|
+
|
360
|
+
else:
|
361
|
+
last_bin = 0
|
362
|
+
|
363
|
+
return last_bin
|
364
|
+
|
365
|
+
|
366
|
+
def _calculate_cluster_measures(arr3d, threshold, conn, tail="upper"):
|
367
|
+
"""Calculate maximum cluster mass and size for an array.
|
368
|
+
|
369
|
+
This method assesses both positive and negative clusters.
|
370
|
+
|
371
|
+
Parameters
|
372
|
+
----------
|
373
|
+
arr3d : :obj:`numpy.ndarray`
|
374
|
+
Unthresholded 3D summary-statistic matrix. This matrix will end up changed in place.
|
375
|
+
threshold : :obj:`float`
|
376
|
+
Uncorrected summary-statistic thresholded for defining clusters.
|
377
|
+
conn : :obj:`numpy.ndarray` of shape (3, 3, 3)
|
378
|
+
Connectivity matrix for defining clusters.
|
379
|
+
|
380
|
+
Returns
|
381
|
+
-------
|
382
|
+
max_size, max_mass : :obj:`float`
|
383
|
+
Maximum cluster size and mass from the matrix.
|
384
|
+
"""
|
385
|
+
if tail == "upper":
|
386
|
+
arr3d[arr3d <= threshold] = 0
|
387
|
+
else:
|
388
|
+
arr3d[np.abs(arr3d) <= threshold] = 0
|
389
|
+
|
390
|
+
labeled_arr3d = np.empty(arr3d.shape, int)
|
391
|
+
labeled_arr3d, _ = ndimage.label(arr3d > 0, conn)
|
392
|
+
|
393
|
+
if tail == "two":
|
394
|
+
# Label positive and negative clusters separately
|
395
|
+
n_positive_clusters = np.max(labeled_arr3d)
|
396
|
+
temp_labeled_arr3d, _ = ndimage.label(arr3d < 0, conn)
|
397
|
+
temp_labeled_arr3d[temp_labeled_arr3d > 0] += n_positive_clusters
|
398
|
+
labeled_arr3d = labeled_arr3d + temp_labeled_arr3d
|
399
|
+
del temp_labeled_arr3d
|
400
|
+
|
401
|
+
clust_sizes = np.bincount(labeled_arr3d.flatten())
|
402
|
+
clust_vals = np.arange(0, clust_sizes.shape[0])
|
403
|
+
|
404
|
+
# Cluster mass-based inference
|
405
|
+
max_mass = 0
|
406
|
+
for unique_val in clust_vals[1:]:
|
407
|
+
ss_vals = np.abs(arr3d[labeled_arr3d == unique_val]) - threshold
|
408
|
+
max_mass = np.maximum(max_mass, np.sum(ss_vals))
|
409
|
+
|
410
|
+
# Cluster size-based inference
|
411
|
+
clust_sizes = clust_sizes[1:] # First cluster is zeros in matrix
|
412
|
+
if clust_sizes.size:
|
413
|
+
max_size = np.max(clust_sizes)
|
414
|
+
else:
|
415
|
+
max_size = 0
|
416
|
+
|
417
|
+
return max_size, max_mass
|
418
|
+
|
419
|
+
|
420
|
+
@jit(nopython=True, cache=True)
|
421
|
+
def _apply_liberal_mask(data):
|
422
|
+
"""Separate input image data in bags of voxels that have a valid value across the same studies.
|
423
|
+
|
424
|
+
Parameters
|
425
|
+
----------
|
426
|
+
data : (S x V) :class:`numpy.ndarray`
|
427
|
+
2D numpy array (S x V) of images, where S is study and V is voxel.
|
428
|
+
|
429
|
+
Returns
|
430
|
+
-------
|
431
|
+
values_lst : :obj:`list` of :obj:`numpy.ndarray`
|
432
|
+
List of 2D numpy arrays (s x v) of images, where the voxel v have a valid
|
433
|
+
value in study s.
|
434
|
+
voxel_mask_lst : :obj:`list` of :obj:`numpy.ndarray`
|
435
|
+
List of 1D numpy arrays (v) of voxel indices for the corresponding bag.
|
436
|
+
study_mask_lst : :obj:`list` of :obj:`numpy.ndarray`
|
437
|
+
List of 1D numpy arrays (s) of study indices for the corresponding bag.
|
438
|
+
|
439
|
+
Notes
|
440
|
+
-----
|
441
|
+
Parts of the function are implemented with nested for loops to
|
442
|
+
improve the speed with the numba compiler.
|
443
|
+
|
444
|
+
"""
|
445
|
+
MIN_STUDY_THRESH = 2
|
446
|
+
|
447
|
+
n_voxels = data.shape[1]
|
448
|
+
# Get indices of non-nan and zero value of studies for each voxel
|
449
|
+
mask = ~np.isnan(data) & (data != 0)
|
450
|
+
study_by_voxels_idxs = [np.where(mask[:, i])[0] for i in range(n_voxels)]
|
451
|
+
|
452
|
+
# Group studies by the same number of non-nan voxels
|
453
|
+
matches = []
|
454
|
+
all_indices = []
|
455
|
+
for col_i in range(n_voxels):
|
456
|
+
if col_i in all_indices:
|
457
|
+
continue
|
458
|
+
|
459
|
+
vox_match = [col_i]
|
460
|
+
all_indices.append(col_i)
|
461
|
+
for col_j in range(col_i + 1, n_voxels):
|
462
|
+
if (
|
463
|
+
len(study_by_voxels_idxs[col_i]) == len(study_by_voxels_idxs[col_j])
|
464
|
+
and np.array_equal(study_by_voxels_idxs[col_i], study_by_voxels_idxs[col_j])
|
465
|
+
and col_j not in all_indices
|
466
|
+
):
|
467
|
+
vox_match.append(col_j)
|
468
|
+
all_indices.append(col_j)
|
469
|
+
|
470
|
+
matches.append(np.array(vox_match))
|
471
|
+
|
472
|
+
values_lst, voxel_mask_lst, study_mask_lst = [], [], []
|
473
|
+
for voxel_mask in matches:
|
474
|
+
n_masked_voxels = len(voxel_mask)
|
475
|
+
# This is the same for all voxels in the match
|
476
|
+
study_mask = study_by_voxels_idxs[voxel_mask[0]]
|
477
|
+
|
478
|
+
if len(study_mask) < MIN_STUDY_THRESH:
|
479
|
+
# TODO: Figure out how raise a warning in numba
|
480
|
+
# warnings.warn(
|
481
|
+
# f"Removing voxels: {voxel_mask} from the analysis. Not present in 2+ studies."
|
482
|
+
# )
|
483
|
+
continue
|
484
|
+
|
485
|
+
values = np.zeros((len(study_mask), n_masked_voxels))
|
486
|
+
for vox_i, vox in enumerate(voxel_mask):
|
487
|
+
for std_i, study in enumerate(study_mask):
|
488
|
+
values[std_i, vox_i] = data[study, vox]
|
489
|
+
|
490
|
+
values_lst.append(values)
|
491
|
+
voxel_mask_lst.append(voxel_mask)
|
492
|
+
study_mask_lst.append(study_mask)
|
493
|
+
|
494
|
+
return values_lst, voxel_mask_lst, study_mask_lst
|