Dual-Echo Denoising with nilearn

Dual-Echo Denoising with nilearn#

Dual-echo fMRI leverages one of the same principles motivating multi-echo fMRI; namely, that BOLD contrast increases with echo time, so earlier echoes tend to be more affected by non-BOLD noise than later ones. At an early enough echo time (<5ms for 3T scanners), the signal is almost entirely driven by non-BOLD noise. When it comes to denoising, this means that, if you acquire data with both an early echo time and a more typical echo time (~30ms for 3T), you can simply regress the earlier echo’s time series out of the later echo’s time series, which will remove a lot of non-BOLD noise.

Additionally, dual-echo fMRI comes at no real cost in terms of temporal or spatial resolution, unlike multi-echo fMRI. For multi-echo denoising to work, you need to have at least one echo time that is later than the typical echo time, which means decreasing your temporal resolution, all else remaining equal. In the case of dual-echo fMRI, you only need a shorter echo time, which occurs in what is essentially “dead time” in the pulse sequence.

Dual-echo denoising was originally proposed in Bright & Murphy (2013).

import os

import matplotlib.pyplot as plt
from book_utils import regress_one_image_out_of_another
from myst_nb import glue
from nilearn import plotting
from repo2data.repo2data import Repo2Data

# Install the data if running locally, or point to cached data if running on neurolibre
DATA_REQ_FILE = os.path.join("../binder/data_requirement.json")

# Download data
repo2data = Repo2Data(DATA_REQ_FILE)
data_path = repo2data.install()
data_path = os.path.abspath(data_path[0])
---- repo2data starting ----
/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/repo2data
Config from file :
../binder/data_requirement.json
Destination:
./../data/multi-echo-data-analysis

Info : ./../data/multi-echo-data-analysis already downloaded
te1_img = os.path.join(
    data_path,
    "sub-04570/func/sub-04570_task-rest_echo-1_space-scanner_desc-partialPreproc_bold.nii.gz",
)
te2_img = os.path.join(
    data_path,
    "sub-04570/func/sub-04570_task-rest_echo-2_space-scanner_desc-partialPreproc_bold.nii.gz",
)
mask_img = os.path.join(
    data_path, "sub-04570/func/sub-04570_task-rest_space-scanner_desc-brain_mask.nii.gz"
)
denoised_img = regress_one_image_out_of_another(te2_img, te1_img, mask_img)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[2], line 12
      5 te2_img = os.path.join(
      6     data_path,
      7     "sub-04570/func/sub-04570_task-rest_echo-2_space-scanner_desc-partialPreproc_bold.nii.gz",
      8 )
      9 mask_img = os.path.join(
     10     data_path, "sub-04570/func/sub-04570_task-rest_space-scanner_desc-brain_mask.nii.gz"
     11 )
---> 12 denoised_img = regress_one_image_out_of_another(te2_img, te1_img, mask_img)

File ~/work/multi-echo-data-analysis/multi-echo-data-analysis/content/book_utils.py:10, in regress_one_image_out_of_another(data_img, nuis_img, mask_img)
      8 """Do what it says on the tin."""
      9 # First, mean-center each image over time
---> 10 mean_data_img = image.mean_img(data_img)
     11 mean_nuis_img = image.mean_img(nuis_img)
     13 data_img_mc = image.math_img(
     14     "img - avg_img[..., None]",
     15     img=data_img,
     16     avg_img=mean_data_img,
     17 )

File /opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/nilearn/image/image.py:531, in mean_img(imgs, target_affine, target_shape, verbose, n_jobs)
    528     imgs = [imgs, ]
    530 imgs_iter = iter(imgs)
--> 531 first_img = check_niimg(next(imgs_iter))
    533 # Compute the first mean to retrieve the reference
    534 # target_affine and target_shape if_needed
    535 n_imgs = 1

File /opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/nilearn/_utils/niimg_conversions.py:271, in check_niimg(niimg, ensure_ndim, atleast_4d, dtype, return_iterator, wildcards)
    269         raise ValueError(message)
    270     else:
--> 271         raise ValueError("File not found: '%s'" % niimg)
    272 elif not os.path.exists(niimg):
    273     raise ValueError("File not found: '%s'" % niimg)

ValueError: File not found: '/home/runner/work/multi-echo-data-analysis/multi-echo-data-analysis/data/multi-echo-data-analysis/sub-04570/func/sub-04570_task-rest_echo-2_space-scanner_desc-partialPreproc_bold.nii.gz'
fig, axes = plt.subplots(figsize=(16, 16), nrows=3)

plotting.plot_carpet(te2_img, axes=axes[0], figure=fig)
axes[0].set_title("First Echo (BAD)", fontsize=20)
plotting.plot_carpet(te1_img, axes=axes[1], figure=fig)
axes[1].set_title("Second Echo (GOOD)", fontsize=20)
plotting.plot_carpet(denoised_img, axes=axes[2], figure=fig)
axes[2].set_title("Denoised Data (GREAT)", fontsize=20)
axes[0].xaxis.set_visible(False)
axes[1].xaxis.set_visible(False)
axes[0].spines["bottom"].set_visible(False)
axes[1].spines["bottom"].set_visible(False)
fig.tight_layout()
glue("figure_dual_echo_results", fig, display=False)