Skip to content
2 changes: 1 addition & 1 deletion src/rbc/bids/longitudinal/anatomical.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def resolve_longitudinal_anat(
Dict with keys matching ``longitudinal_process`` parameters.
"""
return {
"template": tpl_q.expect(tpl_df, suffix=Suffix.T1W),
"template": tpl_q.expect(tpl_df, suffix=Suffix.T1W, without=["res"]),
"subj_to_template_xfm": tpl_q.expect(
tpl_df,
suffix="xfm",
Expand Down
3 changes: 2 additions & 1 deletion src/rbc/bids/longitudinal/functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ def resolve_longitudinal_func(
without=["space"],
)

task = func_df["task"].unique()[0]
Comment thread
kaitj marked this conversation as resolved.
Outdated
return {
"template": tpl_q.expect(tpl_df, suffix="T1w"),
"template": tpl_q.expect(tpl_df, suffix="T1w", res=task),
"anat_to_template_xfm": tpl_q.expect(
tpl_df,
suffix="xfm",
Expand Down
25 changes: 24 additions & 1 deletion src/rbc/bids/longitudinal/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ class TemplateInputs(NamedTuple):
sub: Subject label.
sessions: Per-input session labels (parallel to ``files``).
files: Per-session preprocessed T1w brain volumes.
bold_ref: Per-session preprocessed BOLD volumes (all tasks).
Comment thread
kaitj marked this conversation as resolved.
Outdated
"""

sub: str
sessions: list[str]
files: list[Path]
bold_files: dict[str, Path]


def discover_template_inputs(
Expand Down Expand Up @@ -56,6 +58,13 @@ def discover_template_inputs(
# the mri_robust_template invocation.
pl.col("space").is_null(),
)
bold_rows = df.filter(
pl.col("ses") != "longitudinal",
pl.col("datatype") == "func",
pl.col("desc") == "preproc",
pl.col("suffix") == "bold",
pl.col("space").is_null(),
)

inputs: list[TemplateInputs] = []
skipped: list[str] = []
Expand All @@ -68,7 +77,19 @@ def discover_template_inputs(
files = [
Path(row["root"]) / row["path"] for row in sub_group.iter_rows(named=True)
]
inputs.append(TemplateInputs(sub=sub, sessions=sessions, files=files))
# Filter for first found session; only single reference per task is needed
sub_bold = bold_rows.filter(
(pl.col("sub") == sub) & (pl.col("ses") == sessions[0])
)
bold_files = {
row["task"]: Path(row["root"]) / row["path"]
Comment thread
kaitj marked this conversation as resolved.
Outdated
for row in sub_bold.iter_rows(named=True)
}
Comment thread
kaitj marked this conversation as resolved.
Outdated
inputs.append(
TemplateInputs(
sub=sub, sessions=sessions, files=files, bold_files=bold_files
)
)
return inputs, skipped


Expand All @@ -81,6 +102,8 @@ def export_template(tpl: Bids, outputs: LongitudinalTemplateOutputs) -> None:
outputs: Results from the longitudinal template workflow.
"""
tpl.save(outputs.template, suffix=Suffix.T1W)
for btask, bold_template in outputs.bold_templates.items():
tpl.save(bold_template, res=btask, suffix=Suffix.T1W)
Comment thread
kaitj marked this conversation as resolved.
Outdated
for ses, xfm in zip(outputs.sessions, outputs.transforms, strict=True):
ses_label = bids_safe_label(ses)
tpl.save(
Expand Down
41 changes: 41 additions & 0 deletions src/rbc/core/longitudinal/resampling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""Resampling utilities for longitudinal templates."""

from __future__ import annotations

from typing import TYPE_CHECKING

import nibabel as nib
from nibabel.processing import resample_from_to

if TYPE_CHECKING:
from pathlib import Path

from rbc.core.niwrap import generate_exec_folder


def resample_img_to_bold_grid(bold_ref: Path, img: Path) -> Path:
"""Resample template to BOLD grid if shapes differ.

Args:
bold_ref: BOLD reference volume.
img: 3D image in target space to resample.

Returns:
Resampled 3D image with BOLD grid
"""
bold_ref_img = nib.nifti1.load(bold_ref)
img_obj = nib.nifti1.load(img)

# If 4D, extract first volume
if len(bold_ref_img.shape) > 3:
bold_ref_img = bold_ref_img.slicer[..., 0]
# If same shape, no need to resample
if bold_ref_img.shape == img_obj.shape:
return img
Comment thread
kaitj marked this conversation as resolved.
Outdated

img_resampled = resample_from_to(img_obj, bold_ref_img)
img_resampled_path = (
generate_exec_folder("img_resample_to_bold_grid") / "resampled.nii.gz"
)
nib.save(img_resampled, img_resampled_path)
return img_resampled_path
5 changes: 5 additions & 0 deletions src/rbc/orchestration/longitudinal/qc.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
write_longitudinal_qc_tsv,
)
from rbc.bids.session import iter_session_files
from rbc.core.longitudinal.resampling import resample_img_to_bold_grid
from rbc.core.niwrap import generate_exec_folder
from rbc.core.qc.registration import registration_qc_metrics
from rbc.orchestration import Filters, RunnerConfig, init_runner
Expand Down Expand Up @@ -67,6 +68,10 @@ def process_qc(
Returns:
QC outputs with overlap metrics and pass/fail flag.
"""
# Resample longitudinal anatomical mask to bold grid for QC purposes.
# Longitudinal processed data are registered to the longitudinal template with
# respective modality's native resolution
anat_brain_mask = resample_img_to_bold_grid(bold_mask, anat_brain_mask)
Comment thread
kaitj marked this conversation as resolved.
Outdated
anat_mask_arr = nib.nifti1.load(anat_brain_mask).get_fdata()
bold_mask_arr = nib.nifti1.load(bold_mask).get_fdata()
reg_metrics = registration_qc_metrics(anat_mask_arr, bold_mask_arr)
Expand Down
1 change: 1 addition & 0 deletions src/rbc/orchestration/longitudinal/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def process_subject(
sub=inputs.sub,
sessions=inputs.sessions,
in_files=inputs.files,
bold_files=inputs.bold_files,
)
tpl = pipe_ctx.bids(datatype=Datatype.ANAT).derive(ses="longitudinal")
export_template(tpl, outputs)
Expand Down
14 changes: 13 additions & 1 deletion src/rbc/workflows/longitudinal/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@
fs_to_itk_xfm,
generate_robust_template,
)
from rbc.core.longitudinal.resampling import resample_img_to_bold_grid

if TYPE_CHECKING:
from collections.abc import Sequence
from collections.abc import Mapping, Sequence
from pathlib import Path

_logger = logging.getLogger("rbc")
Expand All @@ -27,11 +28,13 @@ class LongitudinalTemplateOutputs(NamedTuple):

Attributes:
template: Robust within-subject template volume.
bold_template: Within-subject template volume resampled to BOLD resolution.
Comment thread
kaitj marked this conversation as resolved.
Outdated
sessions: Session labels in the same order as ``transforms``.
transforms: Per-session ITK-format session-to-template transforms.
"""

template: Path
bold_templates: dict[str, Path]
sessions: list[str]
transforms: list[Path]

Expand All @@ -40,13 +43,15 @@ def generate_subject_template(
sub: str,
sessions: Sequence[str],
in_files: Sequence[Path],
bold_files: Mapping[str, Path],
) -> LongitudinalTemplateOutputs:
"""Build a robust template and ITK transforms for one subject.

Args:
sub: Subject label (without the ``sub-`` prefix).
sessions: Session labels parallel to ``in_files``.
in_files: Per-session preprocessed T1w volumes (e.g. brain-extracted).
bold_files: Reference bold volumes to resample template for functional data.

Returns:
:class:`LongitudinalTemplateOutputs` ready for BIDS export.
Expand All @@ -65,8 +70,15 @@ def generate_subject_template(
in_xfms=robust.transforms,
)

_logger.info("Creating reference volumes for each functional task")
bold_templates = {
btask: resample_img_to_bold_grid(bfile, robust.template)
for btask, bfile in bold_files.items()
}

return LongitudinalTemplateOutputs(
template=robust.template,
bold_templates=bold_templates,
sessions=list(sessions),
transforms=itk_xfms,
)
1 change: 1 addition & 0 deletions tests/unit/bids/test_longitudinal_anatomical.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ def _anat_row(
"sub": sub,
"ses": ses,
"desc": desc,
"res": None,
"root": "/data",
"path": path,
"extra_entities": extra or [],
Expand Down
Loading
Loading