pyLQT 1.0.dev1__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.
- lesion_metrics/__init__.py +0 -0
- lesion_metrics/gray_matter/__init__.py +0 -0
- lesion_metrics/gray_matter/metrics.py +78 -0
- lesion_metrics/gray_matter/utils.py +30 -0
- lesion_metrics/network/__init__.py +0 -0
- lesion_metrics/network/metrics.py +190 -0
- lesion_metrics/network/utils.py +104 -0
- lesion_metrics/utils/__init__.py +0 -0
- lesion_metrics/utils/configs.py +137 -0
- lesion_metrics/utils/downloads.py +250 -0
- lesion_metrics/utils/registration.py +106 -0
- lesion_metrics/white_matter/__init__.py +0 -0
- lesion_metrics/white_matter/metrics.py +160 -0
- lesion_metrics/white_matter/utils.py +244 -0
- pyLQT_cli.py +180 -0
- pylqt-1.0.dev1.dist-info/METADATA +62 -0
- pylqt-1.0.dev1.dist-info/RECORD +21 -0
- pylqt-1.0.dev1.dist-info/WHEEL +5 -0
- pylqt-1.0.dev1.dist-info/entry_points.txt +2 -0
- pylqt-1.0.dev1.dist-info/licenses/LICENSE +674 -0
- pylqt-1.0.dev1.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import zipfile
|
|
2
|
+
import json
|
|
3
|
+
import warnings
|
|
4
|
+
import requests
|
|
5
|
+
import shutil
|
|
6
|
+
import gzip
|
|
7
|
+
import nibabel as nib
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from templateflow import api as tf
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
def create_tract_list(
|
|
14
|
+
references_folder,
|
|
15
|
+
tracts_folder
|
|
16
|
+
):
|
|
17
|
+
"""Create paired lists of tractogram and reference image paths.
|
|
18
|
+
|
|
19
|
+
Tractograms and their corresponding NIfTI reference images are matched
|
|
20
|
+
based on their relative paths within their respective folders. The
|
|
21
|
+
function also checks that all reference images share the same voxel
|
|
22
|
+
grid and affine.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
references_folder : Path
|
|
27
|
+
Root directory containing the NIfTI reference images.
|
|
28
|
+
tracts_folder : Path
|
|
29
|
+
Root directory containing the tractogram files.
|
|
30
|
+
|
|
31
|
+
Returns
|
|
32
|
+
-------
|
|
33
|
+
tuple[list[Path], list[Path]]
|
|
34
|
+
Two lists containing:
|
|
35
|
+
|
|
36
|
+
- tracts_pathlist : list[Path]
|
|
37
|
+
Paths to the tractogram files.
|
|
38
|
+
- references_pathlist : list[Path]
|
|
39
|
+
Paths to the corresponding NIfTI reference images.
|
|
40
|
+
|
|
41
|
+
Only tractograms with a corresponding NIfTI reference are included.
|
|
42
|
+
Warnings are issued for missing pairs and for reference images with
|
|
43
|
+
mismatched grids or affines.
|
|
44
|
+
"""
|
|
45
|
+
trk_paths = sorted(tracts_folder.rglob("*.trk.gz"))
|
|
46
|
+
nifti_paths = sorted(references_folder.rglob("*.nii.gz"))
|
|
47
|
+
|
|
48
|
+
# Check that every TRK has a corresponding NIfTI
|
|
49
|
+
nifti_relatives = {
|
|
50
|
+
p.relative_to(references_folder)
|
|
51
|
+
for p in nifti_paths
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for trk_path in trk_paths:
|
|
55
|
+
trk_relative = trk_path.relative_to(tracts_folder)
|
|
56
|
+
expected_nifti = Path(
|
|
57
|
+
str(trk_relative).replace(".trk.gz", ".nii.gz")
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if expected_nifti not in nifti_relatives:
|
|
61
|
+
warnings.warn(f"No NIfTI found for {expected_nifti}")
|
|
62
|
+
|
|
63
|
+
# Check that every NIfTI has a corresponding TRK
|
|
64
|
+
trk_relatives = {
|
|
65
|
+
p.relative_to(tracts_folder)
|
|
66
|
+
for p in trk_paths
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for nifti_path in nifti_paths:
|
|
70
|
+
nifti_relative = nifti_path.relative_to(references_folder)
|
|
71
|
+
expected_trk = Path(
|
|
72
|
+
str(nifti_relative).replace(".nii.gz", ".trk.gz")
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if expected_trk not in trk_relatives:
|
|
76
|
+
warnings.warn(f"No TRK found for {expected_trk}")
|
|
77
|
+
|
|
78
|
+
tracts_pathlist = []
|
|
79
|
+
references_pathlist = []
|
|
80
|
+
|
|
81
|
+
for trk_path in trk_paths:
|
|
82
|
+
trk_relative = trk_path.relative_to(tracts_folder)
|
|
83
|
+
|
|
84
|
+
nifti_path = (
|
|
85
|
+
references_folder
|
|
86
|
+
/ Path(str(trk_relative).replace(".trk.gz", ".nii.gz"))
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if not nifti_path.exists():
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
tracts_pathlist.append(trk_path)
|
|
93
|
+
references_pathlist.append(nifti_path)
|
|
94
|
+
|
|
95
|
+
# Verify that all NIfTIs are on the same grid
|
|
96
|
+
reference = None
|
|
97
|
+
|
|
98
|
+
for nifti_path in references_pathlist:
|
|
99
|
+
nifti = nib.nifti1.load(nifti_path)
|
|
100
|
+
|
|
101
|
+
if reference is None:
|
|
102
|
+
reference = nifti
|
|
103
|
+
else:
|
|
104
|
+
if nifti.shape != reference.shape:
|
|
105
|
+
warnings.warn(f"NIfTI grid mismatch: {nifti_path}")
|
|
106
|
+
if not np.allclose(nifti.affine, reference.affine):
|
|
107
|
+
warnings.warn(f"NIfTI affine mismatch: {nifti_path}")
|
|
108
|
+
|
|
109
|
+
return tracts_pathlist, references_pathlist
|
|
110
|
+
|
|
111
|
+
def uncompress_trk_gz(
|
|
112
|
+
trk_path,
|
|
113
|
+
out_path
|
|
114
|
+
):
|
|
115
|
+
"""Uncompress a .trk file if needed and save it to the output path.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
trk_path : Path
|
|
120
|
+
Path to the input .trk or compressed .trk.gz file.
|
|
121
|
+
out_path : Path
|
|
122
|
+
Path where the uncompressed .trk file is saved.
|
|
123
|
+
|
|
124
|
+
Returns
|
|
125
|
+
-------
|
|
126
|
+
Path
|
|
127
|
+
Path to the resulting .trk file.
|
|
128
|
+
"""
|
|
129
|
+
if trk_path.suffix == ".gz":
|
|
130
|
+
with gzip.open(trk_path, "rb") as f_in:
|
|
131
|
+
with open(out_path, "wb") as f_out:
|
|
132
|
+
shutil.copyfileobj(f_in, f_out)
|
|
133
|
+
else:
|
|
134
|
+
shutil.copyfile(trk_path, out_path)
|
|
135
|
+
|
|
136
|
+
return out_path
|
|
137
|
+
|
|
138
|
+
def default_download(out_folder: Path):
|
|
139
|
+
"""Download the default HCP1065 tractography dataset and create its configuration.
|
|
140
|
+
|
|
141
|
+
The function downloads the HCP1065 average tractograms and their
|
|
142
|
+
corresponding NIfTI reference images, extracts them into the output
|
|
143
|
+
directory, and creates a JSON configuration file describing the
|
|
144
|
+
downloaded dataset. Existing folders are reused.
|
|
145
|
+
|
|
146
|
+
Parameters
|
|
147
|
+
----------
|
|
148
|
+
out_folder : pathlib.Path
|
|
149
|
+
Directory where the HCP1065 dataset, reference images, and
|
|
150
|
+
configuration file are stored.
|
|
151
|
+
|
|
152
|
+
Returns
|
|
153
|
+
-------
|
|
154
|
+
pathlib.Path
|
|
155
|
+
Path to the generated HCP1065 configuration JSON file.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
tracts_cfg_folder = out_folder / "HCP1065_cfg"
|
|
159
|
+
tracts_cfg_folder.mkdir(parents=True, exist_ok=True)
|
|
160
|
+
|
|
161
|
+
tracts_folder = tracts_cfg_folder / "hcp1065_avg_tracts_trk"
|
|
162
|
+
|
|
163
|
+
if tracts_folder.exists():
|
|
164
|
+
warnings.warn(
|
|
165
|
+
f"Tracts folder already exists, using it: {tracts_folder}"
|
|
166
|
+
)
|
|
167
|
+
else:
|
|
168
|
+
url = "https://github.com/data-others/atlas/releases/download/hcp1065/hcp1065_avg_tracts_trk.zip"
|
|
169
|
+
zip_path = out_folder / "hcp1065_avg_tracts_trk.zip"
|
|
170
|
+
|
|
171
|
+
response = requests.get(url, stream=True, timeout=60)
|
|
172
|
+
response.raise_for_status()
|
|
173
|
+
|
|
174
|
+
with zip_path.open("wb") as f:
|
|
175
|
+
for chunk in response.iter_content(chunk_size=1024*1024):
|
|
176
|
+
if chunk:
|
|
177
|
+
f.write(chunk)
|
|
178
|
+
|
|
179
|
+
with zipfile.ZipFile(zip_path) as zip_file:
|
|
180
|
+
zip_file.extractall(tracts_folder)
|
|
181
|
+
|
|
182
|
+
zip_path.unlink()
|
|
183
|
+
|
|
184
|
+
references_folder = tracts_cfg_folder / "hcp1065_avg_tracts"
|
|
185
|
+
|
|
186
|
+
if references_folder.exists():
|
|
187
|
+
warnings.warn(
|
|
188
|
+
f"References folder already exists, using it: {references_folder}"
|
|
189
|
+
)
|
|
190
|
+
else:
|
|
191
|
+
url = "https://github.com/data-others/atlas/releases/download/hcp1065/hcp1065_avg_tracts_nifti.zip"
|
|
192
|
+
zip_path = out_folder / "hcp1065_avg_tracts_nifti.zip"
|
|
193
|
+
|
|
194
|
+
response = requests.get(url, stream=True, timeout=60)
|
|
195
|
+
response.raise_for_status()
|
|
196
|
+
|
|
197
|
+
with zip_path.open("wb") as f:
|
|
198
|
+
for chunk in response.iter_content(chunk_size=1024*1024):
|
|
199
|
+
if chunk:
|
|
200
|
+
f.write(chunk)
|
|
201
|
+
|
|
202
|
+
with zipfile.ZipFile(zip_path) as zip_file:
|
|
203
|
+
zip_file.extractall(zip_path.parent)
|
|
204
|
+
|
|
205
|
+
zip_path.unlink()
|
|
206
|
+
|
|
207
|
+
references_folder = zip_path.parent / "nifti"
|
|
208
|
+
references_folder.rename(
|
|
209
|
+
zip_path.parent / "hcp1065_avg_tracts"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
tracts_name = "HCP1065"
|
|
213
|
+
tracts_space = "MNI152NLin2009aAsym"
|
|
214
|
+
|
|
215
|
+
config = {"name": tracts_name,
|
|
216
|
+
"tracts": "hcp1065_avg_tracts_trk",
|
|
217
|
+
"references": "hcp1065_avg_tracts",
|
|
218
|
+
"space": tracts_space}
|
|
219
|
+
|
|
220
|
+
tracts_cfg = Path(tracts_cfg_folder / "HCP1065_config.json")
|
|
221
|
+
|
|
222
|
+
with open(tracts_cfg, "w") as f:
|
|
223
|
+
json.dump(config, f, indent=4)
|
|
224
|
+
|
|
225
|
+
return tracts_cfg
|
|
226
|
+
|
|
227
|
+
def get_ref_space_tf(space: str, res: int = 1):
|
|
228
|
+
"""Load a templateflow T1w template and apply its brain mask.
|
|
229
|
+
|
|
230
|
+
Parameters
|
|
231
|
+
----------
|
|
232
|
+
space : str
|
|
233
|
+
TemplateFlow template identifier.
|
|
234
|
+
res : int, optional
|
|
235
|
+
Template resolution to load. Defaults to 1.
|
|
236
|
+
|
|
237
|
+
Returns
|
|
238
|
+
-------
|
|
239
|
+
nibabel.Nifti1Image
|
|
240
|
+
T1w template with voxels outside the brain mask set to zero.
|
|
241
|
+
"""
|
|
242
|
+
t1w_ref_path = tf.get(space, resolution=res, desc=None, suffix="T1w")
|
|
243
|
+
brain_mask_path = tf.get(space, resolution=res, desc="brain", suffix="mask")
|
|
244
|
+
t1w_ref = nib.load(t1w_ref_path)
|
|
245
|
+
brain_mask = nib.load(brain_mask_path)
|
|
246
|
+
ref_space_volume = t1w_ref.get_fdata().copy()
|
|
247
|
+
ref_space_volume[brain_mask.get_fdata() <= 0] = 0
|
|
248
|
+
ref_space = nib.Nifti1Image(ref_space_volume, t1w_ref.affine, t1w_ref.header)
|
|
249
|
+
|
|
250
|
+
return ref_space
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from dipy.align.imaffine import AffineRegistration, MutualInformationMetric, transform_centers_of_mass
|
|
2
|
+
from dipy.align.transforms import TranslationTransform3D, RigidTransform3D, AffineTransform3D
|
|
3
|
+
from dipy.align.imwarp import SymmetricDiffeomorphicRegistration
|
|
4
|
+
from dipy.align.metrics import CCMetric
|
|
5
|
+
from dipy.align import write_mapping
|
|
6
|
+
|
|
7
|
+
def register_affine(
|
|
8
|
+
moving,
|
|
9
|
+
static,
|
|
10
|
+
out_path=None
|
|
11
|
+
):
|
|
12
|
+
"""Register a moving image to a static image using affine registration.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
moving : nibabel.Nifti1Image
|
|
17
|
+
Image to be registered to the static image.
|
|
18
|
+
static : nibabel.Nifti1Image
|
|
19
|
+
Reference image defining the target space for the registration.
|
|
20
|
+
out_path : pathlib.Path | None, optional
|
|
21
|
+
Path where the resulting registration mapping is saved. If None,
|
|
22
|
+
the mapping is not saved.
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
AffineMap
|
|
27
|
+
Affine mapping from the moving image space to the static image space.
|
|
28
|
+
"""
|
|
29
|
+
metric = MutualInformationMetric()
|
|
30
|
+
|
|
31
|
+
affreg = AffineRegistration(metric=metric)
|
|
32
|
+
|
|
33
|
+
transform = TranslationTransform3D()
|
|
34
|
+
translation = affreg.optimize(
|
|
35
|
+
static.get_fdata(),
|
|
36
|
+
moving.get_fdata(),
|
|
37
|
+
transform,
|
|
38
|
+
params0=None,
|
|
39
|
+
static_grid2world=static.affine,
|
|
40
|
+
moving_grid2world=moving.affine,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
transform = RigidTransform3D()
|
|
44
|
+
rigid = affreg.optimize(
|
|
45
|
+
static.get_fdata(),
|
|
46
|
+
moving.get_fdata(),
|
|
47
|
+
transform,
|
|
48
|
+
starting_affine=translation.affine,
|
|
49
|
+
params0=None,
|
|
50
|
+
static_grid2world=static.affine,
|
|
51
|
+
moving_grid2world=moving.affine,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
transform = AffineTransform3D()
|
|
55
|
+
mapping = affreg.optimize(
|
|
56
|
+
static.get_fdata(),
|
|
57
|
+
moving.get_fdata(),
|
|
58
|
+
transform,
|
|
59
|
+
starting_affine=rigid.affine,
|
|
60
|
+
params0=None,
|
|
61
|
+
static_grid2world=static.affine,
|
|
62
|
+
moving_grid2world=moving.affine,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if out_path:
|
|
66
|
+
write_mapping(mapping, out_path)
|
|
67
|
+
|
|
68
|
+
return mapping
|
|
69
|
+
|
|
70
|
+
def register_syn(
|
|
71
|
+
moving,
|
|
72
|
+
static,
|
|
73
|
+
out_path = None
|
|
74
|
+
):
|
|
75
|
+
"""Register a moving image to a static image using SyN registration,
|
|
76
|
+
with default settings from dipy.
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
moving : nibabel.Nifti1Image
|
|
81
|
+
Image to be registered to the static image.
|
|
82
|
+
static : nibabel.Nifti1Image
|
|
83
|
+
Reference image defining the target space for the registration.
|
|
84
|
+
out_path : pathlib.Path | None, optional
|
|
85
|
+
Path where the resulting registration mapping is saved. If None,
|
|
86
|
+
the mapping is not saved.
|
|
87
|
+
|
|
88
|
+
Returns
|
|
89
|
+
-------
|
|
90
|
+
DiffeomorphicMap
|
|
91
|
+
Diffeomorphic mapping from the moving image space to the static
|
|
92
|
+
image space.
|
|
93
|
+
"""
|
|
94
|
+
metric = CCMetric(dim=3)
|
|
95
|
+
|
|
96
|
+
syn = SymmetricDiffeomorphicRegistration(metric)
|
|
97
|
+
|
|
98
|
+
mapping = syn.optimize(static.get_fdata(),
|
|
99
|
+
moving.get_fdata(),
|
|
100
|
+
static_grid2world=static.affine,
|
|
101
|
+
moving_grid2world=moving.affine)
|
|
102
|
+
|
|
103
|
+
if out_path:
|
|
104
|
+
write_mapping(mapping, out_path)
|
|
105
|
+
|
|
106
|
+
return mapping
|
|
File without changes
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import nibabel as nib
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from nibabel.processing import resample_from_to
|
|
6
|
+
from .utils import get_tract, get_tdi, get_streamlines_disconnection
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_tracts_disconnections_severity(
|
|
12
|
+
tracts_path,
|
|
13
|
+
references_path,
|
|
14
|
+
lesion,
|
|
15
|
+
disc_tracts_out_path = None,
|
|
16
|
+
disc_frame_out_path = None
|
|
17
|
+
):
|
|
18
|
+
|
|
19
|
+
"""Compute the proportion of disconnected streamlines for a list of tracts.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
tracts_path : list[Path]
|
|
24
|
+
Paths to the tractogram files in order.
|
|
25
|
+
references_path : list[Path] | Path
|
|
26
|
+
Paths to the NIfTI reference images corresponding to the tractograms.
|
|
27
|
+
If a single path is provided, it is used as the reference for all
|
|
28
|
+
tractograms. If a list, it should be in the same order as tracts_path.
|
|
29
|
+
lesion : nib.Nifti1Image
|
|
30
|
+
NIfTI lesion mask. Voxels with values greater than zero are considered
|
|
31
|
+
part of the lesion.
|
|
32
|
+
disc_tracts_out_path : Path | None, optional
|
|
33
|
+
Directory where tractograms containing the disconnected streamlines
|
|
34
|
+
are saved. If None, disconnected tractograms are not saved.
|
|
35
|
+
disc_frame_out_path : Path | None, optional
|
|
36
|
+
Directory where the resulting CSV file containing the disconnection
|
|
37
|
+
severity for each tract is saved. If None, the results are not saved.
|
|
38
|
+
|
|
39
|
+
Returns
|
|
40
|
+
-------
|
|
41
|
+
pandas.DataFrame
|
|
42
|
+
DataFrame containing the disconnection proportion for each tract,
|
|
43
|
+
together with its tract and pathway (parent directory) names. The ``Discon`` column
|
|
44
|
+
contains the proportion of streamlines intersecting the lesion.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
if disc_tracts_out_path:
|
|
48
|
+
disc_tracts_out_path.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
disconnections = []
|
|
50
|
+
if isinstance(references_path, Path):
|
|
51
|
+
references_path = [references_path] * len(tracts_path)
|
|
52
|
+
for tract_path, reference_path in zip(tracts_path, references_path):
|
|
53
|
+
|
|
54
|
+
tract_name = tract_path.name.removesuffix(".gz").removesuffix(".trk")
|
|
55
|
+
pathway_name = tract_path.parent.name
|
|
56
|
+
|
|
57
|
+
reference = nib.nifti1.load(reference_path)
|
|
58
|
+
|
|
59
|
+
# grid can differ between lesion and average tracts (due to voxel size notably),
|
|
60
|
+
# resample_from_to shifts/scales the grid of the lesion to match the one of the tract
|
|
61
|
+
lesion_resampled = resample_from_to(lesion,
|
|
62
|
+
reference,
|
|
63
|
+
order=1)
|
|
64
|
+
|
|
65
|
+
lesion_mask = lesion_resampled.get_fdata() > 0
|
|
66
|
+
tract_mask = reference.get_fdata() > 0
|
|
67
|
+
|
|
68
|
+
if not np.any(tract_mask & lesion_mask):
|
|
69
|
+
disconnections.append({
|
|
70
|
+
"Tract": tract_name,
|
|
71
|
+
"Discon": np.round(0.0, 3),
|
|
72
|
+
"Pathway": pathway_name,
|
|
73
|
+
})
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
if disc_tracts_out_path:
|
|
77
|
+
disconnected_tract_path = disc_tracts_out_path / f"{tract_name}_disconnected.trk"
|
|
78
|
+
else:
|
|
79
|
+
disconnected_tract_path = None
|
|
80
|
+
|
|
81
|
+
tract = get_tract(tract_path=tract_path, reference_path=reference_path)
|
|
82
|
+
disconnected_tract, discon = get_streamlines_disconnection(tract=tract,
|
|
83
|
+
lesion=lesion,
|
|
84
|
+
out_path=disconnected_tract_path)
|
|
85
|
+
|
|
86
|
+
disconnections.append({
|
|
87
|
+
"Tract": tract_name,
|
|
88
|
+
"Discon": np.round(discon, 5),
|
|
89
|
+
"Pathway": pathway_name,
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
disconnections_frame = pd.DataFrame(disconnections)
|
|
93
|
+
|
|
94
|
+
if disc_frame_out_path:
|
|
95
|
+
disc_frame_out_path.mkdir(parents=True, exist_ok=True)
|
|
96
|
+
out_csv = disc_frame_out_path / "percent_discon_tract.csv"
|
|
97
|
+
disconnections_frame.to_csv(out_csv, index=False)
|
|
98
|
+
|
|
99
|
+
return disconnections_frame
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_tracts_disconnections_map(
|
|
103
|
+
tracts,
|
|
104
|
+
tracts_disconnected,
|
|
105
|
+
out_path = None
|
|
106
|
+
):
|
|
107
|
+
"""Compute a voxel-wise tracts disconnection map.
|
|
108
|
+
|
|
109
|
+
The disconnection map is computed as the ratio of the tract density
|
|
110
|
+
of disconnected streamlines to the tract density of all streamlines.
|
|
111
|
+
Voxels with no streamlines are not considered.
|
|
112
|
+
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
tracts : StatefulTractogram
|
|
116
|
+
Tractogram containing all streamlines.
|
|
117
|
+
tracts_disconnected : StatefulTractogram
|
|
118
|
+
Tractogram containing the streamlines disconnected.
|
|
119
|
+
out_path : Path | None, optional
|
|
120
|
+
Directory where the TDI images and disconnection map are saved.
|
|
121
|
+
If None, no files are saved.
|
|
122
|
+
|
|
123
|
+
Returns
|
|
124
|
+
-------
|
|
125
|
+
tuple[nib.Nifti1Image, nib.Nifti1Image, nib.Nifti1Image]
|
|
126
|
+
A tuple containing:
|
|
127
|
+
|
|
128
|
+
- disconnections_map : nib.Nifti1Image
|
|
129
|
+
Voxel-wise ratio of disconnected to total streamline density.
|
|
130
|
+
- tdi_aggregated : nib.Nifti1Image
|
|
131
|
+
Track density image of all streamlines.
|
|
132
|
+
- tdi_disconnected : nib.Nifti1Image
|
|
133
|
+
Track density image of disconnected streamlines.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
if out_path:
|
|
138
|
+
out_path.mkdir(parents=True, exist_ok=True)
|
|
139
|
+
tdi_path = out_path / "tdi_mask.nii.gz"
|
|
140
|
+
tdi_disconnected_path = out_path / "tdi_disconnected_mask.nii.gz"
|
|
141
|
+
else:
|
|
142
|
+
tdi_path = None
|
|
143
|
+
tdi_disconnected_path = None
|
|
144
|
+
|
|
145
|
+
tdi_aggregated = get_tdi(tracts, out_path=tdi_path)
|
|
146
|
+
tdi_disconnected = get_tdi(tracts_disconnected, out_path=tdi_disconnected_path)
|
|
147
|
+
|
|
148
|
+
disconnections_map = np.divide(
|
|
149
|
+
tdi_disconnected.get_fdata(),
|
|
150
|
+
tdi_aggregated.get_fdata(),
|
|
151
|
+
out=np.zeros_like(tdi_aggregated.get_fdata(), dtype=float),
|
|
152
|
+
where=tdi_aggregated.get_fdata() != 0
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
disconnections_map = nib.Nifti1Image(disconnections_map, tdi_aggregated.affine, tdi_aggregated.header)
|
|
156
|
+
|
|
157
|
+
if out_path:
|
|
158
|
+
nib.nifti1.save(disconnections_map, (out_path / "disconnections_map.nii.gz"))
|
|
159
|
+
|
|
160
|
+
return disconnections_map, tdi_aggregated, tdi_disconnected
|