Using Labels to reconstruct multi-slice / multi-contrast sequences

This is generally done in pulseq / pypulseq. In that case, the sequence structure automatically contains the labels increment and set extension.

Here we will show how to modify a sequence in KomaMRI to add the extension

using KomaMRI, PlotlyJS, Suppressor # hide
sys = Scanner() # hide
obj = brain_phantom3D()
obj.Δw .= 0; # Removes the off-resonance

How to add label to a sequence

Let's add the label to increment the slice for each seq_EPI block. We will add that to the first block of the sequence

seq_EPI = PulseDesigner.EPI_example()
seq = deepcopy(seq_EPI);

We can create 2 different types of label "Increment" and "Set" with LabelInc and LabelSet

lSlcInc = LabelInc(1,"SLC");

Let's change the extension field of the first block of seq_EPI in order to add an increment to the SLC label

seq_EPI.EXT[1] = [lSlcInc];

Now let's merge 3 seqEPI. We now have 1 EPI sequence without EXTENSION, then we have an increment of the SLC label, and another one at the beginning of the last seqEPI

seq = seq + seq_EPI + seq_EPI
plot_seq(seq);

We can extract the label value with the following function

l = get_label(seq);

And store in a vector only the value of the SLC label

SLC_vec = [l[i].SLC for i in eachindex(l)];

The value is equal to 0 until we reach LabelInc(1,"SLC)

p1 = plot(SLC_vec, Layout(
    xaxis_title="n° blocks",
    yaxis_title="SLC label"
))

Simulate the acquisition and reconstruct the data

Define simulation parameters and perform simulation

sim_params = KomaMRICore.default_sim_params()
raw = @suppress simulate(obj, seq, sys; sim_params)
RawAcquisitionData[SeqName: epi | 303 Profile(s) of 101×1]

The simulated raw data stored the correct label for each profile

for p in [1,101,102,203]
  slc = raw.profiles[p].head.idx.slice |> Int
  println("Profile n°$p : SLC label = $slc")
end
Profile n°1 : SLC label = 0
Profile n°101 : SLC label = 0
Profile n°102 : SLC label = 1
Profile n°203 : SLC label = 2

MRIReco splits the data into 3 when the data are converted to the AcquisitionData structure. The dimensions are (contrast / slice / repetitions)

acqData = AcquisitionData(raw)
acqData.kdata |> size
(1, 3, 1)

For multi-slice acquisition, MRIReco uses the same trajectory. We need to crop the trajectory.

size_readout = size(acqData.traj[1].nodes,2) / 3 |> Int
acqData.traj[1].nodes = acqData.traj[1].nodes[1:2,1:size_readout];

We need to normalize the trajectory to -0.5 to 0.5

C = maximum(2*abs.(acqData.traj[1].nodes[1:2]))
acqData.traj[1].nodes ./= C
acqData.traj[1].circular = false # Removing circular window

Nx, Ny = raw.params["reconSize"][1:2]

recParams = Dict{Symbol,Any}()
recParams[:reconSize] = (Nx, Ny)
recParams[:densityWeighting] = true
rec = reconstruction(acqData, recParams)
image = abs.(reshape(rec,Nx,Ny,:));

Let's take a look at the first 2 images

p2 = plot_image(image[:,:,1], height=400);
p3 = plot_image(image[:,:,2], height=400);

The signal ponderation is changing because we are acquiring the same slice position with a short TR sequence. Thus, the magnetization is not at equilibrium.

Reconstruction using the labels LIN and PAR

Rather than using the k-space trajectory calculated by KomaMRI and performing a NUFFT for the reconstruction, we can use the label LIN (phase encoding) and PAR (partition encoding).

First, we will create an increment label for LIN

seq_LIN = PulseDesigner.EPI_example()
lInc = LabelInc(1,"LIN");

We can put the increment at any stage between 2 ADC blocks. Here we will put it onto each ADC block.

idx_ADC = is_ADC_on.(seq_LIN)
for i in eachindex(idx_ADC)
  idx_ADC[i] == 1 ? seq_LIN.EXT[i] = [lInc] : nothing;
end

Because we want the label of each ADC to start from 0, we set the value to -1 on the first block.

seq_LIN.EXT[1] = [LabelSet(-1,"LIN")];

Let's check the LIN label for each ADC

l = get_label(seq_LIN)
l[idx_ADC][1:10]
10-element Vector{AdcLabels}:
 AdcLabels[ LIN = 0 | PAR = 0 | SLC = 0 | SEG = 0 | REP = 0 | AVG = 0 | SET = 0 | ECO = 0 | PHS = 0 | NAV = 0 | REV = 0 | SMS = 0 ]
 AdcLabels[ LIN = 1 | PAR = 0 | SLC = 0 | SEG = 0 | REP = 0 | AVG = 0 | SET = 0 | ECO = 0 | PHS = 0 | NAV = 0 | REV = 0 | SMS = 0 ]
 AdcLabels[ LIN = 2 | PAR = 0 | SLC = 0 | SEG = 0 | REP = 0 | AVG = 0 | SET = 0 | ECO = 0 | PHS = 0 | NAV = 0 | REV = 0 | SMS = 0 ]
 AdcLabels[ LIN = 3 | PAR = 0 | SLC = 0 | SEG = 0 | REP = 0 | AVG = 0 | SET = 0 | ECO = 0 | PHS = 0 | NAV = 0 | REV = 0 | SMS = 0 ]
 AdcLabels[ LIN = 4 | PAR = 0 | SLC = 0 | SEG = 0 | REP = 0 | AVG = 0 | SET = 0 | ECO = 0 | PHS = 0 | NAV = 0 | REV = 0 | SMS = 0 ]
 AdcLabels[ LIN = 5 | PAR = 0 | SLC = 0 | SEG = 0 | REP = 0 | AVG = 0 | SET = 0 | ECO = 0 | PHS = 0 | NAV = 0 | REV = 0 | SMS = 0 ]
 AdcLabels[ LIN = 6 | PAR = 0 | SLC = 0 | SEG = 0 | REP = 0 | AVG = 0 | SET = 0 | ECO = 0 | PHS = 0 | NAV = 0 | REV = 0 | SMS = 0 ]
 AdcLabels[ LIN = 7 | PAR = 0 | SLC = 0 | SEG = 0 | REP = 0 | AVG = 0 | SET = 0 | ECO = 0 | PHS = 0 | NAV = 0 | REV = 0 | SMS = 0 ]
 AdcLabels[ LIN = 8 | PAR = 0 | SLC = 0 | SEG = 0 | REP = 0 | AVG = 0 | SET = 0 | ECO = 0 | PHS = 0 | NAV = 0 | REV = 0 | SMS = 0 ]
 AdcLabels[ LIN = 9 | PAR = 0 | SLC = 0 | SEG = 0 | REP = 0 | AVG = 0 | SET = 0 | ECO = 0 | PHS = 0 | NAV = 0 | REV = 0 | SMS = 0 ]

We can now simulate the results

raw = @suppress simulate(obj, seq_LIN, sys; sim_params)
RawAcquisitionData[SeqName: epi | 101 Profile(s) of 101×1]

In order to not use the NUFFT reconstruction of MRIReco.jl, we need to change the trajectory name to "cartesian"

raw.params["trajectory"] = "cartesian"
raw.params["encodedSize"] = [seq.DEF["Nx"],seq.DEF["Ny"]];

We also need to estimate the profile center, which will be at the center of the readout. If it is not the case, it should be specified in f.profiles[i].head.center_sample = center_sample and estimateProfileCenter = false

acqData = AcquisitionData(raw,estimateProfileCenter=true);

Let's see the results

recParams = Dict{Symbol,Any}()
rec = reconstruction(acqData, recParams);

p4=plot_image(abs.(rec[:,:,1]), height=400);

This page was generated using Literate.jl.