15
15
Surface hemisphere: 'lh' or 'rh'
16
16
17
17
-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).
19
20
20
21
-n|--cluster-size
21
22
Desired size of cluster, in units of mm^2 or number of vertices depending
39
40
ROIs - the number must match the number of seed coordinates.
40
41
41
42
--surf (optional)
42
- Which surface to use (default = white)
43
+ Which surface to use (default = white). Note: do not use inflated surface.
43
44
44
45
-l|--initial-label (optional)
45
46
Path to surface label file. If provided, will apply this label to the data
67
68
68
69
--sign (optional)
69
70
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).
75
81
76
82
--subjects-dir (optional)
77
83
Directory containing Freesurfer subject directories. If omitted, defaults
@@ -130,7 +136,7 @@ def my_subprocess_run(*args, **kwargs):
130
136
return rtn
131
137
132
138
133
- def RAS_to_vertex (subjects_dir , subject , hemi , vtx_coord ):
139
+ def RAS_to_vertex (subjects_dir , subject , hemi , surf , vtx_coord ):
134
140
"""
135
141
Get index of closest vertex to surfaceRAS co-ordinate.
136
142
@@ -142,23 +148,24 @@ def RAS_to_vertex(subjects_dir, subject, hemi, vtx_coord):
142
148
Subject ID
143
149
hemi : str
144
150
lh or rh
151
+ surf : str
152
+ Surface to use (e.g., white)
145
153
vtx_coord : array-like
146
154
[x, y, z] surface RAS co-ordindate of vertex
147
155
148
-
149
156
Returns
150
157
-------
151
158
vtx : int
152
159
Index of vertex
153
160
"""
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 } ' )
155
162
surf_coords = nib .freesurfer .read_geometry (geom_file )[0 ]
156
163
vtx_coord = np .asarray (vtx_coord , dtype = float )
157
164
return np .linalg .norm (surf_coords - vtx_coord , axis = 1 ).argmin ()
158
165
159
166
160
167
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 ):
162
169
"""
163
170
Run Freesurfer mri_surfcluster command via subprocess
164
171
@@ -181,10 +188,13 @@ def mri_surfcluster(thr, subjects_dir, subject, hemi, surf, infile,
181
188
Path to directory to save outputs. Must already exist.
182
189
sign : str
183
190
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
187
194
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.
188
198
189
199
Outputs
190
200
-------
@@ -213,6 +223,8 @@ def mri_surfcluster(thr, subjects_dir, subject, hemi, surf, infile,
213
223
cmd .extend (['--clabel' , clabel ])
214
224
if save_labels :
215
225
cmd .extend (['--olab' , os .path .join (clusterdir , 'cluster' )])
226
+ if not adjust_pvals :
227
+ cmd .extend (['--no-adjust' ])
216
228
217
229
# Run command
218
230
my_subprocess_run (cmd )
@@ -290,24 +302,26 @@ def get_cluster_size(clusterdir, clusterID, size_metric):
290
302
raise Exception ('Cluster ID not found' )
291
303
292
304
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 ):
295
307
"""
296
308
Cost function to be optimised. Applies clustering, then returns error
297
309
between actual and target cluster size.
298
310
299
311
Arguments
300
312
---------
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.
304
316
seed_vertex : int
305
317
Index of seed vertex
306
318
target_size : float or int
307
319
Target size of cluster; either area (in mm^2) or num vertices
308
320
depending on size metric.
309
321
size_metric : str
310
322
Size metric: 'area' or 'nVtxs'
323
+ mri_surfcluster_kwargs : dict
324
+ Dictionary of further keyword arguments to be passed to mri_surfcluster
311
325
312
326
Returns
313
327
-------
@@ -322,10 +336,10 @@ def cost_func(thr, subjects_dir, subject, hemi, surf, infile, clusterdir,
322
336
raise ValueError ('thr must be float or single-item array' )
323
337
324
338
# Cluster
325
- mri_surfcluster (thr , subjects_dir , subject , hemi , surf , infile , clusterdir ,
326
- sign , clabel = clabel )
339
+ mri_surfcluster (thr , ** mri_surfcluster_kwargs )
327
340
328
341
# Find size of cluster
342
+ clusterdir = mri_surfcluster_kwargs ['clusterdir' ]
329
343
clusterID = get_cluster_at_vertex (clusterdir , seed_vertex )
330
344
if clusterID == 0 :
331
345
cluster_size = 0
@@ -375,6 +389,8 @@ class CustomFormatter(argparse.RawTextHelpFormatter,
375
389
help = 'Upper-bound threshold; set to inf or nan to disable' )
376
390
parser .add_argument ('--sign' , choices = ['abs' , 'pos' , 'neg' ], default = 'pos' ,
377
391
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' )
378
394
parser .add_argument ('--subjects-dir' , help = 'Freesurfer subjects directory' )
379
395
380
396
# If no arguments given, print help and exit
@@ -397,6 +413,7 @@ class CustomFormatter(argparse.RawTextHelpFormatter,
397
413
lower_bound = args .lower_bound
398
414
upper_bound = args .upper_bound
399
415
sign = args .sign
416
+ adjust_pvals = args .adjust_pvals
400
417
subjects_dir = args .subjects_dir
401
418
402
419
# Allocate default subjects dir if necessary
@@ -433,7 +450,7 @@ class CustomFormatter(argparse.RawTextHelpFormatter,
433
450
# If seed in scannerRAS coords, convert to vertex index. If already index,
434
451
# extract value and convert float -> int
435
452
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 )
437
454
elif len (seed_vertex ) == 1 :
438
455
seed_vertex = int (seed_vertex [0 ])
439
456
else :
@@ -446,11 +463,30 @@ class CustomFormatter(argparse.RawTextHelpFormatter,
446
463
# Check lower bound appropriate for seed
447
464
if lower_bound is not None :
448
465
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 :
450
473
seed_val -= np .log10 (2 )
474
+
451
475
if seed_val < lower_bound :
452
476
raise ValueError ('Lower bound cannot exceed value at seed voxel' )
453
477
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
+
454
490
# Loop starting parameters
455
491
all_thr = []
456
492
all_errs = []
@@ -459,17 +495,23 @@ class CustomFormatter(argparse.RawTextHelpFormatter,
459
495
# Otherwise, just convert to float.
460
496
if x0 .endswith ('%' ):
461
497
prop = float (x0 .rstrip ('%' )) / 100
462
- max_ = surf_data [ seed_vertex ]
498
+
463
499
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
+
464
506
x0 = min_ + prop * (max_ - min_ )
465
- if sign in ['pos' ,'neg' ]:
507
+ if sign in ['pos' ,'neg' ] and adjust_pvals :
466
508
x0 += np .log10 (2 )
467
509
else :
468
510
x0 = float (x0 )
469
511
512
+
470
513
# 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 )
473
515
optimres = minimize (cost_func , x0 = [x0 ], args = args , method = 'Nelder-Mead' )
474
516
475
517
# Extract optimised threshold and error values, append to array
@@ -489,8 +531,8 @@ class CustomFormatter(argparse.RawTextHelpFormatter,
489
531
opt_thr = upper_bound
490
532
491
533
# 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 )
494
536
clusterID = get_cluster_at_vertex (tmpdir .name , seed_vertex )
495
537
cluster_size = get_cluster_size (tmpdir .name , clusterID , size_metric )
496
538
@@ -505,4 +547,3 @@ class CustomFormatter(argparse.RawTextHelpFormatter,
505
547
506
548
# Cleanup tmpdir
507
549
tmpdir .cleanup ()
508
-
0 commit comments