pyLQT 1.0.dev1__py3-none-any.whl → 1.0.dev2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. pyLQT/__init__.py +3 -0
  2. pyLQT/cli.py +71 -0
  3. {lesion_metrics → pyLQT/lesion_metrics}/gray_matter/metrics.py +1 -1
  4. {lesion_metrics → pyLQT/lesion_metrics}/utils/downloads.py +82 -9
  5. {lesion_metrics → pyLQT/lesion_metrics}/utils/registration.py +13 -9
  6. pyLQT/pipeline.py +183 -0
  7. pylqt-1.0.dev2.dist-info/METADATA +136 -0
  8. pylqt-1.0.dev2.dist-info/RECORD +23 -0
  9. {pylqt-1.0.dev1.dist-info → pylqt-1.0.dev2.dist-info}/WHEEL +1 -1
  10. pylqt-1.0.dev2.dist-info/entry_points.txt +2 -0
  11. pylqt-1.0.dev2.dist-info/top_level.txt +1 -0
  12. pyLQT_cli.py +0 -180
  13. pylqt-1.0.dev1.dist-info/METADATA +0 -62
  14. pylqt-1.0.dev1.dist-info/RECORD +0 -21
  15. pylqt-1.0.dev1.dist-info/entry_points.txt +0 -2
  16. pylqt-1.0.dev1.dist-info/top_level.txt +0 -2
  17. {lesion_metrics → pyLQT/lesion_metrics}/__init__.py +0 -0
  18. {lesion_metrics → pyLQT/lesion_metrics}/gray_matter/__init__.py +0 -0
  19. {lesion_metrics → pyLQT/lesion_metrics}/gray_matter/utils.py +0 -0
  20. {lesion_metrics → pyLQT/lesion_metrics}/network/__init__.py +0 -0
  21. {lesion_metrics → pyLQT/lesion_metrics}/network/metrics.py +0 -0
  22. {lesion_metrics → pyLQT/lesion_metrics}/network/utils.py +0 -0
  23. {lesion_metrics → pyLQT/lesion_metrics}/utils/__init__.py +0 -0
  24. {lesion_metrics → pyLQT/lesion_metrics}/utils/configs.py +0 -0
  25. {lesion_metrics → pyLQT/lesion_metrics}/white_matter/__init__.py +0 -0
  26. {lesion_metrics → pyLQT/lesion_metrics}/white_matter/metrics.py +0 -0
  27. {lesion_metrics → pyLQT/lesion_metrics}/white_matter/utils.py +0 -0
  28. {pylqt-1.0.dev1.dist-info → pylqt-1.0.dev2.dist-info}/licenses/LICENSE +0 -0
pyLQT/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .pipeline import run_pyLQT
2
+
3
+ __all__ = ["run_pyLQT"]
pyLQT/cli.py ADDED
@@ -0,0 +1,71 @@
1
+ import argparse
2
+
3
+ from pipeline import run_pyLQT
4
+
5
+
6
+ def main():
7
+ """Run the pyLQT pipeline through the command-line interface."""
8
+
9
+ parser = argparse.ArgumentParser(
10
+ prog="pyLQT",
11
+ description="Run the pyLQT lesion quantification pipeline."
12
+ )
13
+
14
+ parser.add_argument(
15
+ "--lesion_cfg",
16
+ required=True,
17
+ help='[Path] Path to lesion config'
18
+ )
19
+
20
+ parser.add_argument(
21
+ "--atlas_cfg",
22
+ default=None,
23
+ help='[Path, optional] Path to atlas config. If none provided, a default atlas is downloaded.'
24
+ )
25
+
26
+ parser.add_argument(
27
+ "--tracts_cfg",
28
+ default=None,
29
+ help='[Path, optional] Path to tracts config. If none provided, default tracts are downloaded.'
30
+ )
31
+
32
+ parser.add_argument(
33
+ "--out_folder",
34
+ required=True,
35
+ help='[Path] Output folder path'
36
+ )
37
+
38
+ parser.add_argument(
39
+ "--registration_type",
40
+ choices=['affine', 'syn'],
41
+ default='syn',
42
+ help='[str, default=syn] Registration type, from lesion space and atlas space to tract space'
43
+ )
44
+
45
+ parser.add_argument(
46
+ "--spared_threshold",
47
+ default=0.5,
48
+ help='[float (0,1), default=0.5] Defines threshold for spared parcel-to-parcel connectivity'
49
+ )
50
+
51
+ parser.add_argument(
52
+ "--delete_tmp",
53
+ default=False,
54
+ help='[bool, default=False] Delete temporary folder'
55
+ )
56
+
57
+ args = parser.parse_args()
58
+
59
+ run_pyLQT(
60
+ lesion_cfg=args.lesion_cfg,
61
+ atlas_cfg=args.atlas_cfg,
62
+ tracts_cfg=args.tracts_cfg,
63
+ out_folder=args.out_folder,
64
+ registration_type=args.registration_type,
65
+ spared_threshold=args.spared_threshold,
66
+ delete_tmp=args.delete_tmp,
67
+ )
68
+
69
+
70
+ if __name__ == "__main__":
71
+ main()
@@ -14,7 +14,7 @@ def get_parcel_damage(
14
14
  ):
15
15
  """Compute lesion damage for each parcel of an atlas.
16
16
 
17
- The lesion is resampled to the atlas space, and the proportion of
17
+ The lesion is resampled to the atlas grid, and the proportion of
18
18
  each parcel overlapping with the lesion is computed. A parcel damage
19
19
  map is created, where each parcel voxel is assigned the damage proportion
20
20
  of its parcel.
@@ -6,6 +6,7 @@ import shutil
6
6
  import gzip
7
7
  import nibabel as nib
8
8
  import numpy as np
9
+ import pandas as pd
9
10
 
10
11
  from templateflow import api as tf
11
12
  from pathlib import Path
@@ -135,7 +136,72 @@ def uncompress_trk_gz(
135
136
 
136
137
  return out_path
137
138
 
138
- def default_download(out_folder: Path):
139
+ def default_atlas_download(out_folder: Path):
140
+ atlas_cfg_folder = out_folder / "glasser360_config"
141
+ atlas_cfg_folder.mkdir(parents=True, exist_ok=True)
142
+
143
+ atlas_path = atlas_cfg_folder / "glasser360_MNI152NLin2009cAsym.nii.gz"
144
+ atlas_labels_path = atlas_cfg_folder / "glasser360_labels.csv"
145
+ atlas_labels_path_txt = atlas_labels_path.with_suffix(".txt")
146
+ atlas_cfg_path = atlas_cfg_folder / "glasser360_config.json"
147
+
148
+ if atlas_path.exists():
149
+ warnings.warn(
150
+ f"atlas already exists, using it: {atlas_path}"
151
+ )
152
+ else:
153
+ url_atlas = "https://ndownloader.figshare.com/files/43043653"
154
+
155
+ response = requests.get(url_atlas, stream=True, timeout=60)
156
+ response.raise_for_status()
157
+
158
+ with atlas_path.open("wb") as f:
159
+ for chunk in response.iter_content(chunk_size=1024*1024):
160
+ if chunk:
161
+ f.write(chunk)
162
+
163
+ if atlas_labels_path.exists():
164
+ warnings.warn(
165
+ f"atlas labels already exist, using it: {atlas_labels_path}"
166
+ )
167
+ else:
168
+ url_labels = "https://ndownloader.figshare.com/files/43041079"
169
+
170
+ response = requests.get(url_labels, stream=True, timeout=60)
171
+ response.raise_for_status()
172
+
173
+ with atlas_labels_path_txt.open("wb") as f:
174
+ for chunk in response.iter_content(chunk_size=1024*1024):
175
+ if chunk:
176
+ f.write(chunk)
177
+
178
+ with open(atlas_labels_path_txt, "r") as f:
179
+ labels = [line.strip() for line in f if line.strip()]
180
+
181
+ atlas_labels = pd.DataFrame({
182
+ "Parcel": range(1, len(labels) + 1),
183
+ "Label": labels
184
+ })
185
+
186
+ atlas_labels.to_csv(atlas_labels_path, index=False)
187
+ atlas_labels_path_txt.unlink()
188
+
189
+ config = {
190
+ "name": "glasser360",
191
+ "atlas": "glasser360_MNI152NLin2009cAsym.nii.gz",
192
+ "atlas_labels": "glasser360_labels.csv",
193
+ "space": "MNI152NLin2009cAsym"
194
+ }
195
+
196
+ atlas_cfg = Path(atlas_cfg_folder / "glasser360_config.json")
197
+
198
+ with open(atlas_cfg, "w") as f:
199
+ json.dump(config, f, indent=4)
200
+
201
+ return atlas_cfg
202
+
203
+
204
+ def default_tracts_download(out_folder: Path):
139
205
  """Download the default HCP1065 tractography dataset and create its configuration.
140
206
 
141
207
  The function downloads the HCP1065 average tractograms and their
@@ -162,7 +228,9 @@ def default_download(out_folder: Path):
162
228
 
163
229
  if tracts_folder.exists():
164
230
  warnings.warn(
165
- f"Tracts folder already exists, using it: {tracts_folder}"
231
+ f"Tracts folder already exists, using it: {tracts_folder}",
232
+ UserWarning,
233
+ stacklevel=2
166
234
  )
167
235
  else:
168
236
  url = "https://github.com/data-others/atlas/releases/download/hcp1065/hcp1065_avg_tracts_trk.zip"
@@ -185,7 +253,9 @@ def default_download(out_folder: Path):
185
253
 
186
254
  if references_folder.exists():
187
255
  warnings.warn(
188
- f"References folder already exists, using it: {references_folder}"
256
+ f"References folder already exists, using it: {references_folder}",
257
+ UserWarning,
258
+ stacklevel=2
189
259
  )
190
260
  else:
191
261
  url = "https://github.com/data-others/atlas/releases/download/hcp1065/hcp1065_avg_tracts_nifti.zip"
@@ -200,14 +270,13 @@ def default_download(out_folder: Path):
200
270
  f.write(chunk)
201
271
 
202
272
  with zipfile.ZipFile(zip_path) as zip_file:
203
- zip_file.extractall(zip_path.parent)
273
+ zip_file.extractall(tracts_cfg_folder)
204
274
 
205
275
  zip_path.unlink()
206
276
 
207
- references_folder = zip_path.parent / "nifti"
208
- references_folder.rename(
209
- zip_path.parent / "hcp1065_avg_tracts"
210
- )
277
+ extracted_niftis = tracts_cfg_folder / "nifti"
278
+ if extracted_niftis.exists():
279
+ extracted_niftis.rename(references_folder)
211
280
 
212
281
  tracts_name = "HCP1065"
213
282
  tracts_space = "MNI152NLin2009aAsym"
@@ -247,4 +316,8 @@ def get_ref_space_tf(space: str, res: int = 1):
247
316
  ref_space_volume[brain_mask.get_fdata() <= 0] = 0
248
317
  ref_space = nib.Nifti1Image(ref_space_volume, t1w_ref.affine, t1w_ref.header)
249
318
 
250
- return ref_space
319
+ return ref_space
320
+
321
+ if __name__=="__main__":
322
+
323
+ default_atlas_download(Path("D:/internship_myspace_lab/pyLQT/test_download_atlas"))
@@ -26,14 +26,18 @@ def register_affine(
26
26
  AffineMap
27
27
  Affine mapping from the moving image space to the static image space.
28
28
  """
29
- metric = MutualInformationMetric()
29
+
30
+ moving_data = moving.get_fdata()
31
+ static_data = static.get_fdata()
32
+
33
+ metric = MutualInformationMetric(nbins=32, sampling_proportion=0.2)
30
34
 
31
- affreg = AffineRegistration(metric=metric)
35
+ affreg = AffineRegistration(metric=metric, level_iters=[500, 250, 100])
32
36
 
33
37
  transform = TranslationTransform3D()
34
38
  translation = affreg.optimize(
35
- static.get_fdata(),
36
- moving.get_fdata(),
39
+ static_data,
40
+ moving_data,
37
41
  transform,
38
42
  params0=None,
39
43
  static_grid2world=static.affine,
@@ -42,8 +46,8 @@ def register_affine(
42
46
 
43
47
  transform = RigidTransform3D()
44
48
  rigid = affreg.optimize(
45
- static.get_fdata(),
46
- moving.get_fdata(),
49
+ static_data,
50
+ moving_data,
47
51
  transform,
48
52
  starting_affine=translation.affine,
49
53
  params0=None,
@@ -53,8 +57,8 @@ def register_affine(
53
57
 
54
58
  transform = AffineTransform3D()
55
59
  mapping = affreg.optimize(
56
- static.get_fdata(),
57
- moving.get_fdata(),
60
+ static_data,
61
+ moving_data,
58
62
  transform,
59
63
  starting_affine=rigid.affine,
60
64
  params0=None,
@@ -93,7 +97,7 @@ def register_syn(
93
97
  """
94
98
  metric = CCMetric(dim=3)
95
99
 
96
- syn = SymmetricDiffeomorphicRegistration(metric)
100
+ syn = SymmetricDiffeomorphicRegistration(metric, level_iters=[20, 10, 5])
97
101
 
98
102
  mapping = syn.optimize(static.get_fdata(),
99
103
  moving.get_fdata(),
pyLQT/pipeline.py ADDED
@@ -0,0 +1,183 @@
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_tracts_download, default_atlas_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
+
14
+ def run_pyLQT(
15
+ lesion_cfg,
16
+ out_folder,
17
+ atlas_cfg=None,
18
+ tracts_cfg=None,
19
+ registration_type="syn",
20
+ spared_threshold=0.5,
21
+ delete_tmp=False,
22
+ ):
23
+ """Run the pyLQT lesion quantification pipeline.
24
+
25
+ Parameters
26
+ ----------
27
+ lesion_cfg : str or pathlib.Path
28
+ Path to the lesion configuration file.
29
+ out_folder : str or pathlib.Path
30
+ Output directory.
31
+ atlas_cfg : str or pathlib.Path, optional
32
+ Path to the atlas configuration file. If None, the default atlas
33
+ is downloaded.
34
+ tracts_cfg : str or pathlib.Path, optional
35
+ Path to the tractography configuration file. If None, the default
36
+ tractography dataset is downloaded.
37
+ registration_type : {"affine", "syn"}, default="syn"
38
+ Registration method used to map the lesion and atlas to tract space.
39
+ spared_threshold : float, default=0.5
40
+ Threshold in (0, 1) for spared parcel-to-parcel connectivity.
41
+ delete_tmp : bool, default=False
42
+ Whether to delete the temporary directory after processing.
43
+ """
44
+
45
+ if registration_type not in ("affine", "syn"):
46
+ raise ValueError("registration_type must be 'affine' or 'syn'")
47
+
48
+ spared_threshold = float(spared_threshold)
49
+ if not 0 < spared_threshold < 1:
50
+ raise ValueError("spared_threshold must be between 0 and 1")
51
+
52
+ out_folder = Path(out_folder)
53
+ out_folder.mkdir(parents=True, exist_ok=True)
54
+ tmp_folder = Path(tempfile.mkdtemp(prefix="LQT_tmp_", dir=out_folder))
55
+
56
+ lesion_cfg_path = Path(lesion_cfg)
57
+ atlas_cfg_path = (
58
+ default_atlas_download(out_folder)
59
+ if atlas_cfg is None
60
+ else Path(atlas_cfg)
61
+ )
62
+ tracts_cfg_path = (
63
+ default_tracts_download(out_folder)
64
+ if tracts_cfg is None
65
+ else Path(tracts_cfg)
66
+ )
67
+
68
+ lesion_cohort_name, lesion_folder, lesion_space = lesion_cfg_read(lesion_cfg_path)
69
+ atlas_name, atlas, atlas_labels, atlas_space = atlas_cfg_read(atlas_cfg_path)
70
+ tracts_name, tracts_folder, references_folder, tracts_space = (tracts_cfg_read(tracts_cfg_path))
71
+
72
+ if lesion_space.endswith((".nii.gz", ".nii")):
73
+ lesion_ref_space = nib.load(lesion_space)
74
+ else:
75
+ lesion_ref_space = get_ref_space_tf(lesion_space)
76
+
77
+ if atlas_space.endswith((".nii.gz", ".nii")):
78
+ atlas_ref_space = nib.load(atlas_space)
79
+ else:
80
+ atlas_ref_space = get_ref_space_tf(atlas_space)
81
+
82
+ if tracts_space.endswith((".nii.gz", ".nii")):
83
+ tracts_ref_space = nib.load(tracts_space)
84
+ else:
85
+ tracts_ref_space = get_ref_space_tf(tracts_space)
86
+
87
+ registrator = (register_affine if registration_type == "affine" else register_syn)
88
+
89
+ lesion_tracts_mapping = registrator(lesion_ref_space, tracts_ref_space)
90
+ atlas_tracts_mapping = registrator(atlas_ref_space, tracts_ref_space)
91
+
92
+ atlas_trans_volume = atlas_tracts_mapping.transform(atlas.get_fdata(), interpolation="nearest")
93
+ atlas_trans = nib.Nifti1Image(
94
+ atlas_trans_volume,
95
+ tracts_ref_space.affine,
96
+ tracts_ref_space.header,
97
+ )
98
+ nib.save(atlas_trans, tmp_folder / "atlas_trans.nii.gz")
99
+
100
+ tracts_pathlist_gz, references_pathlist = create_tract_list(
101
+ references_folder=references_folder,
102
+ tracts_folder=tracts_folder,
103
+ )
104
+
105
+ tracts_pathlist = []
106
+ for tract_path in tracts_pathlist_gz:
107
+ relative_path = tract_path.relative_to(tracts_folder)
108
+ out_path = tmp_folder / relative_path.with_suffix("")
109
+ out_path.parent.mkdir(parents=True, exist_ok=True)
110
+
111
+ tracts_pathlist.append(uncompress_trk_gz(tract_path, out_path))
112
+
113
+ aggregated_tracts_path = tmp_folder / "aggregated_tracts.trk"
114
+ aggregated_tracts = get_aggregated_tracts(
115
+ tracts_pathlist=tracts_pathlist,
116
+ out_path=aggregated_tracts_path,
117
+ )
118
+
119
+ for lesion_path in lesion_folder.glob("*.nii*"):
120
+ lesion_name = lesion_path.name.removesuffix(".nii.gz").removesuffix(".nii")
121
+ out_dir = out_folder / lesion_name
122
+ out_dir.mkdir(parents=True, exist_ok=True)
123
+
124
+ tmp_dir = tmp_folder / lesion_name
125
+ tmp_dir.mkdir(parents=True, exist_ok=True)
126
+
127
+ lesion = nib.load(lesion_path)
128
+ lesion_trans_volume = lesion_tracts_mapping.transform(lesion.get_fdata(), interpolation="linear")
129
+ lesion_trans = nib.Nifti1Image(
130
+ lesion_trans_volume,
131
+ tracts_ref_space.affine,
132
+ tracts_ref_space.header,
133
+ )
134
+ nib.save(lesion_trans, tmp_dir / "lesion_trans.nii.gz")
135
+
136
+ aggregated_tracts_disconnected_path = tmp_dir / "aggregated_disconnected.trk"
137
+ aggregated_tracts_disconnected, _ = get_streamlines_disconnection(
138
+ tract=aggregated_tracts,
139
+ lesion=lesion_trans,
140
+ out_path=aggregated_tracts_disconnected_path,
141
+ )
142
+ aggregated_tracts_disconnected = clip_tractogram_to_grid(aggregated_tracts_disconnected)
143
+
144
+ get_parcel_damage(
145
+ 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
+
152
+ get_tracts_disconnections_severity(
153
+ tracts_path=tracts_pathlist,
154
+ references_path=references_pathlist,
155
+ lesion=lesion_trans,
156
+ disc_tracts_out_path=tmp_dir / "wm_disconnections_severity",
157
+ disc_frame_out_path=out_dir / "wm_disconnections_severity",
158
+ )
159
+
160
+ get_tracts_disconnections_map(
161
+ tracts=aggregated_tracts,
162
+ tracts_disconnected=aggregated_tracts_disconnected,
163
+ out_path=out_dir / "wm_disconnections_map",
164
+ )
165
+
166
+ structural_connectivity, _, _, per_spared_connections = (
167
+ get_parcel_wise_disconnection_severity(
168
+ aggregated_tracts=aggregated_tracts,
169
+ aggregated_tracts_disconnected=aggregated_tracts_disconnected,
170
+ atlas=atlas_trans,
171
+ out_path=out_dir / "parcel_wise_disconnection_severity",
172
+ )
173
+ )
174
+
175
+ get_sspl_increase(
176
+ structural_connectivity=structural_connectivity,
177
+ per_spared_connections=per_spared_connections,
178
+ out_path=out_dir / "sspl_increase",
179
+ spared_threshold=spared_threshold,
180
+ )
181
+
182
+ if delete_tmp:
183
+ shutil.rmtree(tmp_folder)
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyLQT
3
+ Version: 1.0.dev2
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
+ Requires-Dist: networkx>3.6.1
23
+ Dynamic: license-file
24
+
25
+ # pyLQT
26
+
27
+ pyLQT, standing for “Python Lesion Quantification Toolkit”, is a toolbox designed for analysis brain-lesioned patients scans with minimal resources, regrouping several metrics for lesion quantification. It is a sucessor to LQT [[1]](#ref1) which was originally developped on MATLAB and remastered on R [[2]](#ref2).
28
+
29
+ The metrics are divided into three main domains:
30
+
31
+ * **Grey matter:** lesion effects on grey-matter regions and parcels.
32
+ * **White matter:** lesion effects on white-matter tracts and streamlines.
33
+ * **Network:** lesion effects on structural or functional brain networks.
34
+
35
+ ## installation
36
+
37
+ ```bash
38
+ pip install pyLQT
39
+ ```
40
+
41
+ ## Running pyLQT
42
+
43
+ ### Configuration files
44
+
45
+ pyLQT uses three JSON configuration files to define the lesion cohort, atlas, and tractography dataset. Each file specifies the relevant data paths and reference space. For the simplest usage, only the lesion cohort configuration file is required, in which case the atlas and tractogram files are automatically downloaded.
46
+
47
+ ##### Lesion configuration
48
+
49
+ Defines the lesion cohort, the folder containing the lesion NIfTI files, and their reference space.
50
+
51
+ ```json
52
+ {
53
+ "cohort_name": "ExampleCohort",
54
+ "lesion_folder": "lesions",
55
+ "space": "MNI152NLin6Asym"
56
+ }
57
+ ```
58
+
59
+ ##### Atlas configuration
60
+
61
+ Specifies the atlas NIfTI file, its corresponding label CSV, and its reference space. The CSV must contain a `Parcel` column with parcel indices matching the atlas NIfTI and a `Label` column with the corresponding parcel names.
62
+
63
+ ```json
64
+ {
65
+ "name": "glasser360",
66
+ "atlas": "glasser360.nii.gz",
67
+ "atlas_labels": "glasser360_labels.csv",
68
+ "space": "MNI152NLin2009cAsym"
69
+ }
70
+ ```
71
+
72
+ ##### Tractography configuration
73
+
74
+ Specifies the tractography dataset, the folder containing its `.trk` files, the folder containing its reference NIfTI files, and its reference space.
75
+
76
+ ```json
77
+ {
78
+ "name": "HCP1065",
79
+ "tracts": "hcp1065_avg_tracts_trk",
80
+ "references": "hcp1065_avg_tracts",
81
+ "space": "MNI152NLin2009aAsym"
82
+ }
83
+ ```
84
+
85
+ **Notes:**
86
+ - All paths are relative to the corresponding configuration file.
87
+ - NIfTI files can be uncompressed (`.nii`) or gzipped (`.nii.gz`).
88
+ - The `space` field can specify either a standard space supported by TemplateFlow (e.g., `MNI152NLin2009cAsym`) or a relative path to a reference NIfTI file (`.nii` or `.nii.gz`).
89
+ - All paths and names above are examples and should be adapted to your dataset.
90
+
91
+ ### basic use
92
+
93
+ Below is the simplest usage that only requires the [lesion cohort configuration file](#lesion-configuration). By default, the Glasser360 atlas and the HCP1065 tractogram are downloaded if not provided in the arguments.
94
+
95
+ ```bash
96
+ pyLQT --lesion_cfg <path/to/lesion_config> --out_folder <path/to/outputs>
97
+ ```
98
+
99
+
100
+ | Argument | Default | Options / Range | Description |
101
+ |---|---|---|---|
102
+ | `--lesion_cfg` | — | Path to a `.json` file | Path to the lesion cohort configuration file. |
103
+ | `--atlas_cfg` | `None` | Path to a `.json` file or `None` | Path to the atlas configuration file. |
104
+ | `--tracts_cfg` | `None` | Path to a `.json` file or `None` | Path to the tracts configuration file. |
105
+ | `--out_folder` | — | Path to a directory | Path to the output folder. |
106
+ | `--registration_type` | `syn` | `syn`, `affine` | Registration type used to transform the lesion and atlas from their respective spaces to tract space. |
107
+ | `--spared_threshold` | `0.5` | Float between `0` and `1` | Threshold used to define spared parcel-to-parcel connectivity. |
108
+ | `--delete_tmp` | `True` | `True`, `False` | Whether to delete temporary files generated during processing. |
109
+
110
+
111
+ ## Dependencies
112
+
113
+ pyLQT relies on the following libraries:
114
+ - `numpy`
115
+ - `nibabel`
116
+ - `pandas`
117
+ - `scipy`
118
+ - `requests`
119
+ - `dipy`
120
+ - `templateflow`
121
+ - `networkx`
122
+
123
+
124
+ ## Links
125
+
126
+ - [Source](https://github.com/AntoninHUDRY/pyLQT)
127
+ - [documentation]() [TODO, deploy via github]
128
+ - [PyPI](https://pypi.org/project/pyLQT/)
129
+
130
+ ## References
131
+
132
+ <a id="ref1"></a>
133
+ [1] Griffis et al. (2021). [Lesion Quantification Toolkit: A MATLAB software tool for estimating grey matter damage and white matter disconnections in patients with focal brain lesions](https://doi.org/10.1016/j.nicl.2021.102639). *NeuroImage: Clinical*.
134
+
135
+ <a id="ref2"></a>
136
+ [2] Dworkin Jordan (https://github.com/jdwor/LQT)
@@ -0,0 +1,23 @@
1
+ pyLQT/__init__.py,sha256=y4DvjD5OqKInZ-wJary448UmEBajl8gNbZv50dgabwI,58
2
+ pyLQT/cli.py,sha256=aQHoOa8ZXhFOgJbJTRbAVCCeFLPGUc4qpgVnL1QLEfA,1881
3
+ pyLQT/pipeline.py,sha256=PRS_yzaASATGOYz6GjNhYq733szgwF8k2vPlDqeTjsE,7313
4
+ pyLQT/lesion_metrics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ pyLQT/lesion_metrics/gray_matter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ pyLQT/lesion_metrics/gray_matter/metrics.py,sha256=38R3etfmmKo3hFHkYh7Jw2EUrXbW7I0Jkl8So_-5JWA,2616
7
+ pyLQT/lesion_metrics/gray_matter/utils.py,sha256=czxyAESP0neVCue424L0hon-5Tdbsl0pN0zJHWG5HiQ,840
8
+ pyLQT/lesion_metrics/network/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ pyLQT/lesion_metrics/network/metrics.py,sha256=Ka3G8_dIq-8H3b8mvRR96QM7p_gR2yFLKUxQOLYMWM4,8281
10
+ pyLQT/lesion_metrics/network/utils.py,sha256=fcakfKSMortNIE3TfJP8zwanzBiT1McsYnHcTgwbxE4,3283
11
+ pyLQT/lesion_metrics/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ pyLQT/lesion_metrics/utils/configs.py,sha256=FFur04Sh48IlxUTmz4kRkyLKWffi7bvzyw8B2GclpRU,4961
13
+ pyLQT/lesion_metrics/utils/downloads.py,sha256=dXmB_bjt3uEPjJfGThkXw0lojidTTSm_Rx08wyJYafk,10567
14
+ pyLQT/lesion_metrics/utils/registration.py,sha256=iqD9qIPLSxsmAPkGbnvyPZBAeVGfnG5_9P0UYZ0JGIg,3307
15
+ pyLQT/lesion_metrics/white_matter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ pyLQT/lesion_metrics/white_matter/metrics.py,sha256=sujuHry19XpVWFNo52Ntq7h3JkWEcgsZ1-rNFr7E940,6155
17
+ pyLQT/lesion_metrics/white_matter/utils.py,sha256=oo492yzxsPACMvHEyYhoYVqgVRrv8mpqP7FOf5OU_Ms,7087
18
+ pylqt-1.0.dev2.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
19
+ pylqt-1.0.dev2.dist-info/METADATA,sha256=KiPMLFsDYUD4RwjwyrciKoxH4SrAcMPevyL8aecYkek,5225
20
+ pylqt-1.0.dev2.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
21
+ pylqt-1.0.dev2.dist-info/entry_points.txt,sha256=GHGfSFJ0r77qGtkRMJeCuNj2YHm5aynNxdi3JXxyyCc,41
22
+ pylqt-1.0.dev2.dist-info/top_level.txt,sha256=TwID1Q-SuDZ2HjBLKSwruNTXKrRGgAh16rMZ5pjUk4U,6
23
+ pylqt-1.0.dev2.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (84.0.0)
2
+ Generator: setuptools (84.2.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pyLQT = pyLQT.cli:main
@@ -0,0 +1 @@
1
+ pyLQT
pyLQT_cli.py DELETED
@@ -1,180 +0,0 @@
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()
@@ -1,62 +0,0 @@
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`)
@@ -1,21 +0,0 @@
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,,
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- pyLQT = pyLQT_cli:run_pyLQT
@@ -1,2 +0,0 @@
1
- lesion_metrics
2
- pyLQT_cli
File without changes