Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test_full.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ jobs:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v8.0.0
with:
version: "0.11.3"
enable-cache: true # not automatic on self-hosted runners
- run: uv sync

Expand Down
9 changes: 6 additions & 3 deletions scripts/visualize_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,11 @@ def _resample_mosaic(
def _build_mosaic(
data: np.ndarray,
n: int = 7,
slices: list[int] | None = None,
) -> np.ndarray:
"""Build an axial-slice mosaic resampled to standard dimensions."""
slices = _axial_slices(data, n)
if slices is None:
slices = _axial_slices(data, n)
panels = [data[:, :, z].T for z in slices]
mosaic = np.concatenate(panels, axis=1)
return _resample_mosaic(mosaic)
Expand Down Expand Up @@ -285,8 +287,9 @@ def _render_stat_overlay(
) -> None:
"""Lightbox with thresholded stat map overlaid on background."""
bg_vmax = _robust_vmax(bg_data)
bg_mosaic = _build_mosaic(bg_data, n)
stat_mosaic = _build_mosaic(stat_data, n)
slices = _axial_slices(bg_data, n)
bg_mosaic = _build_mosaic(bg_data, n, slices=slices)
stat_mosaic = _build_mosaic(stat_data, n, slices=slices)

ax.imshow(
bg_mosaic,
Expand Down
4 changes: 2 additions & 2 deletions src/rbc/bids/anatomical.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def export_anatomical(anat: Bids, outputs: AnatomicalOutputs) -> None:
anat.save(outputs.wm_mask, suffix=Suffix.MASK, desc="wm")
anat.save(outputs.wm_bbr_mask, suffix=Suffix.MASK, desc="wmBBR")
anat.save(
outputs.forward_xfm,
outputs.anat_to_template_xfm,
suffix="xfm",
extra={
"from": "T1w",
Expand All @@ -75,7 +75,7 @@ def export_anatomical(anat: Bids, outputs: AnatomicalOutputs) -> None:
},
)
anat.save(
outputs.inverse_xfm,
outputs.template_to_anat_xfm,
suffix="xfm",
extra={
"from": TemplateSpace.MNI152NLIN6ASYM,
Expand Down
4 changes: 2 additions & 2 deletions src/rbc/bids/functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ def resolve_functional(
anat_df,
suffix="xfm",
extra={
"from": TemplateSpace.MNI152NLIN6ASYM,
"to": "T1w",
"to": TemplateSpace.MNI152NLIN6ASYM,
"from": "T1w",
"mode": "image",
},
),
Expand Down
20 changes: 14 additions & 6 deletions src/rbc/bids/longitudinal.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from typing import TYPE_CHECKING

from rbc.bids import Suffix
from rbc.bids import Suffix, TemplateSpace

if TYPE_CHECKING:
from pathlib import Path
Expand Down Expand Up @@ -88,14 +88,22 @@ def export_longitudinal_anat(aex: Bids, outputs: AnatomicalLongOutputs) -> None:
desc="wm",
)
aex.save(
outputs.forward_xfm,
outputs.long_to_template_xfm,
suffix="xfm",
extra={"from": "T1w", "to": "longitudinal", "mode": "image"},
extra={
"from": "longitudinal",
"to": TemplateSpace.MNI152NLIN6ASYM,
"mode": "image",
},
)
aex.save(
outputs.inverse_xfm,
outputs.template_to_long_xfm,
suffix="xfm",
extra={"from": "longitudinal", "to": "T1w", "mode": "image"},
extra={
"from": TemplateSpace.MNI152NLIN6ASYM,
"to": "longitudinal",
"mode": "image",
},
)


Expand Down Expand Up @@ -154,7 +162,7 @@ def export_longitudinal_func(fex: Bids, outputs: FunctionalLongOutputs) -> None:
fex.save(outputs.sbref, suffix=Suffix.SBREF)
fex.save(outputs.bold, suffix=Suffix.BOLD, desc="preproc")
fex.save(
outputs.forward_xfm,
outputs.bold_to_long_xfm,
suffix="xfm",
desc="composite",
extra={"from": "bold", "to": "longitudinal", "mode": "image"},
Expand Down
24 changes: 12 additions & 12 deletions src/rbc/core/anatomical/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ class RegistrationOutputs(NamedTuple):

Attributes:
brain: Warped (template-space) skull-stripped brain.
forward: T1w-to-template composite displacement field.
inverse: Template-to-T1w composite displacement field.
forward: anat-to-template composite displacement field.
inverse: Template-to-anat composite displacement field.
"""

brain: Path
forward: Path
inverse: Path
anat_to_template: Path
template_to_anat: Path


def ants_registration(
Expand All @@ -53,8 +53,8 @@ def ants_registration(
(default: MNI152 1 mm).

Returns:
Forward (T1w -> template) and inverse (template -> T1w) composite
transforms.
Transformed brain in template space, forward (T1w -> template) and inverse
(template -> T1w) composite transforms.
"""
registration = ants.ants_registration(
stages=[
Expand Down Expand Up @@ -148,7 +148,7 @@ def ants_registration(
interpolation="LanczosWindowedSinc",
output=f"[{_PREFIX}_,{_PREFIX}_Warped.nii.gz]",
)
fwd = ants.ants_apply_transforms(
anat_to_template = ants.ants_apply_transforms(
reference_image=registration_template,
transform=[
ants.ants_apply_transforms_transform_file_name(
Expand All @@ -159,11 +159,11 @@ def ants_registration(
),
],
output=ants.ants_apply_transforms_composite_displacement_field_output(
composite_displacement_field="forward_xfm.nii.gz",
composite_displacement_field="anat_to_template_xfm.nii.gz",
print_out_composite_warp_file=True,
),
)
rev = ants.ants_apply_transforms(
template_to_anat = ants.ants_apply_transforms(
reference_image=in_file,
transform=[
ants.ants_apply_transforms_transform_file_name(
Expand All @@ -174,14 +174,14 @@ def ants_registration(
),
],
output=ants.ants_apply_transforms_composite_displacement_field_output(
composite_displacement_field="inverse_xfm.nii.gz",
composite_displacement_field="template_to_anat_xfm.nii.gz",
print_out_composite_warp_file=True,
),
)
# ANTs writes the warped image to {prefix}_Warped.nii.gz but NiWrap
# does not expose this path, so we construct it from the output root.
return RegistrationOutputs(
brain=registration.root / f"{_PREFIX}_Warped.nii.gz",
forward=fwd.output.output_image_outfile,
inverse=rev.output.output_image_outfile,
anat_to_template=anat_to_template.output.output_image_outfile,
template_to_anat=template_to_anat.output.output_image_outfile,
)
2 changes: 1 addition & 1 deletion src/rbc/orchestration/all.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def run(
"brain_mask": anat_outputs.brain_mask,
"csf_mask": anat_outputs.csf_mask,
"wm_mask": anat_outputs.wm_mask,
"anat_to_template": anat_outputs.inverse_xfm,
"anat_to_template": anat_outputs.anat_to_template_xfm,
},
tr=tr,
func_template=func_template,
Expand Down
24 changes: 12 additions & 12 deletions src/rbc/workflows/anatomical.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ class AnatomicalOutputs(NamedTuple):
gm_mask: GM tissue mask.
wm_mask: WM tissue mask.
wm_bbr_mask: WM boundary mask for BBR coregistration.
forward_xfm: T1w-to-template composite warp.
inverse_xfm: Template-to-T1w composite warp.
anat_to_template_xfm: anat-to-template composite warp.
template_to_anat_xfm: Template-to-anat composite warp.
"""

brain: Path
Expand All @@ -54,8 +54,8 @@ class AnatomicalOutputs(NamedTuple):
gm_mask: Path
wm_mask: Path
wm_bbr_mask: Path
forward_xfm: Path
inverse_xfm: Path
anat_to_template_xfm: Path
template_to_anat_xfm: Path


def single_session_preprocess(
Expand Down Expand Up @@ -112,8 +112,8 @@ def single_session_preprocess(
gm_mask=tissue_masks.gm,
wm_mask=tissue_masks.wm,
wm_bbr_mask=wm_bbr,
forward_xfm=transforms.forward,
inverse_xfm=transforms.inverse,
anat_to_template_xfm=transforms.anat_to_template,
template_to_anat_xfm=transforms.template_to_anat,
)


Expand All @@ -130,17 +130,17 @@ class AnatomicalLongOutputs(NamedTuple):
or *None* if not provided.
wm_mask: WM tissue mask warped to longitudinal template space,
or *None* if not provided.
forward_xfm: Longitudinal template-to-MNI152 composite warp.
inverse_xfm: MNI152-to-longitudinal template composite warp.
long_to_template_xfm: Longitudinal template-to-MNI152 composite warp.
template_to_long_xfm: MNI152-to-longitudinal template composite warp.
"""

brain: Path
brain_mask: Path | None
csf_mask: Path | None
gm_mask: Path | None
wm_mask: Path | None
forward_xfm: Path
inverse_xfm: Path
long_to_template_xfm: Path
template_to_long_xfm: Path


def longitudinal_process(
Expand Down Expand Up @@ -193,6 +193,6 @@ def _xfm(val: Path | None) -> Path | None:
csf_mask=_xfm(csf_mask),
gm_mask=_xfm(gm_mask),
wm_mask=_xfm(wm_mask),
forward_xfm=transforms.forward,
inverse_xfm=transforms.inverse,
long_to_template_xfm=transforms.anat_to_template,
template_to_long_xfm=transforms.template_to_anat,
)
6 changes: 3 additions & 3 deletions src/rbc/workflows/functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,14 +387,14 @@ class FunctionalLongOutputs(NamedTuple):
"""Outputs from the longitudinal functional preprocessing pipeline.

Attributes:
forward_xfm: BOLD-to-longitudinal-template composite warp.
bold_to_long_xfm: BOLD-to-longitudinal-template composite warp.
sbref: Motion reference volume warped to longitudinal template space.
bold: Preprocessed BOLD warped to longitudinal template space.
bold_mask: Brain mask warped to longitudinal template space,
or *None* if no mask was provided.
"""

forward_xfm: Path
bold_to_long_xfm: Path
sbref: Path
bold: Path
bold_mask: Path | None = None
Expand Down Expand Up @@ -443,5 +443,5 @@ def longitudinal_process(
bold_mask=mask_transform(mask=bold_mask, template=template, xfm=bold_to_tpl_xfm)
if bold_mask
else None,
forward_xfm=bold_to_tpl_xfm,
bold_to_long_xfm=bold_to_tpl_xfm,
)
4 changes: 2 additions & 2 deletions tests/full_pipeline/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,11 @@ def pipeline_data(
brain_mask=anat.brain_mask,
csf_mask=anat.csf_mask,
wm_mask=anat.wm_mask,
anat_to_template=anat.forward_xfm,
anat_to_template=anat.anat_to_template_xfm,
metadata=func_metadata,
)
template_brain_mask = _warp_mask_to_template(
anat.brain_mask, REGISTRATION_TEMPLATES.brain_2mm, anat.forward_xfm
anat.brain_mask, REGISTRATION_TEMPLATES.brain_2mm, anat.anat_to_template_xfm
)
manifest["anat"] = _to_dict(anat)
manifest["func"] = _to_dict(func)
Expand Down
4 changes: 2 additions & 2 deletions tests/integration/test_anatomical.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,5 @@ def test_registration(test_subject: TestSubjectData) -> None:
"""Test anatomical registration."""
reg_outputs = anatomical.ants_registration(in_file=test_subject.t1w)
assert reg_outputs.brain.exists()
assert reg_outputs.forward.exists()
assert reg_outputs.inverse.exists()
assert reg_outputs.anat_to_template.exists()
assert reg_outputs.template_to_anat.exists()
4 changes: 2 additions & 2 deletions tests/unit/bids/test_exports.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ def _make_anat_outputs(w: Path) -> AnatomicalOutputs:
gm_mask=_dummy(w, "gm_mask.nii.gz"),
wm_mask=_dummy(w, "wm_mask.nii.gz"),
wm_bbr_mask=_dummy(w, "wm_bbr_mask.nii.gz"),
forward_xfm=_dummy(w, "forward_xfm.nii.gz"),
inverse_xfm=_dummy(w, "inverse_xfm.nii.gz"),
anat_to_template_xfm=_dummy(w, "anat_to_template_xfm.nii.gz"),
template_to_anat_xfm=_dummy(w, "template_to_anat_xfm.nii.gz"),
)


Expand Down
8 changes: 5 additions & 3 deletions tests/unit/orchestration/test_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,8 @@ def test_anat_outputs_forwarded_as_anat_inputs(self, tmp_path: Path) -> None:
gm_mask=_FAKE / "gm_mask.nii.gz",
wm_mask=_FAKE / "wm_mask.nii.gz",
wm_bbr_mask=_FAKE / "wm_bbr.nii.gz",
forward_xfm=_FAKE / "fwd.nii.gz",
inverse_xfm=_FAKE / "inv.nii.gz",
anat_to_template_xfm=_FAKE / "anat_to_template.nii.gz",
template_to_anat_xfm=_FAKE / "template_to_anat.nii.gz",
)

raw_df = pl.DataFrame(
Expand Down Expand Up @@ -260,4 +260,6 @@ def test_anat_outputs_forwarded_as_anat_inputs(self, tmp_path: Path) -> None:
assert passed_inputs["csf_mask"] == anat_outputs.csf_mask
assert passed_inputs["wm_mask"] == anat_outputs.wm_mask
assert passed_inputs["wm_bbr_mask"] == anat_outputs.wm_bbr_mask
assert passed_inputs["anat_to_template"] == anat_outputs.inverse_xfm
assert (
passed_inputs["anat_to_template"] == anat_outputs.anat_to_template_xfm
)
6 changes: 3 additions & 3 deletions tests/unit/orchestration/test_longitudinal.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ def _mock_anat_outputs() -> Mock:
m.csf_mask = fake / "csf_mask.nii.gz"
m.gm_mask = fake / "gm_mask.nii.gz"
m.wm_mask = fake / "wm_mask.nii.gz"
m.forward_xfm = fake / "fwd_xfm.nii.gz"
m.inverse_xfm = fake / "inverse_xfm.nii.gz"
m.long_to_template_xfm = fake / "long_to_template_xfm.nii.gz"
m.template_to_long_xfm = fake / "template_to_long_xfm.nii.gz"
return m


Expand All @@ -66,7 +66,7 @@ def _mock_func_outputs(*, with_bold_mask: bool = True) -> Mock:
m = Mock()
m.sbref = fake / "sbref.nii.gz"
m.bold = fake / "bold.nii.gz"
m.forward_xfm = fake / "fwd_xfm.nii.gz"
m.bold_to_long_xfm = fake / "bold_to_long_xfm.nii.gz"
m.bold_mask = (fake / "bold_mask.nii.gz") if with_bold_mask else None
return m

Expand Down