Skip to content

Commit 5f1a9ee

Browse files
committed
refactor to use BIDSLayoutIndexer across project
1 parent b45bd0d commit 5f1a9ee

File tree

2 files changed

+93
-25
lines changed

2 files changed

+93
-25
lines changed

petdeface/petdeface.py

Lines changed: 86 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -571,7 +571,13 @@ def write_out_dataset_description_json(input_bids_dir, output_bids_dir=None):
571571

572572

573573
def wrap_up_defacing(
574-
path_to_dataset, output_dir=None, placement="adjacent", remove_existing=True, participant_label_exclude=[], session_label_exclude=[]
574+
path_to_dataset,
575+
output_dir=None,
576+
placement="adjacent",
577+
remove_existing=True,
578+
participant_label_exclude=[],
579+
session_label_exclude=[],
580+
indexer=None,
575581
):
576582
"""
577583
This function maps the output of this pipeline to the original dataset and depending on the
@@ -609,17 +615,21 @@ def wrap_up_defacing(
609615
:type participant_label_exclude: list, optional
610616
:param session_label_exclude: Excludes set of sessions from the finalized output
611617
:type session_label_exclude: list, optional
618+
:param indexer: Pre-built BIDSLayoutIndexer with exclusion patterns, defaults to None
619+
:type indexer: BIDSLayoutIndexer, optional
612620
:raises ValueError: _description_
613621
"""
614-
subjects_to_exclude = [f"sub-{sub}/*" for sub in participant_label_exclude]
615-
sessions_to_exclude = [f"*ses-{ses}/*" for ses in session_label_exclude]
616-
exclude_total = subjects_to_exclude + sessions_to_exclude
617-
618-
# build an indexer
619-
exclude = BIDSLayoutIndexer(ignore=exclude_total)
620-
621-
# get bids layout of dataset
622-
layout = BIDSLayout(path_to_dataset, derivatives=True, validate=False, indexer=exclude)
622+
# Use provided indexer or create one from exclude lists (fallback for compatibility)
623+
if indexer is None:
624+
subjects_to_exclude = [f"sub-{sub}/*" for sub in participant_label_exclude]
625+
sessions_to_exclude = [f"*ses-{ses}/*" for ses in session_label_exclude]
626+
exclude_total = subjects_to_exclude + sessions_to_exclude
627+
indexer = BIDSLayoutIndexer(ignore=exclude_total)
628+
629+
# get bids layout of dataset using the indexer
630+
layout = BIDSLayout(
631+
path_to_dataset, derivatives=True, validate=False, indexer=indexer
632+
)
623633

624634
# collect defaced images
625635
try:
@@ -632,10 +642,9 @@ def wrap_up_defacing(
632642
sys.exit(1)
633643

634644
# collect all original images and jsons
635-
raw_only = BIDSLayout(path_to_dataset, derivatives=False, indexer=exclude)
636-
637-
raw_images_only = raw_only.get(
638-
suffix=["pet", "T1w"])
645+
raw_only = BIDSLayout(path_to_dataset, derivatives=False, indexer=indexer)
646+
647+
raw_images_only = raw_only.get(suffix=["pet", "T1w"])
639648

640649
# if output_dir is not None and is not the same as the input dir we want to clear it out
641650
if output_dir is not None and output_dir != path_to_dataset and remove_existing:
@@ -810,6 +819,9 @@ def __init__(
810819
self.session_label = session_label
811820
self.session_label_exclude = session_label_exclude
812821

822+
# Build comprehensive exclusion indexer considering both include and exclude parameters
823+
self.exclude_indexer = self._build_exclusion_indexer()
824+
813825
# check if freesurfer license is valid
814826
self.fs_license = check_valid_fs_license()
815827
if not self.fs_license:
@@ -821,6 +833,64 @@ def __init__(
821833
f"Using freesurfer license at {self.fs_license} found in system env at $FREESURFER_LICENSE"
822834
)
823835

836+
def _build_exclusion_indexer(self):
837+
"""
838+
Build a comprehensive BIDSLayoutIndexer that excludes subjects and sessions based on:
839+
1. Explicit exclusion lists (participant_label_exclude, session_label_exclude)
840+
2. Implicit exclusions from include-only lists (participant_label, session_label)
841+
842+
Returns:
843+
BIDSLayoutIndexer: Indexer configured to ignore excluded subjects/sessions
844+
"""
845+
# Create a temporary layout to get all available subjects and sessions
846+
temp_layout = BIDSLayout(self.bids_dir, derivatives=False, validate=False)
847+
all_subjects = temp_layout.get_subjects()
848+
all_sessions = temp_layout.get_sessions()
849+
850+
# Start with explicitly excluded subjects
851+
excluded_subjects = set(self.participant_label_exclude)
852+
853+
# If specific subjects are requested (include-only), exclude all others
854+
if self.subject and self.subject != "":
855+
# Handle both string and list formats
856+
if isinstance(self.subject, str):
857+
included_subjects = [self.subject] if self.subject else []
858+
else:
859+
included_subjects = self.subject
860+
861+
# Remove 'sub-' prefix if present
862+
included_subjects = [sub.replace("sub-", "") for sub in included_subjects]
863+
864+
# Add all subjects not in the include list to exclusions
865+
excluded_subjects.update(
866+
sub for sub in all_subjects if sub not in included_subjects
867+
)
868+
869+
# Start with explicitly excluded sessions
870+
excluded_sessions = set(self.session_label_exclude)
871+
872+
# If specific sessions are requested (include-only), exclude all others
873+
if self.session_label:
874+
# Remove 'ses-' prefix if present
875+
included_sessions = [ses.replace("ses-", "") for ses in self.session_label]
876+
877+
# Add all sessions not in the include list to exclusions
878+
excluded_sessions.update(
879+
ses for ses in all_sessions if ses not in included_sessions
880+
)
881+
882+
# Convert to ignore patterns for BIDSLayoutIndexer
883+
ignore_patterns = []
884+
885+
# Add subject exclusion patterns
886+
ignore_patterns.extend([f"sub-{sub}/*" for sub in excluded_subjects])
887+
888+
# Add session exclusion patterns
889+
ignore_patterns.extend([f"*/ses-{ses}/*" for ses in excluded_sessions])
890+
891+
# Build and return the indexer
892+
return BIDSLayoutIndexer(ignore=ignore_patterns)
893+
824894
def run(self):
825895
"""
826896
Runs the defacing workflow given inputs from instiantiation and wraps up defacing by collecting output
@@ -849,7 +919,8 @@ def run(self):
849919
placement=self.placement,
850920
remove_existing=self.remove_existing,
851921
participant_label_exclude=self.participant_label_exclude,
852-
session_label_exclude=self.session_label_exclude
922+
session_label_exclude=self.session_label_exclude,
923+
indexer=self.exclude_indexer,
853924
)
854925

855926

tests/test_dir_layouts.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ def test_anat_in_subject_folder():
124124
)
125125
petdeface.run()
126126

127+
127128
def test_no_anat():
128129
# create a temporary directory to copy the existing dataset into
129130
with tempfile.TemporaryDirectory() as tmpdir:
@@ -136,17 +137,15 @@ def test_no_anat():
136137
# run petdeface on the copied dataset
137138
petdeface = PetDeface(
138139
Path(tmpdir) / "no_anat",
139-
output_dir=Path(tmpdir)
140-
/ "no_anat_defaced"
141-
/ "derivatives"
142-
/ "petdeface",
140+
output_dir=Path(tmpdir) / "no_anat_defaced" / "derivatives" / "petdeface",
143141
n_procs=nthreads,
144142
)
145-
143+
146144
# now we want to assert that this pipeline crashes and print the error
147145
with pytest.raises(FileNotFoundError) as exc_info:
148146
petdeface.run()
149147

148+
150149
def test_invalid_bids():
151150
with tempfile.TemporaryDirectory() as tmpdir:
152151
shutil.copytree(data_dir, Path(tmpdir) / "invalid")
@@ -155,19 +154,17 @@ def test_invalid_bids():
155154
pet_folder = subject_folder / "ses-baseline" / "pet"
156155
for file in pet_folder.glob("sub-01_*"):
157156
shutil.move(
158-
file,
159-
pet_folder / file.name.replace("sub-01", "sub-01-bestsubject")
157+
file, pet_folder / file.name.replace("sub-01", "sub-01-bestsubject")
160158
)
161-
159+
162160
# run petdeface on the invalid dataset
163161
petdeface = PetDeface(
164162
Path(tmpdir) / "invalid",
165163
output_dir=Path(tmpdir) / "invalid_defaced" / "derivatives" / "petdeface",
166164
n_procs=nthreads,
167165
)
168-
166+
169167
# Run it and see what error gets raised
170168
with pytest.raises(InvalidBIDSDataset) as exc_info:
171169
petdeface.run()
172170
assert "Dataset at" in str(exc_info.value)
173-

0 commit comments

Comments
 (0)