Path: blob/main/latex-templates/templates/medical-physics/mri_signal.tex
51 views
unlisted
\documentclass[a4paper, 11pt]{report}1\usepackage[utf8]{inputenc}2\usepackage[T1]{fontenc}3\usepackage{amsmath, amssymb}4\usepackage{graphicx}5\usepackage{siunitx}6\usepackage{booktabs}7\usepackage{xcolor}8\usepackage[makestderr]{pythontex}910\definecolor{t1}{RGB}{231, 76, 60}11\definecolor{t2}{RGB}{46, 204, 113}12\definecolor{pd}{RGB}{52, 152, 219}1314\title{Magnetic Resonance Imaging:\\15Signal Formation, Contrast Mechanisms, and K-Space}16\author{Department of Medical Physics\\Technical Report MP-2024-002}17\date{\today}1819\begin{document}20\maketitle2122\begin{abstract}23This report presents a comprehensive analysis of MRI signal formation and image reconstruction. We implement the Bloch equations, analyze T1 and T2 contrast mechanisms, demonstrate k-space encoding, compare pulse sequences, and evaluate image artifacts. All simulations use PythonTeX for reproducibility.24\end{abstract}2526\tableofcontents2728\chapter{Introduction}2930MRI signal arises from nuclear magnetic resonance. The Bloch equations describe magnetization dynamics:31\begin{align}32\frac{dM_x}{dt} &= \gamma(M_y B_z - M_z B_y) - \frac{M_x}{T_2} \\33\frac{dM_y}{dt} &= \gamma(M_z B_x - M_x B_z) - \frac{M_y}{T_2} \\34\frac{dM_z}{dt} &= \gamma(M_x B_y - M_y B_x) - \frac{M_z - M_0}{T_1}35\end{align}3637\section{Relaxation Times}38\begin{itemize}39\item $T_1$: Spin-lattice (longitudinal) relaxation40\item $T_2$: Spin-spin (transverse) relaxation41\item $T_2^*$: Effective transverse relaxation (includes inhomogeneity)42\end{itemize}4344\begin{pycode}45import numpy as np46import matplotlib.pyplot as plt47from scipy.fft import fft2, ifft2, fftshift48plt.rc('text', usetex=True)49plt.rc('font', family='serif')5051np.random.seed(42)5253# Tissue parameters at 1.5T (T1, T2, T2* in ms, rho relative)54tissues = {55'White Matter': {'T1': 780, 'T2': 90, 'T2s': 70, 'rho': 0.70},56'Gray Matter': {'T1': 920, 'T2': 100, 'T2s': 75, 'rho': 0.80},57'CSF': {'T1': 4000, 'T2': 2000, 'T2s': 1500, 'rho': 1.00},58'Fat': {'T1': 260, 'T2': 80, 'T2s': 60, 'rho': 0.90},59'Muscle': {'T1': 870, 'T2': 50, 'T2s': 35, 'rho': 0.75},60'Blood': {'T1': 1200, 'T2': 150, 'T2s': 100, 'rho': 0.85}61}6263def T1_recovery(t, T1):64return 1 - np.exp(-t/T1)6566def T2_decay(t, T2):67return np.exp(-t/T2)6869def spin_echo_signal(TR, TE, T1, T2, rho):70return rho * (1 - np.exp(-TR/T1)) * np.exp(-TE/T2)7172def gradient_echo_signal(TR, TE, T1, T2s, rho, flip_angle):73alpha = np.radians(flip_angle)74E1 = np.exp(-TR/T1)75return rho * np.sin(alpha) * (1 - E1) / (1 - np.cos(alpha)*E1) * np.exp(-TE/T2s)7677def inversion_recovery(TI, T1, T2, rho, TR=5000, TE=10):78return np.abs(rho * (1 - 2*np.exp(-TI/T1) + np.exp(-TR/T1)) * np.exp(-TE/T2))79\end{pycode}8081\chapter{Relaxation Mechanisms}8283\begin{pycode}84fig, axes = plt.subplots(2, 3, figsize=(14, 8))8586t = np.linspace(0, 4000, 500)87colors = ['#3498db', '#2ecc71', '#e74c3c', '#f39c12', '#9b59b6', '#1abc9c']8889# T1 recovery curves90ax = axes[0, 0]91for (name, params), color in zip(tissues.items(), colors):92M_z = T1_recovery(t, params['T1'])93ax.plot(t, M_z, color=color, linewidth=1.5, label=name)9495ax.axhline(0.63, color='gray', linestyle='--', alpha=0.5)96ax.set_xlabel('Time (ms)')97ax.set_ylabel('$M_z/M_0$')98ax.set_title('T1 Recovery')99ax.legend(fontsize=7, loc='lower right')100ax.grid(True, alpha=0.3)101102# T2 decay curves103ax = axes[0, 1]104t_short = np.linspace(0, 500, 500)105for (name, params), color in zip(tissues.items(), colors):106M_xy = T2_decay(t_short, params['T2'])107ax.plot(t_short, M_xy, color=color, linewidth=1.5, label=name)108109ax.axhline(0.37, color='gray', linestyle='--', alpha=0.5)110ax.set_xlabel('Time (ms)')111ax.set_ylabel('$M_{xy}/M_0$')112ax.set_title('T2 Decay')113ax.legend(fontsize=7)114ax.grid(True, alpha=0.3)115116# T1 vs T2 scatter117ax = axes[0, 2]118for (name, params), color in zip(tissues.items(), colors):119ax.scatter(params['T1'], params['T2'], s=100, c=color, label=name)120ax.set_xlabel('T1 (ms)')121ax.set_ylabel('T2 (ms)')122ax.set_title('T1 vs T2 at 1.5T')123ax.legend(fontsize=7)124ax.grid(True, alpha=0.3)125ax.set_xscale('log')126ax.set_yscale('log')127128# Pulse sequence comparison129ax = axes[1, 0]130# T1-weighted131TR_T1, TE_T1 = 500, 15132signals_T1 = {name: spin_echo_signal(TR_T1, TE_T1, p['T1'], p['T2'], p['rho'])133for name, p in tissues.items()}134135# T2-weighted136TR_T2, TE_T2 = 3000, 100137signals_T2 = {name: spin_echo_signal(TR_T2, TE_T2, p['T1'], p['T2'], p['rho'])138for name, p in tissues.items()}139140# PD-weighted141TR_PD, TE_PD = 3000, 15142signals_PD = {name: spin_echo_signal(TR_PD, TE_PD, p['T1'], p['T2'], p['rho'])143for name, p in tissues.items()}144145x = np.arange(len(tissues))146width = 0.25147tissue_names = list(tissues.keys())148149bars1 = ax.bar(x - width, [signals_T1[n] for n in tissue_names], width,150label='T1w', color='#e74c3c', alpha=0.7)151bars2 = ax.bar(x, [signals_T2[n] for n in tissue_names], width,152label='T2w', color='#2ecc71', alpha=0.7)153bars3 = ax.bar(x + width, [signals_PD[n] for n in tissue_names], width,154label='PDw', color='#3498db', alpha=0.7)155156ax.set_xlabel('Tissue')157ax.set_ylabel('Signal (a.u.)')158ax.set_title('Contrast Comparison')159ax.set_xticks(x)160ax.set_xticklabels([n.split()[0] for n in tissue_names], fontsize=8, rotation=45)161ax.legend()162ax.grid(True, alpha=0.3, axis='y')163164# Inversion recovery for T1 nulling165ax = axes[1, 1]166TI = np.linspace(0, 3000, 300)167for (name, params), color in zip(list(tissues.items())[:4], colors[:4]):168signal = inversion_recovery(TI, params['T1'], params['T2'], params['rho'])169ax.plot(TI, signal, color=color, linewidth=1.5, label=name)170171# Mark CSF null point172T1_csf = tissues['CSF']['T1']173TI_null_csf = T1_csf * np.log(2)174ax.axvline(TI_null_csf, color='gray', linestyle='--', alpha=0.5)175176ax.set_xlabel('TI (ms)')177ax.set_ylabel('Signal (a.u.)')178ax.set_title('Inversion Recovery (FLAIR)')179ax.legend(fontsize=7)180ax.grid(True, alpha=0.3)181182# Flip angle optimization for GRE183ax = axes[1, 2]184flip_angles = np.linspace(1, 90, 100)185TR_gre = 50186TE_gre = 5187188for (name, params), color in zip(list(tissues.items())[:4], colors[:4]):189signals = [gradient_echo_signal(TR_gre, TE_gre, params['T1'], params['T2s'],190params['rho'], alpha) for alpha in flip_angles]191ax.plot(flip_angles, signals, color=color, linewidth=1.5, label=name)192193ax.set_xlabel('Flip Angle (degrees)')194ax.set_ylabel('Signal (a.u.)')195ax.set_title(f'GRE Signal (TR={TR_gre} ms)')196ax.legend(fontsize=7)197ax.grid(True, alpha=0.3)198199plt.tight_layout()200plt.savefig('mri_relaxation.pdf', dpi=150, bbox_inches='tight')201plt.close()202\end{pycode}203204\begin{figure}[htbp]205\centering206\includegraphics[width=0.95\textwidth]{mri_relaxation.pdf}207\caption{MRI relaxation: (a) T1 recovery, (b) T2 decay, (c) T1-T2 relationship, (d) contrast comparison, (e) inversion recovery, (f) flip angle optimization.}208\end{figure}209210\chapter{K-Space and Image Reconstruction}211212\section{Spatial Encoding}213The MRI signal is collected in k-space:214\begin{equation}215S(k_x, k_y) = \iint \rho(x, y) e^{-i2\pi(k_x x + k_y y)} dx \, dy216\end{equation}217218\begin{pycode}219# Create brain phantom220size = 128221phantom = np.zeros((size, size))222y, x = np.ogrid[-size//2:size//2, -size//2:size//2]223224# Brain outline225mask = (x/45)**2 + (y/50)**2 <= 1226phantom[mask] = 0.8227228# Ventricles229mask = ((x+10)/8)**2 + (y/20)**2 <= 1230phantom[mask] = 1.0231mask = ((x-10)/8)**2 + (y/20)**2 <= 1232phantom[mask] = 1.0233234# Gray matter structures235mask = ((x+25)/10)**2 + (y/15)**2 <= 1236phantom[mask] = 0.6237mask = ((x-25)/10)**2 + (y/15)**2 <= 1238phantom[mask] = 0.6239240# Lesion241mask = ((x-15)/5)**2 + ((y+20)/5)**2 <= 1242phantom[mask] = 0.9243244fig, axes = plt.subplots(2, 3, figsize=(14, 8))245246# K-space247kspace = fftshift(fft2(phantom))248kspace_mag = np.log(np.abs(kspace) + 1)249250ax = axes[0, 0]251im = ax.imshow(phantom, cmap='gray')252ax.set_title('Object (Image Space)')253ax.set_xlabel('x')254ax.set_ylabel('y')255plt.colorbar(im, ax=ax, fraction=0.046)256257ax = axes[0, 1]258im = ax.imshow(kspace_mag, cmap='gray')259ax.set_title('K-Space (Log Magnitude)')260ax.set_xlabel('$k_x$')261ax.set_ylabel('$k_y$')262plt.colorbar(im, ax=ax, fraction=0.046)263264ax = axes[0, 2]265im = ax.imshow(np.angle(kspace), cmap='twilight')266ax.set_title('K-Space Phase')267ax.set_xlabel('$k_x$')268ax.set_ylabel('$k_y$')269plt.colorbar(im, ax=ax, fraction=0.046)270271# Partial k-space effects272# Low frequency only (low resolution)273kspace_low = kspace.copy()274mask = np.zeros((size, size), dtype=bool)275mask[size//2-16:size//2+16, size//2-16:size//2+16] = True276kspace_low[~mask] = 0277recon_low = np.abs(ifft2(fftshift(kspace_low)))278279ax = axes[1, 0]280im = ax.imshow(recon_low, cmap='gray')281ax.set_title('Low Frequency Only')282ax.set_xlabel('x')283ax.set_ylabel('y')284plt.colorbar(im, ax=ax, fraction=0.046)285286# High frequency only (edges)287kspace_high = kspace.copy()288kspace_high[mask] = 0289recon_high = np.abs(ifft2(fftshift(kspace_high)))290291ax = axes[1, 1]292im = ax.imshow(recon_high, cmap='gray')293ax.set_title('High Frequency Only')294ax.set_xlabel('x')295ax.set_ylabel('y')296plt.colorbar(im, ax=ax, fraction=0.046)297298# Partial Fourier (half k-space)299kspace_partial = kspace.copy()300kspace_partial[:size//2, :] = 0301recon_partial = np.abs(ifft2(fftshift(kspace_partial)))302303ax = axes[1, 2]304im = ax.imshow(recon_partial, cmap='gray')305ax.set_title('Partial Fourier')306ax.set_xlabel('x')307ax.set_ylabel('y')308plt.colorbar(im, ax=ax, fraction=0.046)309310plt.tight_layout()311plt.savefig('mri_kspace.pdf', dpi=150, bbox_inches='tight')312plt.close()313\end{pycode}314315\begin{figure}[htbp]316\centering317\includegraphics[width=0.95\textwidth]{mri_kspace.pdf}318\caption{K-space encoding: (a) image space, (b) k-space magnitude, (c) k-space phase, (d) low frequency, (e) high frequency, (f) partial Fourier.}319\end{figure}320321\chapter{Image Artifacts}322323\begin{pycode}324fig, axes = plt.subplots(2, 3, figsize=(14, 8))325326# Gibbs ringing (truncation artifact)327kspace_trunc = kspace.copy()328kspace_trunc[:20, :] = 0329kspace_trunc[-20:, :] = 0330kspace_trunc[:, :20] = 0331kspace_trunc[:, -20:] = 0332recon_gibbs = np.abs(ifft2(fftshift(kspace_trunc)))333334ax = axes[0, 0]335im = ax.imshow(recon_gibbs, cmap='gray')336ax.set_title('Gibbs Ringing')337ax.set_xlabel('x')338ax.set_ylabel('y')339plt.colorbar(im, ax=ax, fraction=0.046)340341# Motion artifact342kspace_motion = kspace.copy()343for i in range(0, size, 8):344kspace_motion[:, i] *= np.exp(1j * 0.3 * i)345recon_motion = np.abs(ifft2(fftshift(kspace_motion)))346347ax = axes[0, 1]348im = ax.imshow(recon_motion, cmap='gray')349ax.set_title('Motion Artifact')350ax.set_xlabel('x')351ax.set_ylabel('y')352plt.colorbar(im, ax=ax, fraction=0.046)353354# Aliasing (wrap-around)355kspace_alias = kspace[::2, ::2] # Undersample356recon_alias = np.abs(ifft2(fftshift(kspace_alias)))357358ax = axes[0, 2]359im = ax.imshow(recon_alias, cmap='gray')360ax.set_title('Aliasing (FOV/2)')361ax.set_xlabel('x')362ax.set_ylabel('y')363plt.colorbar(im, ax=ax, fraction=0.046)364365# Chemical shift artifact simulation366phantom_fat = np.zeros((size, size))367mask = ((x-30)/8)**2 + (y/15)**2 <= 1368phantom_fat[mask] = 0.5369370# Fat shifted by 3 pixels371phantom_shifted = np.roll(phantom_fat, 3, axis=1)372combined = phantom + phantom_shifted373374ax = axes[1, 0]375im = ax.imshow(combined, cmap='gray')376ax.set_title('Chemical Shift')377ax.set_xlabel('x')378ax.set_ylabel('y')379plt.colorbar(im, ax=ax, fraction=0.046)380381# Susceptibility artifact382susceptibility_map = np.zeros((size, size))383mask = ((x+20)/5)**2 + ((y-20)/5)**2 <= 1384susceptibility_map[mask] = 1385susceptibility_blur = ndimage.gaussian_filter(susceptibility_map, 5)386recon_suscept = phantom * (1 - 0.5*susceptibility_blur)387388from scipy import ndimage389390ax = axes[1, 1]391im = ax.imshow(recon_suscept, cmap='gray')392ax.set_title('Susceptibility Artifact')393ax.set_xlabel('x')394ax.set_ylabel('y')395plt.colorbar(im, ax=ax, fraction=0.046)396397# SNR comparison398ax = axes[1, 2]399noise_levels = [0, 0.05, 0.1, 0.2]400center_profile = phantom[size//2, :]401402for noise in noise_levels:403if noise == 0:404profile = center_profile405label = 'Original'406else:407noisy_kspace = kspace + noise * np.max(np.abs(kspace)) * \408(np.random.randn(*kspace.shape) + 1j*np.random.randn(*kspace.shape))409recon_noisy = np.abs(ifft2(fftshift(noisy_kspace)))410profile = recon_noisy[size//2, :]411label = f'SNR $\\approx$ {int(1/noise)}'412ax.plot(profile, label=label, alpha=0.7)413414ax.set_xlabel('Position')415ax.set_ylabel('Signal')416ax.set_title('Noise Effects')417ax.legend(fontsize=8)418ax.grid(True, alpha=0.3)419420plt.tight_layout()421plt.savefig('mri_artifacts.pdf', dpi=150, bbox_inches='tight')422plt.close()423424# Calculate contrast values425wm_gm_T1 = signals_T1['White Matter'] - signals_T1['Gray Matter']426wm_gm_T2 = signals_T2['White Matter'] - signals_T2['Gray Matter']427csf_gm_T2 = signals_T2['CSF'] - signals_T2['Gray Matter']428\end{pycode}429430\begin{figure}[htbp]431\centering432\includegraphics[width=0.95\textwidth]{mri_artifacts.pdf}433\caption{MRI artifacts: (a) Gibbs ringing, (b) motion, (c) aliasing, (d) chemical shift, (e) susceptibility, (f) noise effects.}434\end{figure}435436\chapter{Numerical Results}437438\begin{pycode}439results_table = [440('T1-weighted parameters', f'TR={TR_T1}, TE={TE_T1}', 'ms'),441('T2-weighted parameters', f'TR={TR_T2}, TE={TE_T2}', 'ms'),442('PD-weighted parameters', f'TR={TR_PD}, TE={TE_PD}', 'ms'),443('WM-GM contrast (T1w)', f'{wm_gm_T1:.3f}', 'a.u.'),444('WM-GM contrast (T2w)', f'{wm_gm_T2:.3f}', 'a.u.'),445('CSF-GM contrast (T2w)', f'{csf_gm_T2:.3f}', 'a.u.'),446]447\end{pycode}448449\begin{table}[htbp]450\centering451\caption{MRI signal parameters and contrast}452\begin{tabular}{@{}lcc@{}}453\toprule454Parameter & Value & Units \\455\midrule456\py{results_table[0][0]} & \py{results_table[0][1]} & \py{results_table[0][2]} \\457\py{results_table[1][0]} & \py{results_table[1][1]} & \py{results_table[1][2]} \\458\py{results_table[2][0]} & \py{results_table[2][1]} & \py{results_table[2][2]} \\459\py{results_table[3][0]} & \py{results_table[3][1]} & \py{results_table[3][2]} \\460\py{results_table[4][0]} & \py{results_table[4][1]} & \py{results_table[4][2]} \\461\py{results_table[5][0]} & \py{results_table[5][1]} & \py{results_table[5][2]} \\462\bottomrule463\end{tabular}464\end{table}465466\chapter{Conclusions}467468\begin{enumerate}469\item T1-weighted imaging provides anatomical detail470\item T2-weighted imaging highlights pathology471\item K-space center determines contrast, periphery determines resolution472\item Motion during acquisition causes ghosting artifacts473\item Chemical shift causes fat-water misregistration474\item Higher field strength increases SNR but also artifacts475\end{enumerate}476477\end{document}478479480