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.
Files changed (107) hide show
  1. pyNIBS-0.2024.8.dist-info/LICENSE +623 -0
  2. pyNIBS-0.2024.8.dist-info/METADATA +723 -0
  3. pyNIBS-0.2024.8.dist-info/RECORD +107 -0
  4. pyNIBS-0.2024.8.dist-info/WHEEL +5 -0
  5. pyNIBS-0.2024.8.dist-info/top_level.txt +1 -0
  6. pynibs/__init__.py +34 -0
  7. pynibs/coil.py +1367 -0
  8. pynibs/congruence/__init__.py +15 -0
  9. pynibs/congruence/congruence.py +1108 -0
  10. pynibs/congruence/ext_metrics.py +257 -0
  11. pynibs/congruence/stimulation_threshold.py +318 -0
  12. pynibs/data/configuration_exp0.yaml +59 -0
  13. pynibs/data/configuration_linear_MEP.yaml +61 -0
  14. pynibs/data/configuration_linear_RT.yaml +61 -0
  15. pynibs/data/configuration_sigmoid4.yaml +68 -0
  16. pynibs/data/network mapping configuration/configuration guide.md +238 -0
  17. pynibs/data/network mapping configuration/configuration_TEMPLATE.yaml +42 -0
  18. pynibs/data/network mapping configuration/configuration_for_testing.yaml +43 -0
  19. pynibs/data/network mapping configuration/configuration_modelTMS.yaml +43 -0
  20. pynibs/data/network mapping configuration/configuration_reg_isi_05.yaml +43 -0
  21. pynibs/data/network mapping configuration/output_documentation.md +185 -0
  22. pynibs/data/network mapping configuration/recommendations_for_accuracy_threshold.md +77 -0
  23. pynibs/data/neuron/models/L23_PC_cADpyr_biphasic_v1.csv +1281 -0
  24. pynibs/data/neuron/models/L23_PC_cADpyr_monophasic_v1.csv +1281 -0
  25. pynibs/data/neuron/models/L4_LBC_biphasic_v1.csv +1281 -0
  26. pynibs/data/neuron/models/L4_LBC_monophasic_v1.csv +1281 -0
  27. pynibs/data/neuron/models/L4_NBC_biphasic_v1.csv +1281 -0
  28. pynibs/data/neuron/models/L4_NBC_monophasic_v1.csv +1281 -0
  29. pynibs/data/neuron/models/L4_SBC_biphasic_v1.csv +1281 -0
  30. pynibs/data/neuron/models/L4_SBC_monophasic_v1.csv +1281 -0
  31. pynibs/data/neuron/models/L5_TTPC2_cADpyr_biphasic_v1.csv +1281 -0
  32. pynibs/data/neuron/models/L5_TTPC2_cADpyr_monophasic_v1.csv +1281 -0
  33. pynibs/expio/Mep.py +1518 -0
  34. pynibs/expio/__init__.py +8 -0
  35. pynibs/expio/brainsight.py +979 -0
  36. pynibs/expio/brainvis.py +71 -0
  37. pynibs/expio/cobot.py +239 -0
  38. pynibs/expio/exp.py +1876 -0
  39. pynibs/expio/fit_funs.py +287 -0
  40. pynibs/expio/localite.py +1987 -0
  41. pynibs/expio/signal_ced.py +51 -0
  42. pynibs/expio/visor.py +624 -0
  43. pynibs/freesurfer.py +502 -0
  44. pynibs/hdf5_io/__init__.py +10 -0
  45. pynibs/hdf5_io/hdf5_io.py +1857 -0
  46. pynibs/hdf5_io/xdmf.py +1542 -0
  47. pynibs/mesh/__init__.py +3 -0
  48. pynibs/mesh/mesh_struct.py +1394 -0
  49. pynibs/mesh/transformations.py +866 -0
  50. pynibs/mesh/utils.py +1103 -0
  51. pynibs/models/_TMS.py +211 -0
  52. pynibs/models/__init__.py +0 -0
  53. pynibs/muap.py +392 -0
  54. pynibs/neuron/__init__.py +2 -0
  55. pynibs/neuron/neuron_regression.py +284 -0
  56. pynibs/neuron/util.py +58 -0
  57. pynibs/optimization/__init__.py +5 -0
  58. pynibs/optimization/multichannel.py +278 -0
  59. pynibs/optimization/opt_mep.py +152 -0
  60. pynibs/optimization/optimization.py +1445 -0
  61. pynibs/optimization/workhorses.py +698 -0
  62. pynibs/pckg/__init__.py +0 -0
  63. pynibs/pckg/biosig/biosig4c++-1.9.5.src_fixed.tar.gz +0 -0
  64. pynibs/pckg/libeep/__init__.py +0 -0
  65. pynibs/pckg/libeep/pyeep.so +0 -0
  66. pynibs/regression/__init__.py +11 -0
  67. pynibs/regression/dual_node_detection.py +2375 -0
  68. pynibs/regression/regression.py +2984 -0
  69. pynibs/regression/score_types.py +0 -0
  70. pynibs/roi/__init__.py +2 -0
  71. pynibs/roi/roi.py +895 -0
  72. pynibs/roi/roi_structs.py +1233 -0
  73. pynibs/subject.py +1009 -0
  74. pynibs/tensor_scaling.py +144 -0
  75. pynibs/tests/data/InstrumentMarker20200225163611937.xml +19 -0
  76. pynibs/tests/data/TriggerMarkers_Coil0_20200225163443682.xml +14 -0
  77. pynibs/tests/data/TriggerMarkers_Coil1_20200225170337572.xml +6373 -0
  78. pynibs/tests/data/Xdmf.dtd +89 -0
  79. pynibs/tests/data/brainsight_niiImage_nifticoord.txt +145 -0
  80. pynibs/tests/data/brainsight_niiImage_nifticoord_largefile.txt +1434 -0
  81. pynibs/tests/data/brainsight_niiImage_niifticoord_mixedtargets.txt +47 -0
  82. pynibs/tests/data/create_subject_testsub.py +332 -0
  83. pynibs/tests/data/data.hdf5 +0 -0
  84. pynibs/tests/data/geo.hdf5 +0 -0
  85. pynibs/tests/test_coil.py +474 -0
  86. pynibs/tests/test_elements2nodes.py +100 -0
  87. pynibs/tests/test_hdf5_io/test_xdmf.py +61 -0
  88. pynibs/tests/test_mesh_transformations.py +123 -0
  89. pynibs/tests/test_mesh_utils.py +143 -0
  90. pynibs/tests/test_nnav_imports.py +101 -0
  91. pynibs/tests/test_quality_measures.py +117 -0
  92. pynibs/tests/test_regressdata.py +289 -0
  93. pynibs/tests/test_roi.py +17 -0
  94. pynibs/tests/test_rotations.py +86 -0
  95. pynibs/tests/test_subject.py +71 -0
  96. pynibs/tests/test_util.py +24 -0
  97. pynibs/tms_pulse.py +34 -0
  98. pynibs/util/__init__.py +4 -0
  99. pynibs/util/dosing.py +233 -0
  100. pynibs/util/quality_measures.py +562 -0
  101. pynibs/util/rotations.py +340 -0
  102. pynibs/util/simnibs.py +763 -0
  103. pynibs/util/util.py +727 -0
  104. pynibs/visualization/__init__.py +2 -0
  105. pynibs/visualization/para.py +4372 -0
  106. pynibs/visualization/plot_2D.py +137 -0
  107. 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}"))