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.
- clabtoolkit/__init__.py +5 -0
- clabtoolkit/bidstools.py +2260 -0
- clabtoolkit/config/bids.json +147 -0
- clabtoolkit/config/config.json +58 -0
- clabtoolkit/config/lobes.json +112 -0
- clabtoolkit/config/stats_mapping.json +140 -0
- clabtoolkit/config/viz_views.json +268 -0
- clabtoolkit/connectivitytools.py +0 -0
- clabtoolkit/dicomtools.py +941 -0
- clabtoolkit/dwitools.py +1999 -0
- clabtoolkit/freesurfertools.py +5378 -0
- clabtoolkit/imagetools.py +2734 -0
- clabtoolkit/misctools.py +5204 -0
- clabtoolkit/morphometrytools.py +2970 -0
- clabtoolkit/networktools.py +351 -0
- clabtoolkit/parcellationtools.py +3239 -0
- clabtoolkit/pipelinetools.py +1187 -0
- clabtoolkit/plottools.py +682 -0
- clabtoolkit/qcqatools.py +1305 -0
- clabtoolkit/segmentationtools.py +356 -0
- clabtoolkit/surfacetools.py +2785 -0
- clabtoolkit/visualizationtools.py +5575 -0
- clabtoolkit-0.3.3.dist-info/METADATA +212 -0
- clabtoolkit-0.3.3.dist-info/RECORD +28 -0
- clabtoolkit-0.3.3.dist-info/WHEEL +5 -0
- clabtoolkit-0.3.3.dist-info/licenses/AUTHORS.rst +13 -0
- clabtoolkit-0.3.3.dist-info/licenses/LICENSE +16 -0
- clabtoolkit-0.3.3.dist-info/top_level.txt +1 -0
clabtoolkit/bidstools.py
ADDED
|
@@ -0,0 +1,2260 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import time
|
|
5
|
+
import queue
|
|
6
|
+
import threading
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from typing import Union, Dict, Set, Tuple, List, Optional
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
import json
|
|
14
|
+
from glob import glob
|
|
15
|
+
|
|
16
|
+
from rich.progress import (
|
|
17
|
+
Progress,
|
|
18
|
+
BarColumn,
|
|
19
|
+
TimeRemainingColumn,
|
|
20
|
+
TextColumn,
|
|
21
|
+
MofNCompleteColumn,
|
|
22
|
+
SpinnerColumn,
|
|
23
|
+
)
|
|
24
|
+
from rich.console import Console
|
|
25
|
+
from rich.panel import Panel
|
|
26
|
+
|
|
27
|
+
# Importing the clabtoolkit modules
|
|
28
|
+
from . import misctools as cltmisc
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
####################################################################################################
|
|
32
|
+
####################################################################################################
|
|
33
|
+
############ ############
|
|
34
|
+
############ ############
|
|
35
|
+
############ Section 1: Methods dedicated to work with BIDs naming conventions ############
|
|
36
|
+
############ ############
|
|
37
|
+
############ ############
|
|
38
|
+
####################################################################################################
|
|
39
|
+
####################################################################################################
|
|
40
|
+
def str2entity(string: str) -> dict:
|
|
41
|
+
"""
|
|
42
|
+
Converts a formatted string into a dictionary.
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
string : str
|
|
47
|
+
String to convert, with the format `key1-value1_key2-value2...suffix.extension`.
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
dict
|
|
52
|
+
Dictionary containing the entities extracted from the string.
|
|
53
|
+
|
|
54
|
+
Examples
|
|
55
|
+
--------
|
|
56
|
+
>>> str2entity("sub-01_ses-M00_acq-3T_dir-AP_run-01_T1w.nii.gz")
|
|
57
|
+
Returns: {'sub': '01', 'ses': 'M00', 'acq': '3T', 'dir': 'AP', 'run': '01', 'suffix': 'T1w', 'extension': 'nii.gz'}
|
|
58
|
+
|
|
59
|
+
"""
|
|
60
|
+
ent_dict = {}
|
|
61
|
+
suffix, extension = "", ""
|
|
62
|
+
|
|
63
|
+
# Split the string into entities based on underscores.
|
|
64
|
+
ent_list = string.split("_")
|
|
65
|
+
|
|
66
|
+
# Detect suffix and extension
|
|
67
|
+
for ent in ent_list[:]:
|
|
68
|
+
if "-" not in ent:
|
|
69
|
+
# If entity does not contain a '-', it's a suffix or extension.
|
|
70
|
+
if "." in ent:
|
|
71
|
+
# Split suffix and extension parts
|
|
72
|
+
suffix, extension = ent.split(".", 1)
|
|
73
|
+
else:
|
|
74
|
+
suffix = ent
|
|
75
|
+
ent_list.remove(ent)
|
|
76
|
+
|
|
77
|
+
# Process the remaining entities
|
|
78
|
+
for ent in ent_list:
|
|
79
|
+
key, value = ent.split("-", 1) # Split each entity on the first "-"
|
|
80
|
+
ent_dict[key] = value
|
|
81
|
+
|
|
82
|
+
# Add suffix and extension to the dictionary if they were found
|
|
83
|
+
if suffix:
|
|
84
|
+
ent_dict["suffix"] = suffix
|
|
85
|
+
if extension:
|
|
86
|
+
ent_dict["extension"] = extension
|
|
87
|
+
|
|
88
|
+
return ent_dict
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
####################################################################################################
|
|
92
|
+
def entity2str(entity: dict) -> str:
|
|
93
|
+
"""
|
|
94
|
+
Converts an entity dictionary to a string representation.
|
|
95
|
+
|
|
96
|
+
Parameters
|
|
97
|
+
----------
|
|
98
|
+
entity : dict
|
|
99
|
+
Dictionary containing the entities.
|
|
100
|
+
|
|
101
|
+
Returns
|
|
102
|
+
-------
|
|
103
|
+
str
|
|
104
|
+
String containing the entities in the format `key1-value1_key2-value2...suffix.extension`.
|
|
105
|
+
|
|
106
|
+
Examples
|
|
107
|
+
--------
|
|
108
|
+
>>> entity2str({'sub': '01', 'ses': 'M00', 'acq': '3T', 'dir': 'AP', 'run': '01', 'suffix': 'T1w', 'extension': 'nii.gz'})
|
|
109
|
+
Returns: "sub-01_ses-M00_acq-3T_dir-AP_run-01_T1w.nii.gz"
|
|
110
|
+
|
|
111
|
+
"""
|
|
112
|
+
# Make a copy of the entity dictionary to avoid mutating the original.
|
|
113
|
+
entity = entity.copy()
|
|
114
|
+
|
|
115
|
+
# Extract optional 'suffix' and 'extension' fields if present.
|
|
116
|
+
suffix = entity.pop("suffix", "")
|
|
117
|
+
extension = entity.pop("extension", "")
|
|
118
|
+
|
|
119
|
+
# Construct the main part of the string by joining key-value pairs with '_'
|
|
120
|
+
ent_string = "_".join(f"{key}-{str(value)}" for key, value in entity.items())
|
|
121
|
+
|
|
122
|
+
# Append suffix if it exists
|
|
123
|
+
if suffix:
|
|
124
|
+
ent_string += "_" + suffix
|
|
125
|
+
else:
|
|
126
|
+
ent_string = ent_string.rstrip("_") # Remove trailing underscore if no suffix
|
|
127
|
+
|
|
128
|
+
# Append extension if it exists
|
|
129
|
+
if extension:
|
|
130
|
+
ent_string += f".{extension}"
|
|
131
|
+
|
|
132
|
+
return ent_string
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
####################################################################################################
|
|
136
|
+
def delete_entity(
|
|
137
|
+
entity: Union[dict, str], ent2rem: Union[List[str], str, dict]
|
|
138
|
+
) -> Union[dict, str]:
|
|
139
|
+
"""
|
|
140
|
+
Removes specified keys from an entity dictionary or string representation.
|
|
141
|
+
|
|
142
|
+
Parameters
|
|
143
|
+
----------
|
|
144
|
+
entity : dict or str
|
|
145
|
+
Dictionary or string containing the entities.
|
|
146
|
+
|
|
147
|
+
ent2rem : List[str], str or dict
|
|
148
|
+
Entities to be removed from the entity dictionary or string. If ent2rem is a dictionary,
|
|
149
|
+
only the combination key-value will be removed from the filenames.
|
|
150
|
+
|
|
151
|
+
Returns
|
|
152
|
+
-------
|
|
153
|
+
Union[dict, str]
|
|
154
|
+
The updated entity as a dictionary or string (matching the input type).
|
|
155
|
+
|
|
156
|
+
Examples
|
|
157
|
+
--------
|
|
158
|
+
>>> delete_entity("sub-01_ses-M00_acq-3T_dir-AP_run-01_T1w.nii.gz", "acq")
|
|
159
|
+
Returns: "sub-01_ses-M00_dir-AP_run-01_T1w.nii.gz"
|
|
160
|
+
|
|
161
|
+
"""
|
|
162
|
+
# Determine if `entity` is a string and convert if necessary.
|
|
163
|
+
is_string = isinstance(entity, str)
|
|
164
|
+
rem_is_dict = False # Boolean variable in case you want to delete an entity with a certain value and the key2remis a dictionary
|
|
165
|
+
if is_string:
|
|
166
|
+
entity_out = str2entity(entity)
|
|
167
|
+
elif isinstance(entity, dict):
|
|
168
|
+
entity_out = entity.copy()
|
|
169
|
+
else:
|
|
170
|
+
raise ValueError("The entity must be a dictionary or a string.")
|
|
171
|
+
|
|
172
|
+
# Ensure `key2rem` is a list for uniform processing.
|
|
173
|
+
if isinstance(ent2rem, str):
|
|
174
|
+
key2rem = [ent2rem]
|
|
175
|
+
|
|
176
|
+
elif isinstance(ent2rem, list):
|
|
177
|
+
key2rem = list(set(ent2rem)) # Convert to a set for unique keys
|
|
178
|
+
|
|
179
|
+
elif isinstance(ent2rem, dict):
|
|
180
|
+
rem_is_dict = True
|
|
181
|
+
key2rem = list(ent2rem.keys())
|
|
182
|
+
|
|
183
|
+
else:
|
|
184
|
+
raise ValueError(
|
|
185
|
+
"The ent2rem parameter must be a string, list of strings, or dictionary."
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Remove specified keys from the entity dictionary.
|
|
189
|
+
for key in key2rem:
|
|
190
|
+
if rem_is_dict:
|
|
191
|
+
# If `key2rem` is a dictionary, check if the key exists and has the specified value.
|
|
192
|
+
if key in entity_out and entity_out[key] in ent2rem[key]:
|
|
193
|
+
entity_out.pop(key, None)
|
|
194
|
+
else:
|
|
195
|
+
entity_out.pop(key, None) # `pop` with default `None` avoids KeyErrors.
|
|
196
|
+
|
|
197
|
+
# Convert back to string format if original input was a string.
|
|
198
|
+
if is_string:
|
|
199
|
+
return entity2str(entity_out)
|
|
200
|
+
|
|
201
|
+
return entity_out
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
####################################################################################################
|
|
205
|
+
def replace_entity_value(
|
|
206
|
+
entity: Union[dict, str], ent2replace: Union[dict, str], verbose: bool = False
|
|
207
|
+
) -> Union[dict, str]:
|
|
208
|
+
"""
|
|
209
|
+
Replaces values in an entity dictionary or string representation.
|
|
210
|
+
|
|
211
|
+
Parameters
|
|
212
|
+
----------
|
|
213
|
+
entity : dict or str
|
|
214
|
+
Dictionary or string containing the entities.
|
|
215
|
+
|
|
216
|
+
ent2replace : dict or str
|
|
217
|
+
Dictionary or string containing entities to replace with new values.
|
|
218
|
+
|
|
219
|
+
verbose : bool, optional
|
|
220
|
+
If True, prints warnings for non-existent or empty values.
|
|
221
|
+
|
|
222
|
+
Returns
|
|
223
|
+
-------
|
|
224
|
+
Union[dict, str]
|
|
225
|
+
Updated entity as a dictionary or string (matching the input type).
|
|
226
|
+
|
|
227
|
+
Examples
|
|
228
|
+
--------
|
|
229
|
+
>>> replace_entity_value("sub-01_ses-M00_acq-3T_dir-AP_run-01_T1w.nii.gz", {"acq": "7T"})
|
|
230
|
+
Returns: "sub-01_ses-M00_acq-7T_dir-AP_run-01_T1w.nii.gz"
|
|
231
|
+
|
|
232
|
+
"""
|
|
233
|
+
# Determine if `entity` is a string and convert if necessary.
|
|
234
|
+
is_string = isinstance(entity, str)
|
|
235
|
+
if is_string:
|
|
236
|
+
entity_out = str2entity(entity)
|
|
237
|
+
elif isinstance(entity, dict):
|
|
238
|
+
entity_out = entity.copy()
|
|
239
|
+
else:
|
|
240
|
+
raise ValueError("The entity must be a dictionary or a string.")
|
|
241
|
+
|
|
242
|
+
# Adding the possibility to enter a string value. It will convert it to a dictionary
|
|
243
|
+
if isinstance(ent2replace, str):
|
|
244
|
+
ent2replace = str2entity(ent2replace)
|
|
245
|
+
|
|
246
|
+
# Remove any empty keys or values from `ent2replace`.
|
|
247
|
+
ent2replace = {k: v for k, v in ent2replace.items() if v}
|
|
248
|
+
|
|
249
|
+
# Replace values in `entity_out` based on `ent2replace`.
|
|
250
|
+
for key, new_value in ent2replace.items():
|
|
251
|
+
if key in entity_out:
|
|
252
|
+
if new_value:
|
|
253
|
+
entity_out[key] = new_value
|
|
254
|
+
elif verbose:
|
|
255
|
+
print(f"Warning: Replacement value for '{key}' is empty.")
|
|
256
|
+
elif verbose:
|
|
257
|
+
print(f"Warning: Entity '{key}' not found in entity dictionary.")
|
|
258
|
+
|
|
259
|
+
# Convert back to string format if original input was a string.
|
|
260
|
+
if is_string:
|
|
261
|
+
return entity2str(entity_out)
|
|
262
|
+
|
|
263
|
+
return entity_out
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
####################################################################################################
|
|
267
|
+
def replace_entity_key(
|
|
268
|
+
entity: Union[dict, str], keys2replace: Dict[str, str], verbose: bool = False
|
|
269
|
+
) -> Union[dict, str]:
|
|
270
|
+
"""
|
|
271
|
+
Replaces specified keys in an entity dictionary or string representation.
|
|
272
|
+
|
|
273
|
+
Parameters
|
|
274
|
+
----------
|
|
275
|
+
entity : dict or str
|
|
276
|
+
Dictionary containing the entities or a string that follows the BIDS naming specifications.
|
|
277
|
+
|
|
278
|
+
keys2replace : dict
|
|
279
|
+
Dictionary mapping old keys to new keys.
|
|
280
|
+
|
|
281
|
+
verbose : bool, optional
|
|
282
|
+
If True, prints warnings for keys in `keys2replace` that are not found in `entity`.
|
|
283
|
+
|
|
284
|
+
Returns
|
|
285
|
+
-------
|
|
286
|
+
Union[dict, str]
|
|
287
|
+
Updated entity as a dictionary or string (matching the input type).
|
|
288
|
+
|
|
289
|
+
Examples
|
|
290
|
+
--------
|
|
291
|
+
>>> replace_entity_key("sub-01_ses-M00_acq-3T_dir-AP_run-01_T1w.nii.gz", {"acq": "TESTrep1", "dir": "TESTrep2"})
|
|
292
|
+
Returns: "sub-01_ses-M00_TESTrep1-3T_TESTrep2-AP_run-01_T1w.nii.gz"
|
|
293
|
+
|
|
294
|
+
"""
|
|
295
|
+
# Convert `entity` to a dictionary if it's a string
|
|
296
|
+
is_string = isinstance(entity, str)
|
|
297
|
+
if is_string:
|
|
298
|
+
entity = str2entity(entity)
|
|
299
|
+
elif not isinstance(entity, dict):
|
|
300
|
+
raise ValueError("The entity must be a dictionary or a string.")
|
|
301
|
+
|
|
302
|
+
# Validate that `keys2replace` is a dictionary
|
|
303
|
+
if not isinstance(keys2replace, dict):
|
|
304
|
+
raise ValueError("The keys2replace parameter must be a dictionary.")
|
|
305
|
+
|
|
306
|
+
# Filter out any empty keys or values from `keys2replace`
|
|
307
|
+
keys2replace = {k: v for k, v in keys2replace.items() if k and v}
|
|
308
|
+
|
|
309
|
+
# Replace key names in the entity
|
|
310
|
+
entity_out = {}
|
|
311
|
+
for key, value in entity.items():
|
|
312
|
+
# Use the new key if it exists in `keys2replace`, otherwise keep the original key
|
|
313
|
+
new_key = keys2replace.get(key, key)
|
|
314
|
+
entity_out[new_key] = value
|
|
315
|
+
|
|
316
|
+
# Verbose output if the key to replace does not exist in the entity
|
|
317
|
+
if verbose and key in keys2replace and key not in entity:
|
|
318
|
+
print(f"Warning: Key '{key}' not found in the original dictionary.")
|
|
319
|
+
|
|
320
|
+
# Convert back to string format if the original input was a string
|
|
321
|
+
if is_string:
|
|
322
|
+
return entity2str(entity_out)
|
|
323
|
+
|
|
324
|
+
return entity_out
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
####################################################################################################
|
|
328
|
+
def insert_entity(
|
|
329
|
+
entity: Union[dict, str], entity2add: Dict[str, str], prev_entity: str = None
|
|
330
|
+
) -> Union[dict, str]:
|
|
331
|
+
"""
|
|
332
|
+
Adds entities to an existing entity dictionary or string representation.
|
|
333
|
+
|
|
334
|
+
Parameters
|
|
335
|
+
----------
|
|
336
|
+
entity : dict or str
|
|
337
|
+
Dictionary containing the entities or a string that follows the BIDS naming specifications.
|
|
338
|
+
|
|
339
|
+
entity2add : dict
|
|
340
|
+
Dictionary containing the entities to add.
|
|
341
|
+
IMPORTANT: If the `entity2add` contains keys that already exist in the `entity`, they will not be added.
|
|
342
|
+
|
|
343
|
+
prev_entity : str, optional
|
|
344
|
+
Key in `entity` after which to insert the new entities.
|
|
345
|
+
|
|
346
|
+
Returns
|
|
347
|
+
-------
|
|
348
|
+
Union[dict, str]
|
|
349
|
+
Updated entity with the new entities added (matching the input type).
|
|
350
|
+
|
|
351
|
+
Examples
|
|
352
|
+
--------
|
|
353
|
+
>>> insert_entity("sub-01_ses-M00_acq-3T_dir-AP_run-01_T1w.nii.gz", {"task": "rest"})
|
|
354
|
+
Returns: "sub-01_ses-M00_acq-3T_dir-AP_run-01_task-rest_T1w.nii.gz"
|
|
355
|
+
|
|
356
|
+
>>> insert_entity("sub-01_ses-M00_acq-3T_dir-AP_run-01_T1w.nii.gz", {"task": "rest"}, prev_entity="ses")
|
|
357
|
+
Returns: "sub-01_ses-M00_task-rest_acq-3T_dir-AP_run-01_T1w.nii.gz"
|
|
358
|
+
|
|
359
|
+
"""
|
|
360
|
+
|
|
361
|
+
# Determine if `entity` is a string and convert if necessary
|
|
362
|
+
is_string = isinstance(entity, str)
|
|
363
|
+
if is_string:
|
|
364
|
+
entity = str2entity(entity)
|
|
365
|
+
elif not isinstance(entity, dict):
|
|
366
|
+
raise ValueError("The entity must be a dictionary or a string.")
|
|
367
|
+
|
|
368
|
+
# Clean `entity2add` by removing any empty keys or values
|
|
369
|
+
entity2add = {k: v for k, v in entity2add.items() if k and v}
|
|
370
|
+
|
|
371
|
+
# Validate `prev_entity` if provided
|
|
372
|
+
if prev_entity is not None and prev_entity not in entity:
|
|
373
|
+
raise ValueError(
|
|
374
|
+
f"Reference entity '{prev_entity}' is not in the entity dictionary."
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
# Temporarily remove `suffix` and `extension` if they exist
|
|
378
|
+
suffix = entity.pop("suffix", None)
|
|
379
|
+
extension = entity.pop("extension", None)
|
|
380
|
+
|
|
381
|
+
# Check if there are some entities already on the name and removing those items from
|
|
382
|
+
# the dictionary
|
|
383
|
+
if len(cltmisc.list_intercept(list(entity2add.keys()), list(entity.keys()))) > 0:
|
|
384
|
+
# Removes the items from `entity2add` that already exist in `entity`
|
|
385
|
+
entity2add = {k: v for k, v in entity2add.items() if k not in entity}
|
|
386
|
+
|
|
387
|
+
# If `entity2add` is empty after filtering, return the original entity
|
|
388
|
+
if not entity2add:
|
|
389
|
+
if is_string:
|
|
390
|
+
return entity2str(entity)
|
|
391
|
+
return entity
|
|
392
|
+
|
|
393
|
+
else:
|
|
394
|
+
# Build `ent_out` by adding items from `entity`, and insert `entity2add` after `prev_entity` if specified
|
|
395
|
+
ent_out = {}
|
|
396
|
+
for key, value in entity.items():
|
|
397
|
+
ent_out[key] = value
|
|
398
|
+
if key == prev_entity:
|
|
399
|
+
ent_out.update(
|
|
400
|
+
entity2add
|
|
401
|
+
) # Insert new entities immediately after `prev_entity`
|
|
402
|
+
|
|
403
|
+
# If no `prev_entity` is specified or if `prev_entity` is "suffix", append `entity2add` at the end
|
|
404
|
+
if prev_entity is None or prev_entity == "suffix":
|
|
405
|
+
ent_out.update(entity2add)
|
|
406
|
+
|
|
407
|
+
# Restore `suffix` and `extension` if they were removed
|
|
408
|
+
if suffix:
|
|
409
|
+
ent_out["suffix"] = suffix
|
|
410
|
+
if extension:
|
|
411
|
+
ent_out["extension"] = extension
|
|
412
|
+
|
|
413
|
+
# Convert back to string format if the original input was a string
|
|
414
|
+
if is_string:
|
|
415
|
+
return entity2str(ent_out)
|
|
416
|
+
|
|
417
|
+
return ent_out
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
####################################################################################################
|
|
421
|
+
def recursively_replace_entity_value(
|
|
422
|
+
root_dir: str, dict2old: Union[dict, str], dict2new: Union[dict, str]
|
|
423
|
+
):
|
|
424
|
+
"""
|
|
425
|
+
This method replaces the values of certain entities in all the files and folders of a BIDs dataset.
|
|
426
|
+
|
|
427
|
+
Parameters
|
|
428
|
+
----------
|
|
429
|
+
root_dir: str
|
|
430
|
+
Root directory of the BIDs dataset
|
|
431
|
+
|
|
432
|
+
dict2old: dict or str
|
|
433
|
+
Dictionary containing the entities to replace and their old values
|
|
434
|
+
|
|
435
|
+
dict2new: dict or str
|
|
436
|
+
Dictionary containing the entities to replace and their new values
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
"""
|
|
440
|
+
|
|
441
|
+
# Detect if the BIDs directory exists
|
|
442
|
+
if not os.path.isdir(root_dir):
|
|
443
|
+
raise ValueError("The BIDs directory does not exist.")
|
|
444
|
+
|
|
445
|
+
# Convert the strings to dictionaries
|
|
446
|
+
if isinstance(dict2old, str):
|
|
447
|
+
dict2old = str2entity(dict2old)
|
|
448
|
+
if isinstance(dict2new, str):
|
|
449
|
+
dict2new = str2entity(dict2new)
|
|
450
|
+
|
|
451
|
+
# Leave in the dictionaries only the keys that are common
|
|
452
|
+
dict2old = {k: dict2old[k] for k in dict2old if k in dict2new}
|
|
453
|
+
dict2new = {k: dict2new[k] for k in dict2new if k in dict2old}
|
|
454
|
+
|
|
455
|
+
# Order the dictionaries alphabetically by key
|
|
456
|
+
dict2old = dict(sorted(dict2old.items()))
|
|
457
|
+
dict2new = dict(sorted(dict2new.items()))
|
|
458
|
+
|
|
459
|
+
# Creating the list of strings
|
|
460
|
+
dict2old_list = [f"{key}-{value}" for key, value in dict2old.items()]
|
|
461
|
+
dict2new_list = [f"{key}-{value}" for key, value in dict2new.items()]
|
|
462
|
+
|
|
463
|
+
replacements = dict(zip(dict2old_list, dict2new_list))
|
|
464
|
+
|
|
465
|
+
#
|
|
466
|
+
all_files = cltmisc.get_all_files(
|
|
467
|
+
root_dir, or_filter=dict2old_list[0], and_filter=dict2old_list
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
if not all_files:
|
|
471
|
+
print(
|
|
472
|
+
"No files found that match the specified entities. Please check the input parameters."
|
|
473
|
+
)
|
|
474
|
+
return
|
|
475
|
+
|
|
476
|
+
else:
|
|
477
|
+
all_dirs = []
|
|
478
|
+
for file in all_files:
|
|
479
|
+
file_path = os.path.dirname(file)
|
|
480
|
+
file_name = os.path.basename(file)
|
|
481
|
+
all_dirs.append(file_path)
|
|
482
|
+
|
|
483
|
+
for i, subst_x in enumerate(dict2old_list):
|
|
484
|
+
subst_y = dict2new_list[i]
|
|
485
|
+
if subst_x in file_name:
|
|
486
|
+
old_path = os.path.join(file_path, file_name)
|
|
487
|
+
new_name = file_name.replace(subst_x, subst_y)
|
|
488
|
+
new_path = os.path.join(file_path, new_name)
|
|
489
|
+
os.rename(old_path, new_path)
|
|
490
|
+
file_name = (
|
|
491
|
+
new_name # Update old_path to the new path after renaming
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
all_dirs = set(all_dirs) # Remove duplicates from the directory list
|
|
495
|
+
|
|
496
|
+
# Renaming the directories
|
|
497
|
+
cltmisc.rename_folders(all_dirs, replacements)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
####################################################################################################
|
|
501
|
+
def recursively_replace_entity_key(root_dir: str, replacements: dict):
|
|
502
|
+
"""
|
|
503
|
+
This method replaces the keys of certain entities in all the files and folders of a BIDs dataset.
|
|
504
|
+
|
|
505
|
+
Parameters
|
|
506
|
+
----------
|
|
507
|
+
root_dir: str
|
|
508
|
+
Root directory of the BIDs dataset
|
|
509
|
+
|
|
510
|
+
replacements: dict
|
|
511
|
+
Dictionary containing the entities to replace and their new keys.
|
|
512
|
+
Example: {'acq': 'desc', 'run': 'runny'}
|
|
513
|
+
|
|
514
|
+
Returns
|
|
515
|
+
-------
|
|
516
|
+
None
|
|
517
|
+
The method will rename the files and folders in the BIDs dataset. All the files or folders containing the old
|
|
518
|
+
entities' names on their names will be renamed and the old entities will be replaced with the new entities.
|
|
519
|
+
|
|
520
|
+
"""
|
|
521
|
+
# Detect if the BIDs directory exists
|
|
522
|
+
if not os.path.isdir(root_dir):
|
|
523
|
+
raise ValueError("The BIDs directory does not exist.")
|
|
524
|
+
|
|
525
|
+
old_keys = list(replacements.keys())
|
|
526
|
+
new_keys = list(replacements.values())
|
|
527
|
+
|
|
528
|
+
all_files = cltmisc.get_all_files(
|
|
529
|
+
root_dir, or_filter=old_keys[0], and_filter=old_keys
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
if not all_files:
|
|
533
|
+
print(
|
|
534
|
+
"No files found that match the specified entities. Please check the input parameters."
|
|
535
|
+
)
|
|
536
|
+
return
|
|
537
|
+
|
|
538
|
+
else:
|
|
539
|
+
all_dirs = []
|
|
540
|
+
for file in all_files:
|
|
541
|
+
file_path = os.path.dirname(file)
|
|
542
|
+
file_name = os.path.basename(file)
|
|
543
|
+
all_dirs.append(file_path)
|
|
544
|
+
|
|
545
|
+
for i, subst_x in enumerate(old_keys):
|
|
546
|
+
subst_y = new_keys[i]
|
|
547
|
+
if subst_x in file_name:
|
|
548
|
+
old_path = os.path.join(file_path, file_name)
|
|
549
|
+
new_name = file_name.replace(subst_x, subst_y)
|
|
550
|
+
new_path = os.path.join(file_path, new_name)
|
|
551
|
+
os.rename(old_path, new_path)
|
|
552
|
+
file_name = (
|
|
553
|
+
new_name # Update old_path to the new path after renaming
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
all_dirs = set(all_dirs) # Remove duplicates from the directory list
|
|
557
|
+
|
|
558
|
+
# Renaming the directories
|
|
559
|
+
cltmisc.rename_folders(all_dirs, replacements)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def recursively_delete_entity(root_dir: str, key2rem: Union[List[str], str, dict]):
|
|
563
|
+
"""
|
|
564
|
+
This method deletes entities in all the files and folders of a BIDs dataset.
|
|
565
|
+
|
|
566
|
+
Parameters
|
|
567
|
+
----------
|
|
568
|
+
root_dir: str
|
|
569
|
+
Root directory of the BIDs dataset
|
|
570
|
+
|
|
571
|
+
key2rem: list or str
|
|
572
|
+
Key(s) of the entities that will be removed from the files and folders.
|
|
573
|
+
|
|
574
|
+
Returns
|
|
575
|
+
-------
|
|
576
|
+
None
|
|
577
|
+
The method will rename the files and folders in the BIDs dataset, removing from file names and folder names the entities containing the specified keys.
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
"""
|
|
581
|
+
|
|
582
|
+
# Detect if the BIDs directory exists
|
|
583
|
+
root_dir = os.path.abspath(root_dir)
|
|
584
|
+
if not os.path.isdir(root_dir):
|
|
585
|
+
raise ValueError("The BIDs directory does not exist.")
|
|
586
|
+
|
|
587
|
+
# Convert the strings to lists
|
|
588
|
+
if isinstance(key2rem, str):
|
|
589
|
+
key2rem = [key2rem]
|
|
590
|
+
|
|
591
|
+
if isinstance(key2rem, dict):
|
|
592
|
+
tmp_keys = list(key2rem.keys())
|
|
593
|
+
all_files = cltmisc.get_all_files(
|
|
594
|
+
root_dir, or_filter=tmp_keys[0], and_filter=tmp_keys
|
|
595
|
+
)
|
|
596
|
+
else:
|
|
597
|
+
|
|
598
|
+
all_files = cltmisc.get_all_files(
|
|
599
|
+
root_dir, or_filter=key2rem[0], and_filter=key2rem
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
if not all_files:
|
|
603
|
+
print(
|
|
604
|
+
"No files found that match the specified entities. Please check the input parameters."
|
|
605
|
+
)
|
|
606
|
+
return
|
|
607
|
+
|
|
608
|
+
else:
|
|
609
|
+
all_dirs = []
|
|
610
|
+
for file in all_files:
|
|
611
|
+
file_path = os.path.dirname(file)
|
|
612
|
+
file_name = os.path.basename(file)
|
|
613
|
+
all_dirs.append(file_path)
|
|
614
|
+
|
|
615
|
+
new_entity = delete_entity(file_name, key2rem)
|
|
616
|
+
|
|
617
|
+
old_path = os.path.join(file_path, file_name)
|
|
618
|
+
new_path = os.path.join(file_path, new_entity)
|
|
619
|
+
os.rename(old_path, new_path)
|
|
620
|
+
|
|
621
|
+
all_dirs = set(all_dirs) # Remove duplicates from the directory list
|
|
622
|
+
|
|
623
|
+
# Renaming the directories
|
|
624
|
+
key2rem_dict = {key: "" for key in key2rem} # Create a dict with empty values
|
|
625
|
+
cltmisc.rename_folders(all_dirs, key2rem_dict)
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def recursively_insert_entity(
|
|
629
|
+
root_dir: str, entity2add: Dict[str, str], prev_entity: str = None
|
|
630
|
+
) -> None:
|
|
631
|
+
"""
|
|
632
|
+
This method inserts entities in all the files and folders of a BIDs dataset.
|
|
633
|
+
|
|
634
|
+
Parameters
|
|
635
|
+
----------
|
|
636
|
+
root_dir: str
|
|
637
|
+
Root directory of the BIDs dataset
|
|
638
|
+
|
|
639
|
+
entity2add: dict
|
|
640
|
+
Dictionary containing the entities to add.
|
|
641
|
+
Example: {'task': 'rest', 'run': '01'}
|
|
642
|
+
|
|
643
|
+
prev_entity: str, optional
|
|
644
|
+
Key in `entity` after which to insert the new entities. Otherwise it will be added at the end of the file name, just before the suffix.
|
|
645
|
+
|
|
646
|
+
Returns
|
|
647
|
+
-------
|
|
648
|
+
None
|
|
649
|
+
The method will rename the files and folders in the BIDs dataset. All the files or folders containing the old
|
|
650
|
+
entities' names on their names will be renamed and the old entities will be replaced with the new entities.
|
|
651
|
+
|
|
652
|
+
"""
|
|
653
|
+
|
|
654
|
+
# Detect if the BIDs directory exists
|
|
655
|
+
root_dir = os.path.abspath(root_dir)
|
|
656
|
+
if not os.path.isdir(root_dir):
|
|
657
|
+
raise ValueError("The BIDs directory does not exist.")
|
|
658
|
+
|
|
659
|
+
# Convert the strings to dictionaries
|
|
660
|
+
if isinstance(entity2add, str):
|
|
661
|
+
entity2add = str2entity(entity2add)
|
|
662
|
+
|
|
663
|
+
# Order the dictionaries alphabetically by key
|
|
664
|
+
entity2add = dict(sorted(entity2add.items()))
|
|
665
|
+
|
|
666
|
+
# Creating the list of strings
|
|
667
|
+
entity2add_list = [f"{key}-{value}" for key, value in entity2add.items()]
|
|
668
|
+
|
|
669
|
+
if prev_entity is not None:
|
|
670
|
+
all_files = cltmisc.get_all_files(root_dir, or_filter=prev_entity)
|
|
671
|
+
else:
|
|
672
|
+
all_files = cltmisc.get_all_files(root_dir, or_filter="sub-")
|
|
673
|
+
|
|
674
|
+
if not all_files:
|
|
675
|
+
print(
|
|
676
|
+
"No files found that match the specified entities. Please check the input parameters."
|
|
677
|
+
)
|
|
678
|
+
return
|
|
679
|
+
|
|
680
|
+
else:
|
|
681
|
+
all_dirs = []
|
|
682
|
+
for file in all_files:
|
|
683
|
+
file_path = os.path.dirname(file)
|
|
684
|
+
file_name = os.path.basename(file)
|
|
685
|
+
all_dirs.append(file_path)
|
|
686
|
+
|
|
687
|
+
new_entity = insert_entity(file_name, entity2add, prev_entity=prev_entity)
|
|
688
|
+
|
|
689
|
+
old_path = os.path.join(file_path, file_name)
|
|
690
|
+
new_path = os.path.join(file_path, new_entity)
|
|
691
|
+
os.rename(old_path, new_path)
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
####################################################################################################
|
|
695
|
+
def get_all_entities(root_dir: str) -> Tuple[Dict[str, Set[str]], List[str]]:
|
|
696
|
+
"""
|
|
697
|
+
Returns a set of all unique entities found in the BIDS dataset.
|
|
698
|
+
|
|
699
|
+
Parameters
|
|
700
|
+
----------
|
|
701
|
+
root_dir : str
|
|
702
|
+
Root directory of the BIDS dataset.
|
|
703
|
+
|
|
704
|
+
Returns
|
|
705
|
+
-------
|
|
706
|
+
all_entities: Set[str]
|
|
707
|
+
A set of unique entity names found in the dataset.
|
|
708
|
+
|
|
709
|
+
all_suffixes: List[str]
|
|
710
|
+
A list of unique suffixes found in the dataset.
|
|
711
|
+
|
|
712
|
+
Raises
|
|
713
|
+
------
|
|
714
|
+
ValueError
|
|
715
|
+
If the specified root directory does not exist.
|
|
716
|
+
FileNotFoundError
|
|
717
|
+
If the default configuration file is not found.
|
|
718
|
+
ValueError
|
|
719
|
+
If the default configuration JSON does not have the expected structure.
|
|
720
|
+
|
|
721
|
+
Examples
|
|
722
|
+
--------
|
|
723
|
+
>>> get_all_entities('/path/to/bids/dataset')
|
|
724
|
+
{'sub', 'ses', 'task', 'run', ...}
|
|
725
|
+
>>> get_all_entities('/path/to/bids/dataset')
|
|
726
|
+
{'sub', 'ses', 'task', 'run', ...}, ['T1w', 'bold', ...]
|
|
727
|
+
|
|
728
|
+
"""
|
|
729
|
+
|
|
730
|
+
# Check if the root directory exists
|
|
731
|
+
if not os.path.isdir(root_dir):
|
|
732
|
+
raise ValueError(f"The specified root directory does not exist: {root_dir}")
|
|
733
|
+
|
|
734
|
+
# Load the config bids.json file
|
|
735
|
+
cwd = os.path.dirname(os.path.abspath(__file__))
|
|
736
|
+
|
|
737
|
+
try:
|
|
738
|
+
config_data = load_bids_json()
|
|
739
|
+
|
|
740
|
+
# Merge raw and derivatives entities
|
|
741
|
+
if (
|
|
742
|
+
"bids_entities" in config_data
|
|
743
|
+
and "raw_entities" in config_data["bids_entities"]
|
|
744
|
+
and "derivatives_entities" in config_data["bids_entities"]
|
|
745
|
+
):
|
|
746
|
+
entities = {
|
|
747
|
+
**config_data["bids_entities"]["raw_entities"],
|
|
748
|
+
**config_data["bids_entities"]["derivatives_entities"],
|
|
749
|
+
}
|
|
750
|
+
else:
|
|
751
|
+
raise ValueError(
|
|
752
|
+
"Default config JSON does not have the expected structure."
|
|
753
|
+
)
|
|
754
|
+
except FileNotFoundError:
|
|
755
|
+
raise FileNotFoundError(
|
|
756
|
+
f"Default configuration file not found at: {os.path.join(cwd, 'config', 'bids.json')}"
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
# Get all the leaf directories in the BIDS dataset
|
|
760
|
+
bids_folders = cltmisc.get_leaf_directories(root_dir)
|
|
761
|
+
|
|
762
|
+
# Get all the files in the folder
|
|
763
|
+
# Leave only the files that have a correct BIDs suffix
|
|
764
|
+
suffixes = config_data["bids_entities"]["raw_suffix"]
|
|
765
|
+
extensions = config_data["bids_entities"]["extensions"]
|
|
766
|
+
|
|
767
|
+
# Multiply the suffixes and extensions to get all the possible combinations
|
|
768
|
+
suffixes = [f"_{s}{e}" for s in suffixes for e in extensions]
|
|
769
|
+
|
|
770
|
+
bids_files = cltmisc.get_all_files(root_dir, or_filter=suffixes)
|
|
771
|
+
|
|
772
|
+
# Leave only the directories that are BIDs folders
|
|
773
|
+
bids_folders = cltmisc.filter_by_substring(
|
|
774
|
+
bids_folders, config_data["bids_entities"]["bids_folders"]
|
|
775
|
+
)
|
|
776
|
+
|
|
777
|
+
# Leave only the files that are BIDs files
|
|
778
|
+
bids_files = cltmisc.filter_by_substring(bids_files, bids_folders)
|
|
779
|
+
|
|
780
|
+
# Initialize a set to store all unique entities
|
|
781
|
+
all_entities = []
|
|
782
|
+
all_suffixes = []
|
|
783
|
+
for file in bids_files:
|
|
784
|
+
|
|
785
|
+
filename = os.path.basename(file)
|
|
786
|
+
ent_dict = str2entity(filename)
|
|
787
|
+
|
|
788
|
+
# Remove suffix and extension from the entity dictionary
|
|
789
|
+
if "suffix" in ent_dict:
|
|
790
|
+
suffix = ent_dict["suffix"]
|
|
791
|
+
all_suffixes.append(suffix)
|
|
792
|
+
del ent_dict["suffix"]
|
|
793
|
+
|
|
794
|
+
if "extension" in ent_dict:
|
|
795
|
+
del ent_dict["extension"]
|
|
796
|
+
|
|
797
|
+
if "run" in ent_dict:
|
|
798
|
+
del ent_dict["run"]
|
|
799
|
+
|
|
800
|
+
file_ent_keys = list(ent_dict.keys())
|
|
801
|
+
|
|
802
|
+
all_entities.extend(file_ent_keys)
|
|
803
|
+
|
|
804
|
+
# Unique entities
|
|
805
|
+
all_entities = sorted(set(all_entities))
|
|
806
|
+
all_suffixes = sorted(set(all_suffixes))
|
|
807
|
+
|
|
808
|
+
selected_ent_dict = entities4table(selected_entities=all_entities)
|
|
809
|
+
|
|
810
|
+
return selected_ent_dict, all_suffixes
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
####################################################################################################
|
|
814
|
+
def entities4table(
|
|
815
|
+
entities_json: str = None, selected_entities: Union[str, Dict, List] = None
|
|
816
|
+
) -> Dict:
|
|
817
|
+
"""
|
|
818
|
+
Returns the BIDS entities that will be included in the morphometric table.
|
|
819
|
+
|
|
820
|
+
This function loads BIDS entities from a JSON configuration file and filters
|
|
821
|
+
them based on optional selected entities.
|
|
822
|
+
|
|
823
|
+
Parameters
|
|
824
|
+
----------
|
|
825
|
+
entities_json : str, optional
|
|
826
|
+
Path to the JSON file with entity definitions.
|
|
827
|
+
If None, the method uses the default config JSON file.
|
|
828
|
+
selected_entities : Union[str, Dict, List], optional
|
|
829
|
+
Entities to select from the loaded entities. Can be:
|
|
830
|
+
- A string with comma-separated entity names
|
|
831
|
+
- A dictionary with entity names as keys
|
|
832
|
+
- A list of entity names
|
|
833
|
+
If None, all entities are included.
|
|
834
|
+
|
|
835
|
+
Returns
|
|
836
|
+
-------
|
|
837
|
+
Dict
|
|
838
|
+
Dictionary of entity names and their values.
|
|
839
|
+
|
|
840
|
+
Raises
|
|
841
|
+
------
|
|
842
|
+
ValueError
|
|
843
|
+
If the provided JSON file path is invalid or the JSON format is incorrect.
|
|
844
|
+
FileNotFoundError
|
|
845
|
+
If the specified JSON file does not exist.
|
|
846
|
+
|
|
847
|
+
Examples
|
|
848
|
+
--------
|
|
849
|
+
>>> # Using default config file (returns all entities)
|
|
850
|
+
>>> entities4table()
|
|
851
|
+
{'sub': {'...'}, 'ses': {'...'}, ... 'scale': {'...'}}
|
|
852
|
+
|
|
853
|
+
>>> # Using a custom JSON file
|
|
854
|
+
>>> entities4table('path/to/custom/entities.json')
|
|
855
|
+
{'sub': {'...'}, 'ses': {'...'}, ... 'scale': {'...'}}
|
|
856
|
+
|
|
857
|
+
>>> # Selecting specific entities
|
|
858
|
+
>>> entities4table(selected_entities='sub,ses,run')
|
|
859
|
+
{'sub': {'...'}, 'ses': {'...'}, 'run': {'...'}}
|
|
860
|
+
|
|
861
|
+
>>> # Using a dictionary to select entities
|
|
862
|
+
>>> entities4table(selected_entities={'sub': None, 'ses': None})
|
|
863
|
+
{'sub': {'...'}, 'ses': {'...'}}
|
|
864
|
+
|
|
865
|
+
>>> # Using a list to select entities
|
|
866
|
+
>>> entities4table(selected_entities=['sub', 'ses'])
|
|
867
|
+
{'sub': {'...'}, 'ses': {'...'}}
|
|
868
|
+
"""
|
|
869
|
+
import os
|
|
870
|
+
import json
|
|
871
|
+
from typing import Dict, Union, List
|
|
872
|
+
|
|
873
|
+
# Load entities from JSON
|
|
874
|
+
if entities_json is None:
|
|
875
|
+
# Define path to default config JSON
|
|
876
|
+
default_config_path = os.path.join(
|
|
877
|
+
os.path.dirname(__file__), "config", "bids.json"
|
|
878
|
+
)
|
|
879
|
+
try:
|
|
880
|
+
config_data = load_bids_json(default_config_path)
|
|
881
|
+
|
|
882
|
+
# Merge raw and derivatives entities
|
|
883
|
+
if (
|
|
884
|
+
"bids_entities" in config_data
|
|
885
|
+
and "raw_entities" in config_data["bids_entities"]
|
|
886
|
+
and "derivatives_entities" in config_data["bids_entities"]
|
|
887
|
+
):
|
|
888
|
+
ent_out_dict = {
|
|
889
|
+
**config_data["bids_entities"]["raw_entities"],
|
|
890
|
+
**config_data["bids_entities"]["derivatives_entities"],
|
|
891
|
+
}
|
|
892
|
+
else:
|
|
893
|
+
raise ValueError(
|
|
894
|
+
"Default config JSON does not have the expected structure."
|
|
895
|
+
)
|
|
896
|
+
except FileNotFoundError:
|
|
897
|
+
raise FileNotFoundError(
|
|
898
|
+
f"Default configuration file not found at: {default_config_path}"
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
elif isinstance(entities_json, str):
|
|
902
|
+
# Load from provided JSON file path
|
|
903
|
+
if not os.path.isfile(entities_json):
|
|
904
|
+
raise FileNotFoundError(f"JSON file not found: {entities_json}")
|
|
905
|
+
|
|
906
|
+
try:
|
|
907
|
+
|
|
908
|
+
ent_out_dict = cltmisc.extract_string_values(entities_json)
|
|
909
|
+
except json.JSONDecodeError:
|
|
910
|
+
raise ValueError(f"Error parsing the JSON file: {entities_json}")
|
|
911
|
+
else:
|
|
912
|
+
raise TypeError("entities_json must be None or a string path to a JSON file.")
|
|
913
|
+
|
|
914
|
+
# Filter entities based on selected_entities
|
|
915
|
+
if selected_entities is not None:
|
|
916
|
+
selected_entity_keys = []
|
|
917
|
+
|
|
918
|
+
# Handle string input (convert to list of keys)
|
|
919
|
+
if isinstance(selected_entities, str):
|
|
920
|
+
try:
|
|
921
|
+
# Assume it's a comma-separated string
|
|
922
|
+
if "," in selected_entities:
|
|
923
|
+
selected_entity_keys = [
|
|
924
|
+
e.strip() for e in selected_entities.split(",")
|
|
925
|
+
]
|
|
926
|
+
elif is_bids_filename(selected_entities):
|
|
927
|
+
selected_entities = str2entity(selected_entities)
|
|
928
|
+
selected_entity_keys = list(selected_entities.keys())
|
|
929
|
+
|
|
930
|
+
except (ImportError, AttributeError):
|
|
931
|
+
raise ValueError(
|
|
932
|
+
"Cannot parse selected_entities string. Provide a comma-separated list or a BIDs-like string (e.g. sub-XXX_ses-SSS_run-01 )."
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
# Handle dictionary input
|
|
936
|
+
elif isinstance(selected_entities, dict):
|
|
937
|
+
selected_entity_keys = list(selected_entities.keys())
|
|
938
|
+
|
|
939
|
+
# Handle list input
|
|
940
|
+
elif isinstance(selected_entities, list):
|
|
941
|
+
selected_entity_keys = selected_entities
|
|
942
|
+
|
|
943
|
+
# Filter the output dictionary to include only selected entities
|
|
944
|
+
ent_out_dict = {
|
|
945
|
+
k: v for k, v in ent_out_dict.items() if k in selected_entity_keys
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
return ent_out_dict
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
####################################################################################################
|
|
952
|
+
def entities_to_table(
|
|
953
|
+
filepath: str,
|
|
954
|
+
entities_to_extract: Optional[Union[str, List[str], Dict[str, str]]] = None,
|
|
955
|
+
) -> pd.DataFrame:
|
|
956
|
+
"""
|
|
957
|
+
Creates a DataFrame with BIDS entities extracted from a filename.
|
|
958
|
+
|
|
959
|
+
This function parses BIDS-compliant filenames to extract specified entities
|
|
960
|
+
(such as subject, session, task) and organizes them into a DataFrame with
|
|
961
|
+
appropriate column names. It supports special handling for certain entities
|
|
962
|
+
like atlas and description fields.
|
|
963
|
+
|
|
964
|
+
Parameters
|
|
965
|
+
----------
|
|
966
|
+
filepath : str
|
|
967
|
+
Full path to the BIDS file from which to extract entities.
|
|
968
|
+
entities_to_extract : str, list, dict, or None, default=None
|
|
969
|
+
Specifies which entities to extract from the filename:
|
|
970
|
+
- If str: A single entity name to extract
|
|
971
|
+
- If list: Multiple entity names to extract
|
|
972
|
+
- If dict: Keys are entity names, values are custom column names
|
|
973
|
+
- If None: Returns a single column with the full filename
|
|
974
|
+
|
|
975
|
+
Returns
|
|
976
|
+
-------
|
|
977
|
+
pd.DataFrame
|
|
978
|
+
DataFrame containing the extracted entities as columns.
|
|
979
|
+
If the file is not BIDS-compliant, returns an empty DataFrame.
|
|
980
|
+
|
|
981
|
+
Examples
|
|
982
|
+
--------
|
|
983
|
+
>>> # Extract subject and session from a BIDS file
|
|
984
|
+
>>> df = entities_to_table(
|
|
985
|
+
... '/data/sub-01/ses-pre/sub-01_ses-pre_task-rest_bold.nii.gz',
|
|
986
|
+
... ['sub', 'ses']
|
|
987
|
+
... )
|
|
988
|
+
>>> print(df)
|
|
989
|
+
Participant Session
|
|
990
|
+
0 01 pre
|
|
991
|
+
|
|
992
|
+
>>> # Extract entities with custom column names
|
|
993
|
+
>>> df = entities_to_table(
|
|
994
|
+
... '/data/sub-01/anat/sub-01_T1w.nii.gz',
|
|
995
|
+
... {'sub': 'SubjectID'}
|
|
996
|
+
... )
|
|
997
|
+
>>> print(df)
|
|
998
|
+
SubjectID
|
|
999
|
+
0 01
|
|
1000
|
+
|
|
1001
|
+
>>> # Extract atlas with special handling
|
|
1002
|
+
>>> df = entities_to_table(
|
|
1003
|
+
... '/data/sub-01/atlas-chimera123_desc-parcellation.nii.gz',
|
|
1004
|
+
... ['atlas', 'desc']
|
|
1005
|
+
... )
|
|
1006
|
+
>>> print(df)
|
|
1007
|
+
Atlas ChimeraCode Description
|
|
1008
|
+
0 chimera 123 parcellation
|
|
1009
|
+
|
|
1010
|
+
"""
|
|
1011
|
+
# Type checking
|
|
1012
|
+
if filepath is None or not isinstance(filepath, str):
|
|
1013
|
+
raise TypeError("filepath must be a string")
|
|
1014
|
+
|
|
1015
|
+
# Reading the mapping dictionary
|
|
1016
|
+
cwd = os.path.dirname(os.path.abspath(__file__))
|
|
1017
|
+
default_config_path = os.path.join(cwd, "config", "bids.json")
|
|
1018
|
+
try:
|
|
1019
|
+
with open(default_config_path, "r") as f:
|
|
1020
|
+
config_data = json.load(f)
|
|
1021
|
+
|
|
1022
|
+
# Merge raw and derivatives entities
|
|
1023
|
+
if (
|
|
1024
|
+
"bids_entities" in config_data
|
|
1025
|
+
and "raw_entities" in config_data["bids_entities"]
|
|
1026
|
+
and "derivatives_entities" in config_data["bids_entities"]
|
|
1027
|
+
):
|
|
1028
|
+
ent_out_dict = {
|
|
1029
|
+
**config_data["bids_entities"]["raw_entities"],
|
|
1030
|
+
**config_data["bids_entities"]["derivatives_entities"],
|
|
1031
|
+
}
|
|
1032
|
+
else:
|
|
1033
|
+
raise ValueError(
|
|
1034
|
+
"Default config JSON does not have the expected structure."
|
|
1035
|
+
)
|
|
1036
|
+
except FileNotFoundError:
|
|
1037
|
+
raise FileNotFoundError(
|
|
1038
|
+
f"Default configuration file not found at: {default_config_path}"
|
|
1039
|
+
)
|
|
1040
|
+
except json.JSONDecodeError:
|
|
1041
|
+
raise ValueError(
|
|
1042
|
+
f"Error parsing the default configuration file: {default_config_path}"
|
|
1043
|
+
)
|
|
1044
|
+
|
|
1045
|
+
# Extract file directory and name
|
|
1046
|
+
file_directory = os.path.dirname(filepath)
|
|
1047
|
+
filename = os.path.basename(filepath)
|
|
1048
|
+
|
|
1049
|
+
# Create an empty DataFrame
|
|
1050
|
+
result_df = pd.DataFrame()
|
|
1051
|
+
|
|
1052
|
+
# Only process if the file follows BIDS naming convention
|
|
1053
|
+
if is_bids_filename(filename):
|
|
1054
|
+
|
|
1055
|
+
# Parse entities from the filename
|
|
1056
|
+
entities_dict = str2entity(filename)
|
|
1057
|
+
|
|
1058
|
+
# Remove the 'extension' key if it exists
|
|
1059
|
+
if "extension" in entities_dict:
|
|
1060
|
+
entities_dict.pop("extension")
|
|
1061
|
+
# Remove the 'suffix' key if it exists
|
|
1062
|
+
if "suffix" in entities_dict:
|
|
1063
|
+
entities_dict.pop("suffix")
|
|
1064
|
+
|
|
1065
|
+
if entities_to_extract is not None:
|
|
1066
|
+
# Convert string input to list
|
|
1067
|
+
if isinstance(entities_to_extract, str):
|
|
1068
|
+
entities_to_extract = [entities_to_extract]
|
|
1069
|
+
|
|
1070
|
+
# Convert list to dictionary for unified processing
|
|
1071
|
+
if isinstance(entities_to_extract, list):
|
|
1072
|
+
# Create dict with entity names as both keys and values
|
|
1073
|
+
entities_to_extract = {entity: "" for entity in entities_to_extract}
|
|
1074
|
+
else:
|
|
1075
|
+
entities_to_extract = {entity: "" for entity in entities_dict.keys()}
|
|
1076
|
+
|
|
1077
|
+
# Initialize DataFrame with one empty row
|
|
1078
|
+
if result_df.empty:
|
|
1079
|
+
result_df = pd.DataFrame([{}])
|
|
1080
|
+
|
|
1081
|
+
# Process entities in reverse order to maintain column order
|
|
1082
|
+
entity_keys = list(entities_to_extract.keys())
|
|
1083
|
+
for entity in reversed(entity_keys):
|
|
1084
|
+
# Get entity value from filename or empty string if not found
|
|
1085
|
+
value = entities_dict.get(entity, "")
|
|
1086
|
+
|
|
1087
|
+
if entity in ent_out_dict.keys():
|
|
1088
|
+
# If entity is in the mapping dictionary, use its value
|
|
1089
|
+
var_name = ent_out_dict[entity]
|
|
1090
|
+
if entity == "atlas":
|
|
1091
|
+
# Special handling for atlas
|
|
1092
|
+
if "chimera" in value:
|
|
1093
|
+
result_df.insert(0, "ChimeraCode", value.replace("chimera", ""))
|
|
1094
|
+
result_df.insert(0, "Atlas", "chimera")
|
|
1095
|
+
|
|
1096
|
+
else:
|
|
1097
|
+
result_df.insert(0, "ChimeraCode", "")
|
|
1098
|
+
result_df.insert(0, "Atlas", value)
|
|
1099
|
+
|
|
1100
|
+
elif entity == "desc":
|
|
1101
|
+
# Special handling for description
|
|
1102
|
+
result_df.insert(0, "Description", value)
|
|
1103
|
+
if "grow" in value:
|
|
1104
|
+
result_df.insert(0, "GrowIntoWM", value.replace("grow", ""))
|
|
1105
|
+
|
|
1106
|
+
else:
|
|
1107
|
+
result_df.insert(0, var_name, value)
|
|
1108
|
+
else:
|
|
1109
|
+
|
|
1110
|
+
result_df.insert(0, entity.capitalize(), value)
|
|
1111
|
+
|
|
1112
|
+
else:
|
|
1113
|
+
# If no entities specified, use full filename as Participant
|
|
1114
|
+
if result_df.empty:
|
|
1115
|
+
result_df = pd.DataFrame([{}])
|
|
1116
|
+
|
|
1117
|
+
if "Participant" not in result_df.columns:
|
|
1118
|
+
# If 'Participant' column doesn't exist, create it
|
|
1119
|
+
# Remove the extension from the filename in case it exists
|
|
1120
|
+
temp = os.path.splitext(filename)[0]
|
|
1121
|
+
result_df.insert(0, "Participant", temp)
|
|
1122
|
+
|
|
1123
|
+
# Declaring all the columns as string type
|
|
1124
|
+
for col in result_df.columns:
|
|
1125
|
+
result_df[col] = result_df[col].astype(str)
|
|
1126
|
+
|
|
1127
|
+
return result_df
|
|
1128
|
+
|
|
1129
|
+
|
|
1130
|
+
####################################################################################################
|
|
1131
|
+
####################################################################################################
|
|
1132
|
+
############ ############
|
|
1133
|
+
############ ############
|
|
1134
|
+
############ Section 2: Methods dedicated to work with BIDs file organization ############
|
|
1135
|
+
############ ############
|
|
1136
|
+
############ ############
|
|
1137
|
+
####################################################################################################
|
|
1138
|
+
####################################################################################################
|
|
1139
|
+
def get_subjects(bids_dir: str) -> list:
|
|
1140
|
+
"""
|
|
1141
|
+
Get a list of all subjects in the BIDs directory.
|
|
1142
|
+
|
|
1143
|
+
Parameters
|
|
1144
|
+
----------
|
|
1145
|
+
bids_dir : str
|
|
1146
|
+
Path to the BIDs directory.
|
|
1147
|
+
|
|
1148
|
+
Returns
|
|
1149
|
+
-------
|
|
1150
|
+
list
|
|
1151
|
+
List of subject IDs.
|
|
1152
|
+
|
|
1153
|
+
Usage example:
|
|
1154
|
+
>>> bids_dir = "/path/to/bids"
|
|
1155
|
+
>>> print(get_subjects(bids_dir))
|
|
1156
|
+
["sub-01", "sub-02", ...]
|
|
1157
|
+
|
|
1158
|
+
"""
|
|
1159
|
+
subjects = []
|
|
1160
|
+
|
|
1161
|
+
for root, dirs, files in os.walk(bids_dir):
|
|
1162
|
+
for dir_name in dirs:
|
|
1163
|
+
if dir_name.startswith("sub-"):
|
|
1164
|
+
subjects.append(dir_name)
|
|
1165
|
+
|
|
1166
|
+
return subjects
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
####################################################################################################
|
|
1170
|
+
def copy_bids_folder(
|
|
1171
|
+
bids_dir: str,
|
|
1172
|
+
out_dir: str,
|
|
1173
|
+
subjects_to_copy: Union[list, str] = None,
|
|
1174
|
+
folders_to_copy: Union[list, str] = "all",
|
|
1175
|
+
deriv_dir: str = None,
|
|
1176
|
+
include_derivatives: Union[str, list] = None,
|
|
1177
|
+
):
|
|
1178
|
+
"""
|
|
1179
|
+
This function copies the BIDs folder and its derivatives for given subjects to a new location.
|
|
1180
|
+
|
|
1181
|
+
Parameters
|
|
1182
|
+
----------
|
|
1183
|
+
bids_dir : str
|
|
1184
|
+
Path to the BIDs directory.
|
|
1185
|
+
out_dir : str
|
|
1186
|
+
Path to the output directory where the copied BIDs folder will be saved.
|
|
1187
|
+
subjects_to_copy : list or str, optional
|
|
1188
|
+
List of subject IDs to copy. If None, all subjects will be copied.
|
|
1189
|
+
folders_to_copy : list or str, optional
|
|
1190
|
+
List of BIDs folders to copy. If "all", all folders will be copied. Default is "all".
|
|
1191
|
+
deriv_dir : str, optional
|
|
1192
|
+
Path to the derivatives directory. If None, it will be set to "derivatives" in the BIDs directory.
|
|
1193
|
+
include_derivatives : str or list, optional
|
|
1194
|
+
List of derivatives to include. If "all", all derivatives will be included. Default is None.
|
|
1195
|
+
If None, no derivatives will be copied.
|
|
1196
|
+
If "chimera", only the chimera derivatives will be copied.
|
|
1197
|
+
If "all", all derivatives will be copied.
|
|
1198
|
+
If a list, only the derivatives in the list will be copied.
|
|
1199
|
+
If a string, only the derivatives with the name in the string will be copied.
|
|
1200
|
+
|
|
1201
|
+
Returns
|
|
1202
|
+
-------
|
|
1203
|
+
None
|
|
1204
|
+
Copies the specified folders and subjects to the output directory.
|
|
1205
|
+
|
|
1206
|
+
Usage example:
|
|
1207
|
+
>>> bids_dir = "/path/to/bids"
|
|
1208
|
+
>>> out_dir = "/path/to/output"
|
|
1209
|
+
>>> copy_bids_folder(bids_dir, out_dir, subjects_to_copy=["sub-01"], folders_to_copy=["anat"])
|
|
1210
|
+
>>> copy_bids_folder(bids_dir, out_dir, subjects_to_copy=["sub-01"], include_derivatives=["chimera", "freesurfer"])
|
|
1211
|
+
>>> copy_bids_folder(bids_dir, out_dir, subjects_to_copy=["sub-01"], deriv_dir="/path/to/derivatives")
|
|
1212
|
+
|
|
1213
|
+
"""
|
|
1214
|
+
|
|
1215
|
+
bids_dir = cltmisc.remove_trailing_separators(bids_dir)
|
|
1216
|
+
out_dir = cltmisc.remove_trailing_separators(out_dir)
|
|
1217
|
+
|
|
1218
|
+
if not os.path.isdir(bids_dir):
|
|
1219
|
+
raise FileNotFoundError(f"The BIDs directory {bids_dir} does not exist.")
|
|
1220
|
+
|
|
1221
|
+
if not os.path.isdir(out_dir):
|
|
1222
|
+
raise FileNotFoundError(f"The output directory {out_dir} does not exist.")
|
|
1223
|
+
|
|
1224
|
+
if deriv_dir is not None:
|
|
1225
|
+
deriv_dir = cltmisc.remove_trailing_separators(deriv_dir)
|
|
1226
|
+
if not os.path.isdir(deriv_dir):
|
|
1227
|
+
raise FileNotFoundError(
|
|
1228
|
+
f"The derivatives directory {deriv_dir} does not exist."
|
|
1229
|
+
)
|
|
1230
|
+
|
|
1231
|
+
# Selecting the subjects that will be copied
|
|
1232
|
+
if isinstance(subjects_to_copy, str):
|
|
1233
|
+
subjects_to_copy = [subjects_to_copy]
|
|
1234
|
+
|
|
1235
|
+
if subjects_to_copy is None:
|
|
1236
|
+
subjects_to_copy = get_subjects(bids_dir)
|
|
1237
|
+
else:
|
|
1238
|
+
# Check if the cad "sub-" is in the subjects_to_copy of not add it
|
|
1239
|
+
subjects_to_copy = [
|
|
1240
|
+
sub if sub.startswith("sub-") else f"sub-{sub}" for sub in subjects_to_copy
|
|
1241
|
+
]
|
|
1242
|
+
|
|
1243
|
+
# Selecting the BIDs folders that will be copied
|
|
1244
|
+
if isinstance(folders_to_copy, str):
|
|
1245
|
+
folders_to_copy = [folders_to_copy]
|
|
1246
|
+
|
|
1247
|
+
if "all" in folders_to_copy:
|
|
1248
|
+
folders_to_copy = ["all"]
|
|
1249
|
+
|
|
1250
|
+
# Number of subjects to copy
|
|
1251
|
+
n_subj = len(subjects_to_copy)
|
|
1252
|
+
|
|
1253
|
+
# Selecting the derivatives folder
|
|
1254
|
+
if include_derivatives is not None:
|
|
1255
|
+
copy_derivatives = True
|
|
1256
|
+
|
|
1257
|
+
if deriv_dir is None:
|
|
1258
|
+
deriv_dir = os.path.join(bids_dir, "derivatives")
|
|
1259
|
+
|
|
1260
|
+
if not os.path.isdir(deriv_dir):
|
|
1261
|
+
# Lunch a warning message if the derivatives folder does not exist
|
|
1262
|
+
print("WARNING: The derivatives folder does not exist.")
|
|
1263
|
+
print("WARNING: The derivatives folder will not be copied.")
|
|
1264
|
+
copy_derivatives = False
|
|
1265
|
+
else:
|
|
1266
|
+
# Check if the derivatives folder is empty
|
|
1267
|
+
if len(os.listdir(deriv_dir)) == 0:
|
|
1268
|
+
print("WARNING: The derivatives folder is empty.")
|
|
1269
|
+
print("WARNING: The derivatives folder will not be copied.")
|
|
1270
|
+
copy_derivatives = False
|
|
1271
|
+
|
|
1272
|
+
# Selecting all the derivatives folders
|
|
1273
|
+
directories = os.listdir(deriv_dir)
|
|
1274
|
+
der_pipe_folders = []
|
|
1275
|
+
for directory in directories:
|
|
1276
|
+
pipe_dir = os.path.join(deriv_dir, directory)
|
|
1277
|
+
if not directory.startswith(".") and os.path.isdir(pipe_dir):
|
|
1278
|
+
der_pipe_folders.append(pipe_dir)
|
|
1279
|
+
|
|
1280
|
+
if isinstance(include_derivatives, str):
|
|
1281
|
+
include_derivatives = [include_derivatives]
|
|
1282
|
+
|
|
1283
|
+
if "all" not in include_derivatives:
|
|
1284
|
+
include_derivatives = [
|
|
1285
|
+
os.path.join(deriv_dir, i) for i in include_derivatives
|
|
1286
|
+
]
|
|
1287
|
+
der_pipe_folders = cltmisc.list_intercept(
|
|
1288
|
+
der_pipe_folders, include_derivatives
|
|
1289
|
+
)
|
|
1290
|
+
|
|
1291
|
+
if len(der_pipe_folders) == 0:
|
|
1292
|
+
print(
|
|
1293
|
+
"WARNING: No derivatives folders were found with the specified names."
|
|
1294
|
+
)
|
|
1295
|
+
copy_derivatives = False
|
|
1296
|
+
else:
|
|
1297
|
+
copy_derivatives = False
|
|
1298
|
+
der_pipe_folders = []
|
|
1299
|
+
|
|
1300
|
+
progress = Progress(
|
|
1301
|
+
TextColumn("[bold blue]{task.description}", justify="right"),
|
|
1302
|
+
BarColumn(bar_width=None),
|
|
1303
|
+
"[progress.percentage]{task.percentage:>3.0f}%",
|
|
1304
|
+
TimeRemainingColumn(),
|
|
1305
|
+
MofNCompleteColumn(),
|
|
1306
|
+
)
|
|
1307
|
+
with progress:
|
|
1308
|
+
task = progress.add_task(
|
|
1309
|
+
"[cyan]Copying BIDs folder and derivatives...", total=None
|
|
1310
|
+
)
|
|
1311
|
+
progress.update(task, total=n_subj)
|
|
1312
|
+
progress.start_task(task)
|
|
1313
|
+
progress.update(task, completed=0)
|
|
1314
|
+
|
|
1315
|
+
# Loop around all the subjects
|
|
1316
|
+
for i, full_id in enumerate(subjects_to_copy): # Loop along the IDs
|
|
1317
|
+
|
|
1318
|
+
# Extract the subject id
|
|
1319
|
+
subj_entity = str2entity(full_id)
|
|
1320
|
+
|
|
1321
|
+
# Remove the extension from the subj_entity
|
|
1322
|
+
if "extension" in subj_entity.keys():
|
|
1323
|
+
del subj_entity["extension"]
|
|
1324
|
+
|
|
1325
|
+
if "suffix" in subj_entity.keys():
|
|
1326
|
+
suffix = subj_entity["suffix"]
|
|
1327
|
+
del subj_entity["suffix"]
|
|
1328
|
+
|
|
1329
|
+
else:
|
|
1330
|
+
suffix = None
|
|
1331
|
+
|
|
1332
|
+
# Generate a list with the format [key_value]
|
|
1333
|
+
file_filter = [f"{k}-{v}" for k, v in subj_entity.items()]
|
|
1334
|
+
fold_filter = [f"{k}-{v}" for k, v in subj_entity.items()]
|
|
1335
|
+
|
|
1336
|
+
if suffix is not None:
|
|
1337
|
+
file_filter.append(f"_{suffix}")
|
|
1338
|
+
|
|
1339
|
+
if subj_entity is None:
|
|
1340
|
+
print(f"WARNING: The subject ID {full_id} is not valid.")
|
|
1341
|
+
continue
|
|
1342
|
+
|
|
1343
|
+
subj_id = subj_entity["sub"]
|
|
1344
|
+
|
|
1345
|
+
subj_dir = os.path.join(bids_dir, f"sub-{subj_id}")
|
|
1346
|
+
all_subj_subfold = cltmisc.get_leaf_directories(subj_dir)
|
|
1347
|
+
|
|
1348
|
+
# Upgrade the progress bar and include the subject id on the text
|
|
1349
|
+
progress.update(
|
|
1350
|
+
task, description=f"[red]Copying subject: {full_id}", completed=i + 1
|
|
1351
|
+
)
|
|
1352
|
+
|
|
1353
|
+
# Detect the session id if it exists
|
|
1354
|
+
ses_id = None
|
|
1355
|
+
if "ses" in subj_entity.keys():
|
|
1356
|
+
ses_id = subj_entity["ses"]
|
|
1357
|
+
full_ses_id = f"ses-{ses_id}"
|
|
1358
|
+
|
|
1359
|
+
all_subj_subfold = cltmisc.filter_by_substring(
|
|
1360
|
+
input_list=all_subj_subfold, or_filter=full_ses_id
|
|
1361
|
+
)
|
|
1362
|
+
|
|
1363
|
+
####### Copying the BIDs folder
|
|
1364
|
+
if "all" not in folders_to_copy:
|
|
1365
|
+
tmp_list = [os.path.basename(i) for i in all_subj_subfold]
|
|
1366
|
+
indexes = cltmisc.get_indexes_by_substring(tmp_list, folders_to_copy)
|
|
1367
|
+
all_subj_subfold = [all_subj_subfold[i] for i in indexes]
|
|
1368
|
+
|
|
1369
|
+
raw_files_to_copy = []
|
|
1370
|
+
# Loop along all the folders to copy
|
|
1371
|
+
for fc in all_subj_subfold:
|
|
1372
|
+
all_subj_raw_files = cltmisc.get_all_files(fc)
|
|
1373
|
+
|
|
1374
|
+
raw_files_to_copy = raw_files_to_copy + all_subj_raw_files
|
|
1375
|
+
|
|
1376
|
+
# Filtering the files to copy according to the full
|
|
1377
|
+
raw_files_to_copy = cltmisc.filter_by_substring(
|
|
1378
|
+
input_list=raw_files_to_copy,
|
|
1379
|
+
or_filter=f"sub-{subj_id}",
|
|
1380
|
+
and_filter=file_filter,
|
|
1381
|
+
)
|
|
1382
|
+
|
|
1383
|
+
for file in raw_files_to_copy:
|
|
1384
|
+
if os.path.isfile(file):
|
|
1385
|
+
# Copying the file
|
|
1386
|
+
try:
|
|
1387
|
+
dest_dir = file.replace(bids_dir, out_dir)
|
|
1388
|
+
os.makedirs(os.path.dirname(dest_dir), exist_ok=True)
|
|
1389
|
+
shutil.copy2(file, dest_dir)
|
|
1390
|
+
except:
|
|
1391
|
+
print(f"WARNING: The file {file} could not be copied.")
|
|
1392
|
+
continue
|
|
1393
|
+
|
|
1394
|
+
# Copying the Derivatives folder
|
|
1395
|
+
if copy_derivatives:
|
|
1396
|
+
# Loop along all the derivatives folders
|
|
1397
|
+
for pipe_dir in der_pipe_folders:
|
|
1398
|
+
# Check if the derivatives folder exists
|
|
1399
|
+
if os.path.isdir(pipe_dir):
|
|
1400
|
+
# Copying the derivatives folder
|
|
1401
|
+
subj_deriv = glob(os.path.join(pipe_dir, f"sub-{subj_id}*"))
|
|
1402
|
+
|
|
1403
|
+
if len(subj_deriv) > 0:
|
|
1404
|
+
|
|
1405
|
+
# Detecting the files and folders to copy.
|
|
1406
|
+
# We are doing this separation because there are derivatives that contain
|
|
1407
|
+
# only the folders with the name of the subject and not the files (i.e. freesurfer)
|
|
1408
|
+
|
|
1409
|
+
# Loop along all the folders to copy
|
|
1410
|
+
deriv_files_to_copy = []
|
|
1411
|
+
deriv_folds_to_copy = []
|
|
1412
|
+
for deriv in subj_deriv:
|
|
1413
|
+
|
|
1414
|
+
# Detect the files and folders to copy
|
|
1415
|
+
tmp_fold_to_copy = cltmisc.filter_by_substring(
|
|
1416
|
+
input_list=deriv,
|
|
1417
|
+
or_filter=f"sub-{subj_id}",
|
|
1418
|
+
and_filter=fold_filter,
|
|
1419
|
+
)
|
|
1420
|
+
|
|
1421
|
+
if len(tmp_fold_to_copy) > 0:
|
|
1422
|
+
# Detect the files and folders to copy
|
|
1423
|
+
for tmp in tmp_fold_to_copy:
|
|
1424
|
+
if os.path.isdir(tmp):
|
|
1425
|
+
deriv_folds_to_copy.append(tmp)
|
|
1426
|
+
else:
|
|
1427
|
+
deriv_files_to_copy.append(tmp)
|
|
1428
|
+
else:
|
|
1429
|
+
all_subj_deriv_files = cltmisc.get_all_files(deriv)
|
|
1430
|
+
|
|
1431
|
+
# Filtering the files to copy according to the full
|
|
1432
|
+
all_subj_deriv_files = cltmisc.filter_by_substring(
|
|
1433
|
+
input_list=all_subj_deriv_files,
|
|
1434
|
+
or_filter=f"sub-{subj_id}",
|
|
1435
|
+
and_filter=fold_filter,
|
|
1436
|
+
)
|
|
1437
|
+
|
|
1438
|
+
deriv_files_to_copy = (
|
|
1439
|
+
deriv_files_to_copy + all_subj_deriv_files
|
|
1440
|
+
)
|
|
1441
|
+
|
|
1442
|
+
# Copying the folders
|
|
1443
|
+
if len(deriv_folds_to_copy) > 0:
|
|
1444
|
+
for fold in deriv_folds_to_copy:
|
|
1445
|
+
try:
|
|
1446
|
+
dest_dir = fold.replace(
|
|
1447
|
+
deriv_dir,
|
|
1448
|
+
os.path.join(out_dir, "derivatives"),
|
|
1449
|
+
)
|
|
1450
|
+
os.makedirs(dest_dir, exist_ok=True)
|
|
1451
|
+
shutil.copytree(
|
|
1452
|
+
fold, dest_dir, dirs_exist_ok=True
|
|
1453
|
+
)
|
|
1454
|
+
except:
|
|
1455
|
+
print(
|
|
1456
|
+
f"WARNING: The folder or file {fold} could not be copied."
|
|
1457
|
+
)
|
|
1458
|
+
continue
|
|
1459
|
+
|
|
1460
|
+
# Copying the files
|
|
1461
|
+
if len(deriv_files_to_copy) > 0:
|
|
1462
|
+
for file in deriv_files_to_copy:
|
|
1463
|
+
try:
|
|
1464
|
+
dest_dir = file.replace(
|
|
1465
|
+
deriv_dir,
|
|
1466
|
+
os.path.join(out_dir, "derivatives"),
|
|
1467
|
+
)
|
|
1468
|
+
os.makedirs(
|
|
1469
|
+
os.path.dirname(dest_dir), exist_ok=True
|
|
1470
|
+
)
|
|
1471
|
+
shutil.copy2(file, dest_dir)
|
|
1472
|
+
except:
|
|
1473
|
+
print(
|
|
1474
|
+
f"WARNING: The file {file} could not be copied."
|
|
1475
|
+
)
|
|
1476
|
+
continue
|
|
1477
|
+
|
|
1478
|
+
# Update the progress bar to 100%
|
|
1479
|
+
progress.update(task, completed=n_subj)
|
|
1480
|
+
|
|
1481
|
+
|
|
1482
|
+
####################################################################################################
|
|
1483
|
+
def get_bids_database_table(
|
|
1484
|
+
root_dir: str, output_table: Optional[str] = None
|
|
1485
|
+
) -> pd.DataFrame:
|
|
1486
|
+
"""
|
|
1487
|
+
Generate a comprehensive summary table of all neuroimaging files in a BIDS dataset.
|
|
1488
|
+
|
|
1489
|
+
This function scans a BIDS dataset directory structure and creates a detailed table
|
|
1490
|
+
containing all BIDS entities (subject, session, acquisition, etc.) and file counts.
|
|
1491
|
+
The output table provides an overview of the dataset composition, making it easy to
|
|
1492
|
+
identify data availability, missing files, and dataset structure.
|
|
1493
|
+
|
|
1494
|
+
Parameters
|
|
1495
|
+
----------
|
|
1496
|
+
root_dir : str
|
|
1497
|
+
Path to the BIDS dataset root directory. This should be the top-level directory
|
|
1498
|
+
containing subject folders (sub-*) and optionally a dataset_description.json file.
|
|
1499
|
+
|
|
1500
|
+
output_table : str, optional
|
|
1501
|
+
Path where the resulting CSV table should be saved. If None, the table is not
|
|
1502
|
+
saved to disk but still returned as a DataFrame. Default is None.
|
|
1503
|
+
|
|
1504
|
+
Returns
|
|
1505
|
+
-------
|
|
1506
|
+
pd.DataFrame
|
|
1507
|
+
DataFrame with columns for each detected BIDS entity (Subject, Session,
|
|
1508
|
+
Acquisition, etc.), plus 'suffix' (image type like T1w, FLAIR) and 'N'
|
|
1509
|
+
(number of files for each unique combination). Each row represents a unique
|
|
1510
|
+
combination of BIDS entities and their file count.
|
|
1511
|
+
|
|
1512
|
+
Raises
|
|
1513
|
+
------
|
|
1514
|
+
FileNotFoundError
|
|
1515
|
+
If the specified root_dir does not exist.
|
|
1516
|
+
NotADirectoryError
|
|
1517
|
+
If root_dir exists but is not a directory.
|
|
1518
|
+
ValueError
|
|
1519
|
+
If no subjects are found in the BIDS dataset (no sub-* folders).
|
|
1520
|
+
|
|
1521
|
+
Examples
|
|
1522
|
+
--------
|
|
1523
|
+
Basic usage - analyze dataset and return summary table:
|
|
1524
|
+
|
|
1525
|
+
>>> import pandas as pd
|
|
1526
|
+
>>> bids_table = get_bids_table('/path/to/bids/dataset')
|
|
1527
|
+
>>> print(f"Dataset contains {len(bids_table)} unique file combinations")
|
|
1528
|
+
>>> print(f"Total files: {bids_table['N'].sum()}")
|
|
1529
|
+
|
|
1530
|
+
Save summary table to CSV file:
|
|
1531
|
+
|
|
1532
|
+
>>> bids_table = get_bids_table(
|
|
1533
|
+
... root_dir='/data/my_study',
|
|
1534
|
+
... output_table='/data/my_study/bids_summary.csv'
|
|
1535
|
+
... )
|
|
1536
|
+
|
|
1537
|
+
Analyze specific aspects of the dataset:
|
|
1538
|
+
|
|
1539
|
+
>>> # Count files by image type
|
|
1540
|
+
>>> suffix_counts = bids_table.groupby('suffix')['N'].sum()
|
|
1541
|
+
>>> print("Files by image type:")
|
|
1542
|
+
>>> print(suffix_counts)
|
|
1543
|
+
|
|
1544
|
+
>>> # Check data availability per subject
|
|
1545
|
+
>>> subject_counts = bids_table.groupby('Subject')['N'].sum()
|
|
1546
|
+
>>> print("Files per subject:")
|
|
1547
|
+
>>> print(subject_counts)
|
|
1548
|
+
|
|
1549
|
+
>>> # Find subjects with specific image types
|
|
1550
|
+
>>> t1w_subjects = bids_table[bids_table['suffix'] == 'T1w']['Subject'].unique()
|
|
1551
|
+
>>> print(f"Subjects with T1w images: {len(t1w_subjects)}")
|
|
1552
|
+
|
|
1553
|
+
Example output table structure:
|
|
1554
|
+
|
|
1555
|
+
>>> print(bids_table.head())
|
|
1556
|
+
Subject Session Acquisition suffix N
|
|
1557
|
+
0 sub-01 ses-01 acq-mprage T1w 1
|
|
1558
|
+
1 sub-01 ses-01 acq-space T2w 1
|
|
1559
|
+
2 sub-01 ses-01 None FLAIR 1
|
|
1560
|
+
3 sub-02 ses-01 acq-mprage T1w 1
|
|
1561
|
+
4 sub-02 ses-02 acq-mprage T1w 1
|
|
1562
|
+
|
|
1563
|
+
Notes
|
|
1564
|
+
-----
|
|
1565
|
+
- Only processes .nii.gz files (NIfTI compressed format)
|
|
1566
|
+
- Automatically detects all BIDS entities present in the dataset
|
|
1567
|
+
- Groups identical combinations and sums file counts
|
|
1568
|
+
- Results are sorted by Subject, Session, and suffix for readability
|
|
1569
|
+
- Progress is displayed using Rich progress bar during processing
|
|
1570
|
+
- Column names are converted to human-readable format (e.g., 'sub' -> 'Subject')
|
|
1571
|
+
|
|
1572
|
+
See Also
|
|
1573
|
+
--------
|
|
1574
|
+
clabtoolkit.bidstools.get_subjects : Get list of subjects in BIDS dataset
|
|
1575
|
+
clabtoolkit.bidstools.get_all_entities : Extract all BIDS entities from dataset
|
|
1576
|
+
clabtoolkit.bidstools.str2entity : Parse BIDS filename to extract entities
|
|
1577
|
+
"""
|
|
1578
|
+
|
|
1579
|
+
# Check if the root directory exists
|
|
1580
|
+
if not os.path.exists(root_dir):
|
|
1581
|
+
raise FileNotFoundError(f"BIDS root directory not found: {root_dir}")
|
|
1582
|
+
|
|
1583
|
+
if not os.path.isdir(root_dir):
|
|
1584
|
+
raise NotADirectoryError(f"Provided path is not a directory: {root_dir}")
|
|
1585
|
+
|
|
1586
|
+
# Initialize console
|
|
1587
|
+
console = Console()
|
|
1588
|
+
|
|
1589
|
+
# Get all the subjects in the BIDs dataset
|
|
1590
|
+
subj_ids = get_subjects(root_dir)
|
|
1591
|
+
|
|
1592
|
+
if not subj_ids:
|
|
1593
|
+
raise ValueError(f"No subjects found in the BIDS dataset at {root_dir}")
|
|
1594
|
+
|
|
1595
|
+
# Get all entities in the BIDs dataset
|
|
1596
|
+
all_dataset_entities, all_dataset_suffixes = get_all_entities(root_dir)
|
|
1597
|
+
|
|
1598
|
+
console.print(f"[green]Detected entities: {list(all_dataset_entities.keys())}")
|
|
1599
|
+
|
|
1600
|
+
n_subj = len(subj_ids)
|
|
1601
|
+
|
|
1602
|
+
# Create column names
|
|
1603
|
+
column_names = list(all_dataset_entities.keys())
|
|
1604
|
+
column_names.extend(["suffix", "N"])
|
|
1605
|
+
|
|
1606
|
+
# List to store all rows
|
|
1607
|
+
all_rows = []
|
|
1608
|
+
|
|
1609
|
+
# Create Progress with explicit configuration
|
|
1610
|
+
progress = Progress(
|
|
1611
|
+
SpinnerColumn(),
|
|
1612
|
+
TextColumn("[progress.description]{task.description}"),
|
|
1613
|
+
BarColumn(bar_width=None),
|
|
1614
|
+
"[progress.percentage]{task.percentage:>3.1f}%",
|
|
1615
|
+
"•",
|
|
1616
|
+
MofNCompleteColumn(),
|
|
1617
|
+
"•",
|
|
1618
|
+
TimeRemainingColumn(),
|
|
1619
|
+
console=console,
|
|
1620
|
+
auto_refresh=True,
|
|
1621
|
+
refresh_per_second=10,
|
|
1622
|
+
)
|
|
1623
|
+
|
|
1624
|
+
# Use the progress bar
|
|
1625
|
+
with progress:
|
|
1626
|
+
# Add the task
|
|
1627
|
+
task_id = progress.add_task(
|
|
1628
|
+
description="Starting BIDS processing...", total=n_subj
|
|
1629
|
+
)
|
|
1630
|
+
|
|
1631
|
+
# Process each subject
|
|
1632
|
+
for i, subj_id in enumerate(subj_ids):
|
|
1633
|
+
# Update task description at the start of processing
|
|
1634
|
+
progress.update(
|
|
1635
|
+
task_id, description=f"Processing subject {subj_id}...", completed=i
|
|
1636
|
+
)
|
|
1637
|
+
|
|
1638
|
+
subj_dir = os.path.join(root_dir, subj_id)
|
|
1639
|
+
all_files = cltmisc.get_all_files(
|
|
1640
|
+
subj_dir, recursive=True, or_filter=[".nii.gz"]
|
|
1641
|
+
)
|
|
1642
|
+
|
|
1643
|
+
files_processed = 0
|
|
1644
|
+
for file_path in all_files:
|
|
1645
|
+
file_name = os.path.basename(file_path)
|
|
1646
|
+
|
|
1647
|
+
# Parse the BIDS filename to get entities
|
|
1648
|
+
ent_dict = str2entity(file_name)
|
|
1649
|
+
|
|
1650
|
+
# Create a row dictionary with all possible entity columns initialized to None
|
|
1651
|
+
row_dict = {col: None for col in column_names}
|
|
1652
|
+
|
|
1653
|
+
# Fill in the entities that are present in this file
|
|
1654
|
+
for entity, value in ent_dict.items():
|
|
1655
|
+
if entity in row_dict:
|
|
1656
|
+
row_dict[entity] = value
|
|
1657
|
+
|
|
1658
|
+
# Extract suffix from filename
|
|
1659
|
+
row_dict["suffix"] = ent_dict["suffix"]
|
|
1660
|
+
|
|
1661
|
+
# Set N to 1 for individual files (we'll aggregate later)
|
|
1662
|
+
row_dict["N"] = 1
|
|
1663
|
+
|
|
1664
|
+
all_rows.append(row_dict)
|
|
1665
|
+
files_processed += 1
|
|
1666
|
+
|
|
1667
|
+
# Update progress AFTER processing each subject (this is the key fix!)
|
|
1668
|
+
progress.update(
|
|
1669
|
+
task_id,
|
|
1670
|
+
description=f"Completed {subj_id} ({files_processed} files)",
|
|
1671
|
+
completed=i + 1, # i+1 because we just finished this subject
|
|
1672
|
+
)
|
|
1673
|
+
|
|
1674
|
+
# Force refresh to ensure display updates
|
|
1675
|
+
progress.refresh()
|
|
1676
|
+
|
|
1677
|
+
# Small delay to see the progress update
|
|
1678
|
+
time.sleep(0.1)
|
|
1679
|
+
|
|
1680
|
+
# Final update
|
|
1681
|
+
progress.update(task_id, description="Processing complete!", completed=n_subj)
|
|
1682
|
+
|
|
1683
|
+
# Brief pause to show completion
|
|
1684
|
+
time.sleep(0.5)
|
|
1685
|
+
|
|
1686
|
+
# Create DataFrame from all rows
|
|
1687
|
+
df_table = pd.DataFrame(all_rows)
|
|
1688
|
+
|
|
1689
|
+
if df_table.empty:
|
|
1690
|
+
return df_table
|
|
1691
|
+
|
|
1692
|
+
# Group by all columns except 'N' and sum the counts
|
|
1693
|
+
groupby_cols = [col for col in df_table.columns if col != "N"]
|
|
1694
|
+
df_grouped = (
|
|
1695
|
+
df_table.groupby(groupby_cols, dropna=False).agg({"N": "sum"}).reset_index()
|
|
1696
|
+
)
|
|
1697
|
+
|
|
1698
|
+
# Sort the DataFrame for better readability
|
|
1699
|
+
if not df_grouped.empty:
|
|
1700
|
+
sort_cols = []
|
|
1701
|
+
if "sub" in df_grouped.columns:
|
|
1702
|
+
sort_cols.append("sub")
|
|
1703
|
+
if "ses" in df_grouped.columns:
|
|
1704
|
+
sort_cols.append("ses")
|
|
1705
|
+
if "suffix" in df_grouped.columns:
|
|
1706
|
+
sort_cols.append("suffix")
|
|
1707
|
+
|
|
1708
|
+
if sort_cols:
|
|
1709
|
+
df_grouped = df_grouped.sort_values(sort_cols).reset_index(drop=True)
|
|
1710
|
+
|
|
1711
|
+
all_entities = entities4table()
|
|
1712
|
+
|
|
1713
|
+
old_colnames = cltmisc.list_intercept(
|
|
1714
|
+
list(df_grouped.columns), list(all_entities.keys())
|
|
1715
|
+
)
|
|
1716
|
+
new_colnames = list(entities4table(selected_entities=old_colnames).values())
|
|
1717
|
+
|
|
1718
|
+
# Rename columns to match BIDS entities
|
|
1719
|
+
df_grouped.rename(columns=dict(zip(old_colnames, new_colnames)), inplace=True)
|
|
1720
|
+
|
|
1721
|
+
# Save the table if output path is provided
|
|
1722
|
+
if output_table:
|
|
1723
|
+
df_grouped.to_csv(output_table, index=False)
|
|
1724
|
+
|
|
1725
|
+
return df_grouped
|
|
1726
|
+
|
|
1727
|
+
|
|
1728
|
+
####################################################################################################
|
|
1729
|
+
def get_derivatives_folders(
|
|
1730
|
+
deriv_dir: str,
|
|
1731
|
+
) -> list:
|
|
1732
|
+
"""
|
|
1733
|
+
Get a list of all derivatives folders in the specified directory.
|
|
1734
|
+
|
|
1735
|
+
Parameters
|
|
1736
|
+
----------
|
|
1737
|
+
deriv_dir : str
|
|
1738
|
+
Path to the derivatives directory.
|
|
1739
|
+
|
|
1740
|
+
Returns
|
|
1741
|
+
-------
|
|
1742
|
+
list
|
|
1743
|
+
List of derivatives folder names.
|
|
1744
|
+
|
|
1745
|
+
Raises
|
|
1746
|
+
------
|
|
1747
|
+
ValueError
|
|
1748
|
+
If the derivatives directory does not exist.
|
|
1749
|
+
|
|
1750
|
+
TypeError
|
|
1751
|
+
If the derivatives directory is not a string.
|
|
1752
|
+
|
|
1753
|
+
Usage example:
|
|
1754
|
+
>>> deriv_dir = "/path/to/derivatives"
|
|
1755
|
+
>>> print(get_derivatives_folders(deriv_dir))
|
|
1756
|
+
|
|
1757
|
+
"""
|
|
1758
|
+
|
|
1759
|
+
# Check if the derivatives directory exists
|
|
1760
|
+
if not os.path.isdir(deriv_dir):
|
|
1761
|
+
raise ValueError("The derivatives directory does not exist.")
|
|
1762
|
+
|
|
1763
|
+
# Get all directories in the derivatives directory
|
|
1764
|
+
directories = os.listdir(deriv_dir)
|
|
1765
|
+
|
|
1766
|
+
# Filter out hidden directories and keep only valid directories
|
|
1767
|
+
der_pipe_folders = []
|
|
1768
|
+
for directory in directories:
|
|
1769
|
+
pipe_dir = os.path.join(deriv_dir, directory)
|
|
1770
|
+
if not directory.startswith(".") and os.path.isdir(pipe_dir):
|
|
1771
|
+
der_pipe_folders.append(directory)
|
|
1772
|
+
|
|
1773
|
+
# Remove the derivatives folders that do not include folders starting with "sub-"
|
|
1774
|
+
der_pipe_folders = [
|
|
1775
|
+
i
|
|
1776
|
+
for i in der_pipe_folders
|
|
1777
|
+
if any(j.startswith("sub-") for j in os.listdir(os.path.join(deriv_dir, i)))
|
|
1778
|
+
]
|
|
1779
|
+
|
|
1780
|
+
return der_pipe_folders
|
|
1781
|
+
|
|
1782
|
+
|
|
1783
|
+
####################################################################################################
|
|
1784
|
+
def is_bids_filename(filename: str) -> bool:
|
|
1785
|
+
"""
|
|
1786
|
+
Validates a BIDS filename structure, handling extensions and entity order.
|
|
1787
|
+
|
|
1788
|
+
Args:
|
|
1789
|
+
filename (str): The filename to validate.
|
|
1790
|
+
|
|
1791
|
+
Returns
|
|
1792
|
+
bool: True if valid BIDS filename, False otherwise.
|
|
1793
|
+
"""
|
|
1794
|
+
# Remove extension if present
|
|
1795
|
+
base_filename = filename.split(".")[0]
|
|
1796
|
+
|
|
1797
|
+
parts = base_filename.split("_")
|
|
1798
|
+
if not parts:
|
|
1799
|
+
return False
|
|
1800
|
+
|
|
1801
|
+
entity_pattern = re.compile(r"^[a-zA-Z0-9]+-[a-zA-Z0-9]+$")
|
|
1802
|
+
|
|
1803
|
+
# Check that at least one entity-label pair is present
|
|
1804
|
+
has_entity_label = False
|
|
1805
|
+
for part in parts:
|
|
1806
|
+
if "-" in part:
|
|
1807
|
+
has_entity_label = True
|
|
1808
|
+
if not entity_pattern.match(part):
|
|
1809
|
+
return False
|
|
1810
|
+
if not has_entity_label:
|
|
1811
|
+
return False
|
|
1812
|
+
|
|
1813
|
+
return True
|
|
1814
|
+
|
|
1815
|
+
|
|
1816
|
+
####################################################################################################
|
|
1817
|
+
def get_individual_files_and_folders(
|
|
1818
|
+
input_folder: str, cad4query: Union[str, list, dict]
|
|
1819
|
+
):
|
|
1820
|
+
"""
|
|
1821
|
+
This function detects all the files or folders inside a folder and its subfolders containing the strings supplied by the variable cad4query.
|
|
1822
|
+
|
|
1823
|
+
Parameters
|
|
1824
|
+
----------
|
|
1825
|
+
input_folder : str
|
|
1826
|
+
Path to the input folder.
|
|
1827
|
+
|
|
1828
|
+
cad4query : str, list, or dict
|
|
1829
|
+
String or list of strings to filter the files and folders. If a dictionary is provided, it should contain key-value pairs where the key is the string before '-' and the value is the string after '-'.
|
|
1830
|
+
|
|
1831
|
+
Returns
|
|
1832
|
+
-------
|
|
1833
|
+
list
|
|
1834
|
+
List of files or folders that match the query.
|
|
1835
|
+
|
|
1836
|
+
Raises
|
|
1837
|
+
------
|
|
1838
|
+
ValueError
|
|
1839
|
+
If the input folder does not exist.
|
|
1840
|
+
|
|
1841
|
+
TypeError
|
|
1842
|
+
If the input folder is not a string.
|
|
1843
|
+
|
|
1844
|
+
Examples
|
|
1845
|
+
--------
|
|
1846
|
+
>>> input_folder = "/path/to/input/folder"
|
|
1847
|
+
>>> cad4query = "sub-01"
|
|
1848
|
+
>>> files = get_individual_files_and_folders(input_folder, cad4query)
|
|
1849
|
+
|
|
1850
|
+
|
|
1851
|
+
"""
|
|
1852
|
+
|
|
1853
|
+
# Checking if the folder is not a string or if the folder does not exist
|
|
1854
|
+
if not isinstance(input_folder, str):
|
|
1855
|
+
raise TypeError("The input folder must be a string.")
|
|
1856
|
+
|
|
1857
|
+
if not os.path.isdir(input_folder):
|
|
1858
|
+
raise ValueError(f"The input folder {input_folder} does not exist.")
|
|
1859
|
+
|
|
1860
|
+
# Create a dictionary from cad4query where the key is the string before - and the value is the string after -
|
|
1861
|
+
if isinstance(cad4query, str):
|
|
1862
|
+
cad4query = [cad4query]
|
|
1863
|
+
|
|
1864
|
+
if isinstance(cad4query, list):
|
|
1865
|
+
clean_id_dict = dict([i.split("-") for i in cad4query])
|
|
1866
|
+
|
|
1867
|
+
elif isinstance(cad4query, dict):
|
|
1868
|
+
clean_id_dict = cad4query.copy()
|
|
1869
|
+
|
|
1870
|
+
# Detecting the all the files for the reference subject
|
|
1871
|
+
ind_der_dir = glob(os.path.join(input_folder, "sub-" + clean_id_dict["sub"] + "*"))
|
|
1872
|
+
|
|
1873
|
+
# Filter if multiple directories found
|
|
1874
|
+
if len(ind_der_dir) > 1:
|
|
1875
|
+
ind_der_dir = cltmisc.filter_by_substring(
|
|
1876
|
+
ind_der_dir,
|
|
1877
|
+
or_filter=[clean_id_dict["sub"]],
|
|
1878
|
+
and_filter=cad4query,
|
|
1879
|
+
)
|
|
1880
|
+
|
|
1881
|
+
# Set count to 0 if no directory found
|
|
1882
|
+
if len(ind_der_dir) > 0:
|
|
1883
|
+
# Count files for this subject in this pipeline
|
|
1884
|
+
all_files = cltmisc.get_all_files(ind_der_dir[0])
|
|
1885
|
+
all_files = cltmisc.filter_by_substring(
|
|
1886
|
+
all_files, or_filter=clean_id_dict["sub"], and_filter=cad4query
|
|
1887
|
+
)
|
|
1888
|
+
|
|
1889
|
+
else:
|
|
1890
|
+
all_files = []
|
|
1891
|
+
|
|
1892
|
+
return all_files
|
|
1893
|
+
|
|
1894
|
+
|
|
1895
|
+
####################################################################################################
|
|
1896
|
+
def generate_bids_tree(
|
|
1897
|
+
bids_root: str,
|
|
1898
|
+
max_depth: Optional[int] = None,
|
|
1899
|
+
show_hidden: bool = False,
|
|
1900
|
+
exclude_patterns: Optional[Set[str]] = None,
|
|
1901
|
+
save_to_file: Optional[str] = None,
|
|
1902
|
+
) -> str:
|
|
1903
|
+
"""
|
|
1904
|
+
Generate an MS-DOS tree-style visualization of a BIDS folder structure.
|
|
1905
|
+
|
|
1906
|
+
Parameters
|
|
1907
|
+
----------
|
|
1908
|
+
bids_root : str
|
|
1909
|
+
Path to the BIDS root directory.
|
|
1910
|
+
|
|
1911
|
+
max_depth : int, optional
|
|
1912
|
+
Maximum depth to traverse. If None (default), traverses entire directory
|
|
1913
|
+
structure without depth limitation.
|
|
1914
|
+
|
|
1915
|
+
show_hidden : bool, optional
|
|
1916
|
+
Whether to show hidden files and folders (starting with '.').
|
|
1917
|
+
Default is False.
|
|
1918
|
+
|
|
1919
|
+
exclude_patterns : set of str, optional
|
|
1920
|
+
Set of file/folder name patterns to exclude from the tree. If None,
|
|
1921
|
+
defaults to {'.git', '__pycache__', '.DS_Store', 'Thumbs.db'}.
|
|
1922
|
+
|
|
1923
|
+
save_to_file : str, optional
|
|
1924
|
+
Path to save the tree output as a text file. If None, only returns
|
|
1925
|
+
the string without saving.
|
|
1926
|
+
|
|
1927
|
+
Returns
|
|
1928
|
+
-------
|
|
1929
|
+
str
|
|
1930
|
+
MS-DOS tree representation of the BIDS structure with proper tree
|
|
1931
|
+
symbols (├──, └──, │) and directory indicators (/).
|
|
1932
|
+
|
|
1933
|
+
Raises
|
|
1934
|
+
------
|
|
1935
|
+
FileNotFoundError
|
|
1936
|
+
If the specified bids_root path does not exist.
|
|
1937
|
+
|
|
1938
|
+
NotADirectoryError
|
|
1939
|
+
If the specified bids_root path is not a directory.
|
|
1940
|
+
|
|
1941
|
+
PermissionError
|
|
1942
|
+
If there are insufficient permissions to read certain directories.
|
|
1943
|
+
Individual permission errors are handled gracefully and noted in output.
|
|
1944
|
+
|
|
1945
|
+
OSError
|
|
1946
|
+
If there are file system related errors during tree generation or
|
|
1947
|
+
file saving operations.
|
|
1948
|
+
|
|
1949
|
+
Notes
|
|
1950
|
+
-----
|
|
1951
|
+
- Directories are displayed with a trailing '/' to distinguish from files
|
|
1952
|
+
- Items are sorted with directories first, then files, both alphabetically
|
|
1953
|
+
- Hidden files/folders (starting with '.') are excluded by default
|
|
1954
|
+
- Permission errors for individual subdirectories are handled gracefully
|
|
1955
|
+
- The tree uses standard MS-DOS tree symbols for proper visualization
|
|
1956
|
+
- When max_depth is None, the entire directory structure is traversed
|
|
1957
|
+
|
|
1958
|
+
Examples
|
|
1959
|
+
--------
|
|
1960
|
+
Basic usage with unlimited depth:
|
|
1961
|
+
|
|
1962
|
+
>>> tree = generate_bids_tree('/path/to/bids/dataset')
|
|
1963
|
+
>>> print(tree)
|
|
1964
|
+
my-bids-dataset/
|
|
1965
|
+
├── dataset_description.json
|
|
1966
|
+
├── participants.tsv
|
|
1967
|
+
├── sub-01/
|
|
1968
|
+
│ ├── anat/
|
|
1969
|
+
│ │ └── sub-01_T1w.nii.gz
|
|
1970
|
+
│ └── func/
|
|
1971
|
+
│ ├── sub-01_task-rest_bold.nii.gz
|
|
1972
|
+
│ └── sub-01_task-rest_events.tsv
|
|
1973
|
+
└── derivatives/
|
|
1974
|
+
└── preprocessing/
|
|
1975
|
+
└── sub-01/
|
|
1976
|
+
|
|
1977
|
+
Limited depth with file saving:
|
|
1978
|
+
|
|
1979
|
+
>>> tree = generate_bids_tree('/path/to/bids/dataset',
|
|
1980
|
+
... max_depth=2,
|
|
1981
|
+
... save_to_file='bids_tree.txt')
|
|
1982
|
+
>>> print("Tree saved to bids_tree.txt")
|
|
1983
|
+
|
|
1984
|
+
Include hidden files and custom exclusions:
|
|
1985
|
+
|
|
1986
|
+
>>> tree = generate_bids_tree('/path/to/bids/dataset',
|
|
1987
|
+
... show_hidden=True,
|
|
1988
|
+
... exclude_patterns={'temp', 'backup'})
|
|
1989
|
+
"""
|
|
1990
|
+
|
|
1991
|
+
if exclude_patterns is None:
|
|
1992
|
+
exclude_patterns = {".git", "__pycache__", ".DS_Store", "Thumbs.db"}
|
|
1993
|
+
|
|
1994
|
+
bids_path = Path(bids_root)
|
|
1995
|
+
|
|
1996
|
+
if not bids_path.exists():
|
|
1997
|
+
raise FileNotFoundError(f"The specified path does not exist: {bids_root}")
|
|
1998
|
+
|
|
1999
|
+
if not bids_path.is_dir():
|
|
2000
|
+
raise NotADirectoryError(f"The specified path is not a directory: {bids_root}")
|
|
2001
|
+
|
|
2002
|
+
tree_lines = [f"{bids_path.name}/"]
|
|
2003
|
+
|
|
2004
|
+
def _build_tree(current_path: Path, prefix: str = "", depth: int = 0) -> None:
|
|
2005
|
+
"""Recursively build the tree structure."""
|
|
2006
|
+
|
|
2007
|
+
if max_depth is not None and depth >= max_depth:
|
|
2008
|
+
return
|
|
2009
|
+
|
|
2010
|
+
try:
|
|
2011
|
+
# Get all items in current directory
|
|
2012
|
+
items = []
|
|
2013
|
+
for item in current_path.iterdir():
|
|
2014
|
+
# Skip hidden files if not requested
|
|
2015
|
+
if not show_hidden and item.name.startswith("."):
|
|
2016
|
+
continue
|
|
2017
|
+
# Skip excluded patterns
|
|
2018
|
+
if item.name in exclude_patterns:
|
|
2019
|
+
continue
|
|
2020
|
+
items.append(item)
|
|
2021
|
+
|
|
2022
|
+
# Sort items: directories first, then files, both alphabetically
|
|
2023
|
+
items.sort(key=lambda x: (x.is_file(), x.name.lower()))
|
|
2024
|
+
|
|
2025
|
+
for i, item in enumerate(items):
|
|
2026
|
+
is_last = i == len(items) - 1
|
|
2027
|
+
|
|
2028
|
+
# Choose the appropriate tree symbols
|
|
2029
|
+
if is_last:
|
|
2030
|
+
current_prefix = "└── "
|
|
2031
|
+
next_prefix = prefix + " "
|
|
2032
|
+
else:
|
|
2033
|
+
current_prefix = "├── "
|
|
2034
|
+
next_prefix = prefix + "│ "
|
|
2035
|
+
|
|
2036
|
+
# Add item to tree
|
|
2037
|
+
if item.is_dir():
|
|
2038
|
+
tree_lines.append(f"{prefix}{current_prefix}{item.name}/")
|
|
2039
|
+
# Recursively process subdirectory
|
|
2040
|
+
_build_tree(item, next_prefix, depth + 1)
|
|
2041
|
+
else:
|
|
2042
|
+
tree_lines.append(f"{prefix}{current_prefix}{item.name}")
|
|
2043
|
+
|
|
2044
|
+
except PermissionError:
|
|
2045
|
+
tree_lines.append(f"{prefix}├── [Permission Denied]")
|
|
2046
|
+
except Exception as e:
|
|
2047
|
+
tree_lines.append(f"{prefix}├── [Error: {str(e)}]")
|
|
2048
|
+
|
|
2049
|
+
# Build the tree starting from the root
|
|
2050
|
+
_build_tree(bids_path)
|
|
2051
|
+
|
|
2052
|
+
# Join all tree lines
|
|
2053
|
+
tree_output = "\n".join(tree_lines)
|
|
2054
|
+
|
|
2055
|
+
# Save to file if requested
|
|
2056
|
+
if save_to_file:
|
|
2057
|
+
try:
|
|
2058
|
+
with open(save_to_file, "w", encoding="utf-8") as f:
|
|
2059
|
+
f.write(tree_output)
|
|
2060
|
+
except OSError as e:
|
|
2061
|
+
raise OSError(f"Failed to save tree to file '{save_to_file}': {str(e)}")
|
|
2062
|
+
|
|
2063
|
+
return tree_output
|
|
2064
|
+
|
|
2065
|
+
|
|
2066
|
+
####################################################################################################
|
|
2067
|
+
def generate_bids_tree_with_stats(bids_root: str, **kwargs) -> str:
|
|
2068
|
+
"""
|
|
2069
|
+
Generate a BIDS tree with additional statistics.
|
|
2070
|
+
|
|
2071
|
+
Parameters
|
|
2072
|
+
----------
|
|
2073
|
+
bids_root : str
|
|
2074
|
+
Path to the BIDS root directory.
|
|
2075
|
+
|
|
2076
|
+
**kwargs
|
|
2077
|
+
Additional keyword arguments passed to generate_bids_tree().
|
|
2078
|
+
See generate_bids_tree() documentation for available parameters.
|
|
2079
|
+
|
|
2080
|
+
Returns
|
|
2081
|
+
-------
|
|
2082
|
+
str
|
|
2083
|
+
Tree representation with file and folder count statistics appended.
|
|
2084
|
+
|
|
2085
|
+
Raises
|
|
2086
|
+
------
|
|
2087
|
+
FileNotFoundError
|
|
2088
|
+
If the specified bids_root path does not exist.
|
|
2089
|
+
|
|
2090
|
+
NotADirectoryError
|
|
2091
|
+
If the specified bids_root path is not a directory.
|
|
2092
|
+
|
|
2093
|
+
PermissionError
|
|
2094
|
+
If there are insufficient permissions to read directories.
|
|
2095
|
+
|
|
2096
|
+
OSError
|
|
2097
|
+
If there are file system related errors.
|
|
2098
|
+
|
|
2099
|
+
Notes
|
|
2100
|
+
-----
|
|
2101
|
+
Statistics are calculated by recursively counting all files and directories
|
|
2102
|
+
in the BIDS structure, regardless of the max_depth parameter used for
|
|
2103
|
+
tree visualization.
|
|
2104
|
+
|
|
2105
|
+
Examples
|
|
2106
|
+
--------
|
|
2107
|
+
>>> tree_with_stats = generate_bids_tree_with_stats('/path/to/bids/dataset')
|
|
2108
|
+
>>> print(tree_with_stats)
|
|
2109
|
+
my-bids-dataset/
|
|
2110
|
+
├── dataset_description.json
|
|
2111
|
+
└── sub-01/
|
|
2112
|
+
└── anat/
|
|
2113
|
+
└── sub-01_T1w.nii.gz
|
|
2114
|
+
|
|
2115
|
+
Statistics:
|
|
2116
|
+
├── Directories: 2
|
|
2117
|
+
└── Files: 2
|
|
2118
|
+
"""
|
|
2119
|
+
|
|
2120
|
+
tree = generate_bids_tree(bids_root, **kwargs)
|
|
2121
|
+
|
|
2122
|
+
# Count files and directories
|
|
2123
|
+
bids_path = Path(bids_root)
|
|
2124
|
+
file_count = 0
|
|
2125
|
+
dir_count = 0
|
|
2126
|
+
|
|
2127
|
+
try:
|
|
2128
|
+
for item in bids_path.rglob("*"):
|
|
2129
|
+
if item.is_file():
|
|
2130
|
+
file_count += 1
|
|
2131
|
+
elif item.is_dir():
|
|
2132
|
+
dir_count += 1
|
|
2133
|
+
except Exception:
|
|
2134
|
+
pass
|
|
2135
|
+
|
|
2136
|
+
stats = f"\n\nStatistics:\n├── Directories: {dir_count}\n└── Files: {file_count}"
|
|
2137
|
+
|
|
2138
|
+
return tree + stats
|
|
2139
|
+
|
|
2140
|
+
|
|
2141
|
+
####################################################################################################
|
|
2142
|
+
def validate_bids_structure(bids_root: str) -> List[str]:
|
|
2143
|
+
"""
|
|
2144
|
+
Basic validation of BIDS structure and return warnings.
|
|
2145
|
+
|
|
2146
|
+
Parameters
|
|
2147
|
+
----------
|
|
2148
|
+
bids_root : str
|
|
2149
|
+
Path to the BIDS root directory.
|
|
2150
|
+
|
|
2151
|
+
Returns
|
|
2152
|
+
-------
|
|
2153
|
+
list of str
|
|
2154
|
+
List of validation warnings and notes about the BIDS structure.
|
|
2155
|
+
Empty list indicates no issues found.
|
|
2156
|
+
|
|
2157
|
+
Raises
|
|
2158
|
+
------
|
|
2159
|
+
FileNotFoundError
|
|
2160
|
+
If the specified bids_root path does not exist.
|
|
2161
|
+
NotADirectoryError
|
|
2162
|
+
If the specified bids_root path is not a directory.
|
|
2163
|
+
|
|
2164
|
+
Notes
|
|
2165
|
+
-----
|
|
2166
|
+
This function performs basic BIDS validation including:
|
|
2167
|
+
- Checking for required files (dataset_description.json)
|
|
2168
|
+
- Verifying presence of subject directories (sub-*)
|
|
2169
|
+
- Noting presence of derivatives directory
|
|
2170
|
+
|
|
2171
|
+
For comprehensive BIDS validation, consider using the official
|
|
2172
|
+
BIDS validator tool.
|
|
2173
|
+
|
|
2174
|
+
Examples
|
|
2175
|
+
--------
|
|
2176
|
+
>>> warnings = validate_bids_structure('/path/to/bids/dataset')
|
|
2177
|
+
>>> if warnings:
|
|
2178
|
+
... for warning in warnings:
|
|
2179
|
+
... print(f"⚠️ {warning}")
|
|
2180
|
+
>>> else:
|
|
2181
|
+
... print("✅ Basic BIDS structure looks good!")
|
|
2182
|
+
"""
|
|
2183
|
+
|
|
2184
|
+
warnings = []
|
|
2185
|
+
bids_path = Path(bids_root)
|
|
2186
|
+
|
|
2187
|
+
if not bids_path.exists():
|
|
2188
|
+
raise FileNotFoundError(f"The specified path does not exist: {bids_root}")
|
|
2189
|
+
|
|
2190
|
+
if not bids_path.is_dir():
|
|
2191
|
+
raise NotADirectoryError(f"The specified path is not a directory: {bids_root}")
|
|
2192
|
+
|
|
2193
|
+
# Check for required files
|
|
2194
|
+
required_files = ["dataset_description.json"]
|
|
2195
|
+
for req_file in required_files:
|
|
2196
|
+
if not (bids_path / req_file).exists():
|
|
2197
|
+
warnings.append(f"Missing required file: {req_file}")
|
|
2198
|
+
|
|
2199
|
+
# Check for common BIDS directories
|
|
2200
|
+
common_dirs = ["sub-*"] # Using glob pattern
|
|
2201
|
+
subject_dirs = list(bids_path.glob("sub-*"))
|
|
2202
|
+
if not subject_dirs:
|
|
2203
|
+
warnings.append("No subject directories found (sub-*)")
|
|
2204
|
+
|
|
2205
|
+
# Check for derivatives directory
|
|
2206
|
+
derivatives_dir = bids_path / "derivatives"
|
|
2207
|
+
if derivatives_dir.exists():
|
|
2208
|
+
warnings.append("Derivatives directory found")
|
|
2209
|
+
|
|
2210
|
+
return warnings
|
|
2211
|
+
|
|
2212
|
+
|
|
2213
|
+
####################################################################################################
|
|
2214
|
+
####################################################################################################
|
|
2215
|
+
############ ############
|
|
2216
|
+
############ ############
|
|
2217
|
+
############ Section 3: Useful methods for BIDs file organization ############
|
|
2218
|
+
############ ############
|
|
2219
|
+
############ ############
|
|
2220
|
+
####################################################################################################
|
|
2221
|
+
####################################################################################################
|
|
2222
|
+
|
|
2223
|
+
|
|
2224
|
+
# Loading the JSON file containing the BIDs configuration file. It includes the entities and suffixes
|
|
2225
|
+
# of the BIDs dataset.
|
|
2226
|
+
####################################################################################################
|
|
2227
|
+
def load_bids_json(bids_json: str = None):
|
|
2228
|
+
"""
|
|
2229
|
+
Load the JSON file containing the BIDs configuration file.
|
|
2230
|
+
|
|
2231
|
+
Parameters
|
|
2232
|
+
----------
|
|
2233
|
+
bids_json : str
|
|
2234
|
+
JSON file containing the BIDs configuration.
|
|
2235
|
+
|
|
2236
|
+
Returns
|
|
2237
|
+
--------
|
|
2238
|
+
config_dict : dict
|
|
2239
|
+
Dictionary containing the default .
|
|
2240
|
+
|
|
2241
|
+
"""
|
|
2242
|
+
|
|
2243
|
+
# Get the absolute of this file
|
|
2244
|
+
if bids_json is None:
|
|
2245
|
+
cwd = os.path.dirname(os.path.abspath(__file__))
|
|
2246
|
+
bids_json = os.path.join(cwd, "config", "bids.json")
|
|
2247
|
+
else:
|
|
2248
|
+
if not os.path.isfile(bids_json):
|
|
2249
|
+
raise ValueError(
|
|
2250
|
+
"Please, provide a valid JSON file containing the BIDs configuration dictionary."
|
|
2251
|
+
)
|
|
2252
|
+
try:
|
|
2253
|
+
with open(bids_json) as f:
|
|
2254
|
+
config_dict = json.load(f)
|
|
2255
|
+
except FileNotFoundError:
|
|
2256
|
+
raise FileNotFoundError(f"Default configuration file not found at: {bids_json}")
|
|
2257
|
+
except json.JSONDecodeError:
|
|
2258
|
+
raise ValueError(f"Error parsing the default configuration file: {bids_json}")
|
|
2259
|
+
|
|
2260
|
+
return config_dict
|