T1 Mapping - IR GRE#
T1 mapping using a spoiled gradient echo sequence where a single readout line is acquired after an inversion pulse. Multiple images are obtained at different inversion times. A long repetition time (TR) is used to ensure full signal recovery before the next k-space line is acquired.
Imports#
import tempfile
from pathlib import Path
import matplotlib.pyplot as plt
import MRzeroCore as mr0
import numpy as np
import torch
from cmap import Colormap
from einops import rearrange
from mrpro.algorithms.reconstruction import DirectReconstruction
from mrpro.data import KData
from mrpro.data import SpatialDimension
from mrpro.data.traj_calculators import KTrajectoryCartesian
from mrpro.operators import DictionaryMatchOp
from mrpro.operators.models import InversionRecovery
from mrseq.scripts.t1_inv_rec_gre_single_line import main as create_seq
from mrseq.utils import sys_defaults
Settings#
We are going to use a numerical phantom with a matrix size of 128 x 128. The repetition time is set to 20 seconds to ensure we can accurately estimate long T1 times of the CSF.
image_matrix_size = [128, 128]
repetition_time = 20
tmp = tempfile.TemporaryDirectory()
fname_mrd = Path(tmp.name) / 't1_inv.mrd'
Create the digital phantom#
We use the standard Brainweb phantom from MRzero, but we choose the B1-field to be constant everywhere.
phantom = mr0.util.load_phantom(image_matrix_size)
phantom.B1[:] = 1.0
Create the IR-GRE sequence#
To create the IR-GRE sequence, we use the previously imported t1_inv_rec_gre_single_line script.
sequence, fname_seq = create_seq(
system=sys_defaults,
test_report=False,
timing_check=False,
fov_xy=float(phantom.size.numpy()[0]),
tr=repetition_time,
n_readout=image_matrix_size[0],
n_phase_encoding=image_matrix_size[1],
)
Minimum TE: 2.925 ms
Saving sequence file 't1_inv_rec_gre_single_line_200fov_128nx_128ny_7TIs.seq' into folder '/home/runner/work/mrseq/mrseq/examples/output'.
Simulate the sequence#
Now, we pass the sequence and the phantom to the MRzero simulation and save the simulated signal as an (ISMR)MRD file.
mr0_sequence = mr0.Sequence.import_file(str(fname_seq.with_suffix('.seq')))
signal, ktraj_adc = mr0.util.simulate(mr0_sequence, phantom, accuracy=1e-5)
mr0.sig_to_mrd(fname_mrd, signal, sequence)
>>>> Rust - compute_graph(...) >>>
Converting Python -> Rust: 0.009296936 s
Compute Graph
Computing Graph: 0.01839151 s
Analyze Graph
Analyzing Graph: 0.000229111 s
Converting Rust -> Python: 0.003634702 s
<<<< Rust <<<<
/opt/hostedtoolcache/Python/3.12.12/x64/lib/python3.12/site-packages/pypulseq/Sequence/sequence.py:813: RuntimeWarning: invalid value encountered in divide
gw_pp.append(PPoly(np.stack((np.diff(gw[1]) / np.diff(gw[0]), gw[1][:-1])), gw[0], extrapolate=True))
Reconstruct the images at different inversion times#
We use MRpro for the image reconstruction.
kdata = KData.from_file(fname_mrd, trajectory=KTrajectoryCartesian())
kdata.header.encoding_matrix = SpatialDimension(z=1, y=image_matrix_size[1], x=image_matrix_size[0])
kdata.header.recon_matrix = SpatialDimension(z=1, y=image_matrix_size[1], x=image_matrix_size[0])
recon = DirectReconstruction(kdata, csm=None)
idata = recon(kdata)
/opt/hostedtoolcache/Python/3.12.12/x64/lib/python3.12/site-packages/mrpro/data/KData.py:166: UserWarning: No vendor information found. Assuming Siemens time stamp format.
warnings.warn('No vendor information found. Assuming Siemens time stamp format.', stacklevel=1)
We can now plot the images at different inversion times.
idat = idata.data.abs().numpy().squeeze()
fig, ax = plt.subplots(1, idat.shape[0], figsize=(3 * idata.shape[0], 3))
for i in range(idat.shape[0]):
ax[i].imshow(idat[i, :, :], cmap='gray')
ax[i].set_title(f'TI = {int(idata.header.ti[i] * 1000)} ms')
ax[i].set_xticks([])
ax[i].set_yticks([])
Estimate the T1 maps#
We use a dictionary matching approach to estimate the T1 maps. Afterward, we compare them to the input and ensure they match.
dictionary = DictionaryMatchOp(InversionRecovery(ti=idata.header.ti), index_of_scaling_parameter=0)
dictionary.append(torch.tensor(1.0), torch.linspace(0.1, 5.0, 1000)[None, :])
m0_match, t1_match = dictionary(idata.data[:, 0, 0])
t1_input = np.roll(rearrange(phantom.T1.numpy().squeeze()[::-1, ::-1], 'x y -> y x'), shift=(1, 1), axis=(0, 1))
obj_mask = np.zeros_like(t1_input)
obj_mask[t1_input > 0] = 1
t1_measured = t1_match.numpy().squeeze() * obj_mask
fig, ax = plt.subplots(1, 3, figsize=(15, 3))
for cax in ax:
cax.set_xticks([])
cax.set_yticks([])
im = ax[0].imshow(t1_input, vmin=0, vmax=3, cmap=Colormap('lipari').to_mpl())
fig.colorbar(im, ax=ax[0], label='Input T1 (s)')
im = ax[1].imshow(t1_measured, vmin=0, vmax=3, cmap=Colormap('lipari').to_mpl())
fig.colorbar(im, ax=ax[1], label='Measured T1 (s)')
im = ax[2].imshow(t1_measured - t1_input, vmin=-0.3, vmax=0.3, cmap='bwr')
fig.colorbar(im, ax=ax[2], label='Difference T1 (s)')
relative_error = np.sum(np.abs(t1_input - t1_measured)) / np.sum(np.abs(t1_input))
print(f'Relative error {relative_error}')
assert relative_error < 0.01
Relative error 0.004718553740531206