Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ENH] Add the possibility to use ANTsPy instead of ANTs for PETLinear #1254

Draft
wants to merge 2 commits into
base: dev
Choose a base branch
from
Draft
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
7 changes: 7 additions & 0 deletions clinica/pipelines/pet/linear/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@
@cli_param.option.working_directory
@option.global_option_group
@option.n_procs
@cli_param.option.option(
"--use-antspy",
is_flag=True,
help="Use ANTsPy instead of ANTs.",
)
def cli(
bids_directory: str,
caps_directory: str,
Expand All @@ -46,6 +51,7 @@ def cli(
subjects_sessions_tsv: Optional[str] = None,
working_directory: Optional[str] = None,
n_procs: Optional[int] = None,
use_antspy: bool = False,
) -> None:
"""Affine registration of PET images to the MNI standard space.

Expand Down Expand Up @@ -81,6 +87,7 @@ def cli(
base_dir=working_directory,
parameters=parameters,
name=pipeline_name,
use_antspy=use_antspy,
)

exec_pipeline = (
Expand Down
261 changes: 218 additions & 43 deletions clinica/pipelines/pet/linear/pipeline.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Use hash instead of parameters for iterables folder names
# Otherwise path will be too long and generate OSError
from typing import List
from typing import List, Optional

import nipype.pipeline.engine as npe
from nipype import config

from clinica.pipelines.pet.engine import PETPipeline
Expand All @@ -26,6 +27,44 @@ class PETLinear(PETPipeline):
A clinica pipeline object containing the pet_linear pipeline.
"""

def __init__(
self,
bids_directory: Optional[str] = None,
caps_directory: Optional[str] = None,
tsv_file: Optional[str] = None,
overwrite_caps: Optional[bool] = False,
base_dir: Optional[str] = None,
parameters: Optional[dict] = None,
name: Optional[str] = None,
ignore_dependencies: Optional[List[str]] = None,
use_antspy: bool = False,
):
from clinica.utils.stream import cprint

super().__init__(
bids_directory=bids_directory,
caps_directory=caps_directory,
tsv_file=tsv_file,
overwrite_caps=overwrite_caps,
base_dir=base_dir,
parameters=parameters,
ignore_dependencies=ignore_dependencies,
name=name,
)
self.use_antspy = use_antspy
if self.use_antspy:
self._ignore_dependencies.append("ants")
cprint(
(
"The PETLinear pipeline has been configured to use ANTsPy instead of ANTs.\n"
"This means that no installation of ANTs is required, but the antspyx Python "
"package must be installed in your environment.\nThis functionality has been "
"introduced in Clinica 0.9.0 and is considered experimental.\n"
"Please report any issue or unexpected results to the Clinica developer team."
),
lvl="warning",
)

def _check_custom_dependencies(self) -> None:
"""Check dependencies that can not be listed in the `info.json` file."""
pass
Expand Down Expand Up @@ -263,10 +302,13 @@ def _build_output_node(self):
def _build_core_nodes(self):
"""Build and connect the core nodes of the pipeline."""
import nipype.interfaces.utility as nutil
import nipype.pipeline.engine as npe
from nipype.interfaces import ants

from clinica.pipelines.tasks import crop_nifti_task
from clinica.pipelines.tasks import (
crop_nifti_task,
get_filename_no_ext_task,
run_ants_apply_transforms_task,
)

from .tasks import (
clip_task,
Expand All @@ -282,6 +324,14 @@ def _build_core_nodes(self):
),
name="initPipeline",
)
# image_id_node = npe.Node(
# interface=nutil.Function(
# input_names=["filename"],
# output_names=["image_id"],
# function=get_filename_no_ext_task,
# ),
# name="ImageID",
# )
concatenate_node = npe.Node(
interface=nutil.Function(
input_names=["pet_to_t1w_transform", "t1w_to_mni_transform"],
Expand All @@ -291,8 +341,6 @@ def _build_core_nodes(self):
name="concatenateTransforms",
)

# The core (processing) nodes

# 1. Clipping node
clipping_node = npe.Node(
name="clipping",
Expand All @@ -304,52 +352,53 @@ def _build_core_nodes(self):
)
clipping_node.inputs.output_dir = self.base_dir

# 2. `RegistrationSynQuick` by *ANTS*. It uses nipype interface.
ants_registration_node = npe.Node(
name="antsRegistration", interface=ants.RegistrationSynQuick()
)
ants_registration_node.inputs.dimension = 3
ants_registration_node.inputs.transform_type = "r"

# 3. `ApplyTransforms` by *ANTS*. It uses nipype interface. PET to MRI
ants_registration_node = self._build_ants_registration_node()
ants_applytransform_node = npe.Node(
name="antsApplyTransformPET2MNI", interface=ants.ApplyTransforms()
name="antsApplyTransformPET2MNI",
interface=(
nutil.Function(
function=run_ants_apply_transforms_task,
input_names=[
"reference_image",
"input_image",
"transforms",
"output_dir",
],
output_names=["output_image"],
)
if self.use_antspy
else ants.ApplyTransforms()
),
)
ants_applytransform_node.inputs.dimension = 3
if not self.use_antspy:
ants_applytransform_node.inputs.dimension = 3
ants_applytransform_node.inputs.reference_image = self.ref_template

# 4. Normalize the image (using nifti). It uses custom interface, from utils file
ants_registration_nonlinear_node = npe.Node(
name="antsRegistrationT1W2MNI", interface=ants.Registration()
ants_registration_nonlinear_node = (
self._build_ants_registration_nonlinear_node()
)
ants_registration_nonlinear_node.inputs.fixed_image = self.ref_template
ants_registration_nonlinear_node.inputs.metric = ["MI"]
ants_registration_nonlinear_node.inputs.metric_weight = [1.0]
ants_registration_nonlinear_node.inputs.transforms = ["SyN"]
ants_registration_nonlinear_node.inputs.transform_parameters = [(0.1, 3, 0)]
ants_registration_nonlinear_node.inputs.dimension = 3
ants_registration_nonlinear_node.inputs.shrink_factors = [[8, 4, 2]]
ants_registration_nonlinear_node.inputs.smoothing_sigmas = [[3, 2, 1]]
ants_registration_nonlinear_node.inputs.sigma_units = ["vox"]
ants_registration_nonlinear_node.inputs.number_of_iterations = [[200, 50, 10]]
ants_registration_nonlinear_node.inputs.convergence_threshold = [1e-05]
ants_registration_nonlinear_node.inputs.convergence_window_size = [10]
ants_registration_nonlinear_node.inputs.radius_or_number_of_bins = [32]
ants_registration_nonlinear_node.inputs.winsorize_lower_quantile = 0.005
ants_registration_nonlinear_node.inputs.winsorize_upper_quantile = 0.995
ants_registration_nonlinear_node.inputs.collapse_output_transforms = True
ants_registration_nonlinear_node.inputs.use_histogram_matching = False
ants_registration_nonlinear_node.inputs.verbose = True

ants_applytransform_nonlinear_node = npe.Node(
name="antsApplyTransformNonLinear", interface=ants.ApplyTransforms()
name="antsApplyTransformNonLinear",
interface=(
nutil.Function(
function=run_ants_apply_transforms_task,
input_names=[
"reference_image",
"input_image",
"transforms",
"output_dir",
],
output_names=["output_image"],
)
if self.use_antspy
else ants.ApplyTransforms()
),
)
ants_applytransform_nonlinear_node.inputs.dimension = 3
if not self.use_antspy:
ants_applytransform_nonlinear_node.inputs.dimension = 3
ants_applytransform_nonlinear_node.inputs.reference_image = self.ref_template

if random_seed := self.parameters.get("random_seed", None):
ants_registration_nonlinear_node.inputs.random_seed = random_seed

normalize_intensity_node = npe.Node(
name="intensityNormalization",
interface=nutil.Function(
Expand Down Expand Up @@ -385,9 +434,24 @@ def _build_core_nodes(self):

# 7. Optional node: compute PET image in T1w
ants_applytransform_optional_node = npe.Node(
name="antsApplyTransformPET2T1w", interface=ants.ApplyTransforms()
name="antsApplyTransformPET2T1w",
interface=(
nutil.Function(
function=run_ants_apply_transforms_task,
input_names=[
"reference_image",
"input_image",
"transforms",
"output_dir",
],
output_names=["output_image"],
)
if self.use_antspy
else ants.ApplyTransforms()
),
)
ants_applytransform_optional_node.inputs.dimension = 3
if not self.use_antspy:
ants_applytransform_optional_node.inputs.dimension = 3

self.connect(
[
Expand Down Expand Up @@ -514,3 +578,114 @@ def _build_core_nodes(self):
),
]
)

def _build_ants_registration_node(self) -> npe.Node:
import nipype.interfaces.utility as nutil
from nipype.interfaces import ants

from clinica.pipelines.tasks import run_ants_registration_synquick_task
from clinica.pipelines.utils import AntsRegistrationSynQuickTransformType

ants_registration_node = npe.Node(
name="antsRegistration",
interface=(
nutil.Function(
function=run_ants_registration_synquick_task,
input_names=[
"fixed_image",
"moving_image",
"random_seed",
"transform_type",
"output_prefix",
"output_dir",
],
output_names=["warped_image", "out_matrix"],
)
if self.use_antspy
else ants.RegistrationSynQuick()
),
)
ants_registration_node.inputs.fixed_image = self.ref_template
if self.use_antspy:
ants_registration_node.inputs.output_dir = str(self.base_dir)
ants_registration_node.inputs.transform_type = (
AntsRegistrationSynQuickTransformType.RIGID
)
else:
ants_registration_node.inputs.transform_type = "r"
ants_registration_node.inputs.dimension = 3
ants_registration_node.inputs.random_seed = (
self.parameters.get("random_seed", None) or 0
)

return ants_registration_node

def _build_ants_registration_nonlinear_node(self) -> npe.Node:
import nipype.interfaces.utility as nutil
from nipype.interfaces import ants

from clinica.pipelines.tasks import run_ants_registration_task
from clinica.pipelines.utils import AntsRegistrationTransformType

ants_registration_nonlinear_node = npe.Node(
name="antsRegistrationT1W2MNI",
interface=(
nutil.Function(
function=run_ants_registration_task,
input_names=[
"fixed_image",
"moving_image",
"random_seed",
"transform_type",
"output_prefix",
"output_dir",
"shrink_factors",
"smoothing_sigmas",
"number_of_iterations",
"return_inverse_transform",
],
output_names=[
"warped_image",
"out_matrix",
"reverse_forward_transforms",
],
)
if self.use_antspy
else ants.Registration()
),
)
ants_registration_nonlinear_node.inputs.fixed_image = self.ref_template
if self.use_antspy:
ants_registration_nonlinear_node.inputs.transform_type = (
AntsRegistrationTransformType.SYN
)
ants_registration_nonlinear_node.inputs.shrink_factors = (8, 4, 2)
ants_registration_nonlinear_node.inputs.smoothing_sigmas = (3, 2, 1)
ants_registration_nonlinear_node.inputs.number_of_iterations = (200, 50, 10)
ants_registration_nonlinear_node.inputs.return_inverse_transform = True
else:
ants_registration_nonlinear_node.inputs.metric = ["MI"]
ants_registration_nonlinear_node.inputs.metric_weight = [1.0]
ants_registration_nonlinear_node.inputs.transforms = ["SyN"]
ants_registration_nonlinear_node.inputs.dimension = 3
ants_registration_nonlinear_node.inputs.shrink_factors = [[8, 4, 2]]
ants_registration_nonlinear_node.inputs.smoothing_sigmas = [[3, 2, 1]]
ants_registration_nonlinear_node.inputs.sigma_units = ["vox"]
ants_registration_nonlinear_node.inputs.number_of_iterations = [
[200, 50, 10]
]
ants_registration_nonlinear_node.inputs.radius_or_number_of_bins = [32]

ants_registration_nonlinear_node.inputs.transform_parameters = [(0.1, 3, 0)]
ants_registration_nonlinear_node.inputs.convergence_threshold = [1e-05]
ants_registration_nonlinear_node.inputs.convergence_window_size = [10]
ants_registration_nonlinear_node.inputs.winsorize_lower_quantile = 0.005
ants_registration_nonlinear_node.inputs.winsorize_upper_quantile = 0.995
ants_registration_nonlinear_node.inputs.collapse_output_transforms = True
ants_registration_nonlinear_node.inputs.use_histogram_matching = False
ants_registration_nonlinear_node.inputs.verbose = True
ants_registration_nonlinear_node.inputs.random_seed = (
self.parameters.get("random_seed", None) or 0
)

return ants_registration_nonlinear_node
Loading
Loading