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
9 changes: 6 additions & 3 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,18 @@ jobs:
uses: ./.github/actions/setup-venv
- name: Run quick tests
id: run-quick-tests
shell: bash
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pretty sure this is default

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You would think so, but without it the windows runs fail.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah because of the linebreaks with \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

switching to no linebreaks is probably better than emulating bash on windows

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or alternatively run just pytest without coverage there - dont think we need multi platform cov

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah i think i'll probably address this workflow in a separate PR. At least for now this works and gives us some tests, but definitely want to change it up.

run: >
uv run pytest \
-m "not slow and not full_pipeline"
-m "not slow and not full_pipeline" \
--junitxml=pytest-quick.xml \
--cov=src tests \
--log-level=DEBUG \
--verbose

- name: Run slow tests (manual trigger)
if: github.event_name == 'workflow_dispatch'
shell: bash
run: >
uv run pytest \
-m "slow and not full_pipeline" \
Expand All @@ -49,6 +51,7 @@ jobs:

- name: Run full pipeline tests (manual trigger)
if: github.event_name == 'workflow_dispatch' && inputs.e2e == true
shell: bash
run: >
uv run pytest \
-m "full_pipeline" \
Expand All @@ -58,12 +61,12 @@ jobs:
--verbose

- name: Generate coverage report
shell: bash
run: >
uv run pytest \
--cov-report=term-missing:skip-covered \
--cov-report=xml:coverage.xml \
--cov=src \
--cov-report-only
--cov=src

- name: Upload coverage to Codecov
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de
Expand Down
7 changes: 3 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ license = "MIT"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"nibabel>=5.3.3",
"niwrap>=0.8.2",
"numpy>=2.4.1",
"scipy>=1.17.0"
"bids2table>=2.1.2",
"niwrap>=0.8.3",
"niwrap-helper>=0.7.0"
]

[dependency-groups]
Expand Down
11 changes: 0 additions & 11 deletions src/rbc/anatomical/__init__.py

This file was deleted.

3 changes: 3 additions & 0 deletions src/rbc/core/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Core RBC methods for varying workflows."""

CPAC_ANTS_SEED = 77742777
14 changes: 14 additions & 0 deletions src/rbc/core/anatomical/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Anatomical processing.

This module defines anatomical MRI processing methods.

Anatomical processing prepares structural brain images (e.g. T1-weighted) for
analysis. The T1w provides high-resolution anatomy that can be used to align
other modalities (e.g. functional) and identify different tissue types
(gray matter, white matter, CSF).
"""

from .registration import ants_registration
from .segmentation import ants_brain_extraction, fsl_tissue_segmentation

__all__ = ["ants_brain_extraction", "ants_registration", "fsl_tissue_segmentation"]
147 changes: 147 additions & 0 deletions src/rbc/core/anatomical/registration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
"""RBC registration method."""

from pathlib import Path
from types import SimpleNamespace

from niwrap import ants

from rbc.core import CPAC_ANTS_SEED
from rbc.core.resources import MNI_TEMPLATES


def ants_registration(
in_file: Path, output_prefix: str, seed: int = CPAC_ANTS_SEED
) -> SimpleNamespace:
"""ANTs registration to MNI152 template.

Args:
in_file: Path to file to be compute transformation with template.
output_prefix: Prefix of output file.
seed: Seed to use for reproducibility.

Returns:
A namespace mapping forward and inverse transformation paths.
"""
registration = ants.ants_registration(
stages=[
ants.ants_registration_stage(
transform=ants.ants_registration_transform_rigid(gradient_step=0.05),
metric=ants.ants_registration_metric_mutual_information(
fixed_image=MNI_TEMPLATES.brain_1mm,
moving_image=in_file,
metric_weight=1,
number_of_bins=ants.ants_registration_number_of_bins(
number_of_bins_value=32,
sampling_strategy=ants.ants_registration_sampling_strategy_1(
sampling_strategy_value="Regular",
sampling_percentage=ants.ants_registration_sampling_percentage_1(
sampling_percentage_value=0.25
),
),
),
),
convergence=ants.ants_registration_convergence(
convergence="100x100",
convergence_threshold=0.000001,
convergence_window_size=20,
),
smoothing_sigmas="2.0x1.0vox",
shrink_factors="2x1",
use_histogram_matching=True,
),
ants.ants_registration_stage(
transform=ants.ants_registration_transform_affine(gradient_step=0.08),
metric=ants.ants_registration_metric_mutual_information(
fixed_image=MNI_TEMPLATES.brain_1mm,
moving_image=in_file,
metric_weight=1,
number_of_bins=ants.ants_registration_number_of_bins(
number_of_bins_value=32,
sampling_strategy=ants.ants_registration_sampling_strategy_1(
sampling_strategy_value="Regular",
sampling_percentage=ants.ants_registration_sampling_percentage_1(
sampling_percentage_value=0.25
),
),
),
),
convergence=ants.ants_registration_convergence(
convergence="100x100",
convergence_threshold=0.000001,
convergence_window_size=20,
),
smoothing_sigmas="1.0x0.0vox",
shrink_factors="2x1",
use_histogram_matching=True,
),
ants.ants_registration_stage(
transform=ants.ants_registration_transform_syn(
gradient_step=0.1,
update_field_variance_in_voxel_space=ants.ants_registration_update_field_variance_in_voxel_space(
update_field_variance_in_voxel_space_value=3,
total_field_variance_in_voxel_space=ants.ants_registration_total_field_variance_in_voxel_space(
total_field_variance_in_voxel_space_value=0
),
),
),
metric=ants.ants_registration_metric_ants_neighbourhood_cross_correlation(
fixed_image=MNI_TEMPLATES.brain_1mm,
moving_image=in_file,
metric_weight=1,
radius=ants.ants_registration_radius(radius_value=4),
),
convergence=ants.ants_registration_convergence(
convergence="100x70x50x20",
convergence_threshold=0.000001,
convergence_window_size=10,
),
smoothing_sigmas="3.0x2.0x1.0x0.0vox",
shrink_factors="8x4x2x1",
use_histogram_matching=True,
),
],
random_seed=seed,
collapse_output_transforms=True,
dimensionality=3,
initial_moving_transform=ants.ants_registration_initial_moving_transform_initialization_feature(
fixed_image=MNI_TEMPLATES.brain_1mm,
moving_image=in_file,
initialization_feature=0,
),
winsorize_image_intensities=ants.ants_registration_winsorize_image_intensities(
lower_quantile=0.005, upper_quantile=0.995
),
interpolation="LanczosWindowedSinc",
output=f"[{output_prefix}_,{output_prefix}_Warped.nii.gz]",
)
fwd = ants.ants_apply_transforms(
reference_image=MNI_TEMPLATES.brain_1mm,
transform=[
ants.ants_apply_transforms_transform_file_name(
registration.root / f"{output_prefix}_0GenericAffine.mat"
),
ants.ants_apply_transforms_transform_file_name(
registration.root / f"{output_prefix}_1Warp.nii.gz"
),
],
output=ants.ants_apply_transforms_composite_displacement_field_output(
composite_displacement_field=f"{output_prefix}_from-T1w_to-template_mode-image_xfm.nii.gz",
print_out_composite_warp_file=True,
),
)
rev = ants.ants_apply_transforms(
reference_image=in_file,
transform=[
ants.ants_apply_transforms_transform_file_name(
registration.root / f"{output_prefix}_1InverseWarp.nii.gz"
),
ants.ants_apply_transforms_use_inverse(
registration.root / f"{output_prefix}_0GenericAffine.mat"
),
],
output=ants.ants_apply_transforms_composite_displacement_field_output(
composite_displacement_field=f"{output_prefix}_from-template_to-T1w_mode-image_xfm.nii.gz",
print_out_composite_warp_file=True,
),
)
return SimpleNamespace(forward=fwd.output, inverse=rev.output)
63 changes: 63 additions & 0 deletions src/rbc/core/anatomical/segmentation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""RBC skull stripping method."""

from pathlib import Path
from types import SimpleNamespace

from niwrap import ants, fsl

from rbc.core.resources import OASIS_TEMPLATES


def ants_brain_extraction(
in_file: Path, output_prefix: str
) -> ants.AntsBrainExtractionShOutputs:
"""ANTs N4 bias correction and brain extraction.

Args:
in_file: Input anatomical file to perform brain extraction on.
output_prefix: Prefix for output file names

Returns:
ANTs brain extraction output object.
"""
return ants.ants_brain_extraction_sh(
image_dimension=3,
anatomical_image=in_file,
template=OASIS_TEMPLATES.template,
probability_mask=OASIS_TEMPLATES.probability_mask,
brain_extraction_registration_mask=OASIS_TEMPLATES.registration_mask,
output_prefix=output_prefix,
image_file_suffix="nii.gz",
random_seeding=False,
)


def fsl_tissue_segmentation(in_file: Path, output_prefix: str) -> SimpleNamespace:
"""FSL Fast tissue classification.

Args:
in_file: Input anatomical file to perform tissue classification on.
output_prefix: Prefix for output file names

Returns:
Namespace with paths to each tissue mask.
"""
tissues = fsl.fast(
in_files=[in_file],
img_type=1,
number_classes=3,
segments=True,
out_basename=output_prefix,
)
masks = {
tissue_type: fsl.fslmaths(
input_files=[tissues.root / f"{output_prefix}_pve_{idx}.nii.gz"],
operations=[
fsl.fslmaths_operation(thr=0.95),
fsl.fslmaths_operation(bin_=True),
],
output=f"{tissue_type}_mask.nii.gz",
).output_file
for idx, tissue_type in enumerate(["csf", "gm", "wm"])
}
return SimpleNamespace(**masks)
28 changes: 28 additions & 0 deletions src/rbc/core/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""General functions useful across modalities."""

from pathlib import Path

from niwrap import afni

from rbc.core.utils import create_copy


def reorient(in_file: Path, output_fname: str) -> afni.V3dresampleOutputs:
"""AFNI deobliquing and reorientation to RPI.

Sets image into a cardinal orientation if it was acquired obliquely from scanner
and standardize orientation of images ('RPI' is internal assumption from AFNI).

Args:
in_file: Input T1w to reorient
output_fname: Output filename

Returns:
An object representing the outputs from AFNI's 3D resample.
"""
with create_copy(in_file) as tmp_file:
afni.v_3drefit(in_file=tmp_file, deoblique=True)
reorient = afni.v_3dresample(
in_file=tmp_file, prefix=output_fname, orientation="RPI"
)
return reorient
8 changes: 8 additions & 0 deletions src/rbc/core/functional/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Functional processing.

This module defines functional MRI processing methods.

Functional MRI measures brain activity over time via the BOLD signal. Raw
fMRI contains motion artifacts, timing differences, and distortions that
must be corrected before analysis.
"""
8 changes: 8 additions & 0 deletions src/rbc/core/longitudinal/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Longitudinal processing.

This module handles longitudinal processing for multi-session data.

Processing steps include the creation of unbiased within-subject anatomical template
and processes each session relative to it, improving sensitivity for detecting changes
over time.
"""
5 changes: 5 additions & 0 deletions src/rbc/core/qc/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Quality control.

This module defines methods for computing quality control metrics, including metrics
like framewise displacement (FD), DVARS, motion-DVARS correlation, and tSNR.
"""
21 changes: 21 additions & 0 deletions src/rbc/core/resources/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Module containing paths to resources."""

from pathlib import Path
from types import SimpleNamespace

RESOURCES_DIR = Path(__file__).parent.resolve()

# OASIS
# (sourced from C-PAC container ghcr.io/fcp-indi/c-pac:many_pipes)
OASIS_DIR = RESOURCES_DIR / "oasis"
OASIS_TEMPLATES = SimpleNamespace(
template=OASIS_DIR / "T_template0.nii.gz",
probability_mask=OASIS_DIR / "T_template0_BrainCerebellumProbabilityMask.nii.gz",
registration_mask=OASIS_DIR / "T_template0_BrainCerebellumRegistrationMask.nii.gz",
)
# MNI152
# (sourced from FSL6.0 in C-PAC container: ghcr.io/fcp-indi/c-pac:many_pipes
MNI_DIR = RESOURCES_DIR / "mni"
MNI_TEMPLATES = SimpleNamespace(brain_1mm=MNI_DIR / "MNI152_T1_1mm_brain.nii.gz")

__all__ = ["MNI_TEMPLATES", "OASIS_TEMPLATES"]
Binary file not shown.
Binary file added src/rbc/core/resources/oasis/T_template0.nii.gz
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading