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.
@@ -0,0 +1,244 @@
1
+ import numpy as np
2
+ import nibabel as nib
3
+ import dipy.io.stateful_surface
4
+
5
+ from dipy.io.stateful_tractogram import StatefulTractogram
6
+ from dipy.io.utils import Space
7
+ from dipy.io.streamline import load_tractogram, save_tractogram
8
+ from dipy.tracking.utils import target, density_map
9
+
10
+ def get_tract(
11
+ tract_path,
12
+ reference_path = None
13
+ ):
14
+ """Load a tractogram with the possibility of assigning a new
15
+ NIfTI reference. The reference should be in the same world space
16
+ as the tractogram.
17
+
18
+ Parameters
19
+ ----------
20
+ tract_path : Path
21
+ Path to the tractogram file.
22
+ reference_path : Path | None, optional
23
+ Path to a NIfTI image to use as the tractogram reference.
24
+ If provided, the tractogram is reconstructed in RASMM space
25
+ using this reference.
26
+
27
+ Returns
28
+ -------
29
+ StatefulTractogram
30
+ Loaded tractogram, optionally associated with the provided
31
+ reference image.
32
+ """
33
+
34
+ tract = load_tractogram(filename=tract_path,
35
+ reference="same",
36
+ bbox_valid_check=False)
37
+ if reference_path:
38
+ reference = nib.nifti1.load(reference_path)
39
+ tract = StatefulTractogram(tract.streamlines,
40
+ reference=reference,
41
+ space=Space.RASMM)
42
+ return tract
43
+
44
+
45
+ def get_aggregated_tracts(
46
+ tracts_pathlist,
47
+ reference_path = None,
48
+ out_path = None
49
+ ):
50
+ """Aggregate multiple tractograms into a single tractogram.
51
+
52
+ Parameters
53
+ ----------
54
+ tracts_pathlist : list[Path]
55
+ Paths to the tractogram files to aggregate.
56
+ reference_path : Path | None, optional
57
+ Path to a NIfTI image to use as the reference for all tractograms.
58
+ If not provided, the first loaded tractogram is used as the reference.
59
+ out_path : Path | None, optional
60
+ Path where the aggregated tractogram should be saved. If None,
61
+ the tractogram is not saved.
62
+
63
+ Returns
64
+ -------
65
+ StatefulTractogram
66
+ Aggregated tractogram containing the streamlines from all input
67
+ tractograms, clipped to the reference image grid.
68
+ """
69
+
70
+ streamlines = []
71
+
72
+ if reference_path:
73
+ reference = nib.nifti1.load(reference_path)
74
+ else:
75
+ reference = None
76
+
77
+ for tract_path in tracts_pathlist:
78
+ tract = get_tract(tract_path, reference)
79
+ streamlines.extend(tract.streamlines)
80
+
81
+ if reference is None:
82
+ reference = tract
83
+ aggregated_tracts = StatefulTractogram(streamlines,
84
+ reference,
85
+ space=Space.RASMM)
86
+
87
+ aggregated_tracts = clip_tractogram_to_grid(aggregated_tracts)
88
+
89
+ if out_path:
90
+ save_tractogram(aggregated_tracts,
91
+ out_path,
92
+ bbox_valid_check=False,)
93
+
94
+ return aggregated_tracts
95
+
96
+ def clip_streamline_to_grid(
97
+ streamline,
98
+ inv_affine,
99
+ shape
100
+ ):
101
+ """Clip a streamline to the voxel grid of a reference image.
102
+
103
+ Parameters
104
+ ----------
105
+ streamline : ndarray
106
+ Streamline coordinates in world space.
107
+ inv_affine : ndarray
108
+ Inverse affine transformation from world space to voxel space.
109
+ shape : tuple
110
+ Shape of the reference image grid.
111
+
112
+ Returns
113
+ -------
114
+ ndarray
115
+ Streamline containing only the points that fall within the voxel grid.
116
+ """
117
+
118
+ voxel = np.column_stack((
119
+ streamline,
120
+ np.ones(len(streamline))
121
+ )) @ inv_affine.T
122
+
123
+ voxel = voxel[:, :3]
124
+
125
+ valid = np.all(
126
+ (voxel >= 0) &
127
+ (voxel < shape),
128
+ axis=1
129
+ )
130
+
131
+ return streamline[valid]
132
+
133
+
134
+ def clip_tractogram_to_grid(tract):
135
+ """Clip a tractogram to the voxel grid of its reference space.
136
+
137
+ Parameters
138
+ ----------
139
+ tract : StatefulTractogram
140
+ Tractogram to clip. Its streamlines are expected to be in the
141
+ same world space as its reference image.
142
+
143
+ Returns
144
+ -------
145
+ StatefulTractogram
146
+ Tractogram containing only streamline points within the reference
147
+ voxel grid. Streamlines with fewer than two remaining points are
148
+ removed.
149
+ """
150
+
151
+ inv_affine = np.linalg.inv(tract.affine)
152
+ shape = np.asarray(tract.space_attributes[1])
153
+
154
+ clipped_streamlines = [
155
+ clip_streamline_to_grid(
156
+ streamline,
157
+ inv_affine,
158
+ shape
159
+ )
160
+ for streamline in tract.streamlines
161
+ ]
162
+
163
+ clipped_streamlines = [
164
+ streamline
165
+ for streamline in clipped_streamlines
166
+ if len(streamline) >= 2
167
+ ]
168
+
169
+ return StatefulTractogram.from_sft(clipped_streamlines, tract)
170
+
171
+ def get_streamlines_disconnection(
172
+ tract,
173
+ lesion,
174
+ out_path=None
175
+ ):
176
+ """Return the streamlines intersecting a lesion and compute the proportion
177
+ of streamlines intersecting this lesion.
178
+
179
+ Parameters
180
+ ----------
181
+ tract : dipy.io.stateful_tractogram.StatefulTractogram
182
+ Tractogram from which to identify intersecting streamlines.
183
+ lesion : nib.Nifti1Image
184
+ NIfTI lesion. Converted to binary mask where voxels with values
185
+ greater than zero are considered part of the lesion.
186
+ out_path : Path | None, optional
187
+ Path where the disconnected streamlines should be saved. If None,
188
+ the tractogram is not saved.
189
+
190
+ Returns
191
+ -------
192
+ dipy.io.stateful_tractogram.StatefulTractogram
193
+ Tractogram of the intersecting streamlines.
194
+ float
195
+ Proportion of streamlines that intersect the lesion, between 0 and 1.
196
+ """
197
+
198
+ lesion_mask = lesion.get_fdata() > 0
199
+
200
+ disconnected_streamlines = list(
201
+ target(tract.streamlines, lesion.affine, lesion_mask)
202
+ )
203
+
204
+ disconnected_tract = StatefulTractogram.from_sft(
205
+ disconnected_streamlines,
206
+ tract
207
+ )
208
+
209
+ if out_path:
210
+ save_tractogram(
211
+ disconnected_tract,
212
+ out_path,
213
+ bbox_valid_check=False
214
+ )
215
+
216
+ return disconnected_tract, len(disconnected_streamlines) / len(tract.streamlines)
217
+
218
+ def get_tdi(
219
+ tract,
220
+ out_path = None
221
+ ):
222
+ """Compute a track density image (TDI) from a tractogram.
223
+
224
+ Parameters
225
+ ----------
226
+ tract : StatefulTractogram
227
+ Tractogram from which to compute the track density image.
228
+ out_path : Path | None, optional
229
+ Path where the TDI NIfTI image should be saved. If None, the image
230
+ is not saved.
231
+
232
+ Returns
233
+ -------
234
+ nib.Nifti1Image
235
+ NIfTI image containing the number of streamlines passing through
236
+ each voxel.
237
+ """
238
+
239
+ tdi = density_map(tract.streamlines, tract.affine, tract.space_attributes[1])
240
+ tdi = nib.Nifti1Image(tdi.astype("uint32"), tract.affine)
241
+ if out_path:
242
+ nib.nifti1.save(tdi, out_path)
243
+
244
+ return tdi
pyLQT_cli.py ADDED
@@ -0,0 +1,180 @@
1
+ from lesion_metrics.white_matter.metrics import get_tracts_disconnections_severity, get_tracts_disconnections_map
2
+ from lesion_metrics.white_matter.utils import get_streamlines_disconnection, get_aggregated_tracts, clip_tractogram_to_grid
3
+ from lesion_metrics.network.metrics import get_sspl_increase, get_parcel_wise_disconnection_severity
4
+ from lesion_metrics.gray_matter.metrics import get_parcel_damage
5
+ from lesion_metrics.utils.configs import atlas_cfg_read, tracts_cfg_read, lesion_cfg_read
6
+ from lesion_metrics.utils.downloads import default_download, create_tract_list, uncompress_trk_gz, get_ref_space_tf
7
+ from lesion_metrics.utils.registration import register_affine, register_syn
8
+ from pathlib import Path
9
+
10
+ import nibabel as nib
11
+ import shutil
12
+ import tempfile
13
+ import argparse
14
+
15
+
16
+ def run_pyLQT():
17
+ """Run the pyLQT lesion quantification pipeline.
18
+
19
+ Parses the command-line arguments, loads the lesion, atlas, and
20
+ tractography configurations, performs the required registrations,
21
+ and computes the different lesion quantification measures.
22
+ """
23
+
24
+ parser = argparse.ArgumentParser()
25
+
26
+ parser.add_argument(
27
+ "--lesion_cfg",
28
+ required=True,
29
+ help='[Path] Path to lesion config'
30
+ )
31
+
32
+ parser.add_argument(
33
+ "--atlas_cfg",
34
+ required=True,
35
+ help='[Path] Path to atlas config'
36
+ )
37
+
38
+ parser.add_argument(
39
+ "--tracts_cfg",
40
+ default=None,
41
+ help='[Path] Path to tracts config'
42
+ )
43
+
44
+ parser.add_argument(
45
+ "--out_folder",
46
+ required=True,
47
+ help='[Path] Output folder path'
48
+ )
49
+
50
+ parser.add_argument(
51
+ "--registration_type",
52
+ choices=['affine', 'syn'],
53
+ default='syn',
54
+ help='[str, default=syn] Registration type, from lesion space and atlas space to tract space'
55
+ )
56
+
57
+ parser.add_argument(
58
+ "--spared_threshold",
59
+ default=0.5,
60
+ help='[float (0,1), default=0.5] Defines threshold for spared parcel-to-parcel connectivity'
61
+ )
62
+
63
+ parser.add_argument(
64
+ "--delete_tmp",
65
+ default=False,
66
+ help='[bool, default=False] Delete temporary folder'
67
+ )
68
+
69
+ args = parser.parse_args()
70
+
71
+ out_folder = Path(args.out_folder)
72
+ out_folder.mkdir(parents=True, exist_ok=True)
73
+ tmp_folder = Path(tempfile.mkdtemp(prefix="LQT_tmp_", dir=out_folder))
74
+
75
+ lesion_cfg_path = Path(args.lesion_cfg)
76
+ atlas_cfg_path = Path(args.atlas_cfg)
77
+ if not args.tracts_cfg:
78
+ tracts_cfg_path = default_download(out_folder)
79
+ else:
80
+ tracts_cfg_path = Path(args.tracts_cfg)
81
+
82
+ lesion_cohort_name, lesion_folder, lesion_space = lesion_cfg_read(lesion_cfg_path)
83
+ atlas_name, atlas, atlas_labels, atlas_space = atlas_cfg_read(atlas_cfg_path)
84
+ tracts_name, tracts_folder, references_folder, tracts_space = tracts_cfg_read(tracts_cfg_path)
85
+
86
+ if lesion_space.endswith(".nii.gz") or lesion_space.endswith(".nii"):
87
+ lesion_ref_space = nib.nifti1.load(lesion_space)
88
+ else: lesion_ref_space = get_ref_space_tf(lesion_space)
89
+ if atlas_space.endswith(".nii.gz") or atlas_space.endswith(".nii"):
90
+ atlas_ref_space = nib.nifti1.load(atlas_space)
91
+ else: atlas_ref_space = get_ref_space_tf(atlas_space)
92
+ if tracts_space.endswith(".nii.gz") or tracts_space.endswith(".nii"):
93
+ tracts_ref_space = nib.nifti1.load(tracts_space)
94
+ else: tracts_ref_space = get_ref_space_tf(tracts_space)
95
+
96
+ if args.registration_type == "affine":
97
+ registrator = register_affine
98
+ else:
99
+ registrator = register_syn
100
+ lesion_tracts_mapping = registrator(lesion_ref_space, tracts_ref_space)
101
+ atlas_tracts_mapping = registrator(atlas_ref_space, tracts_ref_space)
102
+
103
+ atlas_trans_volume = atlas_tracts_mapping.transform(atlas.get_fdata(), interpolation='nearest')
104
+ atlas_trans = nib.Nifti1Image(atlas_trans_volume, tracts_ref_space.affine, tracts_ref_space.header)
105
+ nib.nifti1.save(atlas_trans, tmp_folder / "atlas_trans.nii.gz")
106
+
107
+ spared_threshold = float(args.spared_threshold)
108
+ tracts_pathlist_gz, references_pathlist = create_tract_list(references_folder=references_folder,
109
+ tracts_folder=tracts_folder)
110
+
111
+ tracts_pathlist = []
112
+ for tract_path in tracts_pathlist_gz:
113
+ relative_path = tract_path.relative_to(tracts_folder)
114
+ out_path = tmp_folder / relative_path.with_suffix("")
115
+
116
+ out_path.parent.mkdir(parents=True, exist_ok=True)
117
+
118
+ tracts_pathlist.append(
119
+ uncompress_trk_gz(tract_path, out_path)
120
+ )
121
+
122
+ aggregated_tracts_path = tmp_folder / "aggregated_tracts.trk"
123
+ aggregated_tracts = get_aggregated_tracts(tracts_pathlist=tracts_pathlist,
124
+ out_path=aggregated_tracts_path)
125
+
126
+ for lesion_path in lesion_folder.glob("*.nii*"):
127
+
128
+ lesion_name = lesion_path.name.removesuffix(".nii.gz").removesuffix(".nii")
129
+ out_dir = out_folder / lesion_name
130
+ out_dir.mkdir(parents=True, exist_ok=True)
131
+ tmp_dir = tmp_folder / lesion_name
132
+ tmp_dir.mkdir(parents=True, exist_ok=True)
133
+
134
+ lesion = nib.nifti1.load(lesion_path)
135
+ lesion_trans_volume = lesion_tracts_mapping.transform(lesion.get_fdata(), interpolation='linear')
136
+ lesion_trans = nib.Nifti1Image(lesion_trans_volume, tracts_ref_space.affine, tracts_ref_space.header)
137
+ nib.nifti1.save(lesion_trans, tmp_dir / "lesion_trans.nii.gz")
138
+
139
+ aggregated_tracts_disconnected_path = tmp_dir / f"aggregated_disconnected.trk"
140
+ aggregated_tracts_disconnected, _ = get_streamlines_disconnection(tract=aggregated_tracts,
141
+ lesion=lesion_trans,
142
+ out_path=aggregated_tracts_disconnected_path)
143
+ aggregated_tracts_disconnected = clip_tractogram_to_grid(aggregated_tracts_disconnected)
144
+
145
+ get_parcel_damage(lesion=lesion_trans,
146
+ atlas=atlas_trans,
147
+ atlas_labels=atlas_labels,
148
+ parcel_column="Parcel",
149
+ out_path=out_dir / "parcel_damage")
150
+
151
+ get_tracts_disconnections_severity(tracts_path=tracts_pathlist,
152
+ references_path=references_pathlist,
153
+ lesion=lesion_trans,
154
+ disc_tracts_out_path=tmp_dir / "wm_disconnections_severity",
155
+ disc_frame_out_path=out_dir / "wm_disconnections_severity")
156
+
157
+ get_tracts_disconnections_map(tracts=aggregated_tracts,
158
+ tracts_disconnected=aggregated_tracts_disconnected,
159
+ out_path=out_dir / "wm_disconnections_map")
160
+
161
+ structural_connectivity, _, _, per_spared_connections = (
162
+ get_parcel_wise_disconnection_severity(
163
+ aggregated_tracts=aggregated_tracts,
164
+ aggregated_tracts_disconnected=aggregated_tracts_disconnected,
165
+ atlas=atlas_trans,
166
+ out_path=out_dir / "parcel_wise_disconnection_severity",
167
+ )
168
+ )
169
+
170
+ get_sspl_increase(structural_connectivity=structural_connectivity,
171
+ per_spared_connections=per_spared_connections,
172
+ out_path=out_dir / "sspl_increase",
173
+ spared_threshold=spared_threshold)
174
+
175
+ if args.delete_tmp == True:
176
+ shutil.rmtree(tmp_folder)
177
+
178
+
179
+ if __name__ == "__main__":
180
+ run_pyLQT()
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyLQT
3
+ Version: 1.0.dev1
4
+ Summary: Python version of LQT from Griffis et al. 2021 (https://www.sciencedirect.com/science/article/pii/S2213158221000838)
5
+ Author-email: Antonin HUDRY <hudryantoninm@gmail.com>
6
+ License-Expression: GPL-3.0-only
7
+ Keywords: LQT,lqt,lesion,brain,
8
+ Classifier: Development Status :: 2 - Pre-Alpha
9
+ Classifier: Intended Audience :: Science/Research
10
+ Classifier: Intended Audience :: Healthcare Industry
11
+ Classifier: Programming Language :: Python
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: numpy<2.0
16
+ Requires-Dist: nibabel>5.0
17
+ Requires-Dist: pandas>3.0
18
+ Requires-Dist: scipy>1.15
19
+ Requires-Dist: requests>2.0
20
+ Requires-Dist: dipy>1.10
21
+ Requires-Dist: templateflow
22
+ Dynamic: license-file
23
+
24
+ # pyLQT
25
+ Python library to revamp the work on LQT, since it is no longer actively maintained.
26
+
27
+ # install
28
+
29
+ Make sure to update your python environment with the `requirements.txt`: `pip install -r requirements.txt`
30
+
31
+ # Running pyLQT
32
+
33
+ The main pyLQT pipeline can be run using the `full_pipeline.py` file.
34
+
35
+ ### Usage
36
+
37
+ ```bash
38
+ python full_pipeline.py \
39
+ --lesion_cfg <path/to/lesion_config> \
40
+ --atlas_cfg <path/to/atlas_config> \
41
+ --tracts_cfg <path/to/tracts_config> \
42
+ --out_folder <path/to/output> \
43
+ --spared_threshold
44
+ ```
45
+
46
+ | Argument | Required | Default | Description |
47
+ | -------------------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------- |
48
+ | `--lesion_cfg` | Yes | — | Path to the lesion configuration file (`.json`). |
49
+ | `--atlas_cfg` | Yes | — | Path to the atlas configuration file (`.json`). |
50
+ | `--tracts_cfg` | No | `None` | Path to the tracts configuration file (`.json`). |
51
+ | `--out_folder` | Yes | — | Path to the output folder. |
52
+ | `--spared_threshold` | No | `0.5` | Threshold between 0 and 1 used to define spared parcel-to-parcel connectivity. |
53
+ | `--delete_tmp` | No | `True` | Whether to delete the temporary files generated during processing. |
54
+
55
+ ### Configuration files
56
+
57
+ The CLI version of pyLQT relies on 3 configuration files, one for the lesions, one for the atlas and one for the tracts:
58
+
59
+ - Lesions: "cohort_name": name of the cohort of lesion used mainly for the naming of subsequent files; "lesion_folder": relative folder (should be next the config file) containing all the lesion niftis. The folder is scanned integrally so that all niftis in the folder and subfolders are processed; "space": world space in which the lesion are registered, can be a nifti directly given as relative path (with extensions) or a standard space name that will be fetched via templateflow (i.e. MNI152NLin2009cAsym).
60
+ - Atlas: "name": name of the atlas used mainly for naming subsequent files; "atlas": relative path to nifti file corresponding to atlas; "atlas_labels": relative path to `.csv` file containing the labels of the atlas. Format should contain columns "Parcel" and "Label", corresponding to the parcel number inside the nifti and the name of the parcel respectively; "space": world space in which the lesion are registered, can be a nifti directly given as relative path (with extensions) or a standard space name that will be fetched via templateflow (i.e. MNI152NLin2009cAsym).
61
+
62
+ Note: niftis can be given as raw or gzipped niftis (`.nii` or `.nii.gz`)
@@ -0,0 +1,21 @@
1
+ pyLQT_cli.py,sha256=FlTLPGcoq-xwPeGwVUfe_W_dg8yMpYV1X1jPBf-EefA,7937
2
+ lesion_metrics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ lesion_metrics/gray_matter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ lesion_metrics/gray_matter/metrics.py,sha256=RzUS9glbin9Xn5ZldB7fGg2XiPS1P21e_7l02ZErk-I,2617
5
+ lesion_metrics/gray_matter/utils.py,sha256=czxyAESP0neVCue424L0hon-5Tdbsl0pN0zJHWG5HiQ,840
6
+ lesion_metrics/network/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ lesion_metrics/network/metrics.py,sha256=Ka3G8_dIq-8H3b8mvRR96QM7p_gR2yFLKUxQOLYMWM4,8281
8
+ lesion_metrics/network/utils.py,sha256=fcakfKSMortNIE3TfJP8zwanzBiT1McsYnHcTgwbxE4,3283
9
+ lesion_metrics/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ lesion_metrics/utils/configs.py,sha256=FFur04Sh48IlxUTmz4kRkyLKWffi7bvzyw8B2GclpRU,4961
11
+ lesion_metrics/utils/downloads.py,sha256=yu-rcuIDcgsDa7gxoLGyIewsk2DF2WGgCYe5bCOso0M,8031
12
+ lesion_metrics/utils/registration.py,sha256=rB4vEMBrlshofOxaKrSqyE3Wvgq2_o3y5T3dCS_9sM0,3174
13
+ lesion_metrics/white_matter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ lesion_metrics/white_matter/metrics.py,sha256=sujuHry19XpVWFNo52Ntq7h3JkWEcgsZ1-rNFr7E940,6155
15
+ lesion_metrics/white_matter/utils.py,sha256=oo492yzxsPACMvHEyYhoYVqgVRrv8mpqP7FOf5OU_Ms,7087
16
+ pylqt-1.0.dev1.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
17
+ pylqt-1.0.dev1.dist-info/METADATA,sha256=gMSuT2w08jR1QSTH_C9TRsLVsJBEnCh98jSDH4_eCGs,3990
18
+ pylqt-1.0.dev1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
19
+ pylqt-1.0.dev1.dist-info/entry_points.txt,sha256=1mhG9fCXeDnL9VEBbsIN34GU90953e85325pG_wo6l8,46
20
+ pylqt-1.0.dev1.dist-info/top_level.txt,sha256=KT_xuwXScSnV5QoZAXnCzeTOq23xQ_V9ESEh4jrfxBM,25
21
+ pylqt-1.0.dev1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pyLQT = pyLQT_cli:run_pyLQT