clabtoolkit 0.4.3__tar.gz → 0.4.4__tar.gz
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-0.4.3 → clabtoolkit-0.4.4}/HISTORY.rst +18 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/PKG-INFO +1 -1
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit/__init__.py +1 -1
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit/dicomtools.py +328 -65
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit/dwitools.py +33 -131
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit/parcellationtools.py +327 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit/tracttools.py +363 -202
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit.egg-info/PKG-INFO +1 -1
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit.egg-info/SOURCES.txt +3 -2
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/docs/source/index.rst +3 -0
- clabtoolkit-0.4.4/docs/source/modules/colorstools.rst +142 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/docs/source/modules/connectivitytools.rst +1 -1
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/docs/source/modules/dicomtools.rst +1 -0
- clabtoolkit-0.4.4/docs/source/modules/dwitools.rst +115 -0
- clabtoolkit-0.4.4/docs/source/modules/freesurfertools.rst +181 -0
- clabtoolkit-0.4.4/docs/source/modules/imagetools.rst +168 -0
- clabtoolkit-0.4.4/docs/source/modules/morphometrytools.rst +151 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/docs/source/modules/networktools.rst +1 -1
- clabtoolkit-0.4.4/docs/source/modules/parcellationtools.rst +154 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/docs/source/modules/pipelinetools.rst +1 -1
- clabtoolkit-0.4.4/docs/source/modules/pointstools.rst +113 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/docs/source/modules/segmentationtools.rst +1 -1
- clabtoolkit-0.4.4/docs/source/modules/surfacetools.rst +143 -0
- clabtoolkit-0.4.4/docs/source/modules/tracttools.rst +168 -0
- clabtoolkit-0.4.4/docs/source/modules/visualizationtools.rst +139 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/pyproject.toml +1 -1
- clabtoolkit-0.4.3/clabtoolkit/connectome.py +0 -723
- clabtoolkit-0.4.3/clabtoolkit/old_dwitool.py +0 -1537
- clabtoolkit-0.4.3/docs/source/modules/dwitools.rst +0 -90
- clabtoolkit-0.4.3/docs/source/modules/freesurfertools.rst +0 -85
- clabtoolkit-0.4.3/docs/source/modules/imagetools.rst +0 -114
- clabtoolkit-0.4.3/docs/source/modules/morphometrytools.rst +0 -96
- clabtoolkit-0.4.3/docs/source/modules/parcellationtools.rst +0 -71
- clabtoolkit-0.4.3/docs/source/modules/surfacetools.rst +0 -95
- clabtoolkit-0.4.3/docs/source/modules/visualizationtools.rst +0 -82
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/AUTHORS.rst +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/CONTRIBUTING.rst +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/LICENSE +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/MANIFEST.in +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/README.rst +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit/bidstools.py +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit/build_visualization_layout.py +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit/colorstools.py +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit/config/bids.json +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit/config/config.json +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit/config/lobes.json +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit/config/stats_mapping.json +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit/config/viz_views.json +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit/config/viz_views_test.json +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit/connectivitytools.py +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit/dwitools_utils.py +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit/freesurfertools.py +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit/imagetools.py +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit/imagetools_utils.py +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit/misctools.py +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit/misctools_utils.py +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit/morphometrytools.py +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit/networktools.py +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit/pipelinetools.py +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit/plottools.py +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit/pointstools.py +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit/qcqatools.py +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit/segmentationtools.py +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit/surfacetools.py +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit/visualization_utils.py +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit/visualizationtools.py +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit.egg-info/dependency_links.txt +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit.egg-info/requires.txt +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/clabtoolkit.egg-info/top_level.txt +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/docs/Makefile +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/docs/clabtoolkit.rst +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/docs/make.bat +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/docs/modules.rst +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/docs/source/_static/clabtoolkit_ecosystem.png +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/docs/source/_static/clabtoolkit_favicon.png +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/docs/source/_static/clabtoolkit_logo.png +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/docs/source/_static/clabtoolkit_logo_notext.png +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/docs/source/conf.py +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/docs/source/modules/bidstools.rst +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/docs/source/modules/misctools.rst +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/docs/source/modules/plottools.rst +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/docs/source/modules/qcqatools.rst +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/setup.cfg +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/usage_examples/VisClasses4Paper.ipynb +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/usage_examples/clabtoolkit-OHBM2026-final.ipynb +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/usage_examples/clabtoolkit_examples.ipynb +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/usage_examples/examples-bidstools-methods.ipynb +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/usage_examples/examples-colorstools-methods.ipynb +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/usage_examples/examples-freesurfertools-methods.ipynb +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/usage_examples/examples-misctools-methods.ipynb +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/usage_examples/examples-parcellationtools-methods.ipynb +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/usage_examples/examples-surfacetools-methods.ipynb +0 -0
- {clabtoolkit-0.4.3 → clabtoolkit-0.4.4}/usage_examples/examples-visualizationtools-methods.ipynb +0 -0
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
History
|
|
3
3
|
=======
|
|
4
4
|
|
|
5
|
+
0.4.4 (2026-07-17)
|
|
6
|
+
------------------
|
|
7
|
+
|
|
8
|
+
* Added get/set accessor methods for Parcellation attributes (data, affine, index, names and colors) with validation
|
|
9
|
+
* Added support for organizing DICOM folders without requiring individual per-series folders
|
|
10
|
+
* Improved the method to compress DICOM sessions
|
|
11
|
+
* Fixed save_tractogram writing the wrong format when overwriting an existing file
|
|
12
|
+
* Fixed colortable loading in tracttools for point-only maps and for maps absent from both data_per_point and data_per_streamline
|
|
13
|
+
* Fixed tractogram merging in tracttools
|
|
14
|
+
* Fixed out_image=None crash and added pathlib.Path support in delete_dwi_volumes
|
|
15
|
+
* Fixed docstrings and type hints in tracttools
|
|
16
|
+
* Renamed the streamline point count variable to nb_points for consistency
|
|
17
|
+
* Removed surf as a variable name for tractograms
|
|
18
|
+
* Removed the old connectome and old_dwitool modules (use connectivitytools and dwitools instead)
|
|
19
|
+
* Reorganized the package imports
|
|
20
|
+
* Documented the tracttools, pointstools and colorstools modules for the first time
|
|
21
|
+
* Corrected the module documentation to match the actual API
|
|
22
|
+
|
|
5
23
|
0.4.3 (2026-06-10)
|
|
6
24
|
------------------
|
|
7
25
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clabtoolkit
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.4
|
|
4
4
|
Summary: A comprehensive toolkit for neuroimaging data processing and analysis
|
|
5
5
|
Author-email: Yasser Alemán-Gómez <yasseraleman@protonmail.com>
|
|
6
6
|
Maintainer-email: Yasser Alemán-Gómez <yasseraleman@protonmail.com>
|
|
@@ -1,23 +1,32 @@
|
|
|
1
|
+
# Standard library
|
|
1
2
|
import os
|
|
2
|
-
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
3
5
|
import tarfile
|
|
4
6
|
import shutil
|
|
5
|
-
|
|
6
|
-
import sys
|
|
7
|
-
import pydicom
|
|
7
|
+
from glob import glob
|
|
8
8
|
from datetime import datetime
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from shutil import copyfile
|
|
11
|
-
from
|
|
12
|
-
|
|
13
|
-
import numpy as np
|
|
14
|
-
from rich.progress import Progress
|
|
11
|
+
from functools import partial
|
|
15
12
|
from threading import Lock
|
|
16
|
-
import time
|
|
17
|
-
|
|
18
13
|
from typing import List, Optional, Union
|
|
14
|
+
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, as_completed
|
|
19
15
|
|
|
20
|
-
#
|
|
16
|
+
# Third-party
|
|
17
|
+
import numpy as np
|
|
18
|
+
import pandas as pd
|
|
19
|
+
import pydicom
|
|
20
|
+
from rich.progress import (
|
|
21
|
+
Progress,
|
|
22
|
+
SpinnerColumn,
|
|
23
|
+
BarColumn,
|
|
24
|
+
TextColumn,
|
|
25
|
+
TimeElapsedColumn,
|
|
26
|
+
MofNCompleteColumn,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Local
|
|
21
30
|
from . import misctools as cltmisc
|
|
22
31
|
|
|
23
32
|
|
|
@@ -359,6 +368,235 @@ def org_conv_dicoms(
|
|
|
359
368
|
compress_dicom_session(out_dic_dir)
|
|
360
369
|
|
|
361
370
|
|
|
371
|
+
##
|
|
372
|
+
def organize_dicom_files(
|
|
373
|
+
in_dic_dir: str,
|
|
374
|
+
out_dic_dir: str,
|
|
375
|
+
no_sub_folder: bool = False,
|
|
376
|
+
demog_file: str = None,
|
|
377
|
+
ids_file: str = None,
|
|
378
|
+
ses_id: str = None,
|
|
379
|
+
nosub: bool = False,
|
|
380
|
+
booldic: bool = True,
|
|
381
|
+
boolcomp: bool = False,
|
|
382
|
+
force: bool = False,
|
|
383
|
+
nthreads: int = 0,
|
|
384
|
+
):
|
|
385
|
+
"""
|
|
386
|
+
Alias for org_conv_dicoms to maintain backward compatibility.
|
|
387
|
+
"""
|
|
388
|
+
|
|
389
|
+
if no_sub_folder:
|
|
390
|
+
org_dicom_folder(
|
|
391
|
+
in_dic_dir=in_dic_dir,
|
|
392
|
+
out_dic_dir=out_dic_dir,
|
|
393
|
+
boolcomp=boolcomp,
|
|
394
|
+
force=force,
|
|
395
|
+
nthreads=nthreads,
|
|
396
|
+
)
|
|
397
|
+
else:
|
|
398
|
+
|
|
399
|
+
org_conv_dicoms(
|
|
400
|
+
in_dic_dir=in_dic_dir,
|
|
401
|
+
out_dic_dir=out_dic_dir,
|
|
402
|
+
demog_file=demog_file,
|
|
403
|
+
ids_file=ids_file,
|
|
404
|
+
ses_id=ses_id,
|
|
405
|
+
nosub=nosub,
|
|
406
|
+
booldic=booldic,
|
|
407
|
+
boolcomp=boolcomp,
|
|
408
|
+
force=force,
|
|
409
|
+
nthreads=nthreads,
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
##########################################################################################################
|
|
414
|
+
def _copy_dicomfile_noid(
|
|
415
|
+
dic_file: str, out_dic_dir: str, force: bool = False
|
|
416
|
+
) -> tuple[str, str | None, str | None]:
|
|
417
|
+
"""
|
|
418
|
+
Module-level worker: read one DICOM file, derive its destination path and copy it.
|
|
419
|
+
Must be defined at module level (not nested) so ProcessPoolExecutor can pickle it.
|
|
420
|
+
|
|
421
|
+
Parameters
|
|
422
|
+
----------
|
|
423
|
+
dic_file : str
|
|
424
|
+
Full path to the source DICOM file.
|
|
425
|
+
out_dic_dir : str
|
|
426
|
+
Root output directory where the organised hierarchy will be written.
|
|
427
|
+
force : bool, optional
|
|
428
|
+
If True, overwrite an existing file at the destination. Default is False.
|
|
429
|
+
|
|
430
|
+
Returns
|
|
431
|
+
-------
|
|
432
|
+
dic_file : str
|
|
433
|
+
The input path (echoed back for caller bookkeeping).
|
|
434
|
+
dest_dic_dir : str or None
|
|
435
|
+
The destination directory the file was copied into, or None on failure.
|
|
436
|
+
error_msg : str or None
|
|
437
|
+
Human-readable reason for failure, or None on success.
|
|
438
|
+
"""
|
|
439
|
+
try:
|
|
440
|
+
dataset = pydicom.dcmread(dic_file, stop_before_pixels=True)
|
|
441
|
+
dic_name = os.path.basename(dic_file)
|
|
442
|
+
|
|
443
|
+
attributes = dataset.dir("")
|
|
444
|
+
if not attributes:
|
|
445
|
+
return dic_file, None, "No DICOM attributes found"
|
|
446
|
+
|
|
447
|
+
subj_id = str(dataset.data_element("PatientID").value).strip()
|
|
448
|
+
ses_id, ser_id = create_session_series_names(dataset)
|
|
449
|
+
ses_id = "ses-" + ses_id
|
|
450
|
+
|
|
451
|
+
dest_dic_dir = os.path.join(out_dic_dir, subj_id, ses_id, ser_id)
|
|
452
|
+
Path(dest_dic_dir).mkdir(parents=True, exist_ok=True)
|
|
453
|
+
|
|
454
|
+
dest_dic = os.path.join(dest_dic_dir, dic_name)
|
|
455
|
+
if force:
|
|
456
|
+
if os.path.isfile(dest_dic):
|
|
457
|
+
os.remove(dest_dic)
|
|
458
|
+
copyfile(dic_file, dest_dic)
|
|
459
|
+
else:
|
|
460
|
+
if not os.path.isfile(dest_dic):
|
|
461
|
+
copyfile(dic_file, dest_dic)
|
|
462
|
+
|
|
463
|
+
return dic_file, dest_dic_dir, None
|
|
464
|
+
|
|
465
|
+
except pydicom.errors.InvalidDicomError:
|
|
466
|
+
return dic_file, None, "Not a valid DICOM file"
|
|
467
|
+
except KeyError as exc:
|
|
468
|
+
return dic_file, None, f"Missing DICOM tag: {exc}"
|
|
469
|
+
except Exception as exc:
|
|
470
|
+
return dic_file, None, f"{type(exc).__name__}: {exc}"
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
##########################################################################################################
|
|
474
|
+
def org_dicom_folder(
|
|
475
|
+
in_dic_dir: str,
|
|
476
|
+
out_dic_dir: str,
|
|
477
|
+
boolcomp: bool = False,
|
|
478
|
+
force: bool = False,
|
|
479
|
+
nthreads: int = 0,
|
|
480
|
+
):
|
|
481
|
+
"""
|
|
482
|
+
Organizes DICOM files into a subject/session/series folder hierarchy.
|
|
483
|
+
The subject folder is named after the PatientID DICOM attribute.
|
|
484
|
+
|
|
485
|
+
Parameters
|
|
486
|
+
----------
|
|
487
|
+
in_dic_dir : str
|
|
488
|
+
Path to the input directory containing DICOM files (searched recursively).
|
|
489
|
+
|
|
490
|
+
out_dic_dir : str
|
|
491
|
+
Path to the output directory where organised files will be written.
|
|
492
|
+
|
|
493
|
+
boolcomp : bool, optional
|
|
494
|
+
If True, compress the organised sessions into tar.gz archives after copying. Default is False.
|
|
495
|
+
|
|
496
|
+
force : bool, optional
|
|
497
|
+
If True, overwrite existing files at the destination. Default is False.
|
|
498
|
+
|
|
499
|
+
nthreads : int, optional
|
|
500
|
+
Number of parallel worker processes.
|
|
501
|
+
0 (default) = use all available CPUs minus one.
|
|
502
|
+
1 = sequential (no parallelism).
|
|
503
|
+
|
|
504
|
+
Returns
|
|
505
|
+
-------
|
|
506
|
+
None
|
|
507
|
+
This function performs file organisation operations and does not return a value.
|
|
508
|
+
|
|
509
|
+
Raises
|
|
510
|
+
------
|
|
511
|
+
FileNotFoundError
|
|
512
|
+
If the input directory does not exist.
|
|
513
|
+
PermissionError
|
|
514
|
+
If there are insufficient permissions to write to the output directory.
|
|
515
|
+
|
|
516
|
+
Examples
|
|
517
|
+
--------
|
|
518
|
+
>>> org_dicom_folder('/path/to/raw/dicoms', '/path/to/organised')
|
|
519
|
+
>>> org_dicom_folder('/path/to/raw/dicoms', '/path/to/organised', force=True, nthreads=8)
|
|
520
|
+
"""
|
|
521
|
+
if not os.path.isdir(in_dic_dir):
|
|
522
|
+
raise FileNotFoundError(f"Input directory not found: {in_dic_dir}")
|
|
523
|
+
|
|
524
|
+
Path(out_dic_dir).mkdir(parents=True, exist_ok=True)
|
|
525
|
+
|
|
526
|
+
dicom_files = cltmisc.get_all_files(in_dic_dir)
|
|
527
|
+
n_files = len(dicom_files)
|
|
528
|
+
|
|
529
|
+
if n_files == 0:
|
|
530
|
+
print("No files found in the input directory.")
|
|
531
|
+
return
|
|
532
|
+
|
|
533
|
+
if nthreads == 0:
|
|
534
|
+
nthreads = max(1, (os.cpu_count() or 2) - 1)
|
|
535
|
+
nthreads = max(1, nthreads)
|
|
536
|
+
|
|
537
|
+
print(f"Organising {n_files} files using {nthreads} worker(s)...")
|
|
538
|
+
|
|
539
|
+
failed_files = []
|
|
540
|
+
|
|
541
|
+
with Progress(
|
|
542
|
+
SpinnerColumn(),
|
|
543
|
+
TextColumn("[bold blue]{task.description}"),
|
|
544
|
+
BarColumn(),
|
|
545
|
+
MofNCompleteColumn(),
|
|
546
|
+
TextColumn("[progress.percentage]{task.percentage:>5.1f}%"),
|
|
547
|
+
TimeElapsedColumn(),
|
|
548
|
+
transient=False,
|
|
549
|
+
) as progress:
|
|
550
|
+
|
|
551
|
+
task = progress.add_task("Copying DICOM files", total=n_files)
|
|
552
|
+
|
|
553
|
+
if nthreads == 1:
|
|
554
|
+
for dic_file in dicom_files:
|
|
555
|
+
try:
|
|
556
|
+
_, dest, reason = _copy_dicomfile_noid(dic_file, out_dic_dir, force)
|
|
557
|
+
except Exception as exc:
|
|
558
|
+
dest, reason = None, f"Unexpected: {exc}"
|
|
559
|
+
finally:
|
|
560
|
+
progress.advance(task)
|
|
561
|
+
if dest is None:
|
|
562
|
+
failed_files.append((dic_file, reason))
|
|
563
|
+
else:
|
|
564
|
+
worker = partial(_copy_dicomfile_noid, out_dic_dir=out_dic_dir, force=force)
|
|
565
|
+
with ProcessPoolExecutor(max_workers=nthreads) as executor:
|
|
566
|
+
futures = {executor.submit(worker, f): f for f in dicom_files}
|
|
567
|
+
for future in as_completed(futures):
|
|
568
|
+
try:
|
|
569
|
+
_, dest, reason = future.result()
|
|
570
|
+
except Exception as exc:
|
|
571
|
+
dest, reason = None, f"Future raised: {exc}"
|
|
572
|
+
finally:
|
|
573
|
+
progress.advance(task)
|
|
574
|
+
if dest is None:
|
|
575
|
+
failed_files.append((futures[future], reason))
|
|
576
|
+
|
|
577
|
+
# Force a final render before the context closes
|
|
578
|
+
progress.refresh()
|
|
579
|
+
|
|
580
|
+
succeeded = n_files - len(failed_files)
|
|
581
|
+
print(f"\n✓ Done. {succeeded}/{n_files} files organised successfully.")
|
|
582
|
+
|
|
583
|
+
# Compress sessions if requested
|
|
584
|
+
|
|
585
|
+
if failed_files:
|
|
586
|
+
print(f"\n⚠ {len(failed_files)} file(s) skipped:")
|
|
587
|
+
for path, reason in failed_files:
|
|
588
|
+
print(f" • {path}\n Reason: {reason}")
|
|
589
|
+
|
|
590
|
+
# Compress sessions if requested
|
|
591
|
+
|
|
592
|
+
if boolcomp:
|
|
593
|
+
|
|
594
|
+
print("\nCompressing organised sessions...")
|
|
595
|
+
print(out_dic_dir)
|
|
596
|
+
compress_dicom_session(out_dic_dir)
|
|
597
|
+
print("Compression complete.")
|
|
598
|
+
|
|
599
|
+
|
|
362
600
|
####################################################################################################
|
|
363
601
|
def copy_dicom_file(
|
|
364
602
|
dic_file: str,
|
|
@@ -765,7 +1003,8 @@ def compress_dicom_session(
|
|
|
765
1003
|
subj_ids : str, list of str, or None, optional
|
|
766
1004
|
Subject IDs to be considered. Can be:
|
|
767
1005
|
- None: consider all subjects in the directory (default)
|
|
768
|
-
- str: path to text file containing subject IDs (one per line)
|
|
1006
|
+
- str: path to a text file containing subject IDs (one per line),
|
|
1007
|
+
or a comma-separated list of subject IDs
|
|
769
1008
|
- list of str: explicit list of subject IDs
|
|
770
1009
|
|
|
771
1010
|
remove_original : bool, optional, default=True
|
|
@@ -818,6 +1057,11 @@ def compress_dicom_session(
|
|
|
818
1057
|
... subj_ids='/path/to/subject_ids.txt'
|
|
819
1058
|
... )
|
|
820
1059
|
"""
|
|
1060
|
+
|
|
1061
|
+
# Normalise Path input
|
|
1062
|
+
if isinstance(subj_ids, Path):
|
|
1063
|
+
subj_ids = str(subj_ids)
|
|
1064
|
+
|
|
821
1065
|
# Validate input directory
|
|
822
1066
|
dic_path = Path(dic_dir)
|
|
823
1067
|
if not dic_path.exists():
|
|
@@ -827,35 +1071,41 @@ def compress_dicom_session(
|
|
|
827
1071
|
|
|
828
1072
|
# Process subject IDs
|
|
829
1073
|
if subj_ids is None:
|
|
830
|
-
#
|
|
831
|
-
subj_ids =
|
|
832
|
-
|
|
833
|
-
for item in dic_path.iterdir()
|
|
834
|
-
if item.is_dir() and item.name.startswith("sub-")
|
|
835
|
-
]
|
|
836
|
-
subj_ids.sort()
|
|
1074
|
+
# Only pick up directories that follow the BIDS 'sub-' convention
|
|
1075
|
+
subj_ids = sorted(item.name for item in dic_path.iterdir() if item.is_dir())
|
|
1076
|
+
|
|
837
1077
|
elif isinstance(subj_ids, str):
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
subj_ids =
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
1078
|
+
if os.path.isfile(subj_ids):
|
|
1079
|
+
# Read one subject ID per line from the file
|
|
1080
|
+
try:
|
|
1081
|
+
with open(subj_ids, "r", encoding="utf-8") as fh:
|
|
1082
|
+
subj_ids = [line.strip() for line in fh if line.strip()]
|
|
1083
|
+
except Exception as e:
|
|
1084
|
+
raise ValueError(f"Error reading subject IDs file: {e}")
|
|
1085
|
+
else:
|
|
1086
|
+
# Treat the string as a comma-separated list of IDs
|
|
1087
|
+
subj_ids = [s.strip() for s in subj_ids.split(",") if s.strip()]
|
|
1088
|
+
if not subj_ids:
|
|
1089
|
+
raise ValueError(
|
|
1090
|
+
f"subj_ids string '{subj_ids}' is neither a valid file path "
|
|
1091
|
+
"nor a comma-separated list of subject IDs"
|
|
1092
|
+
)
|
|
1093
|
+
|
|
846
1094
|
elif isinstance(subj_ids, list):
|
|
847
|
-
|
|
848
|
-
if not all(isinstance(subj_id, str) for subj_id in subj_ids):
|
|
1095
|
+
if not all(isinstance(s, str) for s in subj_ids):
|
|
849
1096
|
raise ValueError("All subject IDs must be strings")
|
|
1097
|
+
|
|
850
1098
|
else:
|
|
851
|
-
raise ValueError(
|
|
1099
|
+
raise ValueError(
|
|
1100
|
+
"subj_ids must be None, str (file path or CSV), or list of str"
|
|
1101
|
+
)
|
|
852
1102
|
|
|
853
1103
|
if not subj_ids:
|
|
854
|
-
print("No subjects found to process")
|
|
1104
|
+
print("No subjects found to process.")
|
|
855
1105
|
return []
|
|
856
1106
|
|
|
857
1107
|
n_subj = len(subj_ids)
|
|
858
|
-
failed_sessions = []
|
|
1108
|
+
failed_sessions: List[str] = []
|
|
859
1109
|
total_sessions = 0
|
|
860
1110
|
compressed_sessions = 0
|
|
861
1111
|
|
|
@@ -867,79 +1117,92 @@ def compress_dicom_session(
|
|
|
867
1117
|
|
|
868
1118
|
pb.update(
|
|
869
1119
|
task_id=task,
|
|
870
|
-
description=f"[green]Processing {subj_id} ({i+1}/{n_subj})",
|
|
871
|
-
completed=i,
|
|
1120
|
+
description=f"[green]Processing {subj_id} ({i + 1}/{n_subj})",
|
|
1121
|
+
completed=i, # mark the *previous* subject as done while this one runs
|
|
872
1122
|
)
|
|
873
1123
|
|
|
874
|
-
# Skip if subject directory doesn't exist
|
|
875
1124
|
if not subj_dir.exists():
|
|
876
|
-
print(f"Warning:
|
|
1125
|
+
print(f"Warning: subject directory '{subj_dir}' not found — skipping.")
|
|
877
1126
|
continue
|
|
878
1127
|
|
|
879
|
-
#
|
|
880
|
-
session_dirs =
|
|
1128
|
+
# Collect BIDS session directories (must start with 'ses-')
|
|
1129
|
+
session_dirs = sorted(
|
|
881
1130
|
item
|
|
882
1131
|
for item in subj_dir.iterdir()
|
|
883
1132
|
if item.is_dir() and item.name.startswith("ses-")
|
|
884
|
-
|
|
1133
|
+
)
|
|
1134
|
+
|
|
1135
|
+
if not session_dirs:
|
|
1136
|
+
print(
|
|
1137
|
+
f"Warning: no 'ses-' directories found in '{subj_dir}' — skipping subject."
|
|
1138
|
+
)
|
|
1139
|
+
continue
|
|
885
1140
|
|
|
886
1141
|
total_sessions += len(session_dirs)
|
|
887
1142
|
|
|
888
1143
|
for ses_dir in session_dirs:
|
|
889
1144
|
tar_file_path = ses_dir.with_suffix(".tar.gz")
|
|
1145
|
+
# ses_dir.with_suffix replaces only the last suffix; for a dir named
|
|
1146
|
+
# 'ses-01' that gives 'ses-01.tar.gz' as intended, but we use
|
|
1147
|
+
# Path(str(ses_dir) + ".tar.gz") to be unambiguous:
|
|
1148
|
+
tar_file_path = Path(str(ses_dir) + ".tar.gz")
|
|
890
1149
|
|
|
891
|
-
# Skip if tar file already exists
|
|
892
1150
|
if tar_file_path.exists():
|
|
893
|
-
print(f"Warning: {tar_file_path} already exists
|
|
1151
|
+
print(f"Warning: '{tar_file_path}' already exists — skipping.")
|
|
894
1152
|
continue
|
|
895
1153
|
|
|
896
1154
|
try:
|
|
897
|
-
# Create tar.gz archive using Python's tarfile module
|
|
898
1155
|
with tarfile.open(tar_file_path, "w:gz") as tar:
|
|
899
|
-
# Add the session directory to the archive
|
|
900
|
-
# Use arcname to preserve the directory structure
|
|
901
1156
|
tar.add(ses_dir, arcname=ses_dir.name)
|
|
902
1157
|
|
|
903
|
-
# Remove original directory if requested and compression succeeded
|
|
904
1158
|
if remove_original:
|
|
905
1159
|
shutil.rmtree(ses_dir)
|
|
906
1160
|
|
|
907
1161
|
compressed_sessions += 1
|
|
908
1162
|
|
|
909
1163
|
except tarfile.TarError as e:
|
|
910
|
-
print(f"Error
|
|
1164
|
+
print(f"Error: failed to compress '{ses_dir}': {e}")
|
|
911
1165
|
failed_sessions.append(str(ses_dir))
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
try:
|
|
915
|
-
tar_file_path.unlink()
|
|
916
|
-
except Exception:
|
|
917
|
-
pass
|
|
1166
|
+
_safe_unlink(tar_file_path)
|
|
1167
|
+
|
|
918
1168
|
except PermissionError as e:
|
|
919
|
-
print(f"
|
|
1169
|
+
print(f"Error: permission denied for '{ses_dir}': {e}")
|
|
920
1170
|
failed_sessions.append(str(ses_dir))
|
|
921
|
-
|
|
922
|
-
|
|
1171
|
+
_safe_unlink(tar_file_path)
|
|
1172
|
+
|
|
1173
|
+
except OSError as e:
|
|
1174
|
+
print(f"Error: OS error with '{ses_dir}': {e}")
|
|
923
1175
|
failed_sessions.append(str(ses_dir))
|
|
1176
|
+
_safe_unlink(tar_file_path)
|
|
924
1177
|
|
|
1178
|
+
# Mark all subjects as complete
|
|
925
1179
|
pb.update(
|
|
926
|
-
task_id=task, description=
|
|
1180
|
+
task_id=task, description="[green]Compression complete", completed=n_subj
|
|
927
1181
|
)
|
|
928
1182
|
|
|
929
|
-
#
|
|
930
|
-
if failed_sessions:
|
|
931
|
-
print("\nTHE PROCESS FAILED TO COMPRESS THE FOLLOWING SESSIONS:")
|
|
932
|
-
for failed_session in failed_sessions:
|
|
933
|
-
print(f" - {failed_session}")
|
|
934
|
-
else:
|
|
935
|
-
print("\nAll sessions compressed successfully!")
|
|
936
|
-
|
|
1183
|
+
# Summary
|
|
937
1184
|
print(
|
|
938
|
-
f"\nProcessed {n_subj}
|
|
1185
|
+
f"\nProcessed {n_subj} subject(s), "
|
|
1186
|
+
f"{compressed_sessions}/{total_sessions} session(s) compressed successfully."
|
|
939
1187
|
)
|
|
1188
|
+
|
|
1189
|
+
if failed_sessions:
|
|
1190
|
+
print("\nThe following sessions could not be compressed:")
|
|
1191
|
+
for s in failed_sessions:
|
|
1192
|
+
print(f" - {s}")
|
|
1193
|
+
|
|
940
1194
|
return failed_sessions
|
|
941
1195
|
|
|
942
1196
|
|
|
1197
|
+
def _safe_unlink(path: Path) -> None:
|
|
1198
|
+
"""Remove a file, silently ignoring errors (used for partial tar cleanup)."""
|
|
1199
|
+
try:
|
|
1200
|
+
if path.exists():
|
|
1201
|
+
path.unlink()
|
|
1202
|
+
except Exception:
|
|
1203
|
+
pass
|
|
1204
|
+
|
|
1205
|
+
|
|
943
1206
|
#####################################################################################################
|
|
944
1207
|
def get_dicom_info(
|
|
945
1208
|
dicom_file: str,
|