clabtoolkit 0.3.3__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,356 @@
1
+ # pylint: disable=import-error
2
+ import os
3
+ import sys
4
+ import subprocess
5
+ from pathlib import Path
6
+ from glob import glob
7
+ from typing import Union
8
+ import numpy as np
9
+ import nibabel as nib
10
+
11
+ # Importing local modules
12
+ from . import misctools as cltmisc
13
+ from . import bidstools as cltbids
14
+ from . import imagetools as cltimg
15
+ from . import parcellationtools as cltparc
16
+
17
+
18
+ ####################################################################################################
19
+ ####################################################################################################
20
+ ############ ############
21
+ ############ ############
22
+ ############ Section 1: Methods dedicated to image segmentation ############
23
+ ############ ############
24
+ ############ ############
25
+ ####################################################################################################
26
+ ####################################################################################################
27
+ def abased_parcellation(
28
+ t1: str,
29
+ t1_temp: str,
30
+ atlas: str,
31
+ out_parc: str,
32
+ xfm_output: str,
33
+ atlas_type: str = "spam",
34
+ interp: str = "Linear",
35
+ cont_tech: str = "local",
36
+ cont_image: str = None,
37
+ force: bool = False,
38
+ ):
39
+ """
40
+ Perform atlas-based parcellation using ANTs registration and transformation.
41
+
42
+ Registers individual T1-weighted image to template space, then applies inverse
43
+ transformation to bring atlas into subject space, creating subject-specific
44
+ parcellation based on template atlas.
45
+
46
+ Parameters
47
+ ----------
48
+ t1 : str
49
+ Path to input T1-weighted image.
50
+
51
+ t1_temp : str
52
+ Path to T1-weighted template image for registration target.
53
+
54
+ atlas : str
55
+ Path to atlas image (either SPAM probabilities or discrete labels).
56
+
57
+ out_parc : str
58
+ Path for output parcellation file.
59
+
60
+ xfm_output : str
61
+ Base path/name for transformation files. Extensions and descriptors
62
+ will be automatically added.
63
+
64
+ atlas_type : str, optional
65
+ Atlas format: 'spam' for probability maps or 'maxprob' for discrete labels.
66
+ Default is 'spam'.
67
+
68
+ interp : str, optional
69
+ Interpolation method: 'Linear', 'NearestNeighbor', 'BSpline'. Default is 'Linear'.
70
+
71
+ cont_tech : str, optional
72
+ Container technology: 'local', 'singularity', 'docker'. Default is 'local'.
73
+
74
+ cont_image : str, optional
75
+ Container image specification. Default is None.
76
+
77
+ force : bool, optional
78
+ Whether to overwrite existing files. Default is False.
79
+
80
+ Returns
81
+ -------
82
+ str
83
+ Path to the generated parcellation file.
84
+
85
+ Notes
86
+ -----
87
+ The method performs the following steps:
88
+ 1. ANTs SyN registration between subject T1 and template
89
+ 2. Generates affine and non-linear transformation files
90
+ 3. Applies inverse transformation to atlas
91
+ 4. For SPAM atlases, preserves probability values
92
+ 5. For maxprob atlases, uses nearest neighbor interpolation
93
+
94
+ Generated transformation files follow BIDS naming conventions with
95
+ descriptors: affine, warp, iwarp.
96
+
97
+ Examples
98
+ --------
99
+ >>> # Basic SPAM atlas parcellation
100
+ >>> parc_file = abased_parcellation(
101
+ ... t1='subject_T1w.nii.gz',
102
+ ... t1_temp='MNI152_T1_1mm.nii.gz',
103
+ ... atlas='AAL_SPAM.nii.gz',
104
+ ... out_parc='subject_AAL.nii.gz',
105
+ ... xfm_output='transforms/subject_to_MNI'
106
+ ... )
107
+ >>>
108
+ >>> # Discrete atlas with nearest neighbor interpolation
109
+ >>> parc_file = abased_parcellation(
110
+ ... t1='T1w.nii.gz',
111
+ ... t1_temp='template.nii.gz',
112
+ ... atlas='discrete_atlas.nii.gz',
113
+ ... out_parc='parcellation.nii.gz',
114
+ ... xfm_output='xfm/transform',
115
+ ... atlas_type='maxprob'
116
+ ... )
117
+ """
118
+
119
+ ######## -- Registration to the template space ------------ #
120
+ # Creating spatial transformation folder
121
+ stransf_dir = Path(os.path.dirname(xfm_output))
122
+ stransf_name = os.path.basename(xfm_output)
123
+ out_parc_dir = Path(os.path.dirname(out_parc))
124
+
125
+ # If the directory does not exist create the directory and if it fails because it does not have write access send an error
126
+ try:
127
+ stransf_dir.mkdir(parents=True, exist_ok=True)
128
+ except PermissionError:
129
+ print(
130
+ "The directory to store the spatial transformations does not have write access."
131
+ )
132
+ sys.exit()
133
+
134
+ try:
135
+ out_parc_dir.mkdir(parents=True, exist_ok=True)
136
+ except PermissionError:
137
+ print("The directory to store the parcellation does not have write access.")
138
+ sys.exit()
139
+
140
+ # Spatial transformation files (Temporal).
141
+ tmp_xfm_basename = os.path.join(stransf_dir, "temp")
142
+ temp_xfm_affine = tmp_xfm_basename + "_0GenericAffine.mat"
143
+ temp_xfm_nl = tmp_xfm_basename + "_1Warp.nii.gz"
144
+ temp_xfm_invnl = tmp_xfm_basename + "_1InverseWarp.nii.gz"
145
+ temp_xfm_invnlw = tmp_xfm_basename + "_InverseWarped.nii.gz"
146
+ temp_xfm_nlw = tmp_xfm_basename + "_Warped.nii.gz"
147
+
148
+ if stransf_name.endswith(".nii.gz"):
149
+ stransf_name = stransf_name[:-7]
150
+ elif stransf_name.endswith(".nii") or stransf_name.endswith(".mat"):
151
+ stransf_name = stransf_name[:-4]
152
+
153
+ if stransf_name.endswith("_xfm"):
154
+ stransf_name = stransf_name[:-4]
155
+
156
+ if "_desc-" in stransf_name:
157
+ affine_name = cltbids.replace_entity_value(stransf_name, {"desc": "affine"})
158
+ nl_name = cltbids.replace_entity_value(stransf_name, {"desc": "warp"})
159
+ invnl_name = cltbids.replace_entity_value(stransf_name, {"desc": "iwarp"})
160
+ else:
161
+ affine_name = stransf_name + "_desc-affine"
162
+ nl_name = stransf_name + "_desc-warp"
163
+ invnl_name = stransf_name + "_desc-iwarp"
164
+
165
+ # Affine transformation filename
166
+ xfm_affine = os.path.join(stransf_dir, affine_name + "_xfm.mat")
167
+
168
+ # Non-linear transformation filename
169
+ xfm_nl = os.path.join(stransf_dir, nl_name + "_xfm.nii.gz")
170
+
171
+ # Filename for the inverse of the Non-linear transformation
172
+ xfm_invnl = os.path.join(stransf_dir, invnl_name + "_xfm.nii.gz")
173
+
174
+ if not os.path.isfile(xfm_invnl) or force:
175
+ # Registration to MNI template
176
+
177
+ cmd_bashargs = [
178
+ "antsRegistrationSyN.sh",
179
+ "-d",
180
+ "3",
181
+ "-f",
182
+ t1_temp,
183
+ "-m",
184
+ t1,
185
+ "-t",
186
+ "s",
187
+ "-o",
188
+ tmp_xfm_basename + "_",
189
+ ]
190
+
191
+ cmd_cont = cltmisc.generate_container_command(
192
+ cmd_bashargs, cont_tech, cont_image
193
+ ) # Generating container command
194
+ subprocess.run(
195
+ cmd_cont, stdout=subprocess.PIPE, universal_newlines=True
196
+ ) # Running container command
197
+
198
+ # Changing the names
199
+ cmd_bashargs = ["mv", temp_xfm_affine, xfm_affine]
200
+ cmd_cont = cltmisc.generate_container_command(
201
+ cmd_bashargs, cont_tech, cont_image
202
+ ) # Generating container command
203
+ subprocess.run(
204
+ cmd_cont, stdout=subprocess.PIPE, universal_newlines=True
205
+ ) # Running container command
206
+
207
+ cmd_bashargs = ["mv", temp_xfm_nl, xfm_nl]
208
+ cmd_cont = cltmisc.generate_container_command(
209
+ cmd_bashargs, cont_tech, cont_image
210
+ ) # Generating container command
211
+ subprocess.run(
212
+ cmd_cont, stdout=subprocess.PIPE, universal_newlines=True
213
+ ) # Running container command
214
+
215
+ cmd_bashargs = ["mv", temp_xfm_invnl, xfm_invnl]
216
+ cmd_cont = cltmisc.generate_container_command(
217
+ cmd_bashargs, cont_tech, cont_image
218
+ ) # Generating container command
219
+ subprocess.run(
220
+ cmd_cont, stdout=subprocess.PIPE, universal_newlines=True
221
+ ) # Running container command
222
+
223
+ # Deleting the warped images
224
+ cmd_bashargs = ["rm", tmp_xfm_basename + "*Warped.nii.gz"]
225
+ warped_imgs = glob(tmp_xfm_basename + "*Warped.nii.gz")
226
+ if len(warped_imgs) > 0:
227
+ for w_img in warped_imgs:
228
+ os.remove(w_img)
229
+
230
+ # Applying spatial transform to the atlas
231
+ if not os.path.isfile(out_parc):
232
+
233
+ if atlas_type == "spam":
234
+ # Applying spatial transform
235
+ cmd_bashargs = [
236
+ "antsApplyTransforms",
237
+ "-d",
238
+ "3",
239
+ "-e",
240
+ "3",
241
+ "-i",
242
+ atlas,
243
+ "-o",
244
+ out_parc,
245
+ "-r",
246
+ t1,
247
+ "-t",
248
+ xfm_invnl,
249
+ "-t",
250
+ "[" + xfm_affine + ",1]",
251
+ "-n",
252
+ interp,
253
+ ]
254
+
255
+ cmd_cont = cltmisc.generate_container_command(
256
+ cmd_bashargs, cont_tech, cont_image
257
+ ) # Generating container command
258
+ subprocess.run(
259
+ cmd_cont, stdout=subprocess.PIPE, universal_newlines=True
260
+ ) # Running container command
261
+
262
+ elif atlas_type == "maxprob":
263
+ # Applying spatial transform
264
+ cmd_bashargs = [
265
+ "antsApplyTransforms",
266
+ "-d",
267
+ "3",
268
+ "-i",
269
+ atlas,
270
+ "-o",
271
+ out_parc,
272
+ "-r",
273
+ t1,
274
+ "-t",
275
+ xfm_invnl,
276
+ "-t",
277
+ "[" + xfm_affine + ",1]",
278
+ "-n",
279
+ "NearestNeighbor",
280
+ ]
281
+
282
+ cmd_cont = cltmisc.generate_container_command(
283
+ cmd_bashargs, cont_tech, cont_image
284
+ ) # Generating container command
285
+ subprocess.run(
286
+ cmd_cont, stdout=subprocess.PIPE, universal_newlines=True
287
+ ) # Running container command
288
+
289
+ tmp_parc = cltparc.Parcellation(parc_file=out_parc)
290
+ tmp_parc.save_parcellation(
291
+ out_file=out_parc,
292
+ affine=tmp_parc.affine,
293
+ save_lut=False,
294
+ save_tsv=False,
295
+ )
296
+
297
+ # Removing the Warped images
298
+ if os.path.isfile(temp_xfm_invnlw):
299
+ os.remove(temp_xfm_invnlw)
300
+
301
+ if os.path.isfile(temp_xfm_nlw):
302
+ os.remove(temp_xfm_nlw)
303
+
304
+ return out_parc
305
+
306
+
307
+ ######################################################################################################
308
+ @staticmethod
309
+ def tissue_seg_table(tsv_filename):
310
+ """
311
+ Create standard tissue segmentation lookup table.
312
+
313
+ Parameters
314
+ ----------
315
+ tsv_filename : str
316
+ Output TSV file path.
317
+
318
+ Returns
319
+ -------
320
+ pd.DataFrame
321
+ DataFrame with tissue segmentation information (CSF, GM, WM).
322
+
323
+ Examples
324
+ --------
325
+ >>> seg_df = Parcellation.tissue_seg_table('tissues.tsv')
326
+ >>> print(seg_df)
327
+ """
328
+
329
+ # Table for tissue segmentation
330
+ # 1. Default values for tissues segmentation table
331
+ seg_rgbcol = np.array([[172, 0, 0], [0, 153, 76], [0, 102, 204]])
332
+ seg_codes = np.array([1, 2, 3])
333
+ seg_names = ["cerebro_spinal_fluid", "gray_matter", "white_matter"]
334
+ seg_acron = ["CSF", "GM", "WM"]
335
+
336
+ # 2. Converting colors to hexidecimal string
337
+ seg_hexcol = []
338
+ nrows, ncols = seg_rgbcol.shape
339
+ for i in np.arange(0, nrows):
340
+ seg_hexcol.append(
341
+ cltmisc.rgb2hex(seg_rgbcol[i, 0], seg_rgbcol[i, 1], seg_rgbcol[i, 2])
342
+ )
343
+
344
+ seg_df = pd.DataFrame(
345
+ {
346
+ "index": seg_codes,
347
+ "name": seg_names,
348
+ "abbreviation": seg_acron,
349
+ "color": seg_hexcol,
350
+ }
351
+ )
352
+ # Save the tsv table
353
+ with open(tsv_filename, "w+") as tsv_file:
354
+ tsv_file.write(seg_df.to_csv(sep="\t", index=False))
355
+
356
+ return seg_df