Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Ok-landscape
GitHub Repository: Ok-landscape/computational-pipeline
Path: blob/main/latex-templates/templates/medical-physics/mri_signal.tex
51 views
unlisted
1
\documentclass[a4paper, 11pt]{report}
2
\usepackage[utf8]{inputenc}
3
\usepackage[T1]{fontenc}
4
\usepackage{amsmath, amssymb}
5
\usepackage{graphicx}
6
\usepackage{siunitx}
7
\usepackage{booktabs}
8
\usepackage{xcolor}
9
\usepackage[makestderr]{pythontex}
10
11
\definecolor{t1}{RGB}{231, 76, 60}
12
\definecolor{t2}{RGB}{46, 204, 113}
13
\definecolor{pd}{RGB}{52, 152, 219}
14
15
\title{Magnetic Resonance Imaging:\\
16
Signal Formation, Contrast Mechanisms, and K-Space}
17
\author{Department of Medical Physics\\Technical Report MP-2024-002}
18
\date{\today}
19
20
\begin{document}
21
\maketitle
22
23
\begin{abstract}
24
This 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.
25
\end{abstract}
26
27
\tableofcontents
28
29
\chapter{Introduction}
30
31
MRI signal arises from nuclear magnetic resonance. The Bloch equations describe magnetization dynamics:
32
\begin{align}
33
\frac{dM_x}{dt} &= \gamma(M_y B_z - M_z B_y) - \frac{M_x}{T_2} \\
34
\frac{dM_y}{dt} &= \gamma(M_z B_x - M_x B_z) - \frac{M_y}{T_2} \\
35
\frac{dM_z}{dt} &= \gamma(M_x B_y - M_y B_x) - \frac{M_z - M_0}{T_1}
36
\end{align}
37
38
\section{Relaxation Times}
39
\begin{itemize}
40
\item $T_1$: Spin-lattice (longitudinal) relaxation
41
\item $T_2$: Spin-spin (transverse) relaxation
42
\item $T_2^*$: Effective transverse relaxation (includes inhomogeneity)
43
\end{itemize}
44
45
\begin{pycode}
46
import numpy as np
47
import matplotlib.pyplot as plt
48
from scipy.fft import fft2, ifft2, fftshift
49
plt.rc('text', usetex=True)
50
plt.rc('font', family='serif')
51
52
np.random.seed(42)
53
54
# Tissue parameters at 1.5T (T1, T2, T2* in ms, rho relative)
55
tissues = {
56
'White Matter': {'T1': 780, 'T2': 90, 'T2s': 70, 'rho': 0.70},
57
'Gray Matter': {'T1': 920, 'T2': 100, 'T2s': 75, 'rho': 0.80},
58
'CSF': {'T1': 4000, 'T2': 2000, 'T2s': 1500, 'rho': 1.00},
59
'Fat': {'T1': 260, 'T2': 80, 'T2s': 60, 'rho': 0.90},
60
'Muscle': {'T1': 870, 'T2': 50, 'T2s': 35, 'rho': 0.75},
61
'Blood': {'T1': 1200, 'T2': 150, 'T2s': 100, 'rho': 0.85}
62
}
63
64
def T1_recovery(t, T1):
65
return 1 - np.exp(-t/T1)
66
67
def T2_decay(t, T2):
68
return np.exp(-t/T2)
69
70
def spin_echo_signal(TR, TE, T1, T2, rho):
71
return rho * (1 - np.exp(-TR/T1)) * np.exp(-TE/T2)
72
73
def gradient_echo_signal(TR, TE, T1, T2s, rho, flip_angle):
74
alpha = np.radians(flip_angle)
75
E1 = np.exp(-TR/T1)
76
return rho * np.sin(alpha) * (1 - E1) / (1 - np.cos(alpha)*E1) * np.exp(-TE/T2s)
77
78
def inversion_recovery(TI, T1, T2, rho, TR=5000, TE=10):
79
return np.abs(rho * (1 - 2*np.exp(-TI/T1) + np.exp(-TR/T1)) * np.exp(-TE/T2))
80
\end{pycode}
81
82
\chapter{Relaxation Mechanisms}
83
84
\begin{pycode}
85
fig, axes = plt.subplots(2, 3, figsize=(14, 8))
86
87
t = np.linspace(0, 4000, 500)
88
colors = ['#3498db', '#2ecc71', '#e74c3c', '#f39c12', '#9b59b6', '#1abc9c']
89
90
# T1 recovery curves
91
ax = axes[0, 0]
92
for (name, params), color in zip(tissues.items(), colors):
93
M_z = T1_recovery(t, params['T1'])
94
ax.plot(t, M_z, color=color, linewidth=1.5, label=name)
95
96
ax.axhline(0.63, color='gray', linestyle='--', alpha=0.5)
97
ax.set_xlabel('Time (ms)')
98
ax.set_ylabel('$M_z/M_0$')
99
ax.set_title('T1 Recovery')
100
ax.legend(fontsize=7, loc='lower right')
101
ax.grid(True, alpha=0.3)
102
103
# T2 decay curves
104
ax = axes[0, 1]
105
t_short = np.linspace(0, 500, 500)
106
for (name, params), color in zip(tissues.items(), colors):
107
M_xy = T2_decay(t_short, params['T2'])
108
ax.plot(t_short, M_xy, color=color, linewidth=1.5, label=name)
109
110
ax.axhline(0.37, color='gray', linestyle='--', alpha=0.5)
111
ax.set_xlabel('Time (ms)')
112
ax.set_ylabel('$M_{xy}/M_0$')
113
ax.set_title('T2 Decay')
114
ax.legend(fontsize=7)
115
ax.grid(True, alpha=0.3)
116
117
# T1 vs T2 scatter
118
ax = axes[0, 2]
119
for (name, params), color in zip(tissues.items(), colors):
120
ax.scatter(params['T1'], params['T2'], s=100, c=color, label=name)
121
ax.set_xlabel('T1 (ms)')
122
ax.set_ylabel('T2 (ms)')
123
ax.set_title('T1 vs T2 at 1.5T')
124
ax.legend(fontsize=7)
125
ax.grid(True, alpha=0.3)
126
ax.set_xscale('log')
127
ax.set_yscale('log')
128
129
# Pulse sequence comparison
130
ax = axes[1, 0]
131
# T1-weighted
132
TR_T1, TE_T1 = 500, 15
133
signals_T1 = {name: spin_echo_signal(TR_T1, TE_T1, p['T1'], p['T2'], p['rho'])
134
for name, p in tissues.items()}
135
136
# T2-weighted
137
TR_T2, TE_T2 = 3000, 100
138
signals_T2 = {name: spin_echo_signal(TR_T2, TE_T2, p['T1'], p['T2'], p['rho'])
139
for name, p in tissues.items()}
140
141
# PD-weighted
142
TR_PD, TE_PD = 3000, 15
143
signals_PD = {name: spin_echo_signal(TR_PD, TE_PD, p['T1'], p['T2'], p['rho'])
144
for name, p in tissues.items()}
145
146
x = np.arange(len(tissues))
147
width = 0.25
148
tissue_names = list(tissues.keys())
149
150
bars1 = ax.bar(x - width, [signals_T1[n] for n in tissue_names], width,
151
label='T1w', color='#e74c3c', alpha=0.7)
152
bars2 = ax.bar(x, [signals_T2[n] for n in tissue_names], width,
153
label='T2w', color='#2ecc71', alpha=0.7)
154
bars3 = ax.bar(x + width, [signals_PD[n] for n in tissue_names], width,
155
label='PDw', color='#3498db', alpha=0.7)
156
157
ax.set_xlabel('Tissue')
158
ax.set_ylabel('Signal (a.u.)')
159
ax.set_title('Contrast Comparison')
160
ax.set_xticks(x)
161
ax.set_xticklabels([n.split()[0] for n in tissue_names], fontsize=8, rotation=45)
162
ax.legend()
163
ax.grid(True, alpha=0.3, axis='y')
164
165
# Inversion recovery for T1 nulling
166
ax = axes[1, 1]
167
TI = np.linspace(0, 3000, 300)
168
for (name, params), color in zip(list(tissues.items())[:4], colors[:4]):
169
signal = inversion_recovery(TI, params['T1'], params['T2'], params['rho'])
170
ax.plot(TI, signal, color=color, linewidth=1.5, label=name)
171
172
# Mark CSF null point
173
T1_csf = tissues['CSF']['T1']
174
TI_null_csf = T1_csf * np.log(2)
175
ax.axvline(TI_null_csf, color='gray', linestyle='--', alpha=0.5)
176
177
ax.set_xlabel('TI (ms)')
178
ax.set_ylabel('Signal (a.u.)')
179
ax.set_title('Inversion Recovery (FLAIR)')
180
ax.legend(fontsize=7)
181
ax.grid(True, alpha=0.3)
182
183
# Flip angle optimization for GRE
184
ax = axes[1, 2]
185
flip_angles = np.linspace(1, 90, 100)
186
TR_gre = 50
187
TE_gre = 5
188
189
for (name, params), color in zip(list(tissues.items())[:4], colors[:4]):
190
signals = [gradient_echo_signal(TR_gre, TE_gre, params['T1'], params['T2s'],
191
params['rho'], alpha) for alpha in flip_angles]
192
ax.plot(flip_angles, signals, color=color, linewidth=1.5, label=name)
193
194
ax.set_xlabel('Flip Angle (degrees)')
195
ax.set_ylabel('Signal (a.u.)')
196
ax.set_title(f'GRE Signal (TR={TR_gre} ms)')
197
ax.legend(fontsize=7)
198
ax.grid(True, alpha=0.3)
199
200
plt.tight_layout()
201
plt.savefig('mri_relaxation.pdf', dpi=150, bbox_inches='tight')
202
plt.close()
203
\end{pycode}
204
205
\begin{figure}[htbp]
206
\centering
207
\includegraphics[width=0.95\textwidth]{mri_relaxation.pdf}
208
\caption{MRI relaxation: (a) T1 recovery, (b) T2 decay, (c) T1-T2 relationship, (d) contrast comparison, (e) inversion recovery, (f) flip angle optimization.}
209
\end{figure}
210
211
\chapter{K-Space and Image Reconstruction}
212
213
\section{Spatial Encoding}
214
The MRI signal is collected in k-space:
215
\begin{equation}
216
S(k_x, k_y) = \iint \rho(x, y) e^{-i2\pi(k_x x + k_y y)} dx \, dy
217
\end{equation}
218
219
\begin{pycode}
220
# Create brain phantom
221
size = 128
222
phantom = np.zeros((size, size))
223
y, x = np.ogrid[-size//2:size//2, -size//2:size//2]
224
225
# Brain outline
226
mask = (x/45)**2 + (y/50)**2 <= 1
227
phantom[mask] = 0.8
228
229
# Ventricles
230
mask = ((x+10)/8)**2 + (y/20)**2 <= 1
231
phantom[mask] = 1.0
232
mask = ((x-10)/8)**2 + (y/20)**2 <= 1
233
phantom[mask] = 1.0
234
235
# Gray matter structures
236
mask = ((x+25)/10)**2 + (y/15)**2 <= 1
237
phantom[mask] = 0.6
238
mask = ((x-25)/10)**2 + (y/15)**2 <= 1
239
phantom[mask] = 0.6
240
241
# Lesion
242
mask = ((x-15)/5)**2 + ((y+20)/5)**2 <= 1
243
phantom[mask] = 0.9
244
245
fig, axes = plt.subplots(2, 3, figsize=(14, 8))
246
247
# K-space
248
kspace = fftshift(fft2(phantom))
249
kspace_mag = np.log(np.abs(kspace) + 1)
250
251
ax = axes[0, 0]
252
im = ax.imshow(phantom, cmap='gray')
253
ax.set_title('Object (Image Space)')
254
ax.set_xlabel('x')
255
ax.set_ylabel('y')
256
plt.colorbar(im, ax=ax, fraction=0.046)
257
258
ax = axes[0, 1]
259
im = ax.imshow(kspace_mag, cmap='gray')
260
ax.set_title('K-Space (Log Magnitude)')
261
ax.set_xlabel('$k_x$')
262
ax.set_ylabel('$k_y$')
263
plt.colorbar(im, ax=ax, fraction=0.046)
264
265
ax = axes[0, 2]
266
im = ax.imshow(np.angle(kspace), cmap='twilight')
267
ax.set_title('K-Space Phase')
268
ax.set_xlabel('$k_x$')
269
ax.set_ylabel('$k_y$')
270
plt.colorbar(im, ax=ax, fraction=0.046)
271
272
# Partial k-space effects
273
# Low frequency only (low resolution)
274
kspace_low = kspace.copy()
275
mask = np.zeros((size, size), dtype=bool)
276
mask[size//2-16:size//2+16, size//2-16:size//2+16] = True
277
kspace_low[~mask] = 0
278
recon_low = np.abs(ifft2(fftshift(kspace_low)))
279
280
ax = axes[1, 0]
281
im = ax.imshow(recon_low, cmap='gray')
282
ax.set_title('Low Frequency Only')
283
ax.set_xlabel('x')
284
ax.set_ylabel('y')
285
plt.colorbar(im, ax=ax, fraction=0.046)
286
287
# High frequency only (edges)
288
kspace_high = kspace.copy()
289
kspace_high[mask] = 0
290
recon_high = np.abs(ifft2(fftshift(kspace_high)))
291
292
ax = axes[1, 1]
293
im = ax.imshow(recon_high, cmap='gray')
294
ax.set_title('High Frequency Only')
295
ax.set_xlabel('x')
296
ax.set_ylabel('y')
297
plt.colorbar(im, ax=ax, fraction=0.046)
298
299
# Partial Fourier (half k-space)
300
kspace_partial = kspace.copy()
301
kspace_partial[:size//2, :] = 0
302
recon_partial = np.abs(ifft2(fftshift(kspace_partial)))
303
304
ax = axes[1, 2]
305
im = ax.imshow(recon_partial, cmap='gray')
306
ax.set_title('Partial Fourier')
307
ax.set_xlabel('x')
308
ax.set_ylabel('y')
309
plt.colorbar(im, ax=ax, fraction=0.046)
310
311
plt.tight_layout()
312
plt.savefig('mri_kspace.pdf', dpi=150, bbox_inches='tight')
313
plt.close()
314
\end{pycode}
315
316
\begin{figure}[htbp]
317
\centering
318
\includegraphics[width=0.95\textwidth]{mri_kspace.pdf}
319
\caption{K-space encoding: (a) image space, (b) k-space magnitude, (c) k-space phase, (d) low frequency, (e) high frequency, (f) partial Fourier.}
320
\end{figure}
321
322
\chapter{Image Artifacts}
323
324
\begin{pycode}
325
fig, axes = plt.subplots(2, 3, figsize=(14, 8))
326
327
# Gibbs ringing (truncation artifact)
328
kspace_trunc = kspace.copy()
329
kspace_trunc[:20, :] = 0
330
kspace_trunc[-20:, :] = 0
331
kspace_trunc[:, :20] = 0
332
kspace_trunc[:, -20:] = 0
333
recon_gibbs = np.abs(ifft2(fftshift(kspace_trunc)))
334
335
ax = axes[0, 0]
336
im = ax.imshow(recon_gibbs, cmap='gray')
337
ax.set_title('Gibbs Ringing')
338
ax.set_xlabel('x')
339
ax.set_ylabel('y')
340
plt.colorbar(im, ax=ax, fraction=0.046)
341
342
# Motion artifact
343
kspace_motion = kspace.copy()
344
for i in range(0, size, 8):
345
kspace_motion[:, i] *= np.exp(1j * 0.3 * i)
346
recon_motion = np.abs(ifft2(fftshift(kspace_motion)))
347
348
ax = axes[0, 1]
349
im = ax.imshow(recon_motion, cmap='gray')
350
ax.set_title('Motion Artifact')
351
ax.set_xlabel('x')
352
ax.set_ylabel('y')
353
plt.colorbar(im, ax=ax, fraction=0.046)
354
355
# Aliasing (wrap-around)
356
kspace_alias = kspace[::2, ::2] # Undersample
357
recon_alias = np.abs(ifft2(fftshift(kspace_alias)))
358
359
ax = axes[0, 2]
360
im = ax.imshow(recon_alias, cmap='gray')
361
ax.set_title('Aliasing (FOV/2)')
362
ax.set_xlabel('x')
363
ax.set_ylabel('y')
364
plt.colorbar(im, ax=ax, fraction=0.046)
365
366
# Chemical shift artifact simulation
367
phantom_fat = np.zeros((size, size))
368
mask = ((x-30)/8)**2 + (y/15)**2 <= 1
369
phantom_fat[mask] = 0.5
370
371
# Fat shifted by 3 pixels
372
phantom_shifted = np.roll(phantom_fat, 3, axis=1)
373
combined = phantom + phantom_shifted
374
375
ax = axes[1, 0]
376
im = ax.imshow(combined, cmap='gray')
377
ax.set_title('Chemical Shift')
378
ax.set_xlabel('x')
379
ax.set_ylabel('y')
380
plt.colorbar(im, ax=ax, fraction=0.046)
381
382
# Susceptibility artifact
383
susceptibility_map = np.zeros((size, size))
384
mask = ((x+20)/5)**2 + ((y-20)/5)**2 <= 1
385
susceptibility_map[mask] = 1
386
susceptibility_blur = ndimage.gaussian_filter(susceptibility_map, 5)
387
recon_suscept = phantom * (1 - 0.5*susceptibility_blur)
388
389
from scipy import ndimage
390
391
ax = axes[1, 1]
392
im = ax.imshow(recon_suscept, cmap='gray')
393
ax.set_title('Susceptibility Artifact')
394
ax.set_xlabel('x')
395
ax.set_ylabel('y')
396
plt.colorbar(im, ax=ax, fraction=0.046)
397
398
# SNR comparison
399
ax = axes[1, 2]
400
noise_levels = [0, 0.05, 0.1, 0.2]
401
center_profile = phantom[size//2, :]
402
403
for noise in noise_levels:
404
if noise == 0:
405
profile = center_profile
406
label = 'Original'
407
else:
408
noisy_kspace = kspace + noise * np.max(np.abs(kspace)) * \
409
(np.random.randn(*kspace.shape) + 1j*np.random.randn(*kspace.shape))
410
recon_noisy = np.abs(ifft2(fftshift(noisy_kspace)))
411
profile = recon_noisy[size//2, :]
412
label = f'SNR $\\approx$ {int(1/noise)}'
413
ax.plot(profile, label=label, alpha=0.7)
414
415
ax.set_xlabel('Position')
416
ax.set_ylabel('Signal')
417
ax.set_title('Noise Effects')
418
ax.legend(fontsize=8)
419
ax.grid(True, alpha=0.3)
420
421
plt.tight_layout()
422
plt.savefig('mri_artifacts.pdf', dpi=150, bbox_inches='tight')
423
plt.close()
424
425
# Calculate contrast values
426
wm_gm_T1 = signals_T1['White Matter'] - signals_T1['Gray Matter']
427
wm_gm_T2 = signals_T2['White Matter'] - signals_T2['Gray Matter']
428
csf_gm_T2 = signals_T2['CSF'] - signals_T2['Gray Matter']
429
\end{pycode}
430
431
\begin{figure}[htbp]
432
\centering
433
\includegraphics[width=0.95\textwidth]{mri_artifacts.pdf}
434
\caption{MRI artifacts: (a) Gibbs ringing, (b) motion, (c) aliasing, (d) chemical shift, (e) susceptibility, (f) noise effects.}
435
\end{figure}
436
437
\chapter{Numerical Results}
438
439
\begin{pycode}
440
results_table = [
441
('T1-weighted parameters', f'TR={TR_T1}, TE={TE_T1}', 'ms'),
442
('T2-weighted parameters', f'TR={TR_T2}, TE={TE_T2}', 'ms'),
443
('PD-weighted parameters', f'TR={TR_PD}, TE={TE_PD}', 'ms'),
444
('WM-GM contrast (T1w)', f'{wm_gm_T1:.3f}', 'a.u.'),
445
('WM-GM contrast (T2w)', f'{wm_gm_T2:.3f}', 'a.u.'),
446
('CSF-GM contrast (T2w)', f'{csf_gm_T2:.3f}', 'a.u.'),
447
]
448
\end{pycode}
449
450
\begin{table}[htbp]
451
\centering
452
\caption{MRI signal parameters and contrast}
453
\begin{tabular}{@{}lcc@{}}
454
\toprule
455
Parameter & Value & Units \\
456
\midrule
457
\py{results_table[0][0]} & \py{results_table[0][1]} & \py{results_table[0][2]} \\
458
\py{results_table[1][0]} & \py{results_table[1][1]} & \py{results_table[1][2]} \\
459
\py{results_table[2][0]} & \py{results_table[2][1]} & \py{results_table[2][2]} \\
460
\py{results_table[3][0]} & \py{results_table[3][1]} & \py{results_table[3][2]} \\
461
\py{results_table[4][0]} & \py{results_table[4][1]} & \py{results_table[4][2]} \\
462
\py{results_table[5][0]} & \py{results_table[5][1]} & \py{results_table[5][2]} \\
463
\bottomrule
464
\end{tabular}
465
\end{table}
466
467
\chapter{Conclusions}
468
469
\begin{enumerate}
470
\item T1-weighted imaging provides anatomical detail
471
\item T2-weighted imaging highlights pathology
472
\item K-space center determines contrast, periphery determines resolution
473
\item Motion during acquisition causes ghosting artifacts
474
\item Chemical shift causes fat-water misregistration
475
\item Higher field strength increases SNR but also artifacts
476
\end{enumerate}
477
478
\end{document}
479
480