Skip to content

Commit 47cbb55

Browse files
committed
Make p-value adjustment optional; bug fixes for negative valued clusters
1 parent 70c3dad commit 47cbb55

File tree

1 file changed

+71
-30
lines changed

1 file changed

+71
-30
lines changed

defineROICluster_Freesurfer.py

+71-30
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
Surface hemisphere: 'lh' or 'rh'
1616
1717
-i|--input
18-
Path to input statistical surface image (probably a -log10(pstat))
18+
Path to input statistical surface image (probably a -log10(pstat) or
19+
zstat).
1920
2021
-n|--cluster-size
2122
Desired size of cluster, in units of mm^2 or number of vertices depending
@@ -39,7 +40,7 @@
3940
ROIs - the number must match the number of seed coordinates.
4041
4142
--surf (optional)
42-
Which surface to use (default = white)
43+
Which surface to use (default = white). Note: do not use inflated surface.
4344
4445
-l|--initial-label (optional)
4546
Path to surface label file. If provided, will apply this label to the data
@@ -67,11 +68,16 @@
6768
6869
--sign (optional)
6970
Tail of statistical map to use. Can be abs, pos (default), or neg. Note
70-
that thresholds will be adjusted if a one-tailed pos/neg test is used, so
71-
that values will correspond to one-tailed p-values. For example, if sign is
72-
'pos' and the threshold is reported as 3.0 (one-tailed p = .001), the
73-
statistical map would actually be thresholded at 3.0 - log(2) ~= 2.7
74-
(two-tailed p = .002).
71+
that for a negative tail, all thresholds and bounds should still be
72+
specified as positive values (they will be sign-flipped internally).
73+
74+
--adjust-pvals (optional)
75+
If specified, adjust threshold for one-tailed tests (ignored if --sign is
76+
'abs'). Assumes that input is a -log10(pstat) image. Values will be
77+
adjusted by -log10(2) (equivalent to dividing the p-value by 2). For
78+
example, if sign is 'pos' and the threshold is reported as 3.0 (one-tailed
79+
p = .001), the input image would actually be thresholded at
80+
3.0 - log(2) ~= 2.7 (two-tailed p = .002).
7581
7682
--subjects-dir (optional)
7783
Directory containing Freesurfer subject directories. If omitted, defaults
@@ -130,7 +136,7 @@ def my_subprocess_run(*args, **kwargs):
130136
return rtn
131137

132138

133-
def RAS_to_vertex(subjects_dir, subject, hemi, vtx_coord):
139+
def RAS_to_vertex(subjects_dir, subject, hemi, surf, vtx_coord):
134140
"""
135141
Get index of closest vertex to surfaceRAS co-ordinate.
136142
@@ -142,23 +148,24 @@ def RAS_to_vertex(subjects_dir, subject, hemi, vtx_coord):
142148
Subject ID
143149
hemi : str
144150
lh or rh
151+
surf : str
152+
Surface to use (e.g., white)
145153
vtx_coord : array-like
146154
[x, y, z] surface RAS co-ordindate of vertex
147155
148-
149156
Returns
150157
-------
151158
vtx : int
152159
Index of vertex
153160
"""
154-
geom_file = os.path.join(subjects_dir, subject, 'surf', hemi + '.white')
161+
geom_file = os.path.join(subjects_dir, subject, 'surf', f'{hemi}.{surf}')
155162
surf_coords = nib.freesurfer.read_geometry(geom_file)[0]
156163
vtx_coord = np.asarray(vtx_coord, dtype=float)
157164
return np.linalg.norm(surf_coords - vtx_coord, axis=1).argmin()
158165

159166

160167
def mri_surfcluster(thr, subjects_dir, subject, hemi, surf, infile,
161-
clusterdir, sign, clabel=None, save_labels=False):
168+
clusterdir, sign, clabel, save_labels, adjust_pvals):
162169
"""
163170
Run Freesurfer mri_surfcluster command via subprocess
164171
@@ -181,10 +188,13 @@ def mri_surfcluster(thr, subjects_dir, subject, hemi, surf, infile,
181188
Path to directory to save outputs. Must already exist.
182189
sign : str
183190
abs, pos, or neg
184-
clabel : str (optional)
185-
Path to label file to apply as pre-mask
186-
save_labels : bool (optional)
191+
clabel : str or None
192+
Optional path to label file to apply as pre-mask
193+
save_labels : bool
187194
If True, also save clusters out as label files. Default is False.
195+
adjust_pvals : bool
196+
If True, assume threshold is for a -log10(pstat), and adjust for
197+
one-tailed test.
188198
189199
Outputs
190200
-------
@@ -213,6 +223,8 @@ def mri_surfcluster(thr, subjects_dir, subject, hemi, surf, infile,
213223
cmd.extend(['--clabel', clabel])
214224
if save_labels:
215225
cmd.extend(['--olab', os.path.join(clusterdir, 'cluster')])
226+
if not adjust_pvals:
227+
cmd.extend(['--no-adjust'])
216228

217229
# Run command
218230
my_subprocess_run(cmd)
@@ -290,24 +302,26 @@ def get_cluster_size(clusterdir, clusterID, size_metric):
290302
raise Exception('Cluster ID not found')
291303

292304

293-
def cost_func(thr, subjects_dir, subject, hemi, surf, infile, clusterdir,
294-
sign, seed_vertex, target_size, size_metric, clabel=None):
305+
def cost_func(thr, seed_vertex, target_size, size_metric,
306+
mri_surfcluster_kwargs):
295307
"""
296308
Cost function to be optimised. Applies clustering, then returns error
297309
between actual and target cluster size.
298310
299311
Arguments
300312
---------
301-
thr, subjects_dir, subject, hemi, surf, clusterdir, surf, clabel
302-
All as per mri_surfcluster function. Threshold may be supplied as
303-
single-item array - this is for compatibility with minimize function.
313+
thr : float
314+
Threshold to apply. May be supplied as single-item array - this is for
315+
compatibility with minimize function.
304316
seed_vertex : int
305317
Index of seed vertex
306318
target_size : float or int
307319
Target size of cluster; either area (in mm^2) or num vertices
308320
depending on size metric.
309321
size_metric : str
310322
Size metric: 'area' or 'nVtxs'
323+
mri_surfcluster_kwargs : dict
324+
Dictionary of further keyword arguments to be passed to mri_surfcluster
311325
312326
Returns
313327
-------
@@ -322,10 +336,10 @@ def cost_func(thr, subjects_dir, subject, hemi, surf, infile, clusterdir,
322336
raise ValueError('thr must be float or single-item array')
323337

324338
# Cluster
325-
mri_surfcluster(thr, subjects_dir, subject, hemi, surf, infile, clusterdir,
326-
sign, clabel=clabel)
339+
mri_surfcluster(thr, **mri_surfcluster_kwargs)
327340

328341
# Find size of cluster
342+
clusterdir = mri_surfcluster_kwargs['clusterdir']
329343
clusterID = get_cluster_at_vertex(clusterdir, seed_vertex)
330344
if clusterID == 0:
331345
cluster_size = 0
@@ -375,6 +389,8 @@ class CustomFormatter(argparse.RawTextHelpFormatter,
375389
help='Upper-bound threshold; set to inf or nan to disable')
376390
parser.add_argument('--sign', choices=['abs', 'pos', 'neg'], default='pos',
377391
help='Tail of statistical map to use')
392+
parser.add_argument('--adjust-pvals', action='store_true',
393+
help='Adjust threshold for one-tailed p-values')
378394
parser.add_argument('--subjects-dir', help='Freesurfer subjects directory')
379395

380396
# If no arguments given, print help and exit
@@ -397,6 +413,7 @@ class CustomFormatter(argparse.RawTextHelpFormatter,
397413
lower_bound = args.lower_bound
398414
upper_bound = args.upper_bound
399415
sign = args.sign
416+
adjust_pvals = args.adjust_pvals
400417
subjects_dir = args.subjects_dir
401418

402419
# Allocate default subjects dir if necessary
@@ -433,7 +450,7 @@ class CustomFormatter(argparse.RawTextHelpFormatter,
433450
# If seed in scannerRAS coords, convert to vertex index. If already index,
434451
# extract value and convert float -> int
435452
if len(seed_vertex) == 3:
436-
seed_vertex = RAS_to_vertex(subjects_dir, subject, hemi, seed_vertex)
453+
seed_vertex = RAS_to_vertex(subjects_dir, subject, hemi, surf, seed_vertex)
437454
elif len(seed_vertex) == 1:
438455
seed_vertex = int(seed_vertex[0])
439456
else:
@@ -446,11 +463,30 @@ class CustomFormatter(argparse.RawTextHelpFormatter,
446463
# Check lower bound appropriate for seed
447464
if lower_bound is not None:
448465
seed_val = surf_data[seed_vertex]
449-
if sign in ['pos','neg']:
466+
467+
if sign == 'abs':
468+
seed_val = abs(seed_val)
469+
elif sign == 'neg':
470+
seed_val = -1 * seed_val
471+
472+
if sign in ['pos','neg'] and adjust_pvals:
450473
seed_val -= np.log10(2)
474+
451475
if seed_val < lower_bound:
452476
raise ValueError('Lower bound cannot exceed value at seed voxel')
453477

478+
# Build dict of kwargs for mri_surfcluster
479+
mri_surfcluster_kwargs = {'subjects_dir':subjects_dir,
480+
'subject':subject,
481+
'hemi':hemi,
482+
'surf':surf,
483+
'infile':infile,
484+
'clusterdir':tmpdir.name,
485+
'sign':sign,
486+
'clabel':initial_label,
487+
'save_labels':False,
488+
'adjust_pvals':adjust_pvals}
489+
454490
# Loop starting parameters
455491
all_thr = []
456492
all_errs = []
@@ -459,17 +495,23 @@ class CustomFormatter(argparse.RawTextHelpFormatter,
459495
# Otherwise, just convert to float.
460496
if x0.endswith('%'):
461497
prop = float(x0.rstrip('%')) / 100
462-
max_ = surf_data[seed_vertex]
498+
463499
min_ = lower_bound if lower_bound is not None else 0
500+
max_ = surf_data[seed_vertex]
501+
if sign == 'abs':
502+
max_ = abs(max_)
503+
elif sign == 'neg':
504+
max_ = -1 * max_
505+
464506
x0 = min_ + prop * (max_ - min_)
465-
if sign in ['pos','neg']:
507+
if sign in ['pos','neg'] and adjust_pvals:
466508
x0 += np.log10(2)
467509
else:
468510
x0 = float(x0)
469511

512+
470513
# Run optimisation
471-
args = (subjects_dir, subject, hemi, surf, infile, tmpdir.name, sign,
472-
seed_vertex, target_size, size_metric, initial_label)
514+
args = (seed_vertex, target_size, size_metric, mri_surfcluster_kwargs)
473515
optimres = minimize(cost_func, x0=[x0], args=args, method='Nelder-Mead')
474516

475517
# Extract optimised threshold and error values, append to array
@@ -489,8 +531,8 @@ class CustomFormatter(argparse.RawTextHelpFormatter,
489531
opt_thr = upper_bound
490532

491533
# Run clustering once more with selected thresh
492-
mri_surfcluster(opt_thr, subjects_dir, subject, hemi, surf, infile,
493-
tmpdir.name, sign, clabel=initial_label, save_labels=True)
534+
mri_surfcluster_kwargs['save_labels'] = True
535+
mri_surfcluster(opt_thr, **mri_surfcluster_kwargs)
494536
clusterID = get_cluster_at_vertex(tmpdir.name, seed_vertex)
495537
cluster_size = get_cluster_size(tmpdir.name, clusterID, size_metric)
496538

@@ -505,4 +547,3 @@ class CustomFormatter(argparse.RawTextHelpFormatter,
505547

506548
# Cleanup tmpdir
507549
tmpdir.cleanup()
508-

0 commit comments

Comments
 (0)