-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathconmap.py
More file actions
executable file
·299 lines (238 loc) · 9.23 KB
/
Copy pathconmap.py
File metadata and controls
executable file
·299 lines (238 loc) · 9.23 KB
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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
# usage: conmap.py [-h] -i INFILES -r ROIFILE -m MASKFILE -o OUTDIR
# [--nmaps NMAPS] [--save_eta2 SAVE_ETA2] [--norm NORM_FLAG]
#
# Developed at DCCN (Donders Centre for Cognitive Neuroimaging), Donders Institute
# for Brain, Cognition and Behaviour. Radboud University, Nijmegen, The Netherlands
#
# Authors: KV Haak, AF Marquand, CF Beckmann.
#
# If you use this code in your research, please quote the following journal reference:
#
# Haak KV, Marquand AF, Beckmann CF (2018) Connectopic mapping with resting-state fMRI.
# NeuroImage 170:83-94.
import numpy as np
def pca(X):
from scipy.linalg import svd
# Center X by subtracting off column means
X -= np.mean(X,0)
# The principal components are the eigenvectors of S = X'*X./(n-1), but computed using SVD
[U,sigma,V] = svd(X,full_matrices=False)
# Project X onto the principal component axes
Y = U*sigma
# Convert the singular values to eigenvalues
sigma /= np.sqrt(X.shape[0]-1)
evals = np.square(sigma)
return V, Y, evals
def corr(X,Y):
Y = Y.T
X = X.T
R = np.zeros((X.shape[0],Y.shape[0]))
for i in range(0,R.shape[1]):
y = Y[i,:]
Xm = np.reshape(np.mean(X,axis=1),(X.shape[0],1))
ym = np.mean(y)
r_num = np.sum((X-Xm)*(y-ym),axis=1)
r_den = np.sqrt(np.sum((X-Xm)**2,axis=1)*np.sum((y-ym)**2))
R[:,i] = r_num / r_den
return R
def eta2(X):
S = np.zeros((X.shape[0],X.shape[0]))
for i in range(0,X.shape[0]):
for j in range(i,X.shape[0]):
mi = np.mean([X[i,:],X[j,:]],0)
mm = np.mean(mi)
ssw = np.sum(np.square(X[i,:]-mi) + np.square(X[j,:]-mi))
sst = np.sum(np.square(X[i,:]-mm) + np.square(X[j,:]-mm))
S[i,j] = 1-ssw/sst
S += S.T
S -= np.eye(S.shape[0])
return S
def norm(X):
from scipy.spatial.distance import pdist
from scipy.spatial.distance import squareform
return squareform(pdist(X))
def adjacency(X):
from networkx import is_connected
from networkx import from_numpy_matrix
emin = 0
emax = np.max(X)
tol = 0.0001
maxiter = 1000
cntr = 0
done = False
while not done:
e = (emin + emax) / 2
A = (X < e) - np.eye(X.shape[0])
G = from_numpy_matrix(A)
if is_connected(G):
emax = e
if (emax - emin) < tol:
done = True
else:
emin = e
cntr += 1
if cntr == maxiter:
done = True
return A
# Main routine
def main(infiles,roifile,maskfile,outdir,nmaps,save_eta2=False,norm_flag=False,proj_flag=False):
import nibabel as nib
import sys
import errno
np.seterr(invalid='ignore')
out_base_name = roifile.split('/')[-1].split('.nii')[0]
# Load the roi
try:
print('Loading roi from: ' + roifile)
roiImg = nib.load(roifile)
roi = roiImg.get_data()
except:
sys.exit('Cannot open ' + roifile | '\nExiting.')
if len(roi.shape) != 3:
sys.exit(roifile + ' is not a 3D image\nExiting.')
# Store the dimensions of the roi data for later use
roidims = roi.shape
nVoxels = np.prod(roidims)
# Reshape roi into a vector of size nVoxels
roi = np.reshape(roi,(nVoxels))
# Find the indices inside roi
roiIndices = np.where(roi>0)
# Load the mask
try:
print('Loading mask from: ' + maskfile)
maskImg = nib.load(maskfile)
mask = maskImg.get_data()
except:
sys.exit('Cannot open ' + maskfile | '\nExiting.')
if len(mask.shape) != 3:
sys.exit(maskfile + ' is not a 3D image\nExiting.')
# Reshape the mask into a vector of size nVoxels
mask = np.reshape(mask,(nVoxels))
# Find the indices outside roi but inside mask
maskIndices = np.where((roi==0) & (mask>0))
# Initialise similarity matrix
S = np.zeros([np.sum(roi>0),np.sum(roi>0)])
# Loop over infiles and create average similarity matrix
for infile in infiles:
print('Processing ' + infile)
# Load functional data
try:
dataImg = nib.load(infile)
data = dataImg.get_data()
except:
sys.exit('Cannot open ' + infile | '\nExiting.')
if len(data.shape) != 4:
sys.exit(infile + ' is not a 4D image\nExiting.')
# Assert absence of nans and infs
if np.any(np.isnan(data)) or np.any(np.isinf(data)):
sys.exit('Data contains invalid values.\nExiting.')
# Reshape and standardise
nFrames = data.shape[3]
data = np.reshape(data,(nVoxels,nFrames))
data -= np.tile(np.mean(data,1),(nFrames,1)).T
data /= np.tile(np.std(data,1),(nFrames,1)).T
# Gather data inside roi
A = data[roiIndices,:][0]
# If the roi contains invalid data it must be due to a division by 0 (stdev)
# since the data themselves do not contain nans or infs. If so, we terminate
# the program and the user should define a roi covering functional data.
if np.any(np.isnan(A)) or np.any(np.isinf(A)):
sys.exit('ROI includes voxels without variance.\nExiting.')
# Gather data outside roi
B = data[maskIndices,:][0]
# Transpose so that the data are stored in a time x space matrix
A = A.T
B = B.T
# A division by 0 (stdev) can also lead to nans and infs in the mask data.
# In this case we can simply throw a warning and ignore all voxels without
# variance.
keepB = ~np.isnan(B).any(axis=0) & ~np.isinf(B).any(axis=0)
if np.any(np.isnan(B)) or np.any(np.isinf(B)):
print('WARNING: Mask includes voxels without variance.')
del data
# Get voxel-wise connectivity fingerprints
print('Computing voxel-wise connectivity fingerprints...')
[evecs,Bhat,evals] = pca(B[:,keepB])
R = corr(A,Bhat)
# Construct similarity matrix of connectivity fingerprints
print('Computing similarity matrix...')
S += eta2(R)
if len(infiles) > 1:
print('Creating average similarity matrix...')
S /= len(infiles)
# If requested, save the similarity matrix as a matlab .mat file
if save_eta2:
import scipy.io
scipy.io.savemat(outdir + "/" + out_base_name + ".eta2", dict(S=S))
# Compute the graph Laplacian
print('Computing the graph Laplacian...')
dist = norm(S)**2
W = np.multiply(adjacency(dist),S)
D = np.diag(np.sum(W,0))
L = np.subtract(D,W)
# Solve generalised eigenvalue problem Ly = lDy
print('Computing the dominant ' + str(nmaps) + ' connectopic maps...')
from scipy.linalg import eigh
l,y = eigh(L,D,eigvals=(0,nmaps))
# The eigenvectors have an intrinsic sign indeterminacy, which is inconvenient
# for spatial statistical modeling. We deal with this by flipping the sign of
# the eigenvectors if they correlate negatively with the reference vector
# defined below.
x0,y0,z0 = np.floor(roidims[0]/2),0,0
X,Y,Z = np.ogrid[0:roidims[0],0:roidims[1],0:roidims[2]]
ref = np.sqrt((X-x0)**2+(Y-y0)**2+(Z-z0)**2)
ref = np.reshape(ref,(np.prod(roidims)))
ref = ref[np.where(roi==1)]
# Deal with sign ambiquity and, if requested, normalize y to range between 0 and 1
for evec in range(0,y.shape[1]):
y[:,evec] = np.multiply(y[:,evec],np.sign(np.corrcoef(y[:,evec],ref)[0,1]))
if norm_flag:
tmp = y[:,evec] - min(y[:,evec])
y[:,evec] = np.divide(tmp,max(tmp))
# Store the eigenmaps as a 4D nifti image
print('Writing connectopic maps to: ' + outdir)
outfile = outdir + "/" + out_base_name + ".cmaps.nii.gz"
yDat = np.zeros(shape=roidims+(nmaps,))
yDat = np.reshape(yDat,(np.prod(roidims),nmaps))
yDat[roiIndices,:] = y[:,1:nmaps+1]
yDat = np.reshape(yDat,roidims+(nmaps,))
yImg = nib.Nifti1Image(yDat,roiImg.get_affine(),roiImg.get_header())
try:
nib.save(yImg,outfile)
except:
sys.exit('Cannot save ' + outfile | '\nExiting.')
# Optionally project eigenmaps onto mask by spatial regression
if proj_flag:
print('Computing projections onto mask...')
outfile = outdir + "/" + out_base_name + ".pmaps.nii.gz"
YDat = np.zeros(shape=roidims+(nmaps,))
YDat = np.reshape(YDat,(np.prod(roidims),nmaps))
for evec in range(1,y.shape[1]):
X = np.vstack([np.ones(y.shape[0]),y[:,evec].T])
beta = np.dot(np.linalg.pinv(X.T),A.T)
Y = np.dot(B.T,beta.T)[:,1]
if norm_flag:
Y -= min(Y)
Y /= max(Y)
YDat[maskIndices,evec-1] = Y
print('Writing projection maps to: ' + outdir)
YDat = np.reshape(YDat,roidims+(nmaps,))
YImg = nib.Nifti1Image(YDat,roiImg.get_affine(),roiImg.get_header())
try:
nib.save(YImg,outfile)
except:
sys.exit('Cannot save ' + outfile | '\nExiting.')
print("Done.")
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="ConGrads")
parser.add_argument("-i",dest="infiles",help="One or more 4D images",required=True,nargs='*')
parser.add_argument("-r",dest="roifile",help="Region-of-Interest (binary 3D nifti)",required=True)
parser.add_argument("-m",dest="maskfile",help="Mask (binary 3D nifti)",required=True)
parser.add_argument("-o",dest="outdir",help="Output directory",required=True)
parser.add_argument("--nmaps",dest="nmaps",default=1,type=int,help="Number of connectopic maps")
parser.add_argument("--save_eta2",dest="save_eta2",action="store_true",help="Store eta2 matrix")
parser.add_argument("--norm",dest="norm_flag",action="store_true",help="Normalise maps")
parser.add_argument("--project",dest="proj_flag",action="store_true",help="Project maps onto mask")
args=parser.parse_args()
main(args.infiles,args.roifile,args.maskfile,args.outdir,args.nmaps,args.save_eta2,args.norm_flag,args.proj_flag)