MR Imaging

Class: Psych 204a

Tutorial: MR Imaging

Author: Wandell

Date: 03.15.04

Duration: 90 minutes

Copyright: Stanford University, Brian A. Wandell

Checked:

  • Oct 2007: Rory Sayres
  • Sep 2009: Jon Winawer

Translated to Python by Michael Waskom, 10/2012

This tutorial covers two related topics. First, pulse sequence methods for measuring T1 or T2 are illustrated. These are introduced by explaining two types of pulse sequences (inversion recovery and spin echo) that selectively emphasize the T1 or T2 tissue properties.

Then, the tutorial continues with a very simple example of how one can form images of the relaxation constants (T1 or T2) at different positions

You should complete the tutorial "mrTutMR" prior to this one.

First we set up the pylab environment and import a few display-relevant functions.


In [ ]:
%pylab inline
import matplotlib as mpl
mpl.rcParams["figure.figsize"] = (8, 6)
mpl.rcParams["axes.grid"] = True
from IPython.display import display, clear_output
from time import sleep

Next we define two small functions to convert between coordinate systems.


In [ ]:
def cart2pol(x, y):
    theta = arctan2(y, x)
    r = sqrt(x ** 2 + y ** 2)
    return theta, r

def pol2cart(theta, r):
    x = r * cos(theta)
    y = r * sin(theta)
    return x, y

Now we will define several functions that will be employed in the tutorial to illustrate concepts. The narrative tutorial begins below the function definitions. If you are curious about how to draw the plots, or to dig deeper into the code demonstrating the principles, feel free to read these. However, they are mostly for manipulating plots, so don't feel the need to concern yourself with the details.


In [ ]:
def inversion_recovery(T1, f=None, ax=None):
    """Graphical illustration of the Inversion Recovery idea."""
    if not all([f, ax]):
        f, ax = subplots(1, 1)
        ax.set_aspect("equal")
        ax.set_xlim(-10, 10)
        ax.set_ylim(-10, 10)   
    
    # Original net magnetization
    m = [0, 10]
    p0, r0 = cart2pol(*m)
    arr = Arrow(0, 0, *m)
    ax.add_patch(arr)
    ax.set_title("Initial magnetization")
    display(f)
    sleep(1.5)

    # Flip the magnetization with a 180 pulse
    ax.set_title("$180^o$ RF pulse")
    p, r = cart2pol(*m)
    p = p0 - pi
    m = pol2cart(p, r)
    arr.remove()
    arr = Arrow(0, 0, *m)
    ax.add_patch(arr)
    clear_output(True)
    display(f)
    sleep(.5)

    # Let the T1 value decay for a while at the T1 rate.
    ax.set_title("Recovery")
    for t in arange(0, 0.8, 0.15):  # Time in seconds
        arr.remove()
        r = r0 - 2 * r0 *exp(-t / T1)
        p = p0 if r > 0 else p
        m = pol2cart(p, abs(r))
        arr = Arrow(0, 0, *m)
        ax.add_patch(arr)
        clear_output(True)
        display(f)
        sleep(.2)
    
    # Rotate the magnetization using a 90 deg pulse to measure it
    ax.set_title("$90^o$ RF pulse")
    for i in range(5):
        p, r = cart2pol(*m)
        p = p - (pi / 2) / 5
        m = pol2cart(p, r)
        arr.remove()
        arr = Arrow(0, 0, *m)
        ax.add_patch(arr)
        clear_output(True)
        display(f)
        sleep(.2)

    ax.set_title("Measure RF now")
    clear_output(True)
    display(f)
    plt.close()

In [ ]:
def spin_echo(TE=16, f=None, ax=None):
    """Graphical illustration of the Spin Echo dephasing and echo formation."""
    if not all(f, ax):
        f, ax = subplots(1, 1)
        ax.set_aspect("equal")
        ax.set_xlim(-10, 10)
        ax.set_ylim(-10, 10)
        ax.set_xlabel("X axis")
        ax.set_ylabel("Y axis")
    
    # Original net magnetization
    ma = [10, 0]
    p0, r0 = cart2pol(*ma)
    arr_a = Arrow(0, 0, *ma, color="blue")
    ax.add_patch(arr_a)
    mb = ma
    arr_b = arrow(0, 0, *mb, color="green")
    ax.set_title("$90^o$ pulse and dephasing")
    display(f)
    sleep(1.2)
    
    for i in range(TE):
        arr_a.remove()
        arr_b.remove()
        pa, ra = cart2pol(*ma)
        pa = pa - pi / 8
        ma = pol2cart(pa, ra)
        arr_a = Arrow(0, 0, *ma, color="blue")
        ax.add_patch(arr_a)
        pb, rb = cart2pol(*mb)
        pb = pb - pi / 10
        mb = pol2cart(pb, rb)
        arr_b = Arrow(0, 0, *mb, color="green")
        ax.add_patch(arr_b)
        clear_output(True)
        display(f)
        sleep(.2)

    # Apply a 180 deg pulse that rotates the spins around the x-axis
    sleep(.8)
    ax.set_title("Inverting ($180^o$ pulse)")
    clear_output(True)
    display(f)
    sleep(.5)
    
    arr_a.remove()
    arr_b.remove()
    pa, ra = cart2pol(*ma)
    pa = -pa
    ma = pol2cart(pa, ra)
    arr_a = Arrow(0, 0, *ma, color="blue")
    ax.add_patch(arr_a)
    pb, rb = cart2pol(*mb)
    pb = -pb
    mb = pol2cart(pb, rb)
    arr_b = Arrow(0, 0, *mb, color="green")
    ax.add_patch(arr_b)
    clear_output(True)
    display(f)
    
    # Now keep going
    ax.set_title("Catching up")
    clear_output(True)
    display(f)
    sleep(.5)

    for i in range(TE):
        arr_a.remove()
        arr_b.remove()
        pa, ra = cart2pol(*ma)
        pa = pa - pi / 8
        ma = pol2cart(pa, ra)
        arr_a = Arrow(0, 0, *ma, color="blue")
        ax.add_patch(arr_a)
        pb, rb = cart2pol(*mb)
        pb = pb - pi / 10
        mb = pol2cart(pb, rb)
        arr_b = Arrow(0, 0, *mb, color="green")
        ax.add_patch(arr_b)
        clear_output(True)
        display(f)
        sleep(.2)    
    
    ax.set_title("The echo arrives")
    clear_output(True)
    display(f)
    
    plt.close()

In [ ]:
def phase_encode(rate, spin_dir, n_steps=15):
    """Visualization of the phase-encoding."""
    f, axes = subplots(2, 2, figsize=(8, 8))
    axes = axes.ravel()
    for ax in axes:
        ax.set_aspect("equal")
        ax.set_xlabel("X axis")
        ax.set_ylabel("Y axis")
        ax.set_xlim(-10, 10)
        ax.set_ylim(-10, 10)
    
    a = empty(4, object)
    for i in range(n_steps):
        sleep(.2)
        pa = zeros(4)
        ra = zeros(4)
        for j, ax in enumerate(axes):
            pa[j], ra[j] = cart2pol(spin_dir[j, 0], spin_dir[j, 1]) 
            pa[j] = pa[j] - rate[j] 
            spin_dir[j, 0], spin_dir[j, 1] = pol2cart(pa[j], ra[j])
            if i:
                a[j].remove()
            a[j] = Arrow(0, 0, *spin_dir[j, :])
            ax.add_patch(a[j])
        clear_output(True)
        display(f)
    plt.close()

Pulse Sequences for measuring T1 and T2 signals

T1 Signals (used for anatomical images)

Inversion-Recovery (IR)

Inversion-Recovery pulse sequences are a method for measuring T1 relaxation (spin-lattice). As the sequence name suggests, the pulse sequence first inverts the net magnetization ($180^o$ pulse). Then, the sequence simply pauses for a time, TI, to let the longitudinal magnetization recover towards steady state across the tissue. Then, a $90^o$ pulse is delivered that places the net magnetization in the transverse plane. The transverse magnetization is measured right away, before significant dephasing, in order to estimate the T1 properties.

To help you visualize the events, run this code a few times. The first plot shows the 180-TI-90 sequence for a relatively slow T1 value.


In [ ]:
inversion_recovery(T1=2.8)

Now suppose the tissue has a faster T1 relaxation


In [ ]:
inversion_recovery(T1=0.6)

Question 1:

If we apply a 90-degree pulse at the time when exactly half the signal has recovered, what do you expect the transverse magnetization to be?

(Extra credit: is this the same as applying a pulse at T1? Why or why not?)

Comparing the two plots, you can see that the amplitude of the net transverse magnetization depends on the value of T1. The final $90^o$ flip let us measure the size of the longitudinal magnetization. Because the measurement takes place immediately after this flip, there is not much time for spin-spin dephasing and we measure mainly properties of T1. That is why such sequences are called 'T1-weighted'.

T2 Signals (used for BOLD images)

Spin Echo (Hahn)

In principle, to make a T2 measurement we need only to flip the net magnetization $90^o$ and then measure the signal decay as the spins dephase. Because the T2 reduction is very fast compared to the T1 reduction, the decay we see will be nearly all due to T2.

The spin-spin dephasing occurs so quickly that it is almost impossible to obtain a T2 measurement soon enough after the 90 deg flip. The Hahn Spin Echo, and its partner the Gradient Echo, make it possible to separate the time between the pulse and the measurement.

The next visualizataion shows two spins within a single voxel. These spins are experiencing slightly differeent local magnetic fields. Consequently, one precesses around the origin slightly faster than the other. After a little time the signals are well out of phase. Then, a 180 deg inverting pulse is introduced. This pulse rotates the spins around the horizontal axis (x-axis). Seen within the plane, this causes the two spins to reverse positions so that the leader becomes the follower. The leader is still in a higher local field, and so after a while it catches up. At this moment the spin phasese come together to create an echo of the signal after the first 90 deg pulse. This echo happens at a point in time that is well separated from the inverting pulse.

The time until the inverse pulse determines when the echo will occur


In [ ]:
spin_echo(TE=16)

MR Image Formation

We now ask: How can we distinguish signals from different locations?

This is the key step in learning how to form an image of the MR time constant's properties.

Consider a simple situation in which we have two beakers sitting next to one another on a table. Both contain water. To start, suppose the magnetic field is uniform, and suppose that we measure T1.

We are going to need these variables to develop the story


In [ ]:
Mo = 1                    # Net magnetization
larmor_freq = [12, 18]    # Larmor frequency in MHz/10
T1 = [1.5, 3]
t = arange(0, 1, .005) * (3 * max(T1))  # Time samples in secs

In [ ]:
def rf_signal(tau=1, net_mag=1, t_samples=None, larmor_freq=12, ph=0):
    """Estimate an rf signal based on the various parameters."""
    if t_samples is None:
        t_samples = arange(0, 1, 0.005) * (4 * tau)

    signal = exp_decay(tau, net_mag, t_samples) * cos(t_samples * larmor_freq + ph)
    return signal

def exp_decay(tau, Mo, t):
    """Create an exponential decay that will be used for either
       longitudinal or transverse decay modeling."""
    return Mo * exp(-t / tau)

The function rf_signal produces the RF signal that we will measure given a particular time constant and Larmor frequency.

The RF signal is periodic. We can summarize the amplitude and frequency of this signal by plotting the Fourier Transform amplitude spectrum. This plot measures the amplitude of each harmonic (temporal frequency) in the signal.

We're going to be making quite a few plots with an RF signal over time and its spectral density. Let's write a general function so that we avoid typing the same thing over and over again. As a general rule of thumb for progamming, any time you find yourself using "copy" and "paste", it's time to write a function.


In [ ]:
def rf_plot(time, signal, title):
    f, ax = subplots(1, 1, figsize=(6, 6))
    ax.plot(time, signal)
    ax.set_xlabel('Time (s)')
    ax.set_ylabel('RF signal')
    ax.set_title(title)

def rf_kspace_plot(time, signal, titles):
    f, (ax1, ax2) = subplots(1, 2, figsize=(14, 6))
    ax1.plot(t, signal)
    ax1.set_xlabel("Time (s)")
    ax1.set_ylabel("RF signal")
    ax1.set_title(titles[0])
    ax2.psd(signal)
    ax2.set_title(titles[1])

In [ ]:
signal = rf_signal(T1[0], Mo, t, larmor_freq[1])
rf_kspace_plot(t, signal, ["Beaker Signal", "Slice Selection"])

Next, consider the signal as the relaxation constant (t_constant) increases. Notice that over the same period of time, the rf_signal has a larger amplitude (there is less decay).


In [ ]:
t_constant = [1, 2, 4] 
f, axes = subplots(1, 3, figsize=(14, 5), sharey=True)
for i, ax in enumerate(axes):
    signal = rf_signal(t_constant[i], Mo, t, larmor_freq[0])
    ax.set_ylim(-1, 1)
    ax.plot(t, signal)

Question 2:

If you were to plot the Fourier Transform for each of the three subplots, how do you expect they would differ?


In [ ]:
t_constant = [1, 2, 4]
f, axes = subplots(1, 3, figsize=(14, 5), sharey=True)
for i, ax in enumerate(axes):
    signal = rf_signal(t_constant[i], Mo, t, larmor_freq[1])
    ax.psd(signal)

Changing magnetic field strength

Now, suppose that we change the magnetic field strength. Remember that the Larmor frequency is proportional to the magnetic field. Consequently, the frequency of the RF signal will increase. We can compare the signals at two different frequencies as follows


In [ ]:
larmor_freqs = [6, 124]
f, axes = subplots(1, 2, figsize=figsize(13, 7), sharey=True)
for i, freq in enumerate(larmor_freqs):
    signal = rf_signal(T1[0], Mo, t, freq);
    axes[i].plot(t, signal)
    axes[i].set_title("Frequency: %d Hz" % freq)

It is easiest to see the change in frequency if we compute the Fourier transform of the two signals.


In [ ]:
f, axes = subplots(1, 2, figsize=figsize(13, 7), sharey=True)
for i, freq in enumerate(larmor_freqs):
    ax = axes[i]
    signal = rf_signal(T1[0], Mo, t, freq)
    ax.psd(signal)
    ax.set_title('Fourier Spectrum for %d Hz Signal' % freq)

This figure is important. It shows that the frequency of the response from each beaker depends on the local magnetic field. We have already seen that the amplitude at the response frequency depends on the time constant. We can take advantage of these two observations by introducing a gradient into the magnetic field.

By inserting a gradient, the two beakers experience slightly different magnetic fields and thus the signals have two different Larmor frequencies. The combined RF signal from the two beakers will be the sum of the signals from the two beakers.


In [ ]:
signal =  rf_signal(T1[0], Mo, t, larmor_freqs[0]) + rf_signal(T1[0], Mo, t, larmor_freqs[1])
rf_plot(t, signal, "Two Beakers in Gradient")

We can see the amplitude of the signals from each of the beakers by plotting the Fourier Transform spectrum


In [ ]:
rf_kspace_plot(t, signal, ["Two beakers in gradient", "Fourier Space"])

Finally, if the two beakers represent substances with different time constants, we will be able to measure this by estimating the amplitudes of the two peaks in the spectrum. Here, we create a signal in which the two beakers are in a gradient and the substances have slightly different time constants.


In [ ]:
signal =  rf_signal(T1[0], Mo, t, larmor_freqs[0]) + rf_signal(T1[1], Mo, t, larmor_freqs[1])
rf_plot(t, signal, "Two beakers in gradient")

You can see that the amplitude of peaks in the signal change, reflecting the different time constants. We can distinguish which amplitude is associated with which beaker because their responses are at different frequencies.


In [ ]:
rf_kspace_plot(t, signal, ["Two Beakers in Gradient", "Fourier Space"])

Question 3:

When computing the RF signal in the last example, which variable(s) represented the magnetic field gradient?

From answering Question 2, and understanding this simple case, you should understand how the gradient associates different RF signal frequencies with different spatial locations.

This is why determining the frequency amplitude of the signal, using the Fourier Transform, is so important: RF signal frequency is associated with spatial position. The amplitude at that frequency can be interpreted as the decay rate.

This simple example provides you with the basic principles of an important MR imaging term: k-space. Specifically, in this example the frequency axis of the Fourier Transform is analogous to k-space.

There are important limitations to the method we have developed up this point.

Mainly, this method works if we only need to make images of an array of beakers along a line. To make estimates of beakers spread across a table top (2D) or filling up a box (3D) we need to do more. These methods will be explained below.

The main difference between this example and general imaging is dimensionality. In MR imaging the stimuli don't fall along one dimension, so we can't simply use a one-dimensional frequency axis to assign position. In general the position in k-space corresponds to a position in a two-dimensional plane that represents various spatial frequencies.

(By the way, there are historical reasons why they call it k-space. I forget these reasons. If someone can remind me, I will give them a firm handshake and a warm smile.)

We will start to extend the ideas on imaging in this first section to slightly more complex cases in the next section.

Slice Selection

In nearly all fMRI imaging, the data are acquired using a series of measurements from different planar sections (slices) of the tissue. The measurements are made by selectively exciting the spins, one planar section at a time. If only one plane is excited, then we can be confident that the signal we measure must arise from a voxel in that plane.

In the next part of this tutorial, we will reivew how we can excite the spins in one planar section. Following that we will review how we can distinguish different positions within the excited plane.

The principles used to understand slice selection are the same as the principles used to distinguish signals from beakers at two positions. The main difference is that in the previous example we used gradients to distinguish received signals. In slice selection we use gradients to selectively deliver an excitation.

The idea is this. We introduce a magnetic field gradient across the sample changing the Larmor frequency across the sample.
When we deliver an RF pulse at a particular frequency, then, only the spins in one portion of the gradient field will be excited.

What would such a pulse look like? Let's create some examples.

Let's perform some example calculations. We initialize the rf_pulse to zero, and then we initialize some parameters.


In [ ]:
n_time = 256.
t = arange(n_time) / n_time - 0.5
rf_pulse = zeros(int(n_time))

pulse_duration = round(n_time / 2)
pulse_start = n_time / 2 - pulse_duration / 2
pulse_stop = pulse_start + pulse_duration - 1
idx = slice(int(pulse_start), int(pulse_stop))

pulse_t = t[idx]

We choose a pulse frequency. This frequency will excite the planar section of the tissue that is at the Larmor frequency we wish to excite.


In [ ]:
pulse_freq = 25

Now, create a sinusoid RF pulse at that frequency:


In [ ]:
rf_pulse[idx] = sin(2 * pi * pulse_freq * pulse_t)
rf_plot(t, rf_pulse, "RF pulse")

Here is the Fourier Transform spectrum of the RF pulse. The frequency content of the pulse determines which plane we excite.


In [ ]:
rf_kspace_plot(t, rf_pulse, ["RF Pulse", "Slice Selection"])

We can control the position that is excited by adjusting the frequency.


In [ ]:
pulse_freq = 50
rf_pulse[idx] = sin(2 * pi * pulse_freq * pulse_t)
rf_kspace_plot(t, rf_pulse, ["RF Pulse", "Slice Selection"])

A second parameter we would like to control is the slice width. There are two ways we can adjust the slice width. One way, as described in the book, is to change the gradient. A second way, illustrated here, is to change the timing of the RF pulse.

In this example, we create an RF pulse that is the product of the sinusoid and a sinc function. (Type sinc? to read about this important function). Each sinc function has its own frequency, too.


In [ ]:
sinc_freq  = 20   # Try 10, 20 and 40.
pulse_freq = 50  # Try 25, 50 and 75.

rf_pulse[idx] = sin(2 * pi * pulse_freq * pulse_t)
rf_pulse[idx] = rf_pulse[idx] * sinc(sinc_freq * pulse_t)

rf_kspace_plot(t, rf_pulse, ["RF Pulse", "Slice Selection"])

Question 4:

Run the above code a few times, varying the pulse and sinc frequency values. What effect does each parameter have on slice position and slice width?

Image Formation using Frequency Encoding and Phase Encoding

The Hornak MRI book has a very good discussion of imaging, including frequency and phase encoding. Please read Chapters 6 and 7 for a useful discussion of the principles further described here. Also, the Huettel et al. book is very clear and useful in describing pulse sequences (Chapters 4 and 5).

Earlier, we reviewed how to use a gradient field to associate different positions along a line with different RF signal frequencies. That method is often called frequency encoding. In this section, we describe how to measure along the second spatial dimension. This measurement is sometimes called the phase-encoding dimension. Taken together, the methods described in this section are sometimes called Fourier Transform imaging.

Consider the problem of identifying signals from 4 beakers placed at the corners of a square. These four beakers are in the planar section and the spins were set in motion using the methods described in the previous section.

First, we set up the basic parameters for the four beakers.


In [ ]:
T1 = array([(1, 2), (3, 4)], float) / 2
larmor_freq = [15., 50.]
ph = [0, pi]
t = arange(0, 5, .02)
Mo = 1

rate = [0., 0., 0., 0.]
spin_dir = array([(10, 0), (10, 0), (10, 0), (10, 0)], float)

Plot the starting positions in the transverse plane:


In [ ]:
phase_encode(rate, spin_dir, 1)

Now define a new plotting function to save some typing.


In [ ]:
def plot_rf_2d(fs, ps, t, T1, Mo=1):
    f, axes = subplots(2, 2, figsize=(8, 8))
    signal = zeros((2, 2, len(t)))
    freq_text = ["F1", "F2"]
    fs = reshape(fs, (2, 2)).T
    ps = reshape(ps, (2, 2)).T
    for i in range(2):
        for j in range(2):
            signal[i, j, :] = rf_signal(T1[i, j], Mo, t, fs[i, j], ps[i, j])
            axes[i, j].plot(t, signal[i, j, :])
            axes[i, j].set_title(freq_text[j])
    return signal

Suppose we apply a gradient across the x-axis. In the presence of this gradient, the signals from the two beakers on the left will differ from the RF signals emitted by the two beakers on the right. When we measure with only the x-gradient (Gx), we obtain the sum of the two beakers on the left in one frequency band and the sum of the two beakers on the right in a second frequency band.


In [ ]:
freqs = [larmor_freq[0]] * 2 + [larmor_freq[1]] * 2
phases = [ph[0]] * 4
signal = plot_rf_2d(freqs, phases, t, T1, Mo)

Here is the total signal:


In [ ]:
s = (signal[0] + signal[1]).sum(axis=0)
rf_kspace_plot(t, s, ["Total RF", "Total RF Signal"])

Now, suppose that we turn off the Gx gradient and introduce a gradient in the y-dimension (Gy). This changes the Larmor frequency of the beakers at the top and bottom of the square. Suppose the frequency for the bottom beakers is a little lower. Then the spins in the bottom beakers rotate more slowly and they end up pointing in a different direction from the spins at the top. After applying Gy for a certain amount of time, the spins at the top and bottom will point in opposite directions.


In [ ]:
rate = [pi / 8, pi / 8, pi / 16, pi / 16]
spin_dir = array([(10, 0), (10, 0), (10, 0), (10, 0)], float)
phase_encode(rate, spin_dir, 15)

Next, we switch off Gy and turn on Gx. As before we will measure the combination of the spins on the left at one frequency and the combination of the spins at the right at a different frequency. Because the spins of the top and bottom beaker oppose one another, however, the total RF signal we obtain now is the difference between the top and bottom.


In [ ]:
ps = [ph[0], ph[1]] * 2
signal = plot_rf_2d(freqs, ps, t, T1, Mo)

Total signal:


In [ ]:
s = (signal[0] + signal[1]).sum(axis=0)
rf_kspace_plot(t, s, ["Total RF", "Total RF Signal"])

These two measurements at the main frequencies provide an estimate of the sum and the difference of the time constants in the upper and lower beakers. Specifically, the two measurements are

m1 = a + b
m2 = a - b

Measuring m1 and m2, we can solve for a and b.

In this example, the frequency of the RF signal codes the x-dimension. This dimension is called the frequency dimension. We keep adjusting the phase of the spins along the y-dimension. Hence, this dimension is called the phase-encode dimension. People keep track of these two dimensions because the phase-encode direction is subject to more measurement error than the frequency encode direction.