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
|
@@ -0,0 +1,1233 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Classes to cope with cortical region of interests (ROIs).
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
import math
|
|
6
|
+
import scipy
|
|
7
|
+
import skimage
|
|
8
|
+
import tqdm
|
|
9
|
+
import trimesh
|
|
10
|
+
import warnings
|
|
11
|
+
import numpy as np
|
|
12
|
+
import nibabel as nib
|
|
13
|
+
import scipy.interpolate
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
import simnibs
|
|
17
|
+
except ModuleNotFoundError:
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
import pynibs
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CorticalLayer:
|
|
24
|
+
__create_key = object()
|
|
25
|
+
|
|
26
|
+
class Settings:
|
|
27
|
+
ROI_SIZE_OFFSET = 5
|
|
28
|
+
GRID_POINTS_PER_MM = 1.5
|
|
29
|
+
TAG_WHITE_MATTER_VOL = 1
|
|
30
|
+
TAG_GRAY_MATTER_VOL = 2
|
|
31
|
+
TAG_WHITE_MATTER_SURF = 1001
|
|
32
|
+
TAG_GRAY_MATTER_SURF = 1002
|
|
33
|
+
NUM_TRIANGLE_SMOOTHING_STEPS = 30
|
|
34
|
+
|
|
35
|
+
def __init__(self, create_key, layer_id, volumetric_mesh=None, roi=None, depth=None, path=None, surface=None,
|
|
36
|
+
id=None):
|
|
37
|
+
"""
|
|
38
|
+
Constructor of the cortical layer class. Three optional ways of construction a CorticalLayer-instance
|
|
39
|
+
a) by providing a path to an already existing layer (simnibs.Msh)
|
|
40
|
+
b) by proving a simnibs.Msh of an already existing layer.
|
|
41
|
+
c) by providing the bounding box of the ROI in which the layer should be created.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
layer_id : str
|
|
46
|
+
Identifier of the layer.
|
|
47
|
+
volumetric_mesh : simnibs.Msh, optional
|
|
48
|
+
The tetrahedral volume mesh, in which the layer should be generated.
|
|
49
|
+
roi : pynibs.RegionOfInterestSurface
|
|
50
|
+
RegionOfInterestSurface.
|
|
51
|
+
depth: float, optional
|
|
52
|
+
Normalized distance of the layer from gray matter surface.
|
|
53
|
+
Provide values in the open interval (0,1).
|
|
54
|
+
path : str, optional
|
|
55
|
+
File path to a region of interest surfe (e.g. midlayer).
|
|
56
|
+
surface : simnibs.Msh, optional
|
|
57
|
+
The surface representation of an already existing layer (e.g. midlayer).
|
|
58
|
+
id : str, optional, deprecated
|
|
59
|
+
This was replaced by layer_id.
|
|
60
|
+
"""
|
|
61
|
+
if id is not None:
|
|
62
|
+
warnings.warn(DeprecationWarning("The parameter 'id' is deprecated and will be removed in future versions. "
|
|
63
|
+
"use 'layer_id' instead."))
|
|
64
|
+
assert layer_id is None, "Please use 'layer_id' instead of 'id'."
|
|
65
|
+
layer_id = id
|
|
66
|
+
|
|
67
|
+
# trying to mimic a private constructor here, from: https://stackoverflow.com/a/46459300
|
|
68
|
+
assert create_key == CorticalLayer.__create_key, \
|
|
69
|
+
"Direct construction not allowed. Please use factory methods " \
|
|
70
|
+
"'init_from_file', 'init_from_surface', 'create_in_bbox'"
|
|
71
|
+
self.id = layer_id
|
|
72
|
+
|
|
73
|
+
CorticalLayer.Settings.ROI_SIZE_OFFSET = 5
|
|
74
|
+
|
|
75
|
+
if path is not None:
|
|
76
|
+
self.surface = simnibs.read_msh(path)
|
|
77
|
+
self.roi = CorticalLayer.roi_bbox_from_points(self.surface.nodes.node_coord,
|
|
78
|
+
CorticalLayer.Settings.ROI_SIZE_OFFSET)
|
|
79
|
+
elif surface is not None:
|
|
80
|
+
self.surface = surface
|
|
81
|
+
self.roi = CorticalLayer.roi_bbox_from_points(self.surface.nodes.node_coord,
|
|
82
|
+
CorticalLayer.Settings.ROI_SIZE_OFFSET)
|
|
83
|
+
elif roi is not None and depth is not None:
|
|
84
|
+
bbox = [
|
|
85
|
+
np.min(roi.node_coord_mid[:, 0]),
|
|
86
|
+
np.max(roi.node_coord_mid[:, 0]),
|
|
87
|
+
np.min(roi.node_coord_mid[:, 1]),
|
|
88
|
+
np.max(roi.node_coord_mid[:, 1]),
|
|
89
|
+
np.min(roi.node_coord_mid[:, 2]),
|
|
90
|
+
np.max(roi.node_coord_mid[:, 2])
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
self.volumetric_mesh = CorticalLayer.crop_mesh_with_box(
|
|
94
|
+
volumetric_mesh,
|
|
95
|
+
bbox,
|
|
96
|
+
True
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
self.surface = None
|
|
100
|
+
self.roi = roi
|
|
101
|
+
self.generate_layer(depth, roi)
|
|
102
|
+
else:
|
|
103
|
+
raise ValueError('At least one set of optional parameters must be assigned')
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def init_from_file(cls, layer_id, fn):
|
|
107
|
+
"""
|
|
108
|
+
Factory method for constructing a CorticalLayer-object from a file.
|
|
109
|
+
|
|
110
|
+
Parameters
|
|
111
|
+
----------
|
|
112
|
+
layer_id : str
|
|
113
|
+
Identifier of the layer.
|
|
114
|
+
fn : str
|
|
115
|
+
File path to a region of interest surfe (e.g. midlayer).
|
|
116
|
+
"""
|
|
117
|
+
return CorticalLayer(cls.__create_key, layer_id=layer_id, path=fn)
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def init_from_surface(cls, layer_id, surf):
|
|
121
|
+
"""
|
|
122
|
+
Factory method for constructing a CorticalLayer-object from a Simnibs-surface object.
|
|
123
|
+
|
|
124
|
+
Parameters
|
|
125
|
+
----------
|
|
126
|
+
layer_id : str
|
|
127
|
+
Identifier of the layer.
|
|
128
|
+
surf : simnibs.Msh
|
|
129
|
+
The surface representation of an already existing layer (e.g. midlayer).
|
|
130
|
+
"""
|
|
131
|
+
return CorticalLayer(cls.__create_key, layer_id=layer_id, surface=surf)
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def create_in_roi(cls, layer_id, roi, depth, volmesh):
|
|
135
|
+
"""
|
|
136
|
+
Factory method for constructing a CorticalLayer-object
|
|
137
|
+
within a region-of-interest and a specified cortical depth.
|
|
138
|
+
|
|
139
|
+
Parameters
|
|
140
|
+
----------
|
|
141
|
+
layer_id : str
|
|
142
|
+
Identifier of the layer.
|
|
143
|
+
roi : pynibs.RegionOfInterestSurface
|
|
144
|
+
RegionOfInterestSurface.
|
|
145
|
+
depth: float
|
|
146
|
+
Normalized distance of the layer from gray matter surface.
|
|
147
|
+
Provide values in the open interval (0,1).
|
|
148
|
+
volmesh : simnibs.Msh
|
|
149
|
+
The tetrahedral volume mesh, in which the layer should be generated.
|
|
150
|
+
"""
|
|
151
|
+
return CorticalLayer(cls.__create_key, layer_id=layer_id, volumetric_mesh=volmesh, roi=roi, depth=depth)
|
|
152
|
+
|
|
153
|
+
@classmethod
|
|
154
|
+
def create_in_bbox(cls, layer_id, bbox, depth, volmesh):
|
|
155
|
+
"""
|
|
156
|
+
Factory method for constructing a CorticalLayer-object
|
|
157
|
+
within a region-of-interest and a specified cortical depth.
|
|
158
|
+
|
|
159
|
+
Parameters
|
|
160
|
+
----------
|
|
161
|
+
layer_id : str
|
|
162
|
+
Identifier of the layer.
|
|
163
|
+
bbox : typing.List[float]
|
|
164
|
+
List of bounding values around the ROI box: [x_min, x_max, y_min, y_max, z_min, z_max].
|
|
165
|
+
depth: float
|
|
166
|
+
Normalized distance of the layer from gray matter surface.
|
|
167
|
+
Provide values in the open interval (0,1).
|
|
168
|
+
volmesh : simnibs.Msh
|
|
169
|
+
The tetrahedral volume mesh, in which the layer should be generated.
|
|
170
|
+
"""
|
|
171
|
+
return CorticalLayer(cls.__create_key, layer_id=layer_id, volumetric_mesh=volmesh, roi=bbox, depth=depth)
|
|
172
|
+
|
|
173
|
+
@staticmethod
|
|
174
|
+
def roi_bbox_from_points(points, offset=0):
|
|
175
|
+
"""
|
|
176
|
+
Find the minimal bounding box around the provided points.
|
|
177
|
+
|
|
178
|
+
Parameters
|
|
179
|
+
----------
|
|
180
|
+
points : simnibs.Msh
|
|
181
|
+
The tetrahedral volume mesh, in which the layer should be generated.
|
|
182
|
+
offset: float
|
|
183
|
+
Normalized distance of the layer from gray matter surface.
|
|
184
|
+
Provide values in the open interval (0,1).
|
|
185
|
+
|
|
186
|
+
Returns
|
|
187
|
+
-------
|
|
188
|
+
bounding_box : typing.List[float]
|
|
189
|
+
List of bounding values around the provided points: [x_min, x_max, y_min, y_max, z_min, z_max].
|
|
190
|
+
"""
|
|
191
|
+
return [points[:, 0].min() - offset, points[:, 0].max() + offset,
|
|
192
|
+
points[:, 1].min() - offset, points[:, 1].max() + offset,
|
|
193
|
+
points[:, 2].min() - offset, points[:, 2].max() + offset]
|
|
194
|
+
|
|
195
|
+
@staticmethod
|
|
196
|
+
def crop_mesh_with_box(mesh, roi, keep_elements=False):
|
|
197
|
+
"""
|
|
198
|
+
Returns the cropped mesh with all points that are inside the region of interest
|
|
199
|
+
|
|
200
|
+
Parameters
|
|
201
|
+
----------
|
|
202
|
+
keep_elements : bool, default = False
|
|
203
|
+
If True, keeps elements with at least one point in roi, else removes them.
|
|
204
|
+
mesh: simnibs.Msh
|
|
205
|
+
The mesh that is supposed to be cropped.
|
|
206
|
+
roi: typing.List[float]
|
|
207
|
+
The bounding box of the region of interest which the mesh should be cropped to.
|
|
208
|
+
[x-min, x-max, y-min, y-max, z-min, z-max].
|
|
209
|
+
|
|
210
|
+
Returns
|
|
211
|
+
-------
|
|
212
|
+
mesh_cropped : simnibs.Msh
|
|
213
|
+
The cropped mesh.
|
|
214
|
+
"""
|
|
215
|
+
node_keep_indexes = np.where(
|
|
216
|
+
np.all(
|
|
217
|
+
np.logical_and(
|
|
218
|
+
mesh.nodes.node_coord <= [roi[1], roi[3], roi[5]],
|
|
219
|
+
[roi[0], roi[2], roi[4]] <= mesh.nodes.node_coord
|
|
220
|
+
),
|
|
221
|
+
axis=1
|
|
222
|
+
)
|
|
223
|
+
)[0] + 1
|
|
224
|
+
|
|
225
|
+
if keep_elements: # crop using node-based indices
|
|
226
|
+
return mesh.crop_mesh(nodes=node_keep_indexes)
|
|
227
|
+
else: # crop using element-based indices
|
|
228
|
+
elements_to_keep = np.where(
|
|
229
|
+
np.all(
|
|
230
|
+
np.isin(
|
|
231
|
+
mesh.elm.node_number_list,
|
|
232
|
+
node_keep_indexes
|
|
233
|
+
).reshape(-1, 4),
|
|
234
|
+
axis=1
|
|
235
|
+
)
|
|
236
|
+
)[0] + 1
|
|
237
|
+
|
|
238
|
+
return mesh.crop_mesh(elements=elements_to_keep)
|
|
239
|
+
|
|
240
|
+
@staticmethod
|
|
241
|
+
def crop_mesh_with_surface(mesh, roi, keep_elements=False, radius=3):
|
|
242
|
+
"""
|
|
243
|
+
Returns the cropped mesh with all points that are close to the surface of interest
|
|
244
|
+
|
|
245
|
+
Parameters
|
|
246
|
+
----------
|
|
247
|
+
keep_elements : bool, default = False
|
|
248
|
+
If True, keeps elements with at least one point in roi, else removes them.
|
|
249
|
+
mesh: simnibs.Msh
|
|
250
|
+
The mesh that is supposed to be cropped.
|
|
251
|
+
roi: RegionOfInterestSurface instance
|
|
252
|
+
RegionOfInterestSurface.
|
|
253
|
+
radius : float, default = 3
|
|
254
|
+
Search radius of mesh elements around ROI nodes.
|
|
255
|
+
|
|
256
|
+
Returns
|
|
257
|
+
-------
|
|
258
|
+
mesh_cropped : simnibs.Msh
|
|
259
|
+
The cropped mesh.
|
|
260
|
+
"""
|
|
261
|
+
node_keep = np.zeros(mesh.nodes.node_coord.shape[0]).astype(bool)
|
|
262
|
+
|
|
263
|
+
for vertex in roi.node_coord_mid:
|
|
264
|
+
node_keep = np.logical_or(node_keep,
|
|
265
|
+
np.linalg.norm(mesh.nodes.node_coord - vertex, axis=1) < radius)
|
|
266
|
+
node_keep_indexes = np.where(node_keep)[0] + 1
|
|
267
|
+
|
|
268
|
+
if keep_elements: # crop using node-based indices
|
|
269
|
+
return mesh.crop_mesh(nodes=node_keep_indexes)
|
|
270
|
+
else: # crop using element-based indices
|
|
271
|
+
elements_to_keep = np.where(
|
|
272
|
+
np.all(
|
|
273
|
+
np.isin(
|
|
274
|
+
mesh.elm.node_number_list,
|
|
275
|
+
node_keep_indexes
|
|
276
|
+
).reshape(-1, 4),
|
|
277
|
+
axis=1
|
|
278
|
+
)
|
|
279
|
+
)[0] + 1
|
|
280
|
+
|
|
281
|
+
return mesh.crop_mesh(elements=elements_to_keep)
|
|
282
|
+
|
|
283
|
+
def generate_layer(self, depth, roi):
|
|
284
|
+
"""
|
|
285
|
+
Create the geometry of the layer at the specified depth using marching cubes.
|
|
286
|
+
|
|
287
|
+
Parameters
|
|
288
|
+
----------
|
|
289
|
+
depth : float
|
|
290
|
+
The depth below the GM surface at which the layer should be generated; in [0,1].
|
|
291
|
+
roi : RegionOfInterestSurface instance
|
|
292
|
+
RegionOfInterestSurface.
|
|
293
|
+
"""
|
|
294
|
+
bbox = [
|
|
295
|
+
np.min(roi.node_coord_mid[:, 0]),
|
|
296
|
+
np.max(roi.node_coord_mid[:, 0]),
|
|
297
|
+
np.min(roi.node_coord_mid[:, 1]),
|
|
298
|
+
np.max(roi.node_coord_mid[:, 1]),
|
|
299
|
+
np.min(roi.node_coord_mid[:, 2]),
|
|
300
|
+
np.max(roi.node_coord_mid[:, 2])
|
|
301
|
+
]
|
|
302
|
+
|
|
303
|
+
roi_extended = [
|
|
304
|
+
bbox[0] - CorticalLayer.Settings.ROI_SIZE_OFFSET,
|
|
305
|
+
bbox[1] + CorticalLayer.Settings.ROI_SIZE_OFFSET,
|
|
306
|
+
bbox[2] - CorticalLayer.Settings.ROI_SIZE_OFFSET,
|
|
307
|
+
bbox[3] + CorticalLayer.Settings.ROI_SIZE_OFFSET,
|
|
308
|
+
bbox[4] - CorticalLayer.Settings.ROI_SIZE_OFFSET,
|
|
309
|
+
bbox[5] + CorticalLayer.Settings.ROI_SIZE_OFFSET
|
|
310
|
+
]
|
|
311
|
+
|
|
312
|
+
# 1) Create grid points used for interpolation.
|
|
313
|
+
grid_x = np.linspace(roi_extended[0], roi_extended[1],
|
|
314
|
+
int(math.fabs(
|
|
315
|
+
roi_extended[0] - roi_extended[1]) * CorticalLayer.Settings.GRID_POINTS_PER_MM))
|
|
316
|
+
grid_y = np.linspace(roi_extended[2], roi_extended[3],
|
|
317
|
+
int(math.fabs(
|
|
318
|
+
roi_extended[2] - roi_extended[3]) * CorticalLayer.Settings.GRID_POINTS_PER_MM))
|
|
319
|
+
grid_z = np.linspace(roi_extended[4], roi_extended[5],
|
|
320
|
+
int(math.fabs(
|
|
321
|
+
roi_extended[4] - roi_extended[5]) * CorticalLayer.Settings.GRID_POINTS_PER_MM))
|
|
322
|
+
grid_points = np.stack(np.meshgrid(grid_x, grid_y, grid_z, indexing='ij'), axis=-1).reshape(-1, 3)
|
|
323
|
+
|
|
324
|
+
# 2) Find points in interpolation grid that are inside/outside the gray matter.
|
|
325
|
+
tet_idcs = self.volumetric_mesh.find_tetrahedron_with_points(grid_points, compute_baricentric=False)
|
|
326
|
+
|
|
327
|
+
point_indices_in_volume = np.where(tet_idcs != -1)[0]
|
|
328
|
+
|
|
329
|
+
point_tissue_tag = np.ones(tet_idcs.shape) * -1
|
|
330
|
+
|
|
331
|
+
point_tissue_tag[point_indices_in_volume] = self.volumetric_mesh.elm.tag1[
|
|
332
|
+
tet_idcs[point_indices_in_volume] - 1 # make indices 0-based
|
|
333
|
+
]
|
|
334
|
+
grid_point_idcs_outside_gm_wm = np.where(
|
|
335
|
+
(point_tissue_tag != CorticalLayer.Settings.TAG_WHITE_MATTER_VOL)
|
|
336
|
+
&
|
|
337
|
+
(point_tissue_tag != CorticalLayer.Settings.TAG_GRAY_MATTER_VOL)
|
|
338
|
+
)[0]
|
|
339
|
+
grid_point_idcs_inside_gm = np.where(point_tissue_tag == CorticalLayer.Settings.TAG_GRAY_MATTER_VOL)[0]
|
|
340
|
+
|
|
341
|
+
# Define vertices of the ROI bounding box and associated data.
|
|
342
|
+
bounding_roi_pts = np.stack(
|
|
343
|
+
np.meshgrid(roi_extended[0:2], roi_extended[2:4], roi_extended[4:6], indexing='ij'),
|
|
344
|
+
axis=-1
|
|
345
|
+
).reshape(-1, 3)
|
|
346
|
+
|
|
347
|
+
bbox_tet_idcs = self.volumetric_mesh.find_tetrahedron_with_points(bounding_roi_pts, compute_baricentric=False)
|
|
348
|
+
bounding_roi_pts_tissue_tags = self.volumetric_mesh.elm.tag1[
|
|
349
|
+
bbox_tet_idcs - 1 # make indices 0-based
|
|
350
|
+
]
|
|
351
|
+
# init interpolation points with 1 for WM and 0 outside WM
|
|
352
|
+
bounding_roi_pts_init_vals = np.ones(bounding_roi_pts_tissue_tags.shape)
|
|
353
|
+
# max non-WM bounding box points
|
|
354
|
+
bounding_roi_pts_init_vals[bounding_roi_pts_tissue_tags != CorticalLayer.Settings.TAG_WHITE_MATTER_VOL] = 0
|
|
355
|
+
# if bounding box point is located outside the volume mesh, treat it as non-WM as well (even if it is WM tissue)
|
|
356
|
+
bounding_roi_pts_init_vals[np.where(bbox_tet_idcs == -1)] = 0
|
|
357
|
+
|
|
358
|
+
# 3) Prepare interpolator and interpolate on grid points that are inside GM.
|
|
359
|
+
wm_surface_nodes = self.volumetric_mesh.crop_mesh(CorticalLayer.Settings.TAG_WHITE_MATTER_SURF).nodes.node_coord
|
|
360
|
+
gm_surface_nodes = self.volumetric_mesh.crop_mesh(CorticalLayer.Settings.TAG_GRAY_MATTER_SURF).nodes.node_coord
|
|
361
|
+
|
|
362
|
+
# Create a gradient (from 0 to 1) between GM and WM.
|
|
363
|
+
data = [1] * len(wm_surface_nodes) # init interpolation points with 1 for grid points inside WM
|
|
364
|
+
data += [0] * len(gm_surface_nodes) # init interpolation points with 0 for grid points inside GM
|
|
365
|
+
# use interpolation points at bbox as "outside points" to ensure
|
|
366
|
+
# there is always a defined interpolation point also outside the GM volume
|
|
367
|
+
data += bounding_roi_pts_init_vals.tolist()
|
|
368
|
+
|
|
369
|
+
data_points = np.concatenate((wm_surface_nodes, gm_surface_nodes, bounding_roi_pts), axis=0)
|
|
370
|
+
|
|
371
|
+
interpolation = scipy.interpolate.LinearNDInterpolator(data_points, data, fill_value=-1)
|
|
372
|
+
gray_matter_interpolation = interpolation(grid_points[grid_point_idcs_inside_gm])
|
|
373
|
+
|
|
374
|
+
# 4) Marching-cubes-based surface creation.
|
|
375
|
+
volume_data = np.empty((len(grid_x), len(grid_y), len(grid_z)))
|
|
376
|
+
volume_data.fill(1)
|
|
377
|
+
outside_x = (grid_point_idcs_outside_gm_wm / (len(grid_z) * len(grid_y))).astype(int)
|
|
378
|
+
outside_y = ((grid_point_idcs_outside_gm_wm / len(grid_z)) % len(grid_y)).astype(int)
|
|
379
|
+
outside_z = (grid_point_idcs_outside_gm_wm % len(grid_z)).astype(int)
|
|
380
|
+
volume_data[(outside_x, outside_y, outside_z)] = 0
|
|
381
|
+
inside_gray_matter_x = (grid_point_idcs_inside_gm / (len(grid_z) * len(grid_y))).astype(int)
|
|
382
|
+
inside_gray_matter_y = ((grid_point_idcs_inside_gm / len(grid_z)) % len(grid_y)).astype(int)
|
|
383
|
+
inside_gray_matter_z = (grid_point_idcs_inside_gm % len(grid_z)).astype(int)
|
|
384
|
+
volume_data[(inside_gray_matter_x, inside_gray_matter_y, inside_gray_matter_z)] = gray_matter_interpolation
|
|
385
|
+
vertices, faces, _, _ = skimage.measure.marching_cubes(
|
|
386
|
+
volume_data,
|
|
387
|
+
level=depth,
|
|
388
|
+
spacing=tuple(
|
|
389
|
+
np.array(
|
|
390
|
+
[grid_x[1] - grid_x[0], grid_y[1] - grid_y[0], grid_z[1] - grid_z[0]],
|
|
391
|
+
dtype='float32'
|
|
392
|
+
)
|
|
393
|
+
),
|
|
394
|
+
step_size=1,
|
|
395
|
+
allow_degenerate=False
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
# 5) prepare surface for output
|
|
399
|
+
self.surface = simnibs.Msh(
|
|
400
|
+
simnibs.Nodes(vertices),
|
|
401
|
+
simnibs.Elements(faces + 1)
|
|
402
|
+
)
|
|
403
|
+
self.remove_unconnected_surfaces()
|
|
404
|
+
self.surface.nodes.node_coord = self.surface.nodes.node_coord + [(roi_extended[0]), (roi_extended[2]),
|
|
405
|
+
(roi_extended[4])]
|
|
406
|
+
|
|
407
|
+
# Crop again for a more precise crop-boundary (earlier cropping was done with larger elements)
|
|
408
|
+
self.surface = CorticalLayer.crop_mesh_with_box(self.surface, bbox, keep_elements=True)
|
|
409
|
+
self.surface = CorticalLayer.crop_mesh_with_surface(self.surface, roi, keep_elements=True)
|
|
410
|
+
self.remove_unconnected_surfaces()
|
|
411
|
+
|
|
412
|
+
# from simnibs.Msh.fix_surface_orientation (not implemented in SimNIBS < v.4)
|
|
413
|
+
idx_tr = self.surface.elm.elm_type == 2
|
|
414
|
+
normals = self.surface.triangle_normals()[:]
|
|
415
|
+
baricenters = self.surface.elements_baricenters()[idx_tr]
|
|
416
|
+
CoG = np.mean(baricenters, axis=0)
|
|
417
|
+
|
|
418
|
+
nr_inward = sum(np.einsum("ij,ij->i", normals, baricenters - CoG) < 0)
|
|
419
|
+
|
|
420
|
+
if nr_inward / sum(idx_tr) > 0.5:
|
|
421
|
+
buffer = self.surface.elm.node_number_list[idx_tr, 1].copy()
|
|
422
|
+
self.surface.elm.node_number_list[idx_tr, 1] = self.surface.elm.node_number_list[idx_tr, 2]
|
|
423
|
+
self.surface.elm.node_number_list[idx_tr, 2] = buffer
|
|
424
|
+
|
|
425
|
+
def get_smoothed_normals(self):
|
|
426
|
+
"""
|
|
427
|
+
Computed the smoothed normals of the surface representation of this layer.
|
|
428
|
+
|
|
429
|
+
Note: For the later stages, we don't want a smoothed surface, but smooth
|
|
430
|
+
normals in order to maintain the location of the cells, but orient
|
|
431
|
+
them more smoothly. Therefore, we use smoothed normals, e.g. for the
|
|
432
|
+
computation of the theta angle, but do not smooth the entire layer
|
|
433
|
+
surface.
|
|
434
|
+
|
|
435
|
+
Returns
|
|
436
|
+
-------
|
|
437
|
+
normals : np.ndarray
|
|
438
|
+
The tetrahedral volume mesh, in which the layer should be generated.
|
|
439
|
+
"""
|
|
440
|
+
return self.surface.triangle_normals(smooth=CorticalLayer.Settings.NUM_TRIANGLE_SMOOTHING_STEPS).value
|
|
441
|
+
|
|
442
|
+
def save(self, fn):
|
|
443
|
+
"""
|
|
444
|
+
Save the current surface representation of this CorticalLayer instance at the specified location.
|
|
445
|
+
|
|
446
|
+
Parameters
|
|
447
|
+
----------
|
|
448
|
+
fn : str
|
|
449
|
+
Target file name of the surface-file of this layer.
|
|
450
|
+
"""
|
|
451
|
+
self.surface.write(fn)
|
|
452
|
+
|
|
453
|
+
def remove_unconnected_surfaces(self):
|
|
454
|
+
"""
|
|
455
|
+
Remove elements small unconnected element-clusters from this layer.
|
|
456
|
+
"""
|
|
457
|
+
surfaces = self.surface.elm.connected_components()
|
|
458
|
+
surfaces.sort(key=len)
|
|
459
|
+
self.surface = self.surface.crop_mesh(elements=surfaces[-1])
|
|
460
|
+
|
|
461
|
+
def get_evenly_spaced_element_subset(self, elements_per_square_mm):
|
|
462
|
+
"""
|
|
463
|
+
Subsample the surface representation of the ayer.
|
|
464
|
+
|
|
465
|
+
Parameters
|
|
466
|
+
----------
|
|
467
|
+
elements_per_square_mm : float
|
|
468
|
+
Number of triangles per mm^2 in the layer.
|
|
469
|
+
|
|
470
|
+
Returns
|
|
471
|
+
-------
|
|
472
|
+
selected elements : Typing.List[int]
|
|
473
|
+
List of indices of selected elements as a result of the subsampling.
|
|
474
|
+
"""
|
|
475
|
+
centers = self.surface.elements_baricenters().value
|
|
476
|
+
min_distance_square = (1 / elements_per_square_mm) * math.sqrt(2) / 2 # = radius of circumference
|
|
477
|
+
selected_elements = np.array([0])
|
|
478
|
+
|
|
479
|
+
# For each element: add to the list of 'selected_elements' if the distance to the already
|
|
480
|
+
# selected elements is larger than the minimum element density.
|
|
481
|
+
for element_index in range(self.surface.elm.nr):
|
|
482
|
+
selected_elements_centers = centers[selected_elements]
|
|
483
|
+
element_center = centers[element_index]
|
|
484
|
+
distances_square = (selected_elements_centers[:, 0] - element_center[0]) ** 2 + \
|
|
485
|
+
(selected_elements_centers[:, 1] - element_center[1]) ** 2 + \
|
|
486
|
+
(selected_elements_centers[:, 2] - element_center[2]) ** 2
|
|
487
|
+
if distances_square.min() > min_distance_square:
|
|
488
|
+
selected_elements = np.append(selected_elements, element_index)
|
|
489
|
+
|
|
490
|
+
return np.array(selected_elements)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
class RegionOfInterestSurface:
|
|
494
|
+
"""
|
|
495
|
+
Region of interest (surface).
|
|
496
|
+
|
|
497
|
+
Attributes
|
|
498
|
+
----------
|
|
499
|
+
node_coord_up : np.ndarray
|
|
500
|
+
(N_points, 3) Coordinates (x,y,z) of upper surface nodes.
|
|
501
|
+
node_coord_mid : np.ndarray
|
|
502
|
+
(N_points, 3) Coordinates (x,y,z) of middle surface nodes.
|
|
503
|
+
node_coord_low : np.ndarray
|
|
504
|
+
(N_points, 3) Coordinates (x,y,z) of lower surface nodes.
|
|
505
|
+
node_number_list : np.ndarray
|
|
506
|
+
(N_points, 3) Connectivity matrix of triangles.
|
|
507
|
+
delta : float
|
|
508
|
+
Distance parameter between WM and GM (0 -> WM, 1 -> GM).
|
|
509
|
+
tet_idx_tri_center_up : np.ndarray [N_points]
|
|
510
|
+
Tetrahedra indices of TetrahedraLinear object instance where the center points of the triangles of the
|
|
511
|
+
upper surface are.
|
|
512
|
+
tet_idx_tri_center_mid : np.ndarray [N_points]
|
|
513
|
+
Tetrahedra indices of TetrahedraLinear object instance where the center points of the triangles of the
|
|
514
|
+
middle surface are.
|
|
515
|
+
tet_idx_tri_center_low : np.ndarray [N_points]
|
|
516
|
+
Tetrahedra indices of TetrahedraLinear object instance where the center points of the triangles of the
|
|
517
|
+
lower surface are.
|
|
518
|
+
tet_idx_node_coord_mid : np.ndarray
|
|
519
|
+
(N_tri,) Tetrahedra indices of TetrahedraLinear object instance where the nodes of the middle surface are.
|
|
520
|
+
tri_center_coord_up : np.ndarray
|
|
521
|
+
(N_tri, 3) Coordinates of roi triangle center of upper surface
|
|
522
|
+
tri_center_coord_mid : np.ndarray
|
|
523
|
+
(N_tri, 3) Coordinates of roi triangle center of middle surface
|
|
524
|
+
tri_center_coord_low : np.ndarray
|
|
525
|
+
(N_tri, 3) Coordinates of roi triangle center of lower surface
|
|
526
|
+
fn_mask : string
|
|
527
|
+
Filename for surface mask in subject space. .mgh file or freesurfer surface file.
|
|
528
|
+
fn_mask_avg : string
|
|
529
|
+
Filename for .mgh mask in fsaverage space. Absolute path or relative to mesh folder.
|
|
530
|
+
fn_mask_nii : string
|
|
531
|
+
Filename for .nii or .nii.gz mask. Absolute path or relative to mesh folder.
|
|
532
|
+
X_ROI : list of float
|
|
533
|
+
Region of interest [Xmin, Xmax], whole X range if empty [0,0] or None
|
|
534
|
+
(left - right)
|
|
535
|
+
Y_ROI : list of float
|
|
536
|
+
Region of interest [Ymin, Ymax], whole Y range if empty [0,0] or None
|
|
537
|
+
(anterior - posterior)
|
|
538
|
+
Z_ROI : list of float
|
|
539
|
+
Region of interest [Zmin, Zmax], whole Z range if empty [0,0] or None
|
|
540
|
+
(inferior - superior)
|
|
541
|
+
template : str
|
|
542
|
+
'MNI', 'fsaverage', 'subject'
|
|
543
|
+
center : list of float
|
|
544
|
+
Center coordinates for spherical ROI in self.template space
|
|
545
|
+
radius : float
|
|
546
|
+
Radius in [mm] for spherical ROI
|
|
547
|
+
gm_surf_fname : str or list of str
|
|
548
|
+
Filename(s) of GM surface generated by freesurfer (lh and/or rh)
|
|
549
|
+
(e.g. in mri2msh: .../fs_ID/surf/lh.pial)
|
|
550
|
+
wm_surf_fname : str or list of str
|
|
551
|
+
Filename(s) of WM surface generated by freesurfer (lh and/or rh)
|
|
552
|
+
(e.g. in mri2msh: .../fs_ID/surf/lh.white)
|
|
553
|
+
layer : int
|
|
554
|
+
Define the number of layers:
|
|
555
|
+
|
|
556
|
+
* 1: one layer
|
|
557
|
+
* 3: additionally upper and lower layers are generated around the central midlayer
|
|
558
|
+
"""
|
|
559
|
+
def __init__(self):
|
|
560
|
+
"""
|
|
561
|
+
Initialize RegionOfInterestSurface class instance
|
|
562
|
+
"""
|
|
563
|
+
|
|
564
|
+
self.node_coord_up = np.empty(0)
|
|
565
|
+
self.node_coord_mid = np.empty(0)
|
|
566
|
+
self.node_coord_low = np.empty(0)
|
|
567
|
+
|
|
568
|
+
self.node_number_list = np.empty(0)
|
|
569
|
+
self.delta = []
|
|
570
|
+
|
|
571
|
+
self.tet_idx_tri_center_up = np.empty(0)
|
|
572
|
+
self.tet_idx_tri_center_mid = np.empty(0)
|
|
573
|
+
self.tet_idx_tri_center_low = np.empty(0)
|
|
574
|
+
|
|
575
|
+
self.tet_idx_node_coord_mid = np.empty(0)
|
|
576
|
+
|
|
577
|
+
self.tri_center_coord_up = []
|
|
578
|
+
self.tri_center_coord_mid = []
|
|
579
|
+
self.tri_center_coord_low = []
|
|
580
|
+
|
|
581
|
+
self.template = None
|
|
582
|
+
self.fn_mask = []
|
|
583
|
+
self.fn_mask_avg = None
|
|
584
|
+
self.fn_mask_nii = None
|
|
585
|
+
|
|
586
|
+
self.X_ROI = []
|
|
587
|
+
self.Y_ROI = []
|
|
588
|
+
self.Z_ROI = []
|
|
589
|
+
|
|
590
|
+
self.center = None
|
|
591
|
+
self.radius = None
|
|
592
|
+
|
|
593
|
+
self.gm_surf_fname = []
|
|
594
|
+
self.wm_surf_fname = []
|
|
595
|
+
self.midlayer_surf_fname = []
|
|
596
|
+
self.layer = []
|
|
597
|
+
self.mesh_folder = []
|
|
598
|
+
self.refine = []
|
|
599
|
+
|
|
600
|
+
self.n_tris = -1
|
|
601
|
+
self.n_nodes = -1
|
|
602
|
+
self.n_tets = -1
|
|
603
|
+
|
|
604
|
+
self.layers = []
|
|
605
|
+
|
|
606
|
+
def project_on_midlayer(self, target, verbose=False):
|
|
607
|
+
"""
|
|
608
|
+
Project a coordinate on the nearest midlayer node
|
|
609
|
+
|
|
610
|
+
Parameters
|
|
611
|
+
----------
|
|
612
|
+
target : np.ndarray
|
|
613
|
+
Coordinate to project as (3,) array
|
|
614
|
+
verbose : bool
|
|
615
|
+
Print some verbosity information. Default: False
|
|
616
|
+
|
|
617
|
+
Returns
|
|
618
|
+
-------
|
|
619
|
+
target_proj : np.ndarray
|
|
620
|
+
Node coordinate of nearest midlayer node.
|
|
621
|
+
"""
|
|
622
|
+
# find midlayer node that is nearest to the target
|
|
623
|
+
target_node_coord_mid = np.where(np.linalg.norm(self.node_coord_mid - target, axis=1) == np.min(
|
|
624
|
+
np.linalg.norm(self.node_coord_mid - target, axis=1)))[0][0]
|
|
625
|
+
|
|
626
|
+
# get coordinates of that node
|
|
627
|
+
target_proj = self.node_coord_mid[target_node_coord_mid]
|
|
628
|
+
|
|
629
|
+
if verbose:
|
|
630
|
+
print(f"Projected {target} to {target_proj} (Dist: {np.linalg.norm(target - target_proj):2.2f}mm)")
|
|
631
|
+
|
|
632
|
+
return target_proj
|
|
633
|
+
|
|
634
|
+
def make_GM_WM_surface(self, gm_surf_fname=None, wm_surf_fname=None, midlayer_surf_fname=None, mesh_folder=None,
|
|
635
|
+
delta=0.5,
|
|
636
|
+
x_roi=None, y_roi=None, z_roi=None,
|
|
637
|
+
layer=1,
|
|
638
|
+
fn_mask=None, refine=False):
|
|
639
|
+
"""
|
|
640
|
+
Generating a surface between WM and GM in a distance of delta 0...1 for ROI,
|
|
641
|
+
given by Freesurfer mask or coordinates.
|
|
642
|
+
|
|
643
|
+
Parameters
|
|
644
|
+
----------
|
|
645
|
+
gm_surf_fname : str or list of str
|
|
646
|
+
Filename(s) of GM FreeSurfer surface(s) (lh and/or rh).
|
|
647
|
+
Either relative to mesh_folder (fs_ID/surf/lh.pial) or absolute (/full/path/to/lh.pial)
|
|
648
|
+
wm_surf_fname : str or list of str
|
|
649
|
+
Filename(s) of WM FreeSurfer surface(s) (lh and/or rh)
|
|
650
|
+
Either relative to mesh_folder (fs_ID/surf/lh.white) or absolute (/full/path/to/lh.white)
|
|
651
|
+
midlayer_surf_fname : str or list of str
|
|
652
|
+
Filename(s) of midlayer surface (lh and/or rh)
|
|
653
|
+
Either relative to mesh_folder (fs_ID/surf/lh.central) or absolute (/full/path/to/lh.central)
|
|
654
|
+
mesh_folder : str
|
|
655
|
+
Root folder of mesh, Needed if paths above are given relative, or refine=True
|
|
656
|
+
[defunct] m2m_mat_fname : str
|
|
657
|
+
Filename of mri2msh transformation matrix
|
|
658
|
+
(e.g. in mri2msh: .../m2m_ProbandID/MNI2conform_6DOF.mat)
|
|
659
|
+
delta : float
|
|
660
|
+
Distance parameter where surface is generated 0...1 (default: 0.5)
|
|
661
|
+
|
|
662
|
+
* 0 -> WM surface
|
|
663
|
+
* 1 -> GM surface
|
|
664
|
+
x_roi : list of float
|
|
665
|
+
Region of interest [Xmin, Xmax], whole X range if empty [0,0] or None
|
|
666
|
+
(left - right)
|
|
667
|
+
y_roi : list of float
|
|
668
|
+
Region of interest [Ymin, Ymax], whole Y range if empty [0,0] or None
|
|
669
|
+
(anterior - posterior)
|
|
670
|
+
z_roi : list of float
|
|
671
|
+
Region of interest [Zmin, Zmax], whole Z range if empty [0,0] or None
|
|
672
|
+
(inferior - superior)
|
|
673
|
+
layer : int
|
|
674
|
+
Define the number of layers:
|
|
675
|
+
|
|
676
|
+
* 1: one layer
|
|
677
|
+
* 3: additionally upper and lower layers are generated around the central midlayer
|
|
678
|
+
fn_mask : str
|
|
679
|
+
Filename for FreeSurfer .mgh mask.
|
|
680
|
+
refine : bool, optional, default: False
|
|
681
|
+
Refine ROI by splitting elements
|
|
682
|
+
|
|
683
|
+
Returns
|
|
684
|
+
-------
|
|
685
|
+
node_coord_up : np.ndarray of float [N_roi_points x 3]
|
|
686
|
+
Node coordinates (x, y, z) of upper epsilon layer of ROI surface
|
|
687
|
+
node_coord_mid : np.ndarray of float [N_roi_points x 3]
|
|
688
|
+
Node coordinates (x, y, z) of ROI surface
|
|
689
|
+
node_coord_low : np.ndarray of float [N_roi_points x 3]
|
|
690
|
+
Node coordinates (x, y, z) of lower epsilon layer of ROI surface
|
|
691
|
+
node_number_list : np.ndarray of int [N_roi_tri x 3]
|
|
692
|
+
Connectivity matrix of intermediate surface layer triangles
|
|
693
|
+
delta : float
|
|
694
|
+
Distance parameter where surface is generated 0...1 (default: 0.5)
|
|
695
|
+
|
|
696
|
+
* 0 -> WM surface
|
|
697
|
+
* 1 -> GM surface
|
|
698
|
+
tri_center_coord_up : np.ndarray of float [N_roi_tri x 3]
|
|
699
|
+
Coordinates (x, y, z) of triangle center of upper epsilon layer of ROI surface
|
|
700
|
+
tri_center_coord_mid : np.ndarray of float [N_roi_tri x 3]
|
|
701
|
+
Coordinates (x, y, z) of triangle center of ROI surface
|
|
702
|
+
tri_center_coord_low : np.ndarray of float [N_roi_tri x 3]
|
|
703
|
+
Coordinates (x, y, z) of triangle center of lower epsilon layer of ROI surface
|
|
704
|
+
fn_mask : str
|
|
705
|
+
Filename for freesurfer mask. If given, this is used instead of *_ROIs
|
|
706
|
+
X_ROI : list of float
|
|
707
|
+
Region of interest [Xmin, Xmax], whole X range if empty [0,0] or None
|
|
708
|
+
(left - right)
|
|
709
|
+
Y_ROI : list of float
|
|
710
|
+
Region of interest [Ymin, Ymax], whole Y range if empty [0,0] or None
|
|
711
|
+
(anterior - posterior)
|
|
712
|
+
Z_ROI : list of float
|
|
713
|
+
Region of interest [Zmin, Zmax], whole Z range if empty [0,0] or None
|
|
714
|
+
(inferior - superior)
|
|
715
|
+
|
|
716
|
+
Example
|
|
717
|
+
-------
|
|
718
|
+
.. code-block:: python
|
|
719
|
+
|
|
720
|
+
make_GM_WM_surface(self, gm_surf_fname, wm_surf_fname, delta, X_ROI, Y_ROI, Z_ROI)
|
|
721
|
+
make_GM_WM_surface(self, gm_surf_fname, wm_surf_fname, delta, mask_fn, layer=3)
|
|
722
|
+
"""
|
|
723
|
+
self.gm_surf_fname = gm_surf_fname
|
|
724
|
+
self.wm_surf_fname = wm_surf_fname
|
|
725
|
+
self.midlayer_surf_fname = midlayer_surf_fname
|
|
726
|
+
self.layer = layer
|
|
727
|
+
self.mesh_folder = mesh_folder
|
|
728
|
+
self.fn_mask = fn_mask
|
|
729
|
+
self.delta = delta
|
|
730
|
+
self.X_ROI = x_roi
|
|
731
|
+
self.Y_ROI = y_roi
|
|
732
|
+
self.Z_ROI = z_roi
|
|
733
|
+
self.refine = refine
|
|
734
|
+
|
|
735
|
+
if type(gm_surf_fname) is not list:
|
|
736
|
+
gm_surf_fname = [gm_surf_fname]
|
|
737
|
+
|
|
738
|
+
if type(wm_surf_fname) is not list:
|
|
739
|
+
wm_surf_fname = [wm_surf_fname]
|
|
740
|
+
|
|
741
|
+
if type(midlayer_surf_fname) is not list:
|
|
742
|
+
midlayer_surf_fname = [midlayer_surf_fname]
|
|
743
|
+
|
|
744
|
+
if len(gm_surf_fname) != len(wm_surf_fname):
|
|
745
|
+
raise ValueError('provide equal number of GM and WM surfaces!')
|
|
746
|
+
|
|
747
|
+
# load surface data
|
|
748
|
+
points_gm = [None for _ in range(len(gm_surf_fname))]
|
|
749
|
+
points_wm = [None for _ in range(len(wm_surf_fname))]
|
|
750
|
+
points_mid = [None for _ in range(len(midlayer_surf_fname))]
|
|
751
|
+
con_gm = [None for _ in range(len(gm_surf_fname))]
|
|
752
|
+
con_mid = [None for _ in range(len(midlayer_surf_fname))]
|
|
753
|
+
|
|
754
|
+
max_idx_gm = 0
|
|
755
|
+
max_idx_mid = 0
|
|
756
|
+
|
|
757
|
+
def read_geom(fn, max_idx):
|
|
758
|
+
"""
|
|
759
|
+
Read freesurfer geometries, either in .central format or as .gii
|
|
760
|
+
"""
|
|
761
|
+
if not fn.startswith(os.sep):
|
|
762
|
+
fn = os.path.join(mesh_folder, fn)
|
|
763
|
+
|
|
764
|
+
# charm uses .gii files
|
|
765
|
+
if fn.endswith('.gii'):
|
|
766
|
+
# FIX: add_data returns DataArrays in the order of appearance in the file
|
|
767
|
+
# This does not necessarily have to be first points then triangles.
|
|
768
|
+
# So "points, con = nib.load(fn).agg_data()" may lead to swapped points and con.
|
|
769
|
+
con = nib.load(fn).agg_data('NIFTI_INTENT_TRIANGLE')
|
|
770
|
+
points = nib.load(fn).agg_data('NIFTI_INTENT_POINTSET')
|
|
771
|
+
|
|
772
|
+
# headreco uses .central files
|
|
773
|
+
else:
|
|
774
|
+
points, con = nib.freesurfer.read_geometry(fn)
|
|
775
|
+
|
|
776
|
+
con += max_idx
|
|
777
|
+
max_idx += points.shape[0] # np.max(con_gm[i]) + 2
|
|
778
|
+
return points, con, max_idx
|
|
779
|
+
|
|
780
|
+
for i in range(len(gm_surf_fname)):
|
|
781
|
+
if gm_surf_fname[i] is not None:
|
|
782
|
+
points_gm[i], con_gm[i], max_idx_gm = read_geom(gm_surf_fname[i], max_idx_gm)
|
|
783
|
+
|
|
784
|
+
if wm_surf_fname[i] is not None:
|
|
785
|
+
points_wm[i], _, max_idx_wm = read_geom(wm_surf_fname[i], max_idx_gm)
|
|
786
|
+
|
|
787
|
+
if midlayer_surf_fname[i] is not None:
|
|
788
|
+
points_mid[i], con_mid[i], max_idx_mid = read_geom(midlayer_surf_fname[i], max_idx_mid)
|
|
789
|
+
|
|
790
|
+
points_gm = np.vstack(points_gm)
|
|
791
|
+
points_wm = np.vstack(points_wm)
|
|
792
|
+
points_mid = np.vstack(points_mid)
|
|
793
|
+
con_gm = np.vstack(con_gm)
|
|
794
|
+
con_mid = np.vstack(con_mid)
|
|
795
|
+
|
|
796
|
+
# Determine 3 layer midlayer if GM and WM surfaces are present otherwise use provided midlayer data
|
|
797
|
+
if gm_surf_fname[0] is not None and wm_surf_fname[0] is not None:
|
|
798
|
+
# determine vector pointing from wm surface to gm surface
|
|
799
|
+
wm_gm_vector = points_gm - points_wm
|
|
800
|
+
|
|
801
|
+
eps_0 = 0.025
|
|
802
|
+
eps, surface_points_upper, surface_points_lower = (False,) * 3
|
|
803
|
+
surface_points_upper = False
|
|
804
|
+
if layer == 3:
|
|
805
|
+
# set epsilon range for upper and lower surface
|
|
806
|
+
if delta < eps_0:
|
|
807
|
+
eps = delta / 2
|
|
808
|
+
elif delta > (1 - eps_0):
|
|
809
|
+
eps = (1 - delta) / 2
|
|
810
|
+
else:
|
|
811
|
+
eps = eps_0
|
|
812
|
+
|
|
813
|
+
# determine wm-gm surfaces
|
|
814
|
+
surface_points_middle = points_wm + wm_gm_vector * delta # type: np.ndarray
|
|
815
|
+
if layer == 3:
|
|
816
|
+
surface_points_upper = surface_points_middle + wm_gm_vector * eps
|
|
817
|
+
surface_points_lower = surface_points_middle - wm_gm_vector * eps
|
|
818
|
+
|
|
819
|
+
con = con_gm
|
|
820
|
+
|
|
821
|
+
elif midlayer_surf_fname[0] is not None:
|
|
822
|
+
self.layer = 1
|
|
823
|
+
|
|
824
|
+
surface_points_upper = None
|
|
825
|
+
surface_points_lower = None
|
|
826
|
+
surface_points_middle = points_mid
|
|
827
|
+
|
|
828
|
+
con = con_mid
|
|
829
|
+
|
|
830
|
+
else:
|
|
831
|
+
raise IOError("Please provide GM and WM surfaces or midlayer "
|
|
832
|
+
"surface directly for midlayer calculation...")
|
|
833
|
+
|
|
834
|
+
# crop region if desired
|
|
835
|
+
x_roi_wb, y_roi_wb, z_roi_wb = (False,) * 3
|
|
836
|
+
if fn_mask is None:
|
|
837
|
+
# crop to region of interest
|
|
838
|
+
if x_roi is None or x_roi == [0, 0]:
|
|
839
|
+
x_roi = [-np.inf, np.inf]
|
|
840
|
+
x_roi_wb = True
|
|
841
|
+
if y_roi is None or y_roi == [0, 0]:
|
|
842
|
+
y_roi = [-np.inf, np.inf]
|
|
843
|
+
y_roi_wb = True
|
|
844
|
+
if z_roi is None or z_roi == [0, 0]:
|
|
845
|
+
z_roi = [-np.inf, np.inf]
|
|
846
|
+
z_roi_wb = True
|
|
847
|
+
|
|
848
|
+
roi_mask_bool = (surface_points_middle[:, 0] > min(x_roi)) & (surface_points_middle[:, 0] < max(x_roi)) & \
|
|
849
|
+
(surface_points_middle[:, 1] > min(y_roi)) & (surface_points_middle[:, 1] < max(y_roi)) & \
|
|
850
|
+
(surface_points_middle[:, 2] > min(z_roi)) & (surface_points_middle[:, 2] < max(z_roi))
|
|
851
|
+
roi_mask_idx = np.where(roi_mask_bool)
|
|
852
|
+
|
|
853
|
+
else:
|
|
854
|
+
if x_roi is not None or y_roi is not None or z_roi is not None:
|
|
855
|
+
raise ValueError(f"Either provide X_ROI, Y_ROI, Z_ROI or fn_mask, not both.")
|
|
856
|
+
|
|
857
|
+
# read mask from freesurfer mask file
|
|
858
|
+
if not fn_mask.startswith(os.sep):
|
|
859
|
+
fn_mask = os.path.join(mesh_folder, fn_mask)
|
|
860
|
+
if fn_mask.endswith('.mgh') or fn_mask.endswith('.mgz'):
|
|
861
|
+
mask = nib.freesurfer.mghformat.MGHImage.from_filename(fn_mask).dataobj[:]
|
|
862
|
+
else:
|
|
863
|
+
mask = nib.freesurfer.read_morph_data(fn_mask)[:, np.newaxis, np.newaxis]
|
|
864
|
+
roi_mask_idx = np.where(mask > 0.5)
|
|
865
|
+
|
|
866
|
+
# redefine connectivity matrix for cropped points (reindexing)
|
|
867
|
+
# get row index where all points are lying inside ROI
|
|
868
|
+
if not (x_roi_wb and y_roi_wb and z_roi_wb):
|
|
869
|
+
con_row_idx = [i for i in range(con.shape[0]) if len(np.intersect1d(con[i, ], roi_mask_idx)) == 3]
|
|
870
|
+
# crop connectivity matrix to ROI
|
|
871
|
+
con_cropped = con[con_row_idx, ]
|
|
872
|
+
else:
|
|
873
|
+
con_cropped = con
|
|
874
|
+
|
|
875
|
+
# evaluate new indices of cropped connectivity matrix
|
|
876
|
+
point_idx_before, point_idx_after = np.unique(con_cropped, return_inverse=True)
|
|
877
|
+
con_cropped_reform = np.reshape(point_idx_after, (con_cropped.shape[0], con_cropped.shape[1]))
|
|
878
|
+
|
|
879
|
+
# crop points to ROI
|
|
880
|
+
surface_points_middle = surface_points_middle[point_idx_before, ]
|
|
881
|
+
if self.layer == 3:
|
|
882
|
+
surface_points_upper = surface_points_upper[point_idx_before, ]
|
|
883
|
+
surface_points_lower = surface_points_lower[point_idx_before, ]
|
|
884
|
+
|
|
885
|
+
# refine
|
|
886
|
+
if refine:
|
|
887
|
+
if not os.path.exists(os.path.join(self.mesh_folder, "roi", "tmp")):
|
|
888
|
+
os.makedirs(os.path.join(self.mesh_folder, "roi", "tmp"))
|
|
889
|
+
|
|
890
|
+
mesh = trimesh.Trimesh(vertices=surface_points_middle,
|
|
891
|
+
faces=con_cropped_reform)
|
|
892
|
+
roi_fn = os.path.join(self.mesh_folder, "roi", "tmp", "roi.stl")
|
|
893
|
+
mesh.export(roi_fn)
|
|
894
|
+
|
|
895
|
+
roi_refined_fn = os.path.join(self.mesh_folder, "roi", "tmp", "roi_refined.stl")
|
|
896
|
+
pynibs.refine_surface(fn_surf=roi_fn,
|
|
897
|
+
fn_surf_refined=roi_refined_fn,
|
|
898
|
+
center=[0, 0, 0],
|
|
899
|
+
radius=np.inf,
|
|
900
|
+
verbose=True,
|
|
901
|
+
repair=False)
|
|
902
|
+
|
|
903
|
+
roi = trimesh.load(roi_refined_fn)
|
|
904
|
+
con_cropped_reform = roi.faces
|
|
905
|
+
surface_points_middle = roi.vertices
|
|
906
|
+
|
|
907
|
+
if self.layer == 3:
|
|
908
|
+
roi = []
|
|
909
|
+
|
|
910
|
+
for p in [surface_points_upper, surface_points_lower]:
|
|
911
|
+
mesh = trimesh.Trimesh(vertices=p,
|
|
912
|
+
faces=con_cropped_reform)
|
|
913
|
+
mesh.export(roi_fn)
|
|
914
|
+
|
|
915
|
+
pynibs.refine_surface(fn_surf=roi_fn,
|
|
916
|
+
fn_surf_refined=roi_refined_fn,
|
|
917
|
+
center=[0, 0, 0],
|
|
918
|
+
radius=np.inf,
|
|
919
|
+
verbose=True,
|
|
920
|
+
repair=False)
|
|
921
|
+
|
|
922
|
+
roi.append(trimesh.load(roi_refined_fn))
|
|
923
|
+
|
|
924
|
+
surface_points_upper = roi[0].vertices
|
|
925
|
+
surface_points_lower = roi[1].vertices
|
|
926
|
+
|
|
927
|
+
self.node_coord_mid = surface_points_middle
|
|
928
|
+
self.node_number_list = con_cropped_reform
|
|
929
|
+
|
|
930
|
+
if self.layer == 3:
|
|
931
|
+
self.node_coord_up = surface_points_upper
|
|
932
|
+
self.node_coord_low = surface_points_lower
|
|
933
|
+
self.tri_center_coord_up = np.average(self.node_coord_up[self.node_number_list], axis=1)
|
|
934
|
+
self.tri_center_coord_low = np.average(self.node_coord_low[self.node_number_list], axis=1)
|
|
935
|
+
|
|
936
|
+
self.tri_center_coord_mid = np.average(self.node_coord_mid[self.node_number_list], axis=1)
|
|
937
|
+
|
|
938
|
+
if self.layer == 3:
|
|
939
|
+
return surface_points_upper, surface_points_middle, surface_points_lower, con_cropped_reform
|
|
940
|
+
else:
|
|
941
|
+
return surface_points_middle, con_cropped_reform
|
|
942
|
+
|
|
943
|
+
def generate_cortical_laminae(self, # use tuple instead of list for immutability
|
|
944
|
+
head_model_mesh,
|
|
945
|
+
bbox=None,
|
|
946
|
+
laminae=(0.06, 0.4, 0.55, 0.65, 0.85),
|
|
947
|
+
layer_ids=("L1", "L23", "L4", "L5", "L6")
|
|
948
|
+
):
|
|
949
|
+
"""
|
|
950
|
+
Create the cortical layering with the provided laminar depths.
|
|
951
|
+
|
|
952
|
+
Defaults to the standard depths of the laminae in the neo-cortex from layer I to VI
|
|
953
|
+
from "Simulation of transcranial magnetic stimulation in head model with morphologically-realistic
|
|
954
|
+
cortical neurons", Aberra et al., https://doi.org/10.1016/j.brs.2019.10.002
|
|
955
|
+
|
|
956
|
+
Parameters
|
|
957
|
+
----------
|
|
958
|
+
head_model_mesh : simnibs.Msh
|
|
959
|
+
The head model volume mesh.
|
|
960
|
+
Inside the GM compartment of this mesh, the layering will be generated.
|
|
961
|
+
bbox : np.ndarray, optional
|
|
962
|
+
Bounding coordinates of the region of interest.
|
|
963
|
+
Optional, if the mid-layer surface is already existing
|
|
964
|
+
(and can thus be used to determine the bounding coordinates).
|
|
965
|
+
laminae : list of float or tuple of float, default: (0.06, 0.4, 0.55, 0.65, 0.85)
|
|
966
|
+
List of depths of the individual to-be created lamiae.
|
|
967
|
+
layer_ids : typing.List[str], default: ("L1", "L23", "L4", "L5", "L6")
|
|
968
|
+
List of layer identifiers.
|
|
969
|
+
"""
|
|
970
|
+
assert bbox is not None or self.node_coord_mid is not None, \
|
|
971
|
+
"Neither mid-layer surface was initialized nor explicit bounding box was provided to construct layer."
|
|
972
|
+
|
|
973
|
+
for idx, lam in tqdm.tqdm(enumerate(laminae)):
|
|
974
|
+
self.layers.append(
|
|
975
|
+
CorticalLayer.create_in_roi(
|
|
976
|
+
layer_id=layer_ids[idx],
|
|
977
|
+
roi=self,
|
|
978
|
+
depth=lam,
|
|
979
|
+
volmesh=head_model_mesh
|
|
980
|
+
)
|
|
981
|
+
)
|
|
982
|
+
|
|
983
|
+
def determine_element_idx_in_mesh(self, msh):
|
|
984
|
+
"""
|
|
985
|
+
Determines tetrahedra indices of msh where the triangle center points of upper, middle and lower surface
|
|
986
|
+
and the nodes of middle surface are
|
|
987
|
+
|
|
988
|
+
Parameters
|
|
989
|
+
----------
|
|
990
|
+
msh : pynibs.mesh.mesh_struct.TetrahedraLinear
|
|
991
|
+
TetrahedraLinear object.
|
|
992
|
+
|
|
993
|
+
Returns
|
|
994
|
+
-------
|
|
995
|
+
RegionOfInterestSurface.tet_idx_tri_center_up : np.ndarray
|
|
996
|
+
(N_points) Tetrahedra indices of TetrahedraLinear object instance where the center points of the
|
|
997
|
+
triangles of the upper surface are.
|
|
998
|
+
RegionOfInterestSurface.tet_idx_tri_center_mid : np.ndarray
|
|
999
|
+
(N_points) Tetrahedra indices of TetrahedraLinear object instance where the center points of the triangles
|
|
1000
|
+
of the middle surface are.
|
|
1001
|
+
RegionOfInterestSurface.tet_idx_tri_center_low : np.ndarray
|
|
1002
|
+
(N_points) Tetrahedra indices of TetrahedraLinear object instance where the center points of the
|
|
1003
|
+
triangles of the lower surface are.
|
|
1004
|
+
RegionOfInterestSurface.tet_idx_node_coord_mid : np.ndarray
|
|
1005
|
+
(N_tri) Tetrahedra indices of TetrahedraLinear object instance where the nodes of the middle
|
|
1006
|
+
surface are.
|
|
1007
|
+
"""
|
|
1008
|
+
# determine tetrahedra indices of triangle center points of upper, middle and lower surface
|
|
1009
|
+
if self.tri_center_coord_low != [] and self.tri_center_coord_up != []:
|
|
1010
|
+
points = [self.tri_center_coord_low,
|
|
1011
|
+
self.tri_center_coord_mid,
|
|
1012
|
+
self.tri_center_coord_up]
|
|
1013
|
+
else:
|
|
1014
|
+
points = [self.tri_center_coord_mid]
|
|
1015
|
+
|
|
1016
|
+
tet_idx = pynibs.determine_element_idx_in_mesh(fname=None,
|
|
1017
|
+
msh=msh,
|
|
1018
|
+
points=points,
|
|
1019
|
+
compute_baricentric=False)
|
|
1020
|
+
|
|
1021
|
+
if self.tri_center_coord_low != [] and self.tri_center_coord_up != []:
|
|
1022
|
+
self.tet_idx_tri_center_low = tet_idx[:, 0]
|
|
1023
|
+
self.tet_idx_tri_center_mid = tet_idx[:, 1]
|
|
1024
|
+
self.tet_idx_tri_center_up = tet_idx[:, 2]
|
|
1025
|
+
else:
|
|
1026
|
+
self.tet_idx_tri_center_low = None
|
|
1027
|
+
self.tet_idx_tri_center_mid = tet_idx[:, 0]
|
|
1028
|
+
self.tet_idx_tri_center_up = None
|
|
1029
|
+
|
|
1030
|
+
# determine tetrahedra indices of nodes of middle surface
|
|
1031
|
+
self.tet_idx_node_coord_mid = pynibs.determine_element_idx_in_mesh(fname=None,
|
|
1032
|
+
msh=msh,
|
|
1033
|
+
points=self.node_coord_mid,
|
|
1034
|
+
compute_baricentric=False)
|
|
1035
|
+
|
|
1036
|
+
def decimate(self, fraction=.075):
|
|
1037
|
+
"""
|
|
1038
|
+
Subsample ROI surface based on a decimation factor and return element indices.
|
|
1039
|
+
(no Freesurfer surfaces associated with the ROI surface required)
|
|
1040
|
+
|
|
1041
|
+
Parameters
|
|
1042
|
+
----------
|
|
1043
|
+
fraction : float, default: .075
|
|
1044
|
+
Multiplied by the total number of ROI elements determines
|
|
1045
|
+
(approximately) the number of remaining ROI elements after decimation.
|
|
1046
|
+
|
|
1047
|
+
Returns
|
|
1048
|
+
-------
|
|
1049
|
+
ele_idx : np.ndarray of float
|
|
1050
|
+
[approx. fraction * n_ele] Element indices of the subsampled surface; sorted.
|
|
1051
|
+
"""
|
|
1052
|
+
|
|
1053
|
+
mesh = trimesh.Trimesh(
|
|
1054
|
+
vertices=self.node_coord_mid,
|
|
1055
|
+
faces=self.node_number_list
|
|
1056
|
+
)
|
|
1057
|
+
|
|
1058
|
+
pts, subsample_idcs = trimesh.sample.sample_surface_even(
|
|
1059
|
+
mesh=mesh,
|
|
1060
|
+
count=int(self.node_number_list.shape[0] * fraction)
|
|
1061
|
+
)
|
|
1062
|
+
|
|
1063
|
+
return np.sort(subsample_idcs)
|
|
1064
|
+
|
|
1065
|
+
def subsample(self, dist=10, fn_sphere=None):
|
|
1066
|
+
"""
|
|
1067
|
+
Subsample ROI surface based on a spacing and return element indices
|
|
1068
|
+
(Freesurfer surfaces associatd with the ROI surface required)
|
|
1069
|
+
|
|
1070
|
+
Parameters
|
|
1071
|
+
----------
|
|
1072
|
+
dist : float
|
|
1073
|
+
Distance in mm the subsampled points lie apart.
|
|
1074
|
+
fn_sphere : str
|
|
1075
|
+
Name of ?.sphere file (freesurfer).
|
|
1076
|
+
|
|
1077
|
+
Returns
|
|
1078
|
+
-------
|
|
1079
|
+
ele_idx : ndarray of float
|
|
1080
|
+
(n_ele) Element indices of the subsampled surface.
|
|
1081
|
+
"""
|
|
1082
|
+
|
|
1083
|
+
# load sphere surface
|
|
1084
|
+
sphere_coords, sphere_faces = nib.freesurfer.io.read_geometry(fn_sphere)
|
|
1085
|
+
|
|
1086
|
+
# sample sphere equally
|
|
1087
|
+
r = np.linalg.norm(sphere_coords[0, :])
|
|
1088
|
+
a0 = 4 * np.pi * r ** 2
|
|
1089
|
+
|
|
1090
|
+
sphere_points_sampled = pynibs.mesh.utils.sample_sphere(n_points=int(np.ceil(a0 / dist ** 2) // 2 * 2 + 1),
|
|
1091
|
+
r=np.linalg.norm(sphere_coords[0, :]))
|
|
1092
|
+
|
|
1093
|
+
# read mask from freesurfer mask file
|
|
1094
|
+
mask = nib.freesurfer.mghformat.MGHImage.from_filename(
|
|
1095
|
+
os.path.join(self.mesh_folder, self.fn_mask)).dataobj[:].flatten()
|
|
1096
|
+
roi_mask_idx = np.where(mask > 0.5)
|
|
1097
|
+
|
|
1098
|
+
con_row_idx = [i for i in range(sphere_faces.shape[0]) if
|
|
1099
|
+
len(np.intersect1d(sphere_faces[i, ], roi_mask_idx)) == 3]
|
|
1100
|
+
|
|
1101
|
+
# crop connectivity matrix to ROI
|
|
1102
|
+
con_cropped = sphere_faces[con_row_idx, ]
|
|
1103
|
+
|
|
1104
|
+
p1_tri = sphere_coords[con_cropped[:, 0], :]
|
|
1105
|
+
p2_tri = sphere_coords[con_cropped[:, 1], :]
|
|
1106
|
+
p3_tri = sphere_coords[con_cropped[:, 2], :]
|
|
1107
|
+
tri_center = 1.0 / 3 * (p1_tri + p2_tri + p3_tri)
|
|
1108
|
+
ele_idx = []
|
|
1109
|
+
|
|
1110
|
+
for i_p_ss in range(sphere_points_sampled.shape[0]):
|
|
1111
|
+
dist = np.linalg.norm(tri_center - sphere_points_sampled[i_p_ss], axis=1)
|
|
1112
|
+
|
|
1113
|
+
if np.min(dist) < 1:
|
|
1114
|
+
ele_idx.append(np.argmin(dist))
|
|
1115
|
+
|
|
1116
|
+
return ele_idx
|
|
1117
|
+
|
|
1118
|
+
|
|
1119
|
+
class RegionOfInterestVolume:
|
|
1120
|
+
"""
|
|
1121
|
+
Region of interest (volume) class
|
|
1122
|
+
|
|
1123
|
+
Attributes
|
|
1124
|
+
----------
|
|
1125
|
+
node_coord : np.ndarray
|
|
1126
|
+
(N_points, 3) Coordinates (x,y,z) of ROI tetrahedra nodes.
|
|
1127
|
+
tet_node_number_list : np.ndarray
|
|
1128
|
+
(N_tet_roi, 3) Connectivity matrix of ROI tetrahedra.
|
|
1129
|
+
tri_node_number_list : np.ndarray
|
|
1130
|
+
(N_tri_roi, 3) Connectivity matrix of ROI tetrahedra.
|
|
1131
|
+
tet_idx_node_coord : np.ndarray
|
|
1132
|
+
(N_points) Tetrahedra indices of TetrahedraLinear object instance where the ROI nodes are.
|
|
1133
|
+
tet_idx_tetrahedra_center : np.ndarray
|
|
1134
|
+
(N_tet_roi) Tetrahedra indices of TetrahedraLinear object instance where the center points of the ROI
|
|
1135
|
+
tetrahedra are.
|
|
1136
|
+
tet_idx_triangle_center : np.ndarray
|
|
1137
|
+
(N_tri_roi) Tetrahedra indices of TetrahedraLinear object instance where the center points of the ROI triangle
|
|
1138
|
+
are. If the ROI is directly generated from the msh instance using "make_roi_volume_from_msh", these
|
|
1139
|
+
indices are the triangle indices of the head mesh since the ROI mesh and the head mesh are overlapping. If
|
|
1140
|
+
the ROI mesh is not the same as the head mesh, the triangle center of the ROI mesh are always lying in a
|
|
1141
|
+
tetrahedra of the head mesh (these indices are given in this case).
|
|
1142
|
+
"""
|
|
1143
|
+
|
|
1144
|
+
def __init__(self):
|
|
1145
|
+
""" Initialize RegionOfInterestVolume class instance """
|
|
1146
|
+
|
|
1147
|
+
self.node_coord = []
|
|
1148
|
+
self.tet_node_number_list = []
|
|
1149
|
+
self.tri_node_number_list = []
|
|
1150
|
+
self.tet_idx_node_coord = []
|
|
1151
|
+
self.tet_idx_tetrahedra_center = []
|
|
1152
|
+
self.tet_idx_triangle_center = []
|
|
1153
|
+
|
|
1154
|
+
def make_roi_volume_from_msh(self, msh, volume_type='box', x_roi=None, y_roi=None, z_roi=None):
|
|
1155
|
+
"""
|
|
1156
|
+
Generate region of interest (volume) and extract nodes, triangles and tetrahedra from msh instance.
|
|
1157
|
+
|
|
1158
|
+
Parameters
|
|
1159
|
+
----------
|
|
1160
|
+
msh: pynibs.mesh.mesh_struct.TetrahedraLinear
|
|
1161
|
+
Mesh object instance of type TetrahedraLinear
|
|
1162
|
+
volume_type: str
|
|
1163
|
+
Type of ROI ('box' or 'sphere')
|
|
1164
|
+
x_roi: list of float
|
|
1165
|
+
|
|
1166
|
+
- type = 'box': [Xmin, Xmax] (in mm), whole X range if empty [0,0] or None (left - right)
|
|
1167
|
+
- type = 'sphere': origin [x,y,z]
|
|
1168
|
+
y_roi: list of float
|
|
1169
|
+
|
|
1170
|
+
- type = 'box': [Ymin, Ymax] (in mm), whole Y range if empty [0,0] or None (anterior - posterior)
|
|
1171
|
+
- type = 'sphere': radius (in mm)
|
|
1172
|
+
z_roi: list of float
|
|
1173
|
+
|
|
1174
|
+
- type = 'box': [Zmin, Zmax] (in mm), whole Z range if empty [0,0] or None (inferior - superior)
|
|
1175
|
+
- type = 'sphere': None
|
|
1176
|
+
|
|
1177
|
+
Returns
|
|
1178
|
+
-------
|
|
1179
|
+
RegionOfInterestVolume.node_coord : np.ndarray [N_points x 3]
|
|
1180
|
+
Coordinates (x,y,z) of ROI tetrahedra nodes
|
|
1181
|
+
RegionOfInterestVolume.tet_node_number_list : np.ndarray [N_tet_roi x 3]
|
|
1182
|
+
Connectivity matrix of ROI tetrahedra
|
|
1183
|
+
RegionOfInterestVolume.tri_node_number_list : np.ndarray [N_tri_roi x 3]
|
|
1184
|
+
Connectivity matrix of ROI tetrahedra
|
|
1185
|
+
RegionOfInterestVolume.tet_idx_node_coord : np.ndarray [N_points]
|
|
1186
|
+
Tetrahedra indices of TetrahedraLinear object instance where the ROI nodes are lying in
|
|
1187
|
+
RegionOfInterestVolume.tet_idx_tetrahedra_center : np.ndarray [N_tet_roi]
|
|
1188
|
+
Tetrahedra indices of TetrahedraLinear object instance where the center points of the ROI tetrahedra are
|
|
1189
|
+
lying in
|
|
1190
|
+
RegionOfInterestVolume.tet_idx_triangle_center : np.ndarray [N_tri_roi]
|
|
1191
|
+
Tetrahedra indices of TetrahedraLinear object instance where the center points of the ROI triangle are
|
|
1192
|
+
. If the ROI is directly generated from the msh instance using "make_roi_volume_from_msh", these
|
|
1193
|
+
indices are the triangle indices of the head mesh since the ROI mesh and the head mesh are overlapping. If
|
|
1194
|
+
the ROI mesh is not the same as the head mesh, the triangle center of the ROI mesh are always. a
|
|
1195
|
+
tetrahedra of the head mesh (these indices are given in this case)
|
|
1196
|
+
"""
|
|
1197
|
+
|
|
1198
|
+
if volume_type == 'box':
|
|
1199
|
+
roi_mask_bool = (msh.points[:, 0] > min(x_roi)) & (msh.points[:, 0] < max(x_roi)) & \
|
|
1200
|
+
(msh.points[:, 1] > min(y_roi)) & (msh.points[:, 1] < max(y_roi)) & \
|
|
1201
|
+
(msh.points[:, 2] > min(z_roi)) & (msh.points[:, 2] < max(z_roi))
|
|
1202
|
+
roi_mask_idx = np.where(roi_mask_bool)[0]
|
|
1203
|
+
|
|
1204
|
+
elif volume_type == 'sphere':
|
|
1205
|
+
roi_mask_bool = np.linalg.norm(msh.points - np.array(x_roi), axis=1) <= y_roi
|
|
1206
|
+
roi_mask_idx = np.where(roi_mask_bool)[0]
|
|
1207
|
+
|
|
1208
|
+
else:
|
|
1209
|
+
raise Exception('region of interest type not specified correctly (either box or sphere)')
|
|
1210
|
+
|
|
1211
|
+
self.node_coord = msh.points[roi_mask_idx, :]
|
|
1212
|
+
|
|
1213
|
+
# crop connectivity matrices of tetrahedra and triangles to ROI
|
|
1214
|
+
tet_con_row_idx = [i for i in range(msh.tetrahedra.shape[0]) if
|
|
1215
|
+
len(np.intersect1d(msh.tetrahedra[i, ], roi_mask_idx)) == 3]
|
|
1216
|
+
tet_con_cropped = msh.tetrahedra[tet_con_row_idx,]
|
|
1217
|
+
|
|
1218
|
+
tri_con_row_idx = [i for i in range(msh.triangles.shape[0]) if
|
|
1219
|
+
len(np.intersect1d(msh.triangles[i, ], roi_mask_idx)) == 3]
|
|
1220
|
+
tri_con_cropped = msh.triangles[tri_con_row_idx, ]
|
|
1221
|
+
|
|
1222
|
+
# evaluate new indices of cropped connectivity matrices of tetrahedra and triangles (starts from 0)
|
|
1223
|
+
tet_point_idx_before, tet_point_idx_after = np.unique(tet_con_cropped, return_inverse=True)
|
|
1224
|
+
self.tet_node_number_list = np.reshape(tet_point_idx_after,
|
|
1225
|
+
(tet_con_cropped.shape[0], tet_con_cropped.shape[1]))
|
|
1226
|
+
|
|
1227
|
+
tri_point_idx_before, tri_point_idx_after = np.unique(tri_con_cropped, return_inverse=True)
|
|
1228
|
+
self.tri_node_number_list = np.reshape(tri_point_idx_after,
|
|
1229
|
+
(tri_con_cropped.shape[0], tri_con_cropped.shape[1]))
|
|
1230
|
+
|
|
1231
|
+
self.tet_idx_node_coord = None
|
|
1232
|
+
self.tet_idx_tetrahedra_center = np.array(tet_con_row_idx)
|
|
1233
|
+
self.tet_idx_triangle_center = np.array(tri_con_row_idx)
|