pyNIBS 0.2024.8__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.
- pyNIBS-0.2024.8.dist-info/LICENSE +623 -0
- pyNIBS-0.2024.8.dist-info/METADATA +723 -0
- pyNIBS-0.2024.8.dist-info/RECORD +107 -0
- pyNIBS-0.2024.8.dist-info/WHEEL +5 -0
- pyNIBS-0.2024.8.dist-info/top_level.txt +1 -0
- pynibs/__init__.py +34 -0
- pynibs/coil.py +1367 -0
- pynibs/congruence/__init__.py +15 -0
- pynibs/congruence/congruence.py +1108 -0
- pynibs/congruence/ext_metrics.py +257 -0
- pynibs/congruence/stimulation_threshold.py +318 -0
- pynibs/data/configuration_exp0.yaml +59 -0
- pynibs/data/configuration_linear_MEP.yaml +61 -0
- pynibs/data/configuration_linear_RT.yaml +61 -0
- pynibs/data/configuration_sigmoid4.yaml +68 -0
- pynibs/data/network mapping configuration/configuration guide.md +238 -0
- pynibs/data/network mapping configuration/configuration_TEMPLATE.yaml +42 -0
- pynibs/data/network mapping configuration/configuration_for_testing.yaml +43 -0
- pynibs/data/network mapping configuration/configuration_modelTMS.yaml +43 -0
- pynibs/data/network mapping configuration/configuration_reg_isi_05.yaml +43 -0
- pynibs/data/network mapping configuration/output_documentation.md +185 -0
- pynibs/data/network mapping configuration/recommendations_for_accuracy_threshold.md +77 -0
- pynibs/data/neuron/models/L23_PC_cADpyr_biphasic_v1.csv +1281 -0
- pynibs/data/neuron/models/L23_PC_cADpyr_monophasic_v1.csv +1281 -0
- pynibs/data/neuron/models/L4_LBC_biphasic_v1.csv +1281 -0
- pynibs/data/neuron/models/L4_LBC_monophasic_v1.csv +1281 -0
- pynibs/data/neuron/models/L4_NBC_biphasic_v1.csv +1281 -0
- pynibs/data/neuron/models/L4_NBC_monophasic_v1.csv +1281 -0
- pynibs/data/neuron/models/L4_SBC_biphasic_v1.csv +1281 -0
- pynibs/data/neuron/models/L4_SBC_monophasic_v1.csv +1281 -0
- pynibs/data/neuron/models/L5_TTPC2_cADpyr_biphasic_v1.csv +1281 -0
- pynibs/data/neuron/models/L5_TTPC2_cADpyr_monophasic_v1.csv +1281 -0
- pynibs/expio/Mep.py +1518 -0
- pynibs/expio/__init__.py +8 -0
- pynibs/expio/brainsight.py +979 -0
- pynibs/expio/brainvis.py +71 -0
- pynibs/expio/cobot.py +239 -0
- pynibs/expio/exp.py +1876 -0
- pynibs/expio/fit_funs.py +287 -0
- pynibs/expio/localite.py +1987 -0
- pynibs/expio/signal_ced.py +51 -0
- pynibs/expio/visor.py +624 -0
- pynibs/freesurfer.py +502 -0
- pynibs/hdf5_io/__init__.py +10 -0
- pynibs/hdf5_io/hdf5_io.py +1857 -0
- pynibs/hdf5_io/xdmf.py +1542 -0
- pynibs/mesh/__init__.py +3 -0
- pynibs/mesh/mesh_struct.py +1394 -0
- pynibs/mesh/transformations.py +866 -0
- pynibs/mesh/utils.py +1103 -0
- pynibs/models/_TMS.py +211 -0
- pynibs/models/__init__.py +0 -0
- pynibs/muap.py +392 -0
- pynibs/neuron/__init__.py +2 -0
- pynibs/neuron/neuron_regression.py +284 -0
- pynibs/neuron/util.py +58 -0
- pynibs/optimization/__init__.py +5 -0
- pynibs/optimization/multichannel.py +278 -0
- pynibs/optimization/opt_mep.py +152 -0
- pynibs/optimization/optimization.py +1445 -0
- pynibs/optimization/workhorses.py +698 -0
- pynibs/pckg/__init__.py +0 -0
- pynibs/pckg/biosig/biosig4c++-1.9.5.src_fixed.tar.gz +0 -0
- pynibs/pckg/libeep/__init__.py +0 -0
- pynibs/pckg/libeep/pyeep.so +0 -0
- pynibs/regression/__init__.py +11 -0
- pynibs/regression/dual_node_detection.py +2375 -0
- pynibs/regression/regression.py +2984 -0
- pynibs/regression/score_types.py +0 -0
- pynibs/roi/__init__.py +2 -0
- pynibs/roi/roi.py +895 -0
- pynibs/roi/roi_structs.py +1233 -0
- pynibs/subject.py +1009 -0
- pynibs/tensor_scaling.py +144 -0
- pynibs/tests/data/InstrumentMarker20200225163611937.xml +19 -0
- pynibs/tests/data/TriggerMarkers_Coil0_20200225163443682.xml +14 -0
- pynibs/tests/data/TriggerMarkers_Coil1_20200225170337572.xml +6373 -0
- pynibs/tests/data/Xdmf.dtd +89 -0
- pynibs/tests/data/brainsight_niiImage_nifticoord.txt +145 -0
- pynibs/tests/data/brainsight_niiImage_nifticoord_largefile.txt +1434 -0
- pynibs/tests/data/brainsight_niiImage_niifticoord_mixedtargets.txt +47 -0
- pynibs/tests/data/create_subject_testsub.py +332 -0
- pynibs/tests/data/data.hdf5 +0 -0
- pynibs/tests/data/geo.hdf5 +0 -0
- pynibs/tests/test_coil.py +474 -0
- pynibs/tests/test_elements2nodes.py +100 -0
- pynibs/tests/test_hdf5_io/test_xdmf.py +61 -0
- pynibs/tests/test_mesh_transformations.py +123 -0
- pynibs/tests/test_mesh_utils.py +143 -0
- pynibs/tests/test_nnav_imports.py +101 -0
- pynibs/tests/test_quality_measures.py +117 -0
- pynibs/tests/test_regressdata.py +289 -0
- pynibs/tests/test_roi.py +17 -0
- pynibs/tests/test_rotations.py +86 -0
- pynibs/tests/test_subject.py +71 -0
- pynibs/tests/test_util.py +24 -0
- pynibs/tms_pulse.py +34 -0
- pynibs/util/__init__.py +4 -0
- pynibs/util/dosing.py +233 -0
- pynibs/util/quality_measures.py +562 -0
- pynibs/util/rotations.py +340 -0
- pynibs/util/simnibs.py +763 -0
- pynibs/util/util.py +727 -0
- pynibs/visualization/__init__.py +2 -0
- pynibs/visualization/para.py +4372 -0
- pynibs/visualization/plot_2D.py +137 -0
- pynibs/visualization/render_3D.py +347 -0
pynibs/roi/roi.py
ADDED
|
@@ -0,0 +1,895 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Functions that operate on region of interest (ROI) data.
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
import h5py
|
|
6
|
+
import math
|
|
7
|
+
import tqdm
|
|
8
|
+
import time
|
|
9
|
+
import scipy
|
|
10
|
+
import trimesh
|
|
11
|
+
import nibabel
|
|
12
|
+
import warnings
|
|
13
|
+
import platform
|
|
14
|
+
import numpy as np
|
|
15
|
+
import nibabel as nib
|
|
16
|
+
import scipy.interpolate
|
|
17
|
+
from itertools import product
|
|
18
|
+
try:
|
|
19
|
+
import simnibs
|
|
20
|
+
except ModuleNotFoundError:
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
import pynibs
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_mask(areas, fn_annot, fn_inflated_fs, fn_out):
|
|
27
|
+
"""
|
|
28
|
+
Determine freesurfer average mask .overlay file, which is needed to generate subject specific ROIs.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
areas : list of str
|
|
33
|
+
Brodmann areas (e.g. ['Brodmann.6', 'Brodmann.4', 'Brodmann.3', 'Brodmann.1'])
|
|
34
|
+
fn_annot : str
|
|
35
|
+
Annotation file of freesurfer (e.g. 'FREESURFER_DIR/fsaverage/label/lh.PALS_B12_Brodmann.annot')
|
|
36
|
+
fn_inflated_fs : str
|
|
37
|
+
Inflated surface of freesurfer average (e.g. 'FREESURFER_DIR/fsaverage/surf/lh.inflated')
|
|
38
|
+
fn_out : str
|
|
39
|
+
Filename of .overlay file of freesurfer mask
|
|
40
|
+
|
|
41
|
+
Returns
|
|
42
|
+
-------
|
|
43
|
+
<File> : .overlay file
|
|
44
|
+
fn_out.overlay file of freesurfer mask
|
|
45
|
+
"""
|
|
46
|
+
# read annotation file
|
|
47
|
+
vertices, colortable, label = nib.freesurfer.io.read_annot(fn_annot)
|
|
48
|
+
|
|
49
|
+
# convert label from numpy bytes str to str
|
|
50
|
+
label = [str(lab.astype(str)) for lab in label]
|
|
51
|
+
|
|
52
|
+
idx_areas = [i_l for i_a, a in enumerate(areas) for i_l, l in enumerate(label) if a == l]
|
|
53
|
+
ourvertices = np.zeros(len(vertices)).astype(bool)
|
|
54
|
+
idx_label = colortable[idx_areas, 4]
|
|
55
|
+
|
|
56
|
+
ourvertices[idx_label] = True
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def elem_workhorse(chunk, points_out, P1_all, P2_all, P3_all, P4_all, N_points_total, N_CPU):
|
|
60
|
+
"""
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
chunk: np.ndarray
|
|
64
|
+
Indices of points the CPU thread is computing the element indices for
|
|
65
|
+
points_out: np.ndarray of float
|
|
66
|
+
(N_points, 3) Coordinates of points, the tetrahedra indices are computed for
|
|
67
|
+
P1_all : np.ndarray of float
|
|
68
|
+
(N_tet, 3) Coordinates of first point of tetrahedra
|
|
69
|
+
P2_all : np.ndarray of float
|
|
70
|
+
(N_tet, 3) Coordinates of second point of tetrahedra
|
|
71
|
+
P3_all : np.ndarray of float
|
|
72
|
+
(N_tet, 3) Coordinates of third point of tetrahedra
|
|
73
|
+
P4_all : np.ndarray of float
|
|
74
|
+
(N_tet, 3) Coordinates of fourth point of tetrahedra
|
|
75
|
+
N_points_total : int
|
|
76
|
+
Total number of points
|
|
77
|
+
N_CPU : int
|
|
78
|
+
Number of CPU cores to use
|
|
79
|
+
|
|
80
|
+
Returns
|
|
81
|
+
-------
|
|
82
|
+
tet_idx_local : np.ndarray of int (N_points,)
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
tet_idx_local = np.zeros([chunk.shape[0]])
|
|
86
|
+
i_local = 0
|
|
87
|
+
|
|
88
|
+
for i in chunk:
|
|
89
|
+
start = time.time()
|
|
90
|
+
|
|
91
|
+
vtest1 = pynibs.mesh.utils.calc_tetrahedra_volume_cross(np.tile(points_out[i, :], (P1_all.shape[0], 1)),
|
|
92
|
+
P2_all,
|
|
93
|
+
P3_all,
|
|
94
|
+
P4_all)
|
|
95
|
+
tet_idx_bool_1 = (vtest1 >= 0)
|
|
96
|
+
tet_idx_1 = np.nonzero(tet_idx_bool_1)[0]
|
|
97
|
+
|
|
98
|
+
vtest2 = pynibs.mesh.utils.calc_tetrahedra_volume_cross(P1_all[tet_idx_1, :],
|
|
99
|
+
np.tile(points_out[i, :], (tet_idx_1.shape[0], 1)),
|
|
100
|
+
P3_all[tet_idx_1, :],
|
|
101
|
+
P4_all[tet_idx_1, :])
|
|
102
|
+
tet_idx_bool_2 = (vtest2 >= 0)
|
|
103
|
+
tet_idx_2 = tet_idx_1[np.nonzero(tet_idx_bool_2)[0]]
|
|
104
|
+
|
|
105
|
+
vtest3 = pynibs.mesh.utils.calc_tetrahedra_volume_cross(P1_all[tet_idx_2, :],
|
|
106
|
+
P2_all[tet_idx_2, :],
|
|
107
|
+
np.tile(points_out[i, :], (tet_idx_2.shape[0], 1)),
|
|
108
|
+
P4_all[tet_idx_2, :])
|
|
109
|
+
tet_idx_bool_3 = (vtest3 >= 0)
|
|
110
|
+
tet_idx_3 = tet_idx_2[np.nonzero(tet_idx_bool_3)[0]]
|
|
111
|
+
|
|
112
|
+
vtest4 = pynibs.mesh.utils.calc_tetrahedra_volume_cross(P1_all[tet_idx_3, :],
|
|
113
|
+
P2_all[tet_idx_3, :],
|
|
114
|
+
P3_all[tet_idx_3, :],
|
|
115
|
+
np.tile(points_out[i, :], (tet_idx_3.shape[0], 1)))
|
|
116
|
+
tet_idx_bool_4 = (vtest4 >= 0)
|
|
117
|
+
|
|
118
|
+
tet_idx_local[i_local] = tet_idx_3[
|
|
119
|
+
np.nonzero(tet_idx_bool_4)[0]].astype(int)
|
|
120
|
+
i_local = i_local + 1
|
|
121
|
+
|
|
122
|
+
stop = time.time()
|
|
123
|
+
print('Determining element index of point: {:s}/{:d} ({:d}/{:d}) \t [{:1.2f} sec] \t {:1.2f}%' \
|
|
124
|
+
.format(str(i).zfill(int(np.floor(np.log10(N_points_total)) + 1)),
|
|
125
|
+
N_points_total,
|
|
126
|
+
i_local,
|
|
127
|
+
len(chunk),
|
|
128
|
+
stop - start,
|
|
129
|
+
float(i_local) / (N_points_total / N_CPU) * 100.0))
|
|
130
|
+
return tet_idx_local
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def load_roi_surface_obj_from_hdf5(fname):
|
|
134
|
+
warnings.warn("DeprecationWarning")
|
|
135
|
+
return read_roi_from_mesh_hdf5(fname)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def read_roi_from_mesh_hdf5(fname, roi_id=None):
|
|
139
|
+
"""
|
|
140
|
+
Loading and initializing RegionOfInterestSurface object/s from .hdf5 mesh file.
|
|
141
|
+
|
|
142
|
+
Parameters
|
|
143
|
+
----------
|
|
144
|
+
fname : str
|
|
145
|
+
Filename (incl. path) of .hdf5 mesh file, e.g. from subject.fn_mesh_hdf5
|
|
146
|
+
roi_id : str, optional
|
|
147
|
+
Which ROI to return. If empty: return all as list.
|
|
148
|
+
|
|
149
|
+
Returns
|
|
150
|
+
-------
|
|
151
|
+
RegionOfInterestSurface : pynibs.roi.RegionOfInterestSurface or list of pynibs.roi.RegionOfInterestSurface
|
|
152
|
+
RegionOfInterestSurface
|
|
153
|
+
"""
|
|
154
|
+
with h5py.File(fname, 'r') as f:
|
|
155
|
+
if "roi_surface" not in f.keys():
|
|
156
|
+
raise ValueError(f"No ROIs found in {fname}.")
|
|
157
|
+
rois = [k for k in f["roi_surface"].keys()]
|
|
158
|
+
roi = dict()
|
|
159
|
+
|
|
160
|
+
# loop over all available roi
|
|
161
|
+
for roi_id_in_mesh in rois:
|
|
162
|
+
if roi_id is not None and roi_id_in_mesh != roi_id:
|
|
163
|
+
continue
|
|
164
|
+
# initialize roi
|
|
165
|
+
roi[roi_id_in_mesh] = pynibs.RegionOfInterestSurface()
|
|
166
|
+
roi[roi_id_in_mesh].mesh_folder = os.path.split(fname)[0]
|
|
167
|
+
|
|
168
|
+
# read all labels
|
|
169
|
+
data_label = list(f[f"roi_surface/{roi_id_in_mesh}"].keys())
|
|
170
|
+
data_label = [str(r) for r in data_label]
|
|
171
|
+
|
|
172
|
+
# read data from .hdf5 file and pass it to object
|
|
173
|
+
for j in range(len(data_label)):
|
|
174
|
+
# numpy array of strings
|
|
175
|
+
if data_label[j] == "layers":
|
|
176
|
+
expr = ""
|
|
177
|
+
layer_ids = f[f'roi_surface/{roi_id_in_mesh}/{data_label[j]}'].keys()
|
|
178
|
+
for layer_id in layer_ids:
|
|
179
|
+
node_coords = f[f'roi_surface/{roi_id_in_mesh}/{data_label[j]}/{layer_id}/node_coord'][:]
|
|
180
|
+
node_number_list = f[f'roi_surface/{roi_id_in_mesh}/{data_label[j]}/{layer_id}/node_number_list'][:]
|
|
181
|
+
|
|
182
|
+
if np.min(node_number_list) == 0:
|
|
183
|
+
node_number_list += 1
|
|
184
|
+
|
|
185
|
+
nodes = simnibs.Nodes(node_coord=node_coords)
|
|
186
|
+
elements = simnibs.Elements(triangles=node_number_list)
|
|
187
|
+
surf = simnibs.Msh(nodes=nodes, elements=elements)
|
|
188
|
+
|
|
189
|
+
layers = getattr(roi[roi_id_in_mesh], data_label[j])
|
|
190
|
+
layers.append(pynibs.CorticalLayer.init_from_surface(layer_id, surf))
|
|
191
|
+
|
|
192
|
+
elif type(f[f'roi_surface/{roi_id_in_mesh}/{data_label[j]}'][()]) == np.ndarray and \
|
|
193
|
+
"S" in str(f[f'roi_surface/{roi_id_in_mesh}/{data_label[j]}'].dtype) and \
|
|
194
|
+
len(f[f'roi_surface/{roi_id_in_mesh}/{data_label[j]}'][()]) > 1:
|
|
195
|
+
expr = "roi[roi_id_in_mesh]." + data_label[
|
|
196
|
+
j] + "= list(f[f'roi_surface/{roi_id_in_mesh}/{data_label[j]}'][:].astype(str))"
|
|
197
|
+
|
|
198
|
+
# a single string (numpy bytes)
|
|
199
|
+
elif type(f[f'roi_surface/{roi_id_in_mesh}/{data_label[j]}'][()]) == np.bytes_ and \
|
|
200
|
+
"S" in str(f[f'roi_surface/{roi_id_in_mesh}/{data_label[j]}'].dtype):
|
|
201
|
+
expr = "roi[roi_id_in_mesh]." + data_label[
|
|
202
|
+
j] + "= str(f[f'roi_surface/{roi_id_in_mesh}/{data_label[j]}'][()].astype(str))"
|
|
203
|
+
|
|
204
|
+
# bytes string (pure Python, utf-8 encoded)
|
|
205
|
+
elif type(f[f'roi_surface/{roi_id_in_mesh}/{data_label[j]}'][()]) == bytes:
|
|
206
|
+
expr = "roi[roi_id_in_mesh]." + data_label[
|
|
207
|
+
j] + "= f[f'roi_surface/{roi_id_in_mesh}/{data_label[j]}'][()].decode('utf-8')"
|
|
208
|
+
|
|
209
|
+
# Python list of bytes string
|
|
210
|
+
elif type(f[f'roi_surface/{roi_id_in_mesh}/{data_label[j]}'][()]) == list and \
|
|
211
|
+
len(f[f'roi_surface/{roi_id_in_mesh}/{data_label[j]}'][()]) > 1:
|
|
212
|
+
expr = "roi[roi_id_in_mesh]." + data_label[
|
|
213
|
+
j] + "= list(f[f'roi_surface/{roi_id_in_mesh}/{data_label[j]}'][:].astype(str))"
|
|
214
|
+
|
|
215
|
+
# single numeric value (integer or float)
|
|
216
|
+
elif type(f[f'roi_surface/{roi_id_in_mesh}/{data_label[j]}'][()]) == np.float64 or \
|
|
217
|
+
type(f[f'roi_surface/{roi_id_in_mesh}/{data_label[j]}'][()]) == np.int64:
|
|
218
|
+
expr = "roi[roi_id_in_mesh]." + data_label[j] + "= f[f'roi_surface/{roi_id_in_mesh}/{data_label[j]}'][()]"
|
|
219
|
+
|
|
220
|
+
# array of numeric values
|
|
221
|
+
else:
|
|
222
|
+
expr = "roi[roi_id_in_mesh]." + data_label[j] + "= np.array(f[f'roi_surface/{roi_id_in_mesh}/{data_label[j]}'])"
|
|
223
|
+
exec(expr)
|
|
224
|
+
|
|
225
|
+
for key in roi[roi_id_in_mesh].__dict__:
|
|
226
|
+
|
|
227
|
+
if type(getattr(roi[roi_id_in_mesh], key)) == str:
|
|
228
|
+
if getattr(roi[roi_id_in_mesh], key) == "None":
|
|
229
|
+
setattr(roi[roi_id_in_mesh], key, None)
|
|
230
|
+
|
|
231
|
+
if type(getattr(roi[roi_id_in_mesh], key)) == list:
|
|
232
|
+
lst_tmp = []
|
|
233
|
+
for i_a, a in enumerate(getattr(roi[roi_id_in_mesh], key)):
|
|
234
|
+
if a == "None":
|
|
235
|
+
lst_tmp.append(None)
|
|
236
|
+
else:
|
|
237
|
+
lst_tmp.append(a)
|
|
238
|
+
setattr(roi[roi_id_in_mesh], key, lst_tmp)
|
|
239
|
+
|
|
240
|
+
roi[roi_id_in_mesh].n_tris = roi[roi_id_in_mesh].node_number_list.shape[0]
|
|
241
|
+
roi[roi_id_in_mesh].n_nodes = roi[roi_id_in_mesh].node_coord_low.shape[0]
|
|
242
|
+
roi[roi_id_in_mesh].n_tets = roi[roi_id_in_mesh].tet_idx_node_coord_mid.shape[0]
|
|
243
|
+
|
|
244
|
+
if not roi:
|
|
245
|
+
raise ValueError(f"ROI {roi_id} not found in {fname}. Available ROIs are: {rois}")
|
|
246
|
+
if roi_id is not None:
|
|
247
|
+
return roi[roi_id]
|
|
248
|
+
else:
|
|
249
|
+
return roi
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def determine_element_idx_in_mesh(fname, msh, points, compute_baricentric=False):
|
|
253
|
+
"""
|
|
254
|
+
Finds the tetrahedron that contains each of the described points using a stochastic walk algorithm.
|
|
255
|
+
Implemented from Devillers et al. (2002) [1]_
|
|
256
|
+
|
|
257
|
+
Parameters
|
|
258
|
+
----------
|
|
259
|
+
msh : pynibs.mesh.mesh_struct.TetrahedraLinear
|
|
260
|
+
fname : str or None
|
|
261
|
+
Filename of saved .txt file containing the element indices (no data is saved when `fname=None` or `fname=''`)
|
|
262
|
+
points : np.ndarray (N, 3) or list of np.ndarray
|
|
263
|
+
List of points to be queried
|
|
264
|
+
compute_baricentric : bool
|
|
265
|
+
Wether or not to compute baricentric coordinates of the points
|
|
266
|
+
|
|
267
|
+
Returns
|
|
268
|
+
-------
|
|
269
|
+
th_with_points : np.ndarray
|
|
270
|
+
List with the tetrahedron that contains each point. If the point is outside
|
|
271
|
+
the mesh, the value will be -1
|
|
272
|
+
baricentric : np.ndarray [n, 4](if compute_baricentric == True)
|
|
273
|
+
Baricentric coordinates of point. If the point is outside, a list of zeros
|
|
274
|
+
|
|
275
|
+
Notes
|
|
276
|
+
-----
|
|
277
|
+
.. [1] Devillers, Olivier, Sylvain Pion, and Monique Teillaud. "Walking in a
|
|
278
|
+
triangulation." International Journal of Foundations of Computer Science 13.02
|
|
279
|
+
(2002): 181-199.
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
if platform.system() == 'Linux':
|
|
283
|
+
import simnibs
|
|
284
|
+
|
|
285
|
+
if int(simnibs.__version__[0]) < 4:
|
|
286
|
+
import simnibs.cython_code.cython_msh as cython_msh
|
|
287
|
+
else:
|
|
288
|
+
from simnibs import cython_msh
|
|
289
|
+
|
|
290
|
+
else:
|
|
291
|
+
raise OSError('This function works currently only under Linux!')
|
|
292
|
+
|
|
293
|
+
if type(points) is not list:
|
|
294
|
+
points = [points]
|
|
295
|
+
|
|
296
|
+
n_pointsets = len(points)
|
|
297
|
+
|
|
298
|
+
# determine size of input data
|
|
299
|
+
n_points = [i.shape[0] for i in points]
|
|
300
|
+
n_points_out_max = np.max(n_points)
|
|
301
|
+
|
|
302
|
+
tet_idx = np.ones((n_points_out_max, n_pointsets))
|
|
303
|
+
baricentric = [[] for _ in range(n_pointsets)]
|
|
304
|
+
|
|
305
|
+
th_indices = np.arange(msh.tetrahedra.shape[0])
|
|
306
|
+
th_nodes = msh.points[msh.tetrahedra]
|
|
307
|
+
|
|
308
|
+
for i in range(n_pointsets):
|
|
309
|
+
# Reduce the number of elements
|
|
310
|
+
points_max = np.max(points[i], axis=0)
|
|
311
|
+
points_min = np.min(points[i], axis=0)
|
|
312
|
+
th_max = np.max(th_nodes, axis=1)
|
|
313
|
+
th_min = np.min(th_nodes, axis=1)
|
|
314
|
+
slack = (points_max - points_min) * .05
|
|
315
|
+
th_in_box = np.where(np.all((th_min <= points_max + slack) * (th_max >= points_min - slack), axis=1))[0]
|
|
316
|
+
th_indices = th_indices[th_in_box]
|
|
317
|
+
th_nodes = th_nodes[th_in_box]
|
|
318
|
+
|
|
319
|
+
# Calculate a few things we will use later
|
|
320
|
+
faces, th_faces, adjacency_list = msh.get_faces(th_indices)
|
|
321
|
+
|
|
322
|
+
# Find initial positions
|
|
323
|
+
th_baricenters = np.average(th_nodes, axis=1)
|
|
324
|
+
kdtree = scipy.spatial.cKDTree(th_baricenters)
|
|
325
|
+
|
|
326
|
+
# Starting position for walking algorithm: the closest baricenter
|
|
327
|
+
_, closest_th = kdtree.query(points[i])
|
|
328
|
+
pts = np.array(points[i], dtype=float)
|
|
329
|
+
th_nodes = np.array(th_nodes, dtype=float)
|
|
330
|
+
closest_th = np.array(closest_th, dtype=int)
|
|
331
|
+
th_faces = np.array(th_faces, dtype=int)
|
|
332
|
+
adjacency_list = np.array(adjacency_list, dtype=int)
|
|
333
|
+
th_with_points = cython_msh.find_tetrahedron_with_points(pts, th_nodes, closest_th, th_faces, adjacency_list)
|
|
334
|
+
|
|
335
|
+
# calculate baricentric coordinates
|
|
336
|
+
inside = th_with_points != -1
|
|
337
|
+
if compute_baricentric:
|
|
338
|
+
M = np.transpose(th_nodes[th_with_points[inside], :3, :3] -
|
|
339
|
+
th_nodes[th_with_points[inside], 3, None, :], (0, 2, 1))
|
|
340
|
+
baricentric[i] = np.zeros((len(points[i]), 4), dtype=float)
|
|
341
|
+
baricentric[i][inside, :3] = np.linalg.solve(M, points[i][inside] - th_nodes[th_with_points[inside], 3, :])
|
|
342
|
+
baricentric[i][inside, 3] = 1 - np.sum(baricentric[i][inside], axis=1)
|
|
343
|
+
|
|
344
|
+
# Return indices
|
|
345
|
+
th_with_points[inside] = th_indices[th_with_points[inside]]
|
|
346
|
+
tet_idx[:, i] = th_with_points.astype(int)
|
|
347
|
+
|
|
348
|
+
if not (fname is None or fname == ''):
|
|
349
|
+
if not os.path.exists(os.path.dirname(fname)):
|
|
350
|
+
os.makedirs(os.path.dirname(fname))
|
|
351
|
+
np.savetxt(fname, tet_idx, '%d')
|
|
352
|
+
print('Saved element indices of points_out in {}'.format(fname))
|
|
353
|
+
|
|
354
|
+
if compute_baricentric:
|
|
355
|
+
return tet_idx, baricentric
|
|
356
|
+
else:
|
|
357
|
+
return tet_idx
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def make_GM_WM_surface(gm_surf_fname, wm_surf_fname, mesh_folder, midlayer_surf_fname=None, delta=0.5,
|
|
361
|
+
x_roi=None, y_roi=None, z_roi=None,
|
|
362
|
+
layer=1,
|
|
363
|
+
fn_mask=None,
|
|
364
|
+
refine=False):
|
|
365
|
+
"""
|
|
366
|
+
Generating a surface between WM and GM in a distance of delta 0...1 for ROI,
|
|
367
|
+
given by freesurfer mask or coordinates.
|
|
368
|
+
|
|
369
|
+
Parameters
|
|
370
|
+
----------
|
|
371
|
+
gm_surf_fname : str or list of str
|
|
372
|
+
Filename(s) of GM surface generated by freesurfer (lh and/or rh)
|
|
373
|
+
(e.g. in mri2msh: fs_ID/surf/lh.pial)
|
|
374
|
+
wm_surf_fname : str or list of str
|
|
375
|
+
Filename(s) of WM surface generated by freesurfer (lh and/or rh)
|
|
376
|
+
(e.g. in mri2msh: fs_ID/surf/lh.white)
|
|
377
|
+
mesh_folder : str
|
|
378
|
+
Path of mesh (parent directory)
|
|
379
|
+
midlayer_surf_fname : str or list of str
|
|
380
|
+
filename(s) of midlayer surface generated by headreco (lh and/or rh)
|
|
381
|
+
(e.g. in headreco: fs_ID/surf/lh.central) (after conversion)
|
|
382
|
+
[defunct] m2m_mat_fname : str
|
|
383
|
+
Filename of mri2msh transformation matrix
|
|
384
|
+
(e.g. in mri2msh: m2m_ProbandID/MNI2conform_6DOF.mat)
|
|
385
|
+
delta : float
|
|
386
|
+
Distance parameter where surface is generated 0...1 (default: 0.5)
|
|
387
|
+
|
|
388
|
+
* 0 -> WM surface
|
|
389
|
+
* 1 -> GM surface
|
|
390
|
+
x_roi : list of float or None
|
|
391
|
+
Region of interest [Xmin, Xmax], whole X range if empty [0,0] or None
|
|
392
|
+
(left - right)
|
|
393
|
+
y_roi : list of float or None
|
|
394
|
+
Region of interest [Ymin, Ymax], whole Y range if empty [0,0] or None
|
|
395
|
+
(anterior - posterior)
|
|
396
|
+
z_roi : list of float or None
|
|
397
|
+
Region of interest [Zmin, Zmax], whole Z range if empty [0,0] or None
|
|
398
|
+
(inferior - superior)
|
|
399
|
+
layer : int
|
|
400
|
+
Define the number of layers:
|
|
401
|
+
|
|
402
|
+
* 1: one layer
|
|
403
|
+
* 3: additionally upper and lower layers are generated around the central midlayer
|
|
404
|
+
fn_mask: string or None
|
|
405
|
+
Filename for freesurfer mask. If given, this is used instead of *_ROIs
|
|
406
|
+
refine : bool, optional, default: False
|
|
407
|
+
Refine ROI by splitting elements
|
|
408
|
+
|
|
409
|
+
Returns
|
|
410
|
+
-------
|
|
411
|
+
if layer == 3:
|
|
412
|
+
surface_points_upper : np.ndarray of float
|
|
413
|
+
(N_points, 3) Coordinates (x, y, z) of surface + epsilon (in GM surface direction)
|
|
414
|
+
surface_points_middle : np.ndarray of float
|
|
415
|
+
(N_points, 3) Coordinates (x, y, z) of surface
|
|
416
|
+
surface_points_lower : np.ndarray of float
|
|
417
|
+
(N_points, 3) Coordinates (x, y, z) of surface - epsilon (in WM surface direction)
|
|
418
|
+
connectivity : np.ndarray of int
|
|
419
|
+
(N_tri x 3) Connectivity of triangles (indexation starts at 0!)
|
|
420
|
+
|
|
421
|
+
else:
|
|
422
|
+
surface_points_middle : np.ndarray of float
|
|
423
|
+
(N_points, 3) Coordinates (x, y, z) of surface
|
|
424
|
+
connectivity : np.ndarray of int
|
|
425
|
+
(N_tri x 3) Connectivity of triangles (indexation starts at 0!)
|
|
426
|
+
|
|
427
|
+
Example
|
|
428
|
+
-------
|
|
429
|
+
|
|
430
|
+
.. code-block:: python
|
|
431
|
+
|
|
432
|
+
make_GM_WM_surface(self, gm_surf_fname, wm_surf_fname, delta, X_ROI, Y_ROI, Z_ROI)
|
|
433
|
+
make_GM_WM_surface(self, gm_surf_fname, wm_surf_fname, delta, mask_fn, layer=3)
|
|
434
|
+
"""
|
|
435
|
+
|
|
436
|
+
if type(gm_surf_fname) is not list:
|
|
437
|
+
gm_surf_fname = [gm_surf_fname]
|
|
438
|
+
|
|
439
|
+
if type(wm_surf_fname) is not list:
|
|
440
|
+
wm_surf_fname = [wm_surf_fname]
|
|
441
|
+
|
|
442
|
+
if type(midlayer_surf_fname) is not list:
|
|
443
|
+
midlayer_surf_fname = [midlayer_surf_fname]
|
|
444
|
+
|
|
445
|
+
if len(gm_surf_fname) != len(wm_surf_fname):
|
|
446
|
+
raise ValueError('Provide equal number of GM and WM surfaces!')
|
|
447
|
+
|
|
448
|
+
# load surface data
|
|
449
|
+
points_gm = [None for _ in range(len(gm_surf_fname))]
|
|
450
|
+
points_wm = [None for _ in range(len(wm_surf_fname))]
|
|
451
|
+
points_mid = [None for _ in range(len(midlayer_surf_fname))]
|
|
452
|
+
con_gm = [None for _ in range(len(gm_surf_fname))]
|
|
453
|
+
con_wm = [None for _ in range(len(wm_surf_fname))]
|
|
454
|
+
con_mid = [None for _ in range(len(midlayer_surf_fname))]
|
|
455
|
+
|
|
456
|
+
max_idx_gm = 0
|
|
457
|
+
max_idx_wm = 0
|
|
458
|
+
max_idx_mid = 0
|
|
459
|
+
|
|
460
|
+
for i in range(len(gm_surf_fname)):
|
|
461
|
+
if gm_surf_fname[i] is not None:
|
|
462
|
+
if gm_surf_fname[i].endswith('.gii'):
|
|
463
|
+
img = nibabel.gifti.giftiio.read(os.path.join(mesh_folder, gm_surf_fname[i]))
|
|
464
|
+
points_gm[i] = img.agg_data('pointset')
|
|
465
|
+
con_gm[i] = img.agg_data('triangle')
|
|
466
|
+
else:
|
|
467
|
+
points_gm[i], con_gm[i] = nib.freesurfer.read_geometry(os.path.join(mesh_folder, gm_surf_fname[i]))
|
|
468
|
+
con_gm[i] = con_gm[i] + max_idx_gm
|
|
469
|
+
max_idx_gm = max_idx_gm + points_gm[i].shape[0] # np.max(con_gm[i]) + 2
|
|
470
|
+
|
|
471
|
+
if wm_surf_fname[i] is not None:
|
|
472
|
+
if wm_surf_fname[i].endswith('.gii'):
|
|
473
|
+
img = nibabel.gifti.giftiio.read(os.path.join(mesh_folder, wm_surf_fname[i]))
|
|
474
|
+
points_wm[i] = img.agg_data('pointset')
|
|
475
|
+
con_wm[i] = img.agg_data('triangle')
|
|
476
|
+
else:
|
|
477
|
+
points_wm[i], con_wm[i] = nib.freesurfer.read_geometry(os.path.join(mesh_folder, wm_surf_fname[i]))
|
|
478
|
+
con_wm[i] = con_wm[i] + max_idx_wm
|
|
479
|
+
max_idx_wm = max_idx_wm + points_wm[i].shape[0] # np.max(con_wm[i]) + 2
|
|
480
|
+
|
|
481
|
+
if midlayer_surf_fname[i] is not None:
|
|
482
|
+
if midlayer_surf_fname[i].endswith('.gii'):
|
|
483
|
+
img = nibabel.gifti.giftiio.read(os.path.join(mesh_folder, midlayer_surf_fname[i]))
|
|
484
|
+
points_mid[i] = img.agg_data('pointset')
|
|
485
|
+
con_mid[i] = img.agg_data('triangle')
|
|
486
|
+
else:
|
|
487
|
+
points_mid[i], con_mid[i] = nib.freesurfer.read_geometry(
|
|
488
|
+
os.path.join(mesh_folder, midlayer_surf_fname[i]))
|
|
489
|
+
con_mid[i] = con_mid[i] + max_idx_mid
|
|
490
|
+
max_idx_mid = max_idx_mid + points_mid[i].shape[0] # np.max(con_wm[i]) + 2
|
|
491
|
+
|
|
492
|
+
points_gm = np.vstack(points_gm)
|
|
493
|
+
points_wm = np.vstack(points_wm)
|
|
494
|
+
points_mid = np.vstack(points_mid)
|
|
495
|
+
con_gm = np.vstack(con_gm)
|
|
496
|
+
con_wm = np.vstack(con_wm)
|
|
497
|
+
con_mid = np.vstack(con_mid)
|
|
498
|
+
|
|
499
|
+
# Determine 3 layer midlayer if GM and WM surfaces are present otherwise use provided midlayer data
|
|
500
|
+
if gm_surf_fname[0] is not None and wm_surf_fname[0] is not None:
|
|
501
|
+
# determine vector pointing from wm surface to gm surface
|
|
502
|
+
wm_gm_vector = points_gm - points_wm
|
|
503
|
+
|
|
504
|
+
eps_0 = 0.025
|
|
505
|
+
eps, surface_points_upper, surface_points_lower = (False,) * 3
|
|
506
|
+
surface_points_upper = False
|
|
507
|
+
if layer == 3:
|
|
508
|
+
# set epsilon range for upper and lower surface
|
|
509
|
+
if delta < eps_0:
|
|
510
|
+
eps = delta / 2
|
|
511
|
+
elif delta > (1 - eps_0):
|
|
512
|
+
eps = (1 - delta) / 2
|
|
513
|
+
else:
|
|
514
|
+
eps = eps_0
|
|
515
|
+
|
|
516
|
+
# determine wm-gm surfaces
|
|
517
|
+
surface_points_middle = points_wm + wm_gm_vector * delta # type: np.ndarray
|
|
518
|
+
if layer == 3:
|
|
519
|
+
surface_points_upper = surface_points_middle + wm_gm_vector * eps
|
|
520
|
+
surface_points_lower = surface_points_middle - wm_gm_vector * eps
|
|
521
|
+
|
|
522
|
+
con = con_gm
|
|
523
|
+
|
|
524
|
+
elif midlayer_surf_fname[0] is not None:
|
|
525
|
+
layer = 1
|
|
526
|
+
|
|
527
|
+
surface_points_upper = None
|
|
528
|
+
surface_points_lower = None
|
|
529
|
+
surface_points_middle = points_mid
|
|
530
|
+
|
|
531
|
+
con = con_mid
|
|
532
|
+
|
|
533
|
+
else:
|
|
534
|
+
raise IOError("Please provide GM and WM surfaces or midlayer "
|
|
535
|
+
"surface directly for midlayer calculation...")
|
|
536
|
+
|
|
537
|
+
# crop region if desired
|
|
538
|
+
x_roi_wb, y_roi_wb, z_roi_wb = (False,) * 3
|
|
539
|
+
if fn_mask is None:
|
|
540
|
+
# crop to region of interest
|
|
541
|
+
if x_roi is None or x_roi == [0, 0]:
|
|
542
|
+
x_roi = [-np.inf, np.inf]
|
|
543
|
+
x_roi_wb = True
|
|
544
|
+
if y_roi is None or y_roi == [0, 0]:
|
|
545
|
+
y_roi = [-np.inf, np.inf]
|
|
546
|
+
y_roi_wb = True
|
|
547
|
+
if z_roi is None or z_roi == [0, 0]:
|
|
548
|
+
z_roi = [-np.inf, np.inf]
|
|
549
|
+
z_roi_wb = True
|
|
550
|
+
|
|
551
|
+
roi_mask_bool = (surface_points_middle[:, 0] > min(x_roi)) & (surface_points_middle[:, 0] < max(x_roi)) & \
|
|
552
|
+
(surface_points_middle[:, 1] > min(y_roi)) & (surface_points_middle[:, 1] < max(y_roi)) & \
|
|
553
|
+
(surface_points_middle[:, 2] > min(z_roi)) & (surface_points_middle[:, 2] < max(z_roi))
|
|
554
|
+
roi_mask_idx = np.where(roi_mask_bool)
|
|
555
|
+
|
|
556
|
+
else:
|
|
557
|
+
# read mask from freesurfer mask file
|
|
558
|
+
mask = nib.freesurfer.mghformat.MGHImage.from_filename(os.path.join(mesh_folder, fn_mask)).dataobj[:]
|
|
559
|
+
roi_mask_idx = np.where(mask > 0.5)
|
|
560
|
+
|
|
561
|
+
# redefine connectivity matrix for cropped points (reindexing)
|
|
562
|
+
# get row index where all points are lying inside ROI
|
|
563
|
+
if not (x_roi_wb and y_roi_wb and z_roi_wb):
|
|
564
|
+
con_row_idx = [i for i in range(con.shape[0]) if len(np.intersect1d(con[i,], roi_mask_idx)) == 3]
|
|
565
|
+
# crop connectivity matrix to ROI
|
|
566
|
+
con_cropped = con[con_row_idx,]
|
|
567
|
+
else:
|
|
568
|
+
con_cropped = con
|
|
569
|
+
|
|
570
|
+
# evaluate new indices of cropped connectivity matrix
|
|
571
|
+
point_idx_before, point_idx_after = np.unique(con_cropped, return_inverse=True)
|
|
572
|
+
con_cropped_reform = np.reshape(point_idx_after, (con_cropped.shape[0], con_cropped.shape[1]))
|
|
573
|
+
|
|
574
|
+
# crop points to ROI
|
|
575
|
+
surface_points_middle = surface_points_middle[point_idx_before,]
|
|
576
|
+
if layer == 3:
|
|
577
|
+
surface_points_upper = surface_points_upper[point_idx_before,]
|
|
578
|
+
surface_points_lower = surface_points_lower[point_idx_before,]
|
|
579
|
+
|
|
580
|
+
# refine
|
|
581
|
+
if refine:
|
|
582
|
+
if not os.path.exists(os.path.join(mesh_folder, "", "tmp")):
|
|
583
|
+
os.makedirs(os.path.join(mesh_folder, "", "tmp"))
|
|
584
|
+
|
|
585
|
+
mesh = trimesh.Trimesh(vertices=surface_points_middle,
|
|
586
|
+
faces=con_cropped_reform)
|
|
587
|
+
roi_fn = os.path.join(mesh_folder, "", "tmp", "roi.stl")
|
|
588
|
+
mesh.export(roi_fn)
|
|
589
|
+
|
|
590
|
+
roi_refined_fn = os.path.join(mesh_folder, "", "tmp", "roi_refined.stl")
|
|
591
|
+
pynibs.refine_surface(fn_surf=roi_fn,
|
|
592
|
+
fn_surf_refined=roi_refined_fn,
|
|
593
|
+
center=[0, 0, 0],
|
|
594
|
+
radius=np.inf,
|
|
595
|
+
verbose=True,
|
|
596
|
+
repair=False,
|
|
597
|
+
remesh=False)
|
|
598
|
+
|
|
599
|
+
roi = trimesh.load(roi_refined_fn)
|
|
600
|
+
con_cropped_reform = roi.faces
|
|
601
|
+
surface_points_middle = roi.vertices
|
|
602
|
+
|
|
603
|
+
if layer == 3:
|
|
604
|
+
roi = []
|
|
605
|
+
|
|
606
|
+
for p in [surface_points_upper, surface_points_lower]:
|
|
607
|
+
mesh = trimesh.Trimesh(vertices=p,
|
|
608
|
+
faces=con_cropped_reform)
|
|
609
|
+
mesh.export(roi_fn)
|
|
610
|
+
|
|
611
|
+
pynibs.refine_surface(fn_surf=roi_fn,
|
|
612
|
+
fn_surf_refined=roi_refined_fn,
|
|
613
|
+
center=[0, 0, 0],
|
|
614
|
+
radius=np.inf,
|
|
615
|
+
verbose=True,
|
|
616
|
+
repair=False)
|
|
617
|
+
|
|
618
|
+
roi.append(trimesh.load(roi_refined_fn))
|
|
619
|
+
|
|
620
|
+
surface_points_upper = roi[0].vertices
|
|
621
|
+
surface_points_lower = roi[1].vertices
|
|
622
|
+
|
|
623
|
+
if layer == 3:
|
|
624
|
+
return surface_points_upper, surface_points_middle, surface_points_lower, con_cropped_reform
|
|
625
|
+
else:
|
|
626
|
+
return surface_points_middle, con_cropped_reform
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def get_sphere_in_nii(center, radius, nii=None, out_fn=None,
|
|
630
|
+
thresh_by_nii=True, val_in=1, val_out=0,
|
|
631
|
+
outside_val=0, outside_radius=np.inf):
|
|
632
|
+
"""
|
|
633
|
+
Computes a spherical ROI for a given Nifti image (defaults to SimNIBS MNI T1 tissue). The ROI area is defined
|
|
634
|
+
in nifti coordinates.
|
|
635
|
+
By default, everything inside the ROI is set to 1, areas outside = 0.
|
|
636
|
+
The ROI is further thresholded by the nifti.
|
|
637
|
+
A nib.Nifti image is returned and optionally saved.
|
|
638
|
+
|
|
639
|
+
Parameters
|
|
640
|
+
----------
|
|
641
|
+
center : array-like
|
|
642
|
+
X, Y, Z coordinates in nifti space
|
|
643
|
+
radius : float
|
|
644
|
+
radius of sphere
|
|
645
|
+
nii : string or nib.nifti1.Nifti1Image, optional
|
|
646
|
+
The nifti image to work with.
|
|
647
|
+
out_fn : string, optional
|
|
648
|
+
If provided, sphere ROI image is saved here
|
|
649
|
+
outside_val : float, default = None
|
|
650
|
+
Value outside of outside_radius.
|
|
651
|
+
outside_radius : float, default = None
|
|
652
|
+
Distance factor to define the 'outside' area: oudsidefactor * radius -> outside
|
|
653
|
+
|
|
654
|
+
Returns
|
|
655
|
+
-------
|
|
656
|
+
sphere_img : nib.nifti1.Nifti1Image
|
|
657
|
+
sphere_img : <file>, optional
|
|
658
|
+
|
|
659
|
+
Other Parameters
|
|
660
|
+
----------------
|
|
661
|
+
thresh_by_nii : bool, optional
|
|
662
|
+
Mask sphere by nii != 0
|
|
663
|
+
val_in : float, optional
|
|
664
|
+
Value within ROI
|
|
665
|
+
val_out : float, optional
|
|
666
|
+
Value outside ROI
|
|
667
|
+
|
|
668
|
+
Raises
|
|
669
|
+
------
|
|
670
|
+
ValueError
|
|
671
|
+
If the final ROI is empty.
|
|
672
|
+
"""
|
|
673
|
+
if outside_radius is None:
|
|
674
|
+
outside_radius = np.inf
|
|
675
|
+
# load image for affine and tissue borders
|
|
676
|
+
if nii is None:
|
|
677
|
+
from simnibs import SIMNIBSDIR
|
|
678
|
+
mni_fn = f"{SIMNIBSDIR}/resources/templates/spmprior_tissue.nii"
|
|
679
|
+
nii = nib.load(mni_fn)
|
|
680
|
+
elif isinstance(nii, str):
|
|
681
|
+
nii = nib.load(nii)
|
|
682
|
+
|
|
683
|
+
center = np.atleast_1d(center).astype(int)
|
|
684
|
+
assert center.shape == (3,)
|
|
685
|
+
|
|
686
|
+
roi_data = np.zeros_like(nii.get_fdata())
|
|
687
|
+
roi_data[:] = val_out
|
|
688
|
+
|
|
689
|
+
center = np.atleast_1d(center)
|
|
690
|
+
|
|
691
|
+
# speed up the long loop below
|
|
692
|
+
affine_inv = np.linalg.inv(nii.affine)
|
|
693
|
+
norm = np.linalg.norm
|
|
694
|
+
|
|
695
|
+
center_vox = nib.affines.apply_affine(affine_inv, center)
|
|
696
|
+
|
|
697
|
+
if outside_radius != np.inf:
|
|
698
|
+
# search space is whole nifti space
|
|
699
|
+
x_min, y_min, z_min = 0, 0, 0
|
|
700
|
+
x_max, y_max, z_max = nii.shape[:3]
|
|
701
|
+
|
|
702
|
+
else:
|
|
703
|
+
# if outside_val should not be set we only need to search within center-radius
|
|
704
|
+
x_min, y_min, z_min = (center_vox - radius).astype(int)
|
|
705
|
+
x_max, y_max, z_max = (center_vox + radius).astype(int)
|
|
706
|
+
|
|
707
|
+
# compute spherical roi around nifti coordinates
|
|
708
|
+
# do this in original coordinates to have correct mm radius
|
|
709
|
+
|
|
710
|
+
pixdim = nii.header['pixdim'][1:4]
|
|
711
|
+
for xyz in tqdm.tqdm(product(range(x_min, x_max), range(y_min, y_max), range(z_min, z_max)),
|
|
712
|
+
total=(x_max - x_min) * (y_max - y_min) * (z_max - z_min),
|
|
713
|
+
desc="Finding ROI elements."):
|
|
714
|
+
# set to val_in if within radius
|
|
715
|
+
dist = norm(np.multiply(pixdim, center_vox - xyz))
|
|
716
|
+
if dist < radius:
|
|
717
|
+
roi_data[xyz] = val_in
|
|
718
|
+
elif dist > outside_radius:
|
|
719
|
+
# Set values outside of outside_radius to outside_val
|
|
720
|
+
roi_data[xyz] = outside_val
|
|
721
|
+
|
|
722
|
+
# check if there's something left in the ROI
|
|
723
|
+
if np.sum(roi_data) == 0:
|
|
724
|
+
raise ValueError(f"No tissue found in {radius} mm sphere around {center}.")
|
|
725
|
+
|
|
726
|
+
# threshold roi by tissue
|
|
727
|
+
if thresh_by_nii:
|
|
728
|
+
roi_data[nii.get_fdata() == 0] = 0
|
|
729
|
+
roi_data[np.isnan(nii.get_fdata())] = 0
|
|
730
|
+
|
|
731
|
+
roi_data[np.isnan(roi_data)] = outside_val
|
|
732
|
+
|
|
733
|
+
roi_mni_img = nib.Nifti1Image(roi_data, nii.affine, nii.header)
|
|
734
|
+
|
|
735
|
+
if out_fn is not None:
|
|
736
|
+
# make new roi image and save file
|
|
737
|
+
nib.save(roi_mni_img, out_fn)
|
|
738
|
+
|
|
739
|
+
return roi_mni_img
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def create_refine_spherical_roi(center, radius, final_tissues_nii, out_fn, target_size=.5,
|
|
743
|
+
outside_size=None, outside_factor=3,
|
|
744
|
+
out_spher_fn=None, tissue_types=None, verbose=False):
|
|
745
|
+
"""
|
|
746
|
+
Create a spherical roi nifti for simnibs 4 refinement.
|
|
747
|
+
Only tissue types accoring to _tissue_types will be refined.
|
|
748
|
+
|
|
749
|
+
Use the resulting output file as input for --sizing_field in SimNIBS-4/simnibs/cli/meshmesh.py
|
|
750
|
+
|
|
751
|
+
Parameters
|
|
752
|
+
----------
|
|
753
|
+
center : list of float
|
|
754
|
+
Center of spherical ROI in mm
|
|
755
|
+
radius : float
|
|
756
|
+
Radius of spherical ROI in mm
|
|
757
|
+
final_tissues_nii : string or nib.nifti1.Nifti1Image
|
|
758
|
+
final_tissues.nii.gz to create roi for.
|
|
759
|
+
out_fn : str
|
|
760
|
+
Final output filename
|
|
761
|
+
target_size : float, default = 0.5
|
|
762
|
+
Target element size of refined areas in mm (?)
|
|
763
|
+
outside_size : float, default = None
|
|
764
|
+
Element size outside of target size.
|
|
765
|
+
outside_factor : float, default = None
|
|
766
|
+
Distance factor to define the 'outside' area: oudsidefactor * radius -> outside
|
|
767
|
+
|
|
768
|
+
Other parameters
|
|
769
|
+
----------------
|
|
770
|
+
out_spher_fn : str, optional
|
|
771
|
+
Output filename of orignal, raw spherical ROI
|
|
772
|
+
tissue_types : list of float, default = [1,2,3]
|
|
773
|
+
Which tissue types to refine. Defaults to WM, GM, CSF
|
|
774
|
+
verbose : bool, optional, default=False
|
|
775
|
+
Print additional information
|
|
776
|
+
"""
|
|
777
|
+
# get spherical ROI, masked to all tissues
|
|
778
|
+
roi_img = get_sphere_in_nii(
|
|
779
|
+
center=center,
|
|
780
|
+
radius=radius,
|
|
781
|
+
nii=final_tissues_nii,
|
|
782
|
+
val_in=target_size,
|
|
783
|
+
out_fn=out_spher_fn,
|
|
784
|
+
outside_val=outside_size, outside_radius=radius * outside_factor)
|
|
785
|
+
|
|
786
|
+
# get tissue_types data
|
|
787
|
+
if isinstance(final_tissues_nii, str):
|
|
788
|
+
final_tissues_nii = nib.load(final_tissues_nii)
|
|
789
|
+
org_img_data = final_tissues_nii.get_fdata()
|
|
790
|
+
|
|
791
|
+
if tissue_types is None:
|
|
792
|
+
tissue_types = [1, 2, 3]
|
|
793
|
+
|
|
794
|
+
data = roi_img.get_fdata()
|
|
795
|
+
if verbose:
|
|
796
|
+
print(f"{np.sum(data == target_size): >6} elements found in spherical roi.")
|
|
797
|
+
if outside_size is not None:
|
|
798
|
+
print(f"{np.sum(data == outside_size): >6} elements outside area in spherical roi.")
|
|
799
|
+
# apply tissue_types list mask
|
|
800
|
+
data[~np.isin(org_img_data, tissue_types) & (data == target_size)] = 0
|
|
801
|
+
data[np.isnan(data)] = 0
|
|
802
|
+
|
|
803
|
+
# apply refined element size
|
|
804
|
+
# data[data == 1] = target_size
|
|
805
|
+
|
|
806
|
+
if verbose:
|
|
807
|
+
print(f"{np.sum(data != 0): >6} elements found in spherical roi for tissues {tissue_types}.")
|
|
808
|
+
|
|
809
|
+
# write final image
|
|
810
|
+
roi_refine_img = nib.Nifti2Image(affine=roi_img.affine, dataobj=data, header=roi_img.header)
|
|
811
|
+
roi_refine_img.to_filename(out_fn)
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
def clean_roi(img, vox_thres=.5, fn_out=None):
|
|
815
|
+
"""
|
|
816
|
+
Remove values < vox thres from image.
|
|
817
|
+
|
|
818
|
+
Parameters
|
|
819
|
+
----------
|
|
820
|
+
img : str or nibabel.nifti1.Nifti1Image
|
|
821
|
+
vox_thres : float, optional
|
|
822
|
+
fn_out : str
|
|
823
|
+
|
|
824
|
+
Returns
|
|
825
|
+
-------
|
|
826
|
+
img_thres : nibabel.nifti1.Nifti1Image
|
|
827
|
+
img_thres : <file>
|
|
828
|
+
If fn_out is specified, thresholded image is saved here
|
|
829
|
+
"""
|
|
830
|
+
# threshold subject space image to remove speckles
|
|
831
|
+
if isinstance(img, str):
|
|
832
|
+
nii = nib.load(img)
|
|
833
|
+
else:
|
|
834
|
+
nii = img
|
|
835
|
+
data = nii.get_fdata()
|
|
836
|
+
data[(data < vox_thres)] = 0
|
|
837
|
+
nii = nib.Nifti1Image(data, nii.affine)
|
|
838
|
+
if fn_out is not None:
|
|
839
|
+
nib.save(nii, fn_out)
|
|
840
|
+
return nii
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
def nii2msh(mesh, m2m_dir, nii, out_folder, hem, out_fsaverage=False, roi_name='ROI'):
|
|
844
|
+
"""
|
|
845
|
+
Transform a nifti ROI image to subject space .mgh file.
|
|
846
|
+
|
|
847
|
+
Parameters
|
|
848
|
+
----------
|
|
849
|
+
mesh : simnibs.Mesh or str
|
|
850
|
+
m2m_dir : str
|
|
851
|
+
nii : nibabel.nifti1.Nifti1Image or str
|
|
852
|
+
out_folder : str
|
|
853
|
+
hem : str
|
|
854
|
+
'lh' or 'rh'
|
|
855
|
+
out_fsaverage : bool
|
|
856
|
+
|
|
857
|
+
Returns
|
|
858
|
+
-------
|
|
859
|
+
roi : file
|
|
860
|
+
f"{out_folder}/{hem}.mesh.central.{roi_name}"
|
|
861
|
+
|
|
862
|
+
Other Parameters
|
|
863
|
+
----------------
|
|
864
|
+
roi_name : str
|
|
865
|
+
How to name the ROI
|
|
866
|
+
"""
|
|
867
|
+
from simnibs import mesh_io, transformations
|
|
868
|
+
if isinstance(mesh, str):
|
|
869
|
+
mesh = mesh_io.read_msh(mesh)
|
|
870
|
+
if isinstance(nii, str):
|
|
871
|
+
nii = nib.load(nii)
|
|
872
|
+
assert hem in ['lh', 'rh'], f"hem argument must be one of ('lh','rh). You specified hem={hem}."
|
|
873
|
+
|
|
874
|
+
fn_mesh_out = os.path.join(out_folder, 'mesh.msh')
|
|
875
|
+
vol = nii.dataobj
|
|
876
|
+
affine = nii.affine
|
|
877
|
+
|
|
878
|
+
# Interpolating data in NifTI file to mesh
|
|
879
|
+
# nd = mesh_io.NodeData.from_data_grid(mesh, vol, affine, 'from_volume')
|
|
880
|
+
|
|
881
|
+
# meshify
|
|
882
|
+
ed = mesh_io.ElementData.from_data_grid(mesh, vol, affine, roi_name)
|
|
883
|
+
mesh.nodedata = []
|
|
884
|
+
mesh.elmdata = [ed]
|
|
885
|
+
mesh_io.write_msh(mesh, fn_mesh_out)
|
|
886
|
+
|
|
887
|
+
# trasform to midlayer
|
|
888
|
+
if out_fsaverage:
|
|
889
|
+
out_fsaverage = out_folder
|
|
890
|
+
else:
|
|
891
|
+
out_fsaverage = None
|
|
892
|
+
transformations.middle_gm_interpolation(fn_mesh_out, m2m_folder=m2m_dir,
|
|
893
|
+
out_folder=out_folder, out_fsaverage=out_fsaverage)
|
|
894
|
+
|
|
895
|
+
return nib.freesurfer.read_morph_data(os.path.join(out_folder, f"{hem}.mesh.central.{roi_name}"))
|