-
Notifications
You must be signed in to change notification settings - Fork 0
/
ptfce_zstat.R
executable file
·235 lines (205 loc) · 9.8 KB
/
ptfce_zstat.R
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
#!/usr/bin/env Rscript
#
# This is a modification of the original pTFCE shell script: https://spisakt.github.io/pTFCE/
# The goal is to apply pTFCE to a single zstat image based on relevant inputs
print_help <- function() {
cat(paste("This script runs pTFCE (probabilistic threshold-free cluster enhancement) on a single",
"z-statistic image, as produced by FSL FEAT, for example.",
"Options:",
" --zstat <z_img>: A NIfTI file containing a z-statistic map.",
" --mask <mask_img>: A required NIfTI file containing the brain mask for the --zstat input",
" --help: print the help",
" --residuals <residuals_img>: Estimate smoothness from an image containing GLM residuals.",
" --dof <integer>: The degrees of freedom for the corresponding GLM analysis.",
" --fsl_smoothest <smoothness_file>: A smoothness file produced by fsl smoothest, ideally from res4D.nii.gz",
" --twosided: If specified, then both the positive and negative z-statistics are corrected (DEFAULT).",
" --onesided: If specified, only the positive z-statistics are FWE-corrected and negative voxels are dropped.",
" --fwep <.05>: The p-values for which the map is corrected for multiple comparisons. Multiple values may be specified.",
" --write_thresh_imgs: If specified, then each p-value supplied in --fwep will be applied to the pTFCE-enhanced image, then saved",
" --verbose: If specified, the verbose option of pTFCE will be turned on, which prints out progress and diagnostics.",
"\n\n",
sep = "\n"
))
}
#read in command line arguments
args <- commandArgs(trailingOnly = FALSE)
scriptpath <- dirname(sub("--file=", "", grep("--file=", args, fixed=TRUE, value=TRUE), fixed=TRUE))
argpos <- grep("--args", args, fixed=TRUE)
if (length(argpos) > 0L) {
args <- args[(argpos+1):length(args)]
} else {
args <- c()
}
if (length(args) == 0L) {
message("ptfce_zstat expects a single z-statistc image from a GLM analysis using --zstat <z_img> and a corresponding --mask <mask_img>.\n")
print_help()
quit(save = "no", 1, FALSE)
}
if (!require("pacman")) {
install.packages("pacman")
library(pacman)
}
pacman::p_load(oro.nifti, checkmate, pTFCE, dplyr)
z_img <- NA_character_
mask_img <- NA_character_
residuals_img <- NA_character_
dof <- NA_integer_
fsl_smoothest <- NA_character_
two_sided <- TRUE
fwe_p <- .05
write_thresh_imgs <- FALSE # whether to write hard-thresholded images at each FWE p-value
verbose <- FALSE
argpos <- 1
while (argpos <= length(args)) {
#print(args[argpos])
if (args[argpos] == "--zstat") {
z_img <- args[argpos + 1] # name of z-stat image
argpos <- argpos + 2
} else if (args[argpos] == "--mask") {
mask_img <- args[argpos + 1] # name of mask image
argpos <- argpos + 2
} else if (args[argpos] == "--residuals") {
residuals_img <- args[argpos + 1] # name of residuals image
checkmate::assert_file_exists(residuals_img)
argpos <- argpos + 2
} else if (args[argpos] == "--fsl_smoothest") {
fsl_smoothest <- args[argpos + 1]
checkmate::assert_file_exists(fsl_smoothest)
argpos <- argpos + 2
} else if (args[argpos] == "--help") {
print_help()
quit(save = "no", 0, FALSE)
} else if (args[argpos] == "--dof") {
dof_arg <- args[argpos + 1]
if (checkmate::test_integerish(dof_arg)) {
dof <- as.integer(dof_arg)
} else if (checkmate::test_file_exists(dof_arg)) {
dof <- as.integer(readLines(dof_arg))
}
argpos <- argpos + 2
} else if (args[argpos] == "--twosided") {
two_sided <- TRUE
argpos <- argpos + 1
} else if (args[argpos] == "--onesided") {
two_sided <- FALSE
argpos <- argpos + 1
} else if (args[argpos] == "--fwep") {
named_args <- grep("^--", args)
if (any(named_args > argpos)) {
next_arg <- named_args[named_args > argpos][1L] # first named argument after --fwep
if (next_arg == argpos + 1) {
stop("No valid --fwep values provided")
}
last_el <- next_arg - 1
} else {
last_el <- length(args)
}
fwep_inp <- as.numeric(args[(argpos + 1):last_el])
checkmate::assert_numeric(fwep_inp, lower = 1e-10, upper = .9999, any.missing = FALSE)
argpos <- argpos + 1 + length(fwep_inp)
fwe_p <- fwep_inp
} else if (args[argpos] == "--write_thresh_imgs") {
write_thresh_imgs <- TRUE
argpos <- argpos + 1
} else if (args[argpos] == "--verbose") {
verbose <- TRUE
argpos <- argpos + 1
} else {
stop("Not sure what to do with argument: ", args[argpos])
}
}
ptfce_worker <- function(args_list) {
do.call(pTFCE::ptfce, args_list)
}
# for testing
# z_img <- "/proj/mnhallqlab/users/michael/mmclock_pe/mmclock_nov2021/feat_l3/L1m-abspe/L2m-l2_l2c-overall/L3m-int_only/FEAT_l1c-EV_abspe.gfeat/cope1.feat/stats/zstat1.nii.gz"
# mask_img <- "/proj/mnhallqlab/users/michael/mmclock_pe/mmclock_nov2021/feat_l3/L1m-abspe/L2m-l2_l2c-overall/L3m-int_only/FEAT_l1c-EV_abspe.gfeat/cope1.feat/mask.nii.gz"
# residuals_img <- "/proj/mnhallqlab/users/michael/mmclock_pe/mmclock_nov2021/feat_l3/L1m-abspe/L2m-l2_l2c-overall/L3m-int_only/FEAT_l1c-EV_abspe.gfeat/cope1.feat/stats/res4D.nii.gz"
# dof_arg <- "/proj/mnhallqlab/users/michael/mmclock_pe/mmclock_nov2021/feat_l3/L1m-abspe/L2m-l2_l2c-overall/L3m-int_only/FEAT_l1c-EV_abspe.gfeat/cope1.feat/stats/dof"
# fsl_smoothest <- "/proj/mnhallqlab/users/michael/mmclock_pe/mmclock_nov2021/feat_l3/L1m-abspe/L2m-l2_l2c-overall/L3m-int_only/FEAT_l1c-EV_abspe.gfeat/cope1.feat/stats/smoothness"
# both z and mask are required inputs
checkmate::assert_file_exists(z_img)
checkmate::assert_file_exists(mask_img)
z_dir <- normalizePath(dirname(z_img))
ext <- sub(".*(\\.nii(\\.gz)?)$", "\\1", z_img, perl = TRUE)
base <- sub(ext, "", basename(z_img), fixed = TRUE)
if (!is.na(residuals_img)) {
if (is.na(dof)) {
stop("If --residuals image is specified, then --dof must be provided, too!")
}
cat("Using residuals image to calculate smoothness\n")
call_list <- list(residual=residuals_img, dof=dof, verbose = verbose)
} else if (!is.na(fsl_smoothest)) {
cat("Using FSL smoothest file:", fsl_smoothest, "\n")
smooth_data <- read.table(fsl_smoothest, nrow = 3) # DLH, VOLUME, RESELS
V <- smooth_data[2, 2] # VOLUME
Rd <- smooth_data[1, 2] * smooth_data[2, 2] # DLH * VOLUME
resels <- smooth_data[3, 2] # RESELS
call_list <- list(V = V, Rd = Rd, resels = resels, verbose = verbose)
} else {
cat("Estimating smoothness internally from z-stat image\n")
call_list <- list(verbose = verbose)
}
# run ptfce
cat("pTFCE running: ", as.character(Sys.time()), "\n")
Z <- oro.nifti::readNIfTI(z_img, reorient = FALSE)
mask <- oro.nifti::readNIfTI(mask_img, reorient = FALSE)
call_list[["mask"]] <- mask
# always use the positive z-stats for FWE (both one- and two-sided)
#z_pos <- Z # for now, we use the whole image (based on Smith and Nichols 2009)
#z_pos@.Data[z_pos@.Data < 0] <- 0 # zero negative values
call_list[["img"]] <- Z #z_pos
ptfce_pos <- ptfce_worker(call_list)
# The pTFCE correction produces massive negative values for the other tail of the distribution, including outside of
# the mask. I think this is just a coding oversight on their end since the positive values are the only ones of interest.
# Still, it makes for weird images, and if we want to combine two-sided images, we can't simply add them.
# Thus, zero out anything below zero in both cases for clarity.
ptfce_pos$Z@.Data[ptfce_pos$Z@.Data < 0] <- 0
# There is also a problem where some images (including mine) produce NaNs in the TFCE-enhanced image. This is documented here:
# https://github.com/spisakt/pTFCE/issues/8. But it is not entirely resolved. For now, rather than leave NAs in the image, set
# them to zero and produce a warning.
nmiss <- sum(is.na(ptfce_pos$Z))
if (nmiss > 0L) {
warning("NAs produced in ", nmiss, " voxels for the positive z-statistic TFCE. These will be set to zero.")
ptfce_pos$Z@.Data[is.na(ptfce_pos$Z@.Data)] <- 0
}
if (isTRUE(two_sided)) {
z_neg <- -1*Z # the literal interpretation of Smith and Nichols is to use the whole image
#z_neg@.Data[z_neg@.Data > 0] <- 0 # zero positive values
#z_neg@.Data[z_neg@.Data < 0] <- z_neg@.Data[z_neg@.Data < 0] * -1 # invert negative stats
call_list[["img"]] <- z_neg
ptfce_neg <- ptfce_worker(call_list)
ptfce_neg$Z@.Data[ptfce_neg$Z@.Data < 0] <- 0
nmiss <- sum(is.na(ptfce_neg$Z))
if (nmiss > 0L) {
warning("NAs produced in ", nmiss, " voxels for the negative z-statistic TFCE. These will be set to zero.")
ptfce_neg$Z@.Data[is.na(ptfce_neg$Z@.Data)] <- 0
}
ptfce_neg$Z <- -1 * ptfce_neg$Z # make negative z-stats negative again
# create combined pTFCE-corrected image with positive and negative results
# ptfce_obj@.Data[ptfce_neg$Z > 0] <- -1 * ptfce_neg$Z[ptfce_neg$Z > 0] # fill in negative z-stats
ptfce_obj <- ptfce_pos$Z + ptfce_neg$Z
} else {
ptfce_obj <- ptfce_pos$Z # only positive values
}
p_list <- list()
for (pv in seq_along(fwe_p)) {
this_p <- ifelse(isTRUE(two_sided), fwe_p[pv] / 2, fwe_p[pv])
z_thresh <- pTFCE::fwe.p2z(ptfce_pos$number_of_resels, FWEP = this_p)
if (isTRUE(write_thresh_imgs)) {
zi <- ptfce_obj
zi[abs(zi) < z_thresh] <- 0 # zero out below-threshold values
writeNIfTI(zi, file.path(z_dir, paste0(base, "_ptfce_fwep_", round(fwe_p[pv], 3))))
}
p_list[[pv]] <- data.frame(
z_ptfce = round(z_thresh, 4), p_value = fwe_p[pv], two_sided = two_sided,
number_of_resels = round(ptfce_pos$number_of_resels, 4)
)
}
p_df <- dplyr::bind_rows(p_list)
# write overall pTFCE image here
writeNIfTI(ptfce_obj, file.path(z_dir, paste0(base, "_ptfce")))
write.csv(p_df, file.path(z_dir, paste0(base, "_ptfce_zthresh.csv")), row.names=FALSE)
cat("pTFCE completed: ", as.character(Sys.time()), "\n")
# if we want to build out the fsleyes call to look at the image.
#echo "fsleyes $FSLDIR/data/standard/MNI152_T1_1mm_brain.nii.gz -cm greyscale pTFCE_Z_$BASE -dr `cat thres_z_$BASE.txt` `fslstats pTFCE_Z_$BASE -p 100` -cm red-yellow &"