Skip to content

Commit

Permalink
[ENH] Add new possibilities to pet-linear (#1355)
Browse files Browse the repository at this point in the history
* Added node to remove the background

* Added node to clip the image

* Inverse crop nifti and remove background nodes

* Added node to clip the image

* Inverse crop nifti and remove background nodes

* Add function to load MNI binary mask

* Add docstrings

* Add dilated binary mni mask

* Change pet linear cli according to new options

* Resolve remaining conflict

* Fixed input node issues and output node names

* Added the otsu thresholding to remove background

* Fix clipping node

* clip with max of histogram

* select min between 0 and clip thresholg for clipping

* correct clip threshold

* take clip image for transformation

* removed remove background option
  • Loading branch information
ravih18 authored Nov 22, 2024
1 parent cc5e9ba commit 7ab0809
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 60 deletions.
131 changes: 71 additions & 60 deletions clinica/pipelines/pet/linear/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,35 +227,23 @@ def _build_output_node(self):
]
)
if not (self.parameters.get("uncropped_image")):
self.connect(
[
(
self.output_node,
rename_files,
[("outfile_crop", "pet_preprocessed_image_filename")],
),
(
rename_files,
write_node,
[("pet_filename_caps", "@registered_pet")],
),
]
)
node_out_name = "outfile_crop"
else:
self.connect(
[
(
self.output_node,
rename_files,
[("suvr_pet", "pet_preprocessed_image_filename")],
),
(
rename_files,
write_node,
[("pet_filename_caps", "@registered_pet")],
),
]
)
node_out_name = "suvr_pet"
self.connect(
[
(
self.output_node,
rename_files,
[(node_out_name, "pet_preprocessed_image_filename")],
),
(
rename_files,
write_node,
[("pet_filename_caps", "@registered_pet")],
),
]
)
if self.parameters.get("save_PETinT1w"):
self.connect(
[
Expand All @@ -280,7 +268,10 @@ def _build_core_nodes(self):

from clinica.pipelines.tasks import crop_nifti_task

from .tasks import perform_suvr_normalization_task
from .tasks import (
clip_task,
perform_suvr_normalization_task,
)
from .utils import concatenate_transforms, init_input_node, print_end_pipeline

init_node = npe.Node(
Expand All @@ -302,21 +293,31 @@ def _build_core_nodes(self):

# The core (processing) nodes

# 1. `RegistrationSynQuick` by *ANTS*. It uses nipype interface.
# 1. Clipping node
clipping_node = npe.Node(
name="clipping",
interface=nutil.Function(
function=clip_task,
input_names=["input_pet"],
output_names=["output_image"],
),
)

# 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"

# 2. `ApplyTransforms` by *ANTS*. It uses nipype interface. PET to MRI
# 3. `ApplyTransforms` by *ANTS*. It uses nipype interface. PET to MRI
ants_applytransform_node = npe.Node(
name="antsApplyTransformPET2MNI", interface=ants.ApplyTransforms()
)
ants_applytransform_node.inputs.dimension = 3
ants_applytransform_node.inputs.reference_image = self.ref_template

# 3. Normalize the image (using nifti). It uses custom interface, from utils file
# 4. Normalize the image (using nifti). It uses custom interface, from utils file
ants_registration_nonlinear_node = npe.Node(
name="antsRegistrationT1W2MNI", interface=ants.Registration()
)
Expand Down Expand Up @@ -357,12 +358,12 @@ def _build_core_nodes(self):
"normalizing_image_path",
"reference_mask_path",
],
output_names=["output_img"],
output_names=["output_image"],
),
)
normalize_intensity_node.inputs.reference_mask_path = self.ref_mask

# 4. Crop image (using nifti). It uses custom interface, from utils file
# 5. Crop image (using nifti). It uses custom interface, from utils file
crop_nifti_node = npe.Node(
name="cropNifti",
interface=nutil.Function(
Expand All @@ -373,15 +374,15 @@ def _build_core_nodes(self):
)
crop_nifti_node.inputs.output_path = self.base_dir

# 5. Print end message
# 6. Print end message
print_end_message = npe.Node(
interface=nutil.Function(
input_names=["pet", "final_file"], function=print_end_pipeline
),
name="WriteEndMessage",
)

# 6. Optional node: compute PET image in T1w
# 7. Optional node: compute PET image in T1w
ants_applytransform_optional_node = npe.Node(
name="antsApplyTransformPET2T1w", interface=ants.ApplyTransforms()
)
Expand All @@ -390,10 +391,16 @@ def _build_core_nodes(self):
self.connect(
[
(self.input_node, init_node, [("pet", "pet")]),
# STEP 1
(self.input_node, ants_registration_node, [("t1w", "fixed_image")]),
(init_node, ants_registration_node, [("pet", "moving_image")]),
# STEP 1:
(init_node, clipping_node, [("pet", "input_pet")]),
# STEP 2
(
clipping_node,
ants_registration_node,
[("output_image", "moving_image")],
),
(self.input_node, ants_registration_node, [("t1w", "fixed_image")]),
# STEP 3
(
ants_registration_node,
concatenate_node,
Expand All @@ -404,13 +411,17 @@ def _build_core_nodes(self):
concatenate_node,
[("t1w_to_mni", "t1w_to_mni_transform")],
),
(self.input_node, ants_applytransform_node, [("pet", "input_image")]),
(
clipping_node,
ants_applytransform_node,
[("output_image", "input_image")],
),
(
concatenate_node,
ants_applytransform_node,
[("transforms_list", "transforms")],
),
# STEP 3
# STEP 4
(
self.input_node,
ants_registration_nonlinear_node,
Expand Down Expand Up @@ -445,50 +456,50 @@ def _build_core_nodes(self):
(
normalize_intensity_node,
self.output_node,
[("output_img", "suvr_pet")],
[("output_image", "suvr_pet")],
),
(self.input_node, print_end_message, [("pet", "pet")]),
]
)
# STEP 4
# STEP 5
# Case 1: crop the image
if not (self.parameters.get("uncropped_image")):
self.connect(
[
(
normalize_intensity_node,
crop_nifti_node,
[("output_img", "input_image")],
[("output_image", "input_image")],
),
(
crop_nifti_node,
self.output_node,
[("output_image", "outfile_crop")],
),
(
crop_nifti_node,
print_end_message,
[("output_image", "final_file")],
),
]
)
last_node = crop_nifti_node
# Case 2: don't crop the image
else:
self.connect(
[
(
normalize_intensity_node,
print_end_message,
[("output_img", "final_file")],
),
]
)
last_node = normalize_intensity_node
self.connect(
[
(
last_node,
print_end_message,
[("output_image", "final_file")],
),
]
)

# STEP 6: Optional argument
if self.parameters.get("save_PETinT1w"):
self.connect(
[
(
self.input_node,
clipping_node,
ants_applytransform_optional_node,
[("pet", "input_image"), ("t1w", "reference_image")],
[("output_image", "input_image"), ("t1w", "reference_image")],
),
(
ants_registration_node,
Expand Down
10 changes: 10 additions & 0 deletions clinica/pipelines/pet/linear/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ def perform_suvr_normalization_task(
)


def clip_task(
input_pet: str,
) -> str:
from pathlib import Path

from clinica.pipelines.pet.linear.utils import clip_img

return str(clip_img(Path(input_pet)))


def rename_into_caps_task(
pet_bids_image_filename: str,
pet_preprocessed_image_filename: str,
Expand Down
38 changes: 38 additions & 0 deletions clinica/pipelines/pet/linear/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,44 @@ def perform_suvr_normalization(
return output_image


def clip_img(
pet_image_path: Path,
) -> Path:
"""Clip PET images to remove preprocessing artifacts leading to negative values
Parameters
----------
pet_image_path : Path
The path to the image to be processed.
Returns
-------
output_img : Path
The path to the normalized nifti image.
"""
import nibabel as nib
import numpy as np

from clinica.utils.filemanip import get_filename_no_ext

pet_image = nib.load(pet_image_path)

unique, counts = np.unique(
pet_image.get_fdata().reshape(-1), axis=0, return_counts=True
)
clip_threshold = max(0.0, unique[np.argmax(counts)])

data = np.clip(
pet_image.get_fdata(dtype="float32"), a_min=clip_threshold, a_max=None
)

output_image = Path.cwd() / f"{get_filename_no_ext(pet_image_path)}_clipped.nii.gz"
clipped_pet = nib.Nifti1Image(data, pet_image.affine, header=pet_image.header)
clipped_pet.to_filename(output_image)

return output_image


def rename_into_caps(
pet_bids_image_filename: Path,
pet_preprocessed_image_filename: Path,
Expand Down
Binary file not shown.

0 comments on commit 7ab0809

Please sign in to comment.