├── GRAPPA.pdf ├── README.md ├── data └── data.mat ├── examples.m ├── grappa-practical.html ├── grappa.m ├── grappa_apply_weights.m ├── grappa_estimate_weights.m ├── grappa_get_indices.m ├── grappa_get_pad_size.m ├── grappa_pad_data.m ├── grappa_unpad_data.m ├── show_quad.m └── solution ├── grappa_apply_weights.m ├── grappa_estimate_weights.m ├── grappa_get_indices.m ├── grappa_get_pad_size.m ├── grappa_pad_data.m └── grappa_unpad_data.m /GRAPPA.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mchiew/grappa-tutorial/a38145383fb476e06191ccdc0e36279fa12d4d46/GRAPPA.pdf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GRAPPA Parallel Imaging Tutorial 2 | Mark Chiew (mchiew@fmrib.ox.ac.uk) 3 | 4 | ## Introduction 5 | Parallel imaging, broadly speaking, refers to the use of multi-channel receive coil information to reconstruct under-sampled data. 6 | It is called "parallel" because the set of receive channels all acquire data "in parallel" - i.e., they all record at the same time. 7 | GRAPPA (Griswold et al., MRM 2002) is one of the most popular techniques for performing parallel imaging reconstruction, among many. 8 | Others include SENSE (Pruessmann et al., MRM 1999) and ESPIRiT (Uecker et al., MRM 2014), 9 | 10 | So, in this tutorial, we'll go over the basics of _what_ GRAPPA does, and _how_ to do it. 11 | What we won't cover is _why_ GRAPPA, or parallel imaging works. I refer you to one of the many review papers on Parallel Imaging. 12 | 13 | ## The GRAPPA Problem 14 | ### Problem Definition 15 | First, this is an example of the type of problem we are trying to solve: 16 | 17 | Coil #1 k-space Coil #2 k-space 18 | 19 | oooooooooooooooo oooooooooooooooo 20 | ---------------- ---------------- 21 | oooooooooooooooo oooooooooooooooo o : Acquired k-space data point 22 | ---------------- ---------------- - : Missing k-space data 23 | oooooooooooooooo oooooooooooooooo 24 | ---------------- ---------------- 25 | oooooooooooooooo oooooooooooooooo 26 | ---------------- ---------------- 27 | oooooooooooooooo oooooooooooooooo 28 | ---------------- ---------------- 29 | oooooooooooooooo oooooooooooooooo 30 | ---------------- ---------------- 31 | 32 | The acquisition above is missing every other line `(R=2)`. To generate our output images, we will need to fill in these missing lines. 33 | GRAPPA gives us a set of steps to fill in these missing lines. We do this by using a weighted combination of surrounding points, from all coils. 34 | 35 | ### Overview 36 | Here's what this practical will help you with: 37 | 38 | 1. Understanding and defining GRAPPA kernel geometries 39 | 2. Learning how to construct the GRAPPA synthesis problem as a linear system 40 | 3. Learning how to estimate the kernel weights needed to perform GRAPPA reconstruction 41 | 42 | ### Step-by-Step 43 | Let's consider one of the missing points we want to reconstruct, from coil 1, denoted `X`: 44 | 45 | Coil #1 k-space Coil #2 k-space 46 | 47 | oooooooooooooooo oooooooooooooooo 48 | ---------------- ---------------- 49 | oooooooooooooooo oooooooooooooooo o : Acquired k-space data point 50 | ---------------- ---------------- - : Missing k-space data 51 | oooooooooooooooo oooooooooooooooo 52 | --------X------- ---------------- X : Reconstruction Target 53 | oooooooooooooooo oooooooooooooooo 54 | ---------------- ---------------- 55 | oooooooooooooooo oooooooooooooooo 56 | ---------------- ---------------- 57 | oooooooooooooooo oooooooooooooooo 58 | ---------------- ---------------- 59 | 60 | To recover the target point `X`, we need to choose a local neighbourhood of surrounding acquired points, as well as the "parallel" neighbourhoods of the other coils. 61 | 62 | Coil #1 k-space Coil #2 k-space 63 | 64 | oooooooooooooooo oooooooooooooooo 65 | ---------------- ---------------- 66 | oooooooooooooooo oooooooooooooooo o : Acquired k-space data point 67 | ---------------- ---------------- - : Missing k-space data 68 | ooooooo***oooooo ooooooo***oooooo 69 | --------X------- --------Y------- X : Reconstruction target 70 | ooooooo***oooooo ooooooo***oooooo Y : Reconstruction target position in other coils 71 | ---------------- ---------------- 72 | oooooooooooooooo oooooooooooooooo * : Neighbourhood sources 73 | ---------------- ---------------- 74 | oooooooooooooooo oooooooooooooooo 75 | ---------------- ---------------- 76 | 77 | We chose a neighbourhood, or "kernel" of 3 points in the x-direction, and 2 points in the y-direction, centred on the target position. 78 | These source points come from the same coil as the target point, as well as in the same locations from the other parallel coils. 79 | Zooming in on the target point, and its sources: 80 | 81 | Coil #1 k-space Coil #2 k-space 82 | 83 | * * * * * * 84 | X : Reconstruction target 85 | - X - - Y - Y : Reconstruction target position in other coils 86 | 87 | * * * * * * * : Neighbourhood sources 88 | 89 | We can see that there are 3x2=6 source points per coil. This is generally referred to as a 3x2 kernel geometry. 90 | If we label all the source points in this 2-coil example: 91 | 92 | Coil #1 k-space Coil #2 k-space 93 | 94 | a b c g h i 95 | X : Reconstruction target 96 | - X - - Y - Y : Reconstruction target position in other coils 97 | 98 | d e f j k l [a-l] : Source points 99 | 100 | The GRAPPA weighted combination formulation means that: 101 | 102 | X = wx_a*a + wx_b*b + wx_c*c + ... + wx_l*l; {Eq. 1a} 103 | 104 | where `wx_n` refers to the weight for source n, and `a-l` are the complex k-space data points. 105 | While the kernels are shift-invariant over the entire k-space, they are specific to the target coils. 106 | 107 | Therefore, to reconstruct target point Y in the second coil, a different set of weights are required: 108 | 109 | Y = wy_a*a + wy_b*b + wy_c*c + ... + wy_l*l; {Eq. 1b} 110 | 111 | So, for example, if you have 8 coils, using 3x2 kernel geometries, you will have 8x6 different kernel 112 | weights, where the entire group of weights for all coils is called the kernel or weight set. 113 | 114 | If we write this as a matrix equation, we can see that: 115 | 116 | M = W*S; {Eq. 2a} 117 | or 118 | [X] [wx_a wx_b wx_c ... wx_l] [a] 119 | [Y] [wy_a wy_b wy_c ... wy_l] [b] 120 | . = [ . ] [c] {Eq. 2b} 121 | . [ . ] [d] 122 | [Z] [wz_a wz_b wz_c ... wz_l] [e] 123 | [.] 124 | [.] 125 | [.] 126 | [l] 127 | 128 | where `M=(X,Y,...Z)'` are the missing points from each coil, `W` is the kernel weight matrix where each coil's weights comprise one row. 129 | `S` is a vector of source points - order is not important, but it is critical to ensure it is consistent with the weights. 130 | 131 | Finally, this expression only holds for a fixed kernel geometry (i.e. source - target geometry). For `R > 2` (i.e. `R-1` missing lines for each measured line), 132 | there will be `R-1` distinct kernel sets. 133 | 134 | For example, in the case of `R=3`, you have 2 distinct kernel geometries to work with: 135 | 136 | Coil #1 k-space Coil #2 k-space 137 | 138 | oooooooooooooooo oooooooooooooooo 139 | ---------------- ---------------- 140 | ---------------- ---------------- o : Acquired k-space data point 141 | oooooooooooooooo oooooooooooooooo - : Missing k-space data 142 | ---------------- ---------------- 143 | ---------------- ---------------- --- ooo 144 | oooooooooooooooo oooooooooooooooo ooo First kernel --- Second kernel 145 | -------X-------- ---------------- -X- geometry -Y- geometry 146 | -------Y-------- ---------------- --- ooo 147 | oooooooooooooooo oooooooooooooooo ooo --- 148 | ---------------- ---------------- 149 | ---------------- ---------------- 150 | 151 | If you want to generalise this to `R > 2`, ultimately, for `C` coils, with kernel size `[Nx,Ny]`, and acceleration factor `R`, in totality, 152 | you will need to estimate `C*(R-1)*C*Nx*Ny` weight coefficients. You can solve this as a single comprehensive system, or because the problems are uncoupled, 153 | you may find it easier to solve each `(R-1)` class of sub-problems separately. 154 | 155 | So Eq. 2 completely describes how you solve for missing points, given acquired data in some neighbourhood around it. 156 | 157 | The one final piece of information we need is fully sampled "calibration" or "training" data, so that we can actually find the kernel weights. 158 | To do this, we simply solve Eq. 2 for the weights, typically in a least-squares sense (no pun intended), over the calibration data. 159 | In this case, M and S are both matrices, containing known information, representing source-target relationships across the entire calibration region. 160 | 161 | This is what "fitting the kernel" refers to: 162 | 163 | W = M*pinv(S); {Eq. 3a} 164 | or 165 | W = M*S'*(S*S')^-1; {Eq. 3a} 166 | 167 | To ensure a robust and well-conditioned fit, typically a relatively large calibration region is used fit `W`. 168 | Over the calibration data, nearly every point is a potential source and target. Because all the points are present, 169 | we can use this data to learn the shift-invariant geometric relationships in the k-space x coil data. 170 | 171 | Coil #1 k-space Coil #2 k-space 172 | 173 | oooooooooooooooo oooooooooooooooo 174 | oooooooooooooooo oooooooooooooooo 175 | oooooooooooooooo oooooooooooooooo o : Acquired k-space calibration data 176 | oooooooooooooooo oooooooooooooooo 177 | oooooooooooooooo oooooooooooooooo 178 | oooooooooooooooo oooooooooooooooo 179 | oooooooooooooooo oooooooooooooooo 180 | oooooooooooooooo oooooooooooooooo 181 | oooooooooooooooo oooooooooooooooo 182 | oooooooooooooooo oooooooooooooooo 183 | oooooooooooooooo oooooooooooooooo 184 | oooooooooooooooo oooooooooooooooo 185 | 186 | So to solve Eq. 3, we "move" the kernel over the entire calibration space, and for every source-target pairing, we get an additional 187 | column in `M` and `S`: 188 | 189 | Coil #1 k-space Coil #2 k-space 190 | 191 | a1 b1 c1 - g1 h1 i1 - [X1] [wx_a wx_b wx_c ... wx_l] [a1] 192 | [Y1] [wy_a wy_b wy_c ... wy_l] [b1] 193 | - X1 - - - Y1 - - [ .] = [ . ] [c1] 194 | [ .] [ . ] [d1] 195 | d1 e1 f1 - j1 k1 l1 - [Z1] [wz_a wz_b wz_c ... wz_l] [e1] 196 | [ .] 197 | - - - - - - - - [ .] 198 | [l1] 199 | 200 | Coil #1 k-space Coil #2 k-space 201 | 202 | - a2 b2 c2 - g2 h2 i2 [X1 X2] [wx_a wx_b wx_c ... wx_l] [a1 a2] 203 | [Y1 Y2] [wy_a wy_b wy_c ... wy_l] [b1 b2] 204 | - - X2 - - - Y2 - [ . .] = [ . ] [c1 c2] 205 | [ . .] [ . ] [d1 d2] 206 | - d2 e2 f2 - j2 k2 l2 [Z1 Z2] [wz_a wz_b wz_c ... wz_l] [e1 e2] 207 | [ . .] 208 | - - - - - - - - [ . .] 209 | [l1 l2] 210 | 211 | Coil #1 k-space Coil #2 k-space 212 | 213 | - - - - - - - - [X1 X2 ... Xn] [wx_a wx_b wx_c ... wx_l] [a1 a2 ... an] 214 | [Y1 Y2 ... Yn] [wy_a wy_b wy_c ... wy_l] [b1 b2 ... bn] 215 | - an bn cn - gn hn in [ . . ... .] = [ . ] [c1 c2 ... cn] 216 | [ . . ... .] [ . ] [d1 d2 ... dn] 217 | - - Xn - - - Yn - [Z1 Z2 ... Zn] [wz_a wz_b wz_c ... wz_l] [e1 e2 ... en] 218 | [ . . ... .] 219 | - dn en fn - jn kn ln [ . . ... .] 220 | [l1 l2 ... ln] 221 | 222 | Now that you have `M` and `S` fully populated, you can solve Eq. 3 in the least squares sense, by pseudo-inverting `S`: 223 | 224 | [wx_a wx_b wx_c ... wx_l] [X1 X2 ... Xn] [a1 a2 ... an] 225 | [wy_a wy_b wy_c ... wy_l] [Y1 Y2 ... Yn] [b1 b2 ... bn] 226 | [ . ] = [ . . ... .] *pinv([c1 c2 ... cn]) Eq. {4} 227 | [ . ] [ . . ... .] [d1 d2 ... dn] 228 | [wz_a wz_b wz_c ... wz_l] [Z1 Z2 ... Zn] [e1 e2 ... en] 229 | [ . . ... .] 230 | [ . . ... .] 231 | [l1 l2 ... ln] 232 | ### Summary 233 | Ultimately, the entire GRAPPA algorithm boils down to: 234 | 235 | 1. Choosing desired kernel geometries 236 | 2. Solving Eq. 3 to fit for `W` over the calibration data 237 | 3. Applying Eq. 2 to solve for `M`, using the calibrated `W`, over the actual under-sampled data 238 | 239 | ## Practical 240 | Now that you have a basic sense of the internal logic behind GRAPPA (the _what_), we'll get into a step-by-step practical on 241 | how to actually go through the mechanics of writing a GRAPPA-based image reconstruction program (the _how_). 242 | 243 | I've included skeleton code for each step, that you're free to use if you like. I've also included a full working step-by-step solution if 244 | you get stuck on any step. Use as much or as little of the provided solution as you like. 245 | 246 | At the end of the tutorial, I'll just briefly walk through my solution code. 247 | 248 | ### Step 1 - Organising the reconstruction code 249 | Source file: `grappa.m` 250 | 251 | We'll use `grappa.m` as our main function that takes in undersampled data and returns reconstructed data. 252 | We'll also be defining separate functions for most of the other steps, and the provided `grappa.m` file is already organised this way for you. 253 | I recommend you use the provided `grappa.m` file. 254 | 255 | ### Step 2 - Pad data to deal with kernels applied at k-space boundaries 256 | Source files: `grappa_get_pad.m`, `grappa_pad_data.m`, `grappa_unpad_data.m` 257 | 258 | We need to pad the k-space data that we're working with in order to accommodate kernels being applied at the boundary of the actual data. 259 | Because the kernels extend for some width beyond the target point, if the reconstruction target is at the edge, the kernel will necessarily 260 | need to grab data from beyond. 261 | 262 | This is organised into 2 parts: 263 | `grappa_get_pad.m` should return you the size of padding needed in each dimension given your kernel size and under-sampling factor 264 | `grappa_pad_data.m` should perform the padding operation 265 | `grappa_unpad_data.m` should perform the un-padding operation 266 | 267 | ### Step 3 - Compute relative indices for source points relative to target 268 | Source file: `grappa_get_indices.m` 269 | 270 | This is in my opinion the trickiest part of the practical GRAPPA problem. In order to perform weight estimation and application, you 271 | will need to be able to know what the co-ordinates are of every source point relative to its target point. It's not difficult to picture 272 | in your head, but making sure you've got your indexing correct is pretty crucial. 273 | 274 | You will need to return an array of source indices, where each column is paired with the corresponding element in the target index vector. 275 | I use linear indexing for this (i.e., I index the 3D data arrays from 1 to C*Nx*Ny linearly, instead of using subscripts). 276 | 277 | I *strongly* recommend simply using the provided solution, unless you're feeling particularly keen. In my solution, for simplicity, I require that 278 | the kernel size be odd in the kx-direction, and even in the ky-direction. 279 | 280 | ### Step 4 - Perform weight estimation 281 | Source file: `grappa_estimate_weights.m` 282 | 283 | Given paired set of source and target indices, you must now use those to collect corresponding dataset pairs from the calibration k-space 284 | in order to perform weight estimation. Don't forget that the weights must map data from _all_ coils to the target points in _each_ coil. 285 | 286 | You can do whatever you like here, if you know what you're doing. Otherwise, I recommend you perform a least squares fit. 287 | The easiest way to do that, is to recognise that this is simply a linear regression problem as laid out above, and use the pseudoinverse (pinv in MATLAB) 288 | Eq. {4} is basically what you should get. 289 | 290 | The way I have structured my solution is to estimate weights for each of the R-1 missing line groups (or kernel geometries) separately. 291 | This makes the organisation a bit simpler, and is mathematically equivalent to solving for all weight sets at once. Feel free to try to implement 292 | the all-in-one approach if you have extra time. 293 | 294 | ### Step 5 - Apply weights to reconstruct missing data 295 | Source file: `grappa_apply_weights.m` 296 | 297 | Finally, the weights estimated need to be used to reconstruct missing data. Here you also need to identify source and target indices from the 298 | actual under-sampled data, and then use the weights you derive to solve the reconstruction problem (Eq. {2}). 299 | 300 | Again, depending on whether you separated the weight estimation into R-1 subproblems or one big problem, you will need to either loop over 301 | all your subproblems to get the final reconstruction, or simply apply your weights to all missing points at once. 302 | 303 | ### Step 6 - Test Reconstructions 304 | Source file: `example.m` 305 | 306 | To evaluate your reconstruction, I have provided a simple script and some data that perform some toy under-sampling problems. 307 | If you've implemented `grappa.m` and everything else correctly, this should run and give you the expected outputs. 308 | 309 | ### Bonus Steps 310 | Using this exact framework, it is relatively simple to perform the following extensions: 311 | (I have code for these, they're all in some form or another on psg.fmrib.ox.ac.uk/u/mchiew/projects) 312 | 313 | * Regularised GRAPPA (Tikhonov, PRUNO/ESPIRiT-style SVD-truncation) 314 | * SENSE-style image-based reconstruction using "sensitivities" derived from Fourier Transforming the GRAPPA kernel 315 | * Analytical g-factor maps derived from the GRAPPA kernel 316 | * 2D GRAPPA (under-sampling in both directions, with and without CAIPI-style sampling) 317 | * Slice-GRAPPA and Split-Slice-GRAPPA multi-band slice separation 318 | -------------------------------------------------------------------------------- /data/data.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mchiew/grappa-tutorial/a38145383fb476e06191ccdc0e36279fa12d4d46/data/data.mat -------------------------------------------------------------------------------- /examples.m: -------------------------------------------------------------------------------- 1 | % examples.m 2 | % mchiew@fmrib.ox.ac.uk 3 | 4 | % Load example data 5 | input = matfile('data/data.mat'); 6 | truth = input.truth; 7 | calib = input.calib; 8 | 9 | % ============================================================================ 10 | % The R=2 problem 11 | % ============================================================================ 12 | 13 | R = [1,2]; 14 | kernel = [3,4]; 15 | 16 | mask = false(32,96,96); 17 | mask(:,:,1:2:end) = true; 18 | 19 | data = truth.*mask; 20 | 21 | recon = grappa(data, calib, R, kernel); 22 | 23 | show_quad(data, recon, 'R=2'); 24 | 25 | 26 | % ============================================================================ 27 | % The R=3 problem 28 | % ============================================================================ 29 | 30 | R = [1,3]; 31 | kernel = [3,4]; 32 | 33 | mask = false(32,96,96); 34 | mask(:,:,1:3:end) = true; 35 | 36 | data = truth.*mask; 37 | 38 | recon = grappa(data, calib, R, kernel); 39 | 40 | show_quad(data, recon, 'R=3'); 41 | 42 | 43 | % ============================================================================ 44 | % The R=6 problem 45 | % ============================================================================ 46 | 47 | R = [1,6]; 48 | kernel = [3,2]; 49 | 50 | mask = false(32,96,96); 51 | mask(:,:,1:6:end) = true; 52 | 53 | data = truth.*mask; 54 | 55 | recon = grappa(data, calib, R, kernel); 56 | 57 | show_quad(data, recon, 'R=6'); 58 | 59 | 60 | % ============================================================================ 61 | % The noisy R=6 problem 62 | % ============================================================================ 63 | 64 | R = [1,6]; 65 | kernel = [3,2]; 66 | 67 | mask = false(32,96,96); 68 | mask(:,:,1:6:end) = true; 69 | 70 | noise = 1E-6*(randn(size(mask)) + 1j*randn(size(mask))); 71 | data = (truth + noise).*mask; 72 | 73 | recon = grappa(data, calib, R, kernel); 74 | 75 | show_quad(data, recon, 'R=6 with noise'); 76 | -------------------------------------------------------------------------------- /grappa-practical.html: -------------------------------------------------------------------------------- 1 |

GRAPPA Parallel Imaging Tutorial

2 | 3 |

Mark Chiew (mchiew@fmrib.ox.ac.uk)

4 | 5 |

Introduction

6 | 7 |

Parallel imaging, broadly speaking, refers to the use of multi-channel receive coil information to reconstruct under-sampled data. 8 | It is called "parallel" because the set of receive channels all acquire data "in parallel" - i.e., they all record at the same time. 9 | GRAPPA (Griswold et al., MRM 2002) is one of the most popular techniques for performing parallel imaging reconstruction, among many. 10 | Others include SENSE (Pruessmann et al., MRM 1999) and ESPIRiT (Uecker et al., MRM 2014),

11 | 12 |

So, in this tutorial, we'll go over the basics of what GRAPPA does, and how to do it. 13 | What we won't cover is why GRAPPA, or parallel imaging works. I refer you to one of the many review papers on Parallel Imaging.

14 | 15 |

The GRAPPA Problem

16 | 17 |

Problem Definition

18 | 19 |

First, this is an example of the type of problem we are trying to solve:

20 | 21 |
Coil #1 k-space    Coil #2 k-space
 22 | 
 23 | oooooooooooooooo   oooooooooooooooo
 24 | ----------------   ----------------
 25 | oooooooooooooooo   oooooooooooooooo             o : Acquired k-space data point
 26 | ----------------   ----------------             - : Missing k-space data
 27 | oooooooooooooooo   oooooooooooooooo
 28 | ----------------   ----------------
 29 | oooooooooooooooo   oooooooooooooooo
 30 | ----------------   ----------------
 31 | oooooooooooooooo   oooooooooooooooo
 32 | ----------------   ----------------
 33 | oooooooooooooooo   oooooooooooooooo
 34 | ----------------   ----------------
 35 | 
36 | 37 |

The acquisition above is missing every other line (R=2). To generate our output images, we will need to fill in these missing lines. 38 | GRAPPA gives us a set of steps to fill in these missing lines. We do this by using a weighted combination of surrounding points, from all coils.

39 | 40 |

Overview

41 | 42 |

Here's what this practical will help you with:

43 | 44 |
    45 |
  1. Understanding and defining GRAPPA kernel geometries
  2. 46 | 47 |
  3. Learning how to construct the GRAPPA synthesis problem as a linear system
  4. 48 | 49 |
  5. Learning how to estimate the kernel weights needed to perform GRAPPA reconstruction
  6. 50 |
51 | 52 |

Step-by-Step

53 | 54 |

Let's consider one of the missing points we want to reconstruct, from coil 1, denoted X:

55 | 56 |
Coil #1 k-space    Coil #2 k-space
 57 | 
 58 | oooooooooooooooo   oooooooooooooooo
 59 | ----------------   ----------------
 60 | oooooooooooooooo   oooooooooooooooo             o : Acquired k-space data point
 61 | ----------------   ----------------             - : Missing k-space data
 62 | oooooooooooooooo   oooooooooooooooo
 63 | --------X-------   ----------------             X : Reconstruction Target
 64 | oooooooooooooooo   oooooooooooooooo
 65 | ----------------   ----------------
 66 | oooooooooooooooo   oooooooooooooooo
 67 | ----------------   ----------------
 68 | oooooooooooooooo   oooooooooooooooo
 69 | ----------------   ----------------
 70 | 
71 | 72 |

To recover the target point X, we need to choose a local neighbourhood of surrounding acquired points, as well as the "parallel" neighbourhoods of the other coils.

73 | 74 |
Coil #1 k-space    Coil #2 k-space
 75 | 
 76 | oooooooooooooooo   oooooooooooooooo
 77 | ----------------   ----------------
 78 | oooooooooooooooo   oooooooooooooooo             o : Acquired k-space data point
 79 | ----------------   ----------------             - : Missing k-space data
 80 | ooooooo***oooooo   ooooooo***oooooo
 81 | --------X-------   --------Y-------             X : Reconstruction target
 82 | ooooooo***oooooo   ooooooo***oooooo             Y : Reconstruction target position in other coils
 83 | ----------------   ----------------
 84 | oooooooooooooooo   oooooooooooooooo             * : Neighbourhood sources
 85 | ----------------   ----------------
 86 | oooooooooooooooo   oooooooooooooooo
 87 | ----------------   ----------------
 88 | 
89 | 90 |

We chose a neighbourhood, or "kernel" of 3 points in the x-direction, and 2 points in the y-direction, centred on the target position. 91 | These source points come from the same coil as the target point, as well as in the same locations from the other parallel coils. 92 | Zooming in on the target point, and its sources:

93 | 94 |
Coil #1 k-space    Coil #2 k-space
 95 | 
 96 |    *   *   *          *   *   *
 97 |                                                 X : Reconstruction target
 98 |    -   X   -          -   Y   -                 Y : Reconstruction target position in other coils
 99 | 
100 |    *   *   *          *   *   *                 * : Neighbourhood sources
101 | 
102 | 103 |

We can see that there are 3x2=6 source points per coil. This is generally referred to as a 3x2 kernel geometry. 104 | If we label all the source points in this 2-coil example:

105 | 106 |
Coil #1 k-space    Coil #2 k-space
107 | 
108 |    a   b   c          g   h   i
109 |                                                 X : Reconstruction target
110 |    -   X   -          -   Y   -                 Y : Reconstruction target position in other coils
111 | 
112 |    d   e   f          j   k   l                 [a-l] : Source points
113 | 
114 | 115 |

The GRAPPA weighted combination formulation means that:

116 | 117 |
X = wx_a*a + wx_b*b + wx_c*c + ... + wx_l*l;    {Eq. 1a}
118 | 
119 | 120 |

where wx_n refers to the weight for source n, and a-l are the complex k-space data points. 121 | While the kernels are shift-invariant over the entire k-space, they are specific to the target coils.

122 | 123 |

Therefore, to reconstruct target point Y in the second coil, a different set of weights are required:

124 | 125 |
Y = wy_a*a + wy_b*b + wy_c*c + ... + wy_l*l;    {Eq. 1b}
126 | 
127 | 128 |

So, for example, if you have 8 coils, using 3x2 kernel geometries, you will have 8x6 different kernel 129 | weights, where the entire group of weights for all coils is called the kernel or weight set.

130 | 131 |

If we write this as a matrix equation, we can see that:

132 | 133 |
M = W*S;                                        {Eq. 2a}
134 | or
135 | [X]   [wx_a wx_b wx_c ... wx_l] [a]
136 | [Y]   [wy_a wy_b wy_c ... wy_l] [b]
137 |  .  = [           .           ] [c]             {Eq. 2b}
138 |  .    [           .           ] [d]
139 | [Z]   [wz_a wz_b wz_c ... wz_l] [e]
140 |                                 [.]
141 |                                 [.]
142 |                                 [.]
143 |                                 [l]
144 | 
145 | 146 |

where M=(X,Y,...Z)' are the missing points from each coil, W is the kernel weight matrix where each coil's weights comprise one row. 147 | S is a vector of source points - order is not important, but it is critical to ensure it is consistent with the weights.

148 | 149 |

Finally, this expression only holds for a fixed kernel geometry (i.e. source - target geometry). For R > 2 (i.e. R-1 missing lines for each measured line), 150 | there will be R-1 distinct kernel sets.

151 | 152 |

For example, in the case of R=3, you have 2 distinct kernel geometries to work with:

153 | 154 |
Coil #1 k-space    Coil #2 k-space
155 | 
156 | oooooooooooooooo   oooooooooooooooo
157 | ----------------   ----------------
158 | ----------------   ----------------             o : Acquired k-space data point
159 | oooooooooooooooo   oooooooooooooooo             - : Missing k-space data
160 | ----------------   ----------------
161 | ----------------   ----------------             ---                     ooo
162 | oooooooooooooooo   oooooooooooooooo             ooo First kernel        --- Second kernel
163 | -------X--------   ----------------             -X- geometry            -Y- geometry
164 | -------Y--------   ----------------             ---                     ooo
165 | oooooooooooooooo   oooooooooooooooo             ooo                     ---
166 | ----------------   ----------------
167 | ----------------   ----------------
168 | 
169 | 170 |

If you want to generalise this to R > 2, ultimately, for C coils, with kernel size [Nx,Ny], and acceleration factor R, in totality, 171 | you will need to estimate C*(R-1)*C*Nx*Ny weight coefficients. You can solve this as a single comprehensive system, or because the problems are uncoupled, 172 | you may find it easier to solve each (R-1) class of sub-problems separately.

173 | 174 |

So Eq. 2 completely describes how you solve for missing points, given acquired data in some neighbourhood around it.

175 | 176 |

The one final piece of information we need is fully sampled "calibration" or "training" data, so that we can actually find the kernel weights. 177 | To do this, we simply solve Eq. 2 for the weights, typically in a least-squares sense (no pun intended), over the calibration data. 178 | In this case, M and S are both matrices, containing known information, representing source-target relationships across the entire calibration region.

179 | 180 |

This is what "fitting the kernel" refers to:

181 | 182 |
W = M*pinv(S);                                  {Eq. 3a}
183 | or
184 | W = M*S'*(S*S')^-1;                             {Eq. 3a}
185 | 
186 | 187 |

To ensure a robust and well-conditioned fit, typically a relatively large calibration region is used fit W. 188 | Over the calibration data, nearly every point is a potential source and target. Because all the points are present, 189 | we can use this data to learn the shift-invariant geometric relationships in the k-space x coil data.

190 | 191 |
Coil #1 k-space    Coil #2 k-space
192 | 
193 | oooooooooooooooo   oooooooooooooooo
194 | oooooooooooooooo   oooooooooooooooo
195 | oooooooooooooooo   oooooooooooooooo             o : Acquired k-space calibration data
196 | oooooooooooooooo   oooooooooooooooo             
197 | oooooooooooooooo   oooooooooooooooo
198 | oooooooooooooooo   oooooooooooooooo
199 | oooooooooooooooo   oooooooooooooooo
200 | oooooooooooooooo   oooooooooooooooo
201 | oooooooooooooooo   oooooooooooooooo
202 | oooooooooooooooo   oooooooooooooooo
203 | oooooooooooooooo   oooooooooooooooo
204 | oooooooooooooooo   oooooooooooooooo
205 | 
206 | 207 |

So to solve Eq. 3, we "move" the kernel over the entire calibration space, and for every source-target pairing, we get an additional 208 | column in M and S:

209 | 210 |
Coil #1 k-space    Coil #2 k-space
211 | 
212 |    a1  b1  c1  -      g1  h1  i1  -             [X1]   [wx_a wx_b wx_c ... wx_l] [a1]
213 |                                                 [Y1]   [wy_a wy_b wy_c ... wy_l] [b1]
214 |    -   X1  -   -      -   Y1  -   -             [ .] = [           .           ] [c1]            
215 |                                                 [ .]   [           .           ] [d1]
216 |    d1  e1  f1  -      j1  k1  l1  -             [Z1]   [wz_a wz_b wz_c ... wz_l] [e1]
217 |                                                                                  [ .] 
218 |    -   -   -   -      -   -   -   -                                              [ .]      
219 |                                                                                  [l1]
220 | 
221 | Coil #1 k-space    Coil #2 k-space
222 | 
223 |    -   a2  b2  c2     -   g2  h2  i2            [X1 X2]   [wx_a wx_b wx_c ... wx_l] [a1 a2]
224 |                                                 [Y1 Y2]   [wy_a wy_b wy_c ... wy_l] [b1 b2]
225 |    -   -   X2  -      -   -   Y2  -             [ .  .] = [           .           ] [c1 c2]            
226 |                                                 [ .  .]   [           .           ] [d1 d2]
227 |    -   d2  e2  f2     -   j2  k2  l2            [Z1 Z2]   [wz_a wz_b wz_c ... wz_l] [e1 e2]
228 |                                                                                     [ .  .]
229 |    -   -   -   -      -   -   -   -                                                 [ .  .]
230 |                                                                                     [l1 l2]
231 | 
232 | Coil #1 k-space    Coil #2 k-space
233 | 
234 |    -   -   -   -      -   -   -   -             [X1 X2 ... Xn]   [wx_a wx_b wx_c ... wx_l] [a1 a2 ... an]
235 |                                                 [Y1 Y2 ... Yn]   [wy_a wy_b wy_c ... wy_l] [b1 b2 ... bn]
236 |    -   an  bn  cn     -   gn  hn  in            [ .  . ...  .] = [           .           ] [c1 c2 ... cn]            
237 |                                                 [ .  . ...  .]   [           .           ] [d1 d2 ... dn]
238 |    -   -   Xn  -      -   -   Yn  -             [Z1 Z2 ... Zn]   [wz_a wz_b wz_c ... wz_l] [e1 e2 ... en]
239 |                                                                                            [ .  . ...  .]
240 |    -   dn  en  fn     -   jn  kn  ln                                                       [ .  . ...  .]
241 |                                                                                            [l1 l2 ... ln]
242 | 
243 | 244 |

Now that you have M and S fully populated, you can solve Eq. 3 in the least squares sense, by pseudo-inverting S:

245 | 246 |
   [wx_a wx_b wx_c ... wx_l]    [X1 X2 ... Xn]       [a1 a2 ... an]
247 |    [wy_a wy_b wy_c ... wy_l]    [Y1 Y2 ... Yn]       [b1 b2 ... bn]
248 |    [           .           ] =  [ .  . ...  .] *pinv([c1 c2 ... cn])    Eq. {4}
249 |    [           .           ]    [ .  . ...  .]       [d1 d2 ... dn]
250 |    [wz_a wz_b wz_c ... wz_l]    [Z1 Z2 ... Zn]       [e1 e2 ... en]
251 |                                                      [ .  . ...  .]
252 |                                                      [ .  . ...  .]
253 |                                                      [l1 l2 ... ln]
254 | 
255 | 256 |

Summary

257 | 258 |

Ultimately, the entire GRAPPA algorithm boils down to:

259 | 260 |
    261 |
  1. Choosing desired kernel geometries
  2. 262 | 263 |
  3. Solving Eq. 3 to fit for W over the calibration data
  4. 264 | 265 |
  5. Applying Eq. 2 to solve for M, using the calibrated W, over the actual under-sampled data
  6. 266 |
267 | 268 |

Practical

269 | 270 |

Now that you have a basic sense of the internal logic behind GRAPPA (the what), we'll get into a step-by-step practical on 271 | how to actually go through the mechanics of writing a GRAPPA-based image reconstruction program (the how).

272 | 273 |

I've included skeleton code for each step, that you're free to use if you like. I've also included a full working step-by-step solution if 274 | you get stuck on any step. Use as much or as little of the provided solution as you like.

275 | 276 |

At the end of the tutorial, I'll just briefly walk through my solution code.

277 | 278 |

Step 1 - Organising the reconstruction code

279 | 280 |

Source file: grappa.m

281 | 282 |

We'll use grappa.m as our main function that takes in undersampled data and returns reconstructed data. 283 | We'll also be defining separate functions for most of the other steps, and the provided grappa.m file is already organised this way for you. 284 | I recommend you use the provided grappa.m file.

285 | 286 |

Step 2 - Pad data to deal with kernels applied at k-space boundaries

287 | 288 |

Source files: grappa_get_pad.m, grappa_pad_data.m, grappa_unpad_data.m

289 | 290 |

We need to pad the k-space data that we're working with in order to accommodate kernels being applied at the boundary of the actual data. 291 | Because the kernels extend for some width beyond the target point, if the reconstruction target is at the edge, the kernel will necessarily 292 | need to grab data from beyond.

293 | 294 |

This is organised into 2 parts: 295 | grappa_get_pad.m should return you the size of padding needed in each dimension given your kernel size and under-sampling factor 296 | grappa_pad_data.m should perform the padding operation 297 | grappa_unpad_data.m should perform the un-padding operation

298 | 299 |

Step 3 - Compute relative indices for source points relative to target

300 | 301 |

Source file: grappa_get_indices.m

302 | 303 |

This is in my opinion the trickiest part of the practical GRAPPA problem. In order to perform weight estimation and application, you 304 | will need to be able to know what the co-ordinates are of every source point relative to its target point. It's not difficult to picture 305 | in your head, but making sure you've got your indexing correct is pretty crucial.

306 | 307 |

You will need to return an array of source indices, where each column is paired with the corresponding element in the target index vector. 308 | I use linear indexing for this (i.e., I index the 3D data arrays from 1 to CNxNy linearly, instead of using subscripts).

309 | 310 |

I strongly recommend simply using the provided solution, unless you're feeling particularly keen. In my solution, for simplicity, I require that 311 | the kernel size be odd in the kx-direction, and even in the ky-direction.

312 | 313 |

Step 4 - Perform weight estimation

314 | 315 |

Source file: grappa_estimate_weights.m

316 | 317 |

Given paired set of source and target indices, you must now use those to collect corresponding dataset pairs from the calibration k-space 318 | in order to perform weight estimation. Don't forget that the weights must map data from all coils to the target points in each coil.

319 | 320 |

You can do whatever you like here, if you know what you're doing. Otherwise, I recommend you perform a least squares fit. 321 | The easiest way to do that, is to recognise that this is simply a linear regression problem as laid out above, and use the pseudoinverse (pinv in MATLAB) 322 | Eq. {4} is basically what you should get.

323 | 324 |

The way I have structured my solution is to estimate weights for each of the R-1 missing line groups (or kernel geometries) separately. 325 | This makes the organisation a bit simpler, and is mathematically equivalent to solving for all weight sets at once. Feel free to try to implement 326 | the all-in-one approach if you have extra time.

327 | 328 |

Step 5 - Apply weights to reconstruct missing data

329 | 330 |

Source file: grappa_apply_weights.m

331 | 332 |

Finally, the weights estimated need to be used to reconstruct missing data. Here you also need to identify source and target indices from the 333 | actual under-sampled data, and then use the weights you derive to solve the reconstruction problem (Eq. {2}).

334 | 335 |

Again, depending on whether you separated the weight estimation into R-1 subproblems or one big problem, you will need to either loop over 336 | all your subproblems to get the final reconstruction, or simply apply your weights to all missing points at once.

337 | 338 |

Step 6 - Test Reconstructions

339 | 340 |

Source file: example.m

341 | 342 |

To evaluate your reconstruction, I have provided a simple script and some data that perform some toy under-sampling problems. 343 | If you've implemented grappa.m and everything else correctly, this should run and give you the expected outputs.

344 | 345 |

Bonus Steps

346 | 347 |

Using this exact framework, it is relatively simple to perform the following extensions: 348 | (I have code for these, they're all in some form or another on psg.fmrib.ox.ac.uk/u/mchiew/projects)

349 | 350 | -------------------------------------------------------------------------------- /grappa.m: -------------------------------------------------------------------------------- 1 | % grappa.m 2 | % mchiew@fmrib.ox.ac.uk 3 | % 4 | % inputs: 5 | % data - (c, nx, ny) complex undersampled k-space data 6 | % calib - (c, cx, cy) complex calibration k-space data 7 | % R - [Rx, Ry] 1x2 array or just Ry scalar 8 | % kernel - [kx, ky] kernel size (kx odd, ky even) 9 | % 10 | % output: 11 | % recon - (c, nx, ny) complex reconstructed k-space data 12 | 13 | function recon = grappa(data, calib, R, kernel) 14 | 15 | % Check if R is scalar and set Rx=1 16 | if isscalar(R) 17 | R = [1, R]; 18 | end 19 | 20 | %% Pad data to deal with kernels applied at k-space boundaries 21 | pad = grappa_get_pad_size(kernel, R); 22 | pdata = grappa_pad_data(data, pad); 23 | 24 | % Define and pad the sampling mask, squeeze it because we don't need the coil dimension 25 | mask = grappa_pad_data(data~=0, pad); 26 | 27 | %% Loop over R-1 different kernel types 28 | for type = 1:R(2)-1 29 | %% Collect source and target calibration points for weight estimation 30 | [src_calib, trg_calib] = grappa_get_indices(kernel, true(size(calib)), pad, R, type); 31 | 32 | %% Perform weight estimation 33 | weights = grappa_estimate_weights(calib, src_calib, trg_calib); 34 | 35 | %% Collect source points in under-sampled data for weight application 36 | [src, trg] = grappa_get_indices(kernel, circshift(mask,type,3), pad, R, type); 37 | 38 | %% Apply weights to reconstruct missing data 39 | pdata = grappa_apply_weights(pdata, weights, src, trg); 40 | end 41 | 42 | %% Un-pad reconstruction to get original image size back 43 | recon = grappa_unpad_data(pdata, pad); 44 | -------------------------------------------------------------------------------- /grappa_apply_weights.m: -------------------------------------------------------------------------------- 1 | % grappa_apply_weights.m 2 | % mchiew@fmrib.ox.ac.uk 3 | % 4 | % inputs: 5 | % data - (c, kx, ky) complex undersampled k-space data 6 | % weights - kernel weights 7 | % src_idx - source point indices 8 | % trg_idx - target point indices 9 | % 10 | % output: 11 | % data - (c, kx, ky) complex reconstructed k-space data 12 | 13 | function data = grappa_apply_weights(data, weights, src_idx, trg_idx) 14 | 15 | % Collect source and target points based on provided indices 16 | 17 | % Apply weights and insert synthesized target points into data 18 | -------------------------------------------------------------------------------- /grappa_estimate_weights.m: -------------------------------------------------------------------------------- 1 | % grappa_estimate_weights.m 2 | % mchiew@fmrib.ox.ac.uk 3 | % 4 | % inputs: 5 | % calib - (c, kx, ky) complex k-space data 6 | % src_idx - source point indices 7 | % trg_idx - target point indices 8 | % 9 | % output: 10 | % weights - kernel weights 11 | 12 | function weights = grappa_estimate_weights(calib, src_idx, trg_idx) 13 | 14 | % Collect source and target points based on provided indices 15 | 16 | % Least squares fit for weights 17 | % Hint: Use pinv 18 | -------------------------------------------------------------------------------- /grappa_get_indices.m: -------------------------------------------------------------------------------- 1 | % grappa_get_indices.m 2 | % mchiew@fmrib.ox.ac.uk 3 | % 4 | % inputs: 5 | % kernel - [sx, sy] kernel size in each dimension 6 | % samp - (c, nx, ny) sampling mask 7 | % pad - [pad_x, pad_y] size of padding in each direction 8 | % type - (scalar, must be < R) indicates which of the R-1 kernels 9 | % you are trying to index over 10 | % 11 | % output: 12 | % src - linear indices for all source points (c*sx*sy, all possible targets) 13 | % trg - linear indices for all the target points (c, all possible targets) 14 | 15 | function [src, trg] = grappa_get_indices(kernel, samp, pad, R, type) 16 | 17 | % Get dimensions 18 | dims = size(samp); 19 | 20 | % Make sure the under-sampling is in y-only 21 | % There are a few things here that require that assumption 22 | if R(1) > 1 23 | error('x-direction must be fully sampled'); 24 | end 25 | 26 | % Make sure the kernel is odd in x, and even in y 27 | if mod(kernel(1),2)==0 || mod(kernel(2),2)==1 28 | error('Kernel geometry is not allowed'); 29 | end 30 | 31 | % Make sure the type parameter makes sense 32 | % It should be between 1 and R-1 (inclusive) 33 | if type < 1 || type > R(2)-1 34 | error('Type parameter is inconsistent with R'); 35 | end 36 | 37 | % To get absolute kernel distances, multiply kernel and R 38 | kernel = kernel.*R; 39 | 40 | % Find the limits of all possible target points given padding 41 | kx = 1+pad(1):dims(2)-pad(1); 42 | ky = 1+pad(2):dims(3)-pad(2); 43 | 44 | %% Compute indices for a single coil 45 | 46 | % Find relative indices for kernel source points 47 | mask = false(dims(2:3)); 48 | mask(1:R(1):kernel(1), 1:R(2):kernel(2)) = true; 49 | k_idx = find(mask); 50 | 51 | % Find the index for the desired target point (depends on type parameter) 52 | % To simply things, we require than kernel size in x is odd 53 | % and that kernel size in y is even 54 | mask = false(dims(2:3)); 55 | mask((kernel(1)+1)/2, (kernel(2)/2-R(2)+1)+type) = true; 56 | k_trg = find(mask); 57 | 58 | % Subtract the target index from source indices 59 | % to get relative linear indices for all source points 60 | % relative to the target point (index 0, target position) 61 | k_idx = k_idx - k_trg; 62 | 63 | % Find all possible target indices 64 | mask = false(dims(2:3)); 65 | mask(kx,ky) = squeeze(samp(1,kx,ky)); 66 | trg = find(mask); 67 | 68 | % Find all source indices associated with the target points in trg 69 | src = bsxfun(@plus, k_idx, trg'); 70 | 71 | %% Now replicate indexing over all coils 72 | 73 | % Final shape of trg should be (#coils, all possible target points) 74 | trg = (trg'-1)*dims(1)+1; 75 | trg = bsxfun(@plus, trg, (0:dims(1)-1)'); 76 | 77 | % Final shape of src should be (#coils*sx*sy, all possible target points) 78 | src = (src-1)*dims(1)+1; 79 | src = bsxfun(@plus, src(:)', (0:dims(1)-1)'); 80 | src = reshape(src,[], size(trg,2)); 81 | 82 | 83 | %% Note: You can achieve the same outcome, although much slower, by 84 | %% using nested for-loops, and zooming over all of the k-space target 85 | %% points, identifying the indices for each target point and its 86 | %% corresponding source points 87 | -------------------------------------------------------------------------------- /grappa_get_pad_size.m: -------------------------------------------------------------------------------- 1 | % grappa_get_pad_size.m 2 | % mchiew@fmrib.ox.ac.uk 3 | % 4 | % inputs: 5 | % kernel - [sx, sy] kernel size in each dimension 6 | % R - [Rx, Ry] undersampling factors 7 | % 8 | % output: 9 | % pad - [pad_x, pad_y] size of padding in each direction 10 | 11 | function pad = grappa_get_pad_size(kernel, R) 12 | 13 | % Compute size of padding needed in each direction 14 | -------------------------------------------------------------------------------- /grappa_pad_data.m: -------------------------------------------------------------------------------- 1 | % grappa_pad_data.m 2 | % mchiew@fmrib.ox.ac.uk 3 | % 4 | % inputs: 5 | % data - (c, kx, ky) complex k-space data 6 | % pad - [pad_x, pad_y] size of padding in each direction 7 | % 8 | % output: 9 | % pdata - (c, kx+pad, ky+pad) complex padded k-space data 10 | 11 | function pdata = grappa_pad_data(data, pad) 12 | 13 | % Zero-Pad 14 | % Apply zero-padding to kx, ky directions (don't pad coil dimension) 15 | % Hint: use padarray 16 | 17 | % Cyclic-Pad 18 | % Here we additionally copy data so that k-space has cyclic boundary conditions 19 | % This isn't absolutely necessary - if you like, just leave it zero-padded 20 | % First pad left boundary 21 | 22 | % Next pad right boundary 23 | 24 | % Third, pad top boundary 25 | 26 | % Finally, pad bottom boundary 27 | -------------------------------------------------------------------------------- /grappa_unpad_data.m: -------------------------------------------------------------------------------- 1 | % grappa_unpad_data.m 2 | % mchiew@fmrib.ox.ac.uk 3 | % 4 | % inputs: 5 | % pdata - (c, kx+pad_x, ky+pad_y) complex padded k-space data 6 | % pad - [pad_x, pad_y] size of padding in each direction 7 | % 8 | % output: 9 | % data - (c, kx, ky) complex k-space data 10 | 11 | function data = grappa_unpad_data(pdata, pad) 12 | 13 | % Subselect inner data absent the padded points 14 | -------------------------------------------------------------------------------- /show_quad.m: -------------------------------------------------------------------------------- 1 | % show_quad.m 2 | % mchiew@fmrib.ox.ac.uk 3 | % 4 | % inputs: 5 | % X,Y - (c, kx, ky) k-space data 6 | 7 | function show_quad(X, Y, text) 8 | 9 | figure(); 10 | subplot(2,2,1); 11 | imshow(squeeze(log10(abs(X(1,:,:))))',[]); 12 | set(gca,'YDir', 'Normal'); 13 | title('Undersampled k-space coil #1'); 14 | 15 | subplot(2,2,2); 16 | imshow(squeeze(log10(abs(Y(1,:,:))))',[]); 17 | set(gca,'YDir', 'Normal'); 18 | title('Reconstructed k-space coil #1'); 19 | 20 | subplot(2,2,3); 21 | imshow(squeeze(sum(abs(ifftdim(X,2:3)).^2,1).^0.5)',[]); 22 | set(gca,'YDir', 'Normal'); 23 | title('Undersampled Image'); 24 | 25 | subplot(2,2,4); 26 | imshow(squeeze(sum(abs(ifftdim(Y,2:3)).^2,1).^0.5)',[]); 27 | set(gca,'YDir', 'Normal'); 28 | title('Reconstructed Image'); 29 | 30 | annotation('textbox', [0 0.9 1 0.1], ... 31 | 'String', text, ... 32 | 'EdgeColor', 'none', ... 33 | 'HorizontalAlignment', 'center', ... 34 | 'FontSize', 16); 35 | 36 | function M = fftdim(M, dim) 37 | for i = dim 38 | M = fftshift(fft(ifftshift(M, i), [], i), i)/sqrt(size(M,i)); 39 | end 40 | 41 | function M = ifftdim(M, dim) 42 | for i = dim 43 | M = fftshift(ifft(ifftshift(M, i), [], i), i)*sqrt(size(M,i)); 44 | end 45 | -------------------------------------------------------------------------------- /solution/grappa_apply_weights.m: -------------------------------------------------------------------------------- 1 | % grappa_apply_weights.m 2 | % mchiew@fmrib.ox.ac.uk 3 | % 4 | % inputs: 5 | % data - (c, kx, ky) complex undersampled k-space data 6 | % weights - kernel weights 7 | % src_idx - source point indices 8 | % trg_idx - target point indices 9 | % 10 | % output: 11 | % data - (c, kx, ky) complex reconstructed k-space data 12 | 13 | function data = grappa_apply_weights(data, weights, src_idx, trg_idx) 14 | 15 | % Collect source and target points based on provided indices 16 | src = data(src_idx); 17 | 18 | % Apply weights and insert synthesized target points into data 19 | data(trg_idx) = weights*src; 20 | -------------------------------------------------------------------------------- /solution/grappa_estimate_weights.m: -------------------------------------------------------------------------------- 1 | % grappa_estimate_weights.m 2 | % mchiew@fmrib.ox.ac.uk 3 | % 4 | % inputs: 5 | % calib - (c, kx, ky) complex k-space data 6 | % src_idx - source point indices 7 | % trg_idx - target point indices 8 | % lambda - {OPTIONAL} Tikhonov Regularisation Parameter 9 | % 10 | % output: 11 | % weights - kernel weights 12 | 13 | function weights = grappa_estimate_weights(calib, src_idx, trg_idx, lambda) 14 | 15 | % If no lambda provided, don't regularise 16 | if nargin < 4 17 | lambda = 0; 18 | end 19 | 20 | % Collect source and target points based on provided indices 21 | src = calib(src_idx); 22 | trg = calib(trg_idx); 23 | 24 | % Least squares fit for weights 25 | %weights = trg*pinv(src); 26 | weights = trg*src'*inv(src*src' + norm(src)*lambda*eye(size(src,1))); 27 | -------------------------------------------------------------------------------- /solution/grappa_get_indices.m: -------------------------------------------------------------------------------- 1 | % grappa_get_indices.m 2 | % mchiew@fmrib.ox.ac.uk 3 | % 4 | % inputs: 5 | % kernel - [sx, sy] kernel size in each dimension 6 | % samp - (c, nx, ny) sampling mask 7 | % pad - [pad_x, pad_y] size of padding in each direction 8 | % type - (scalar, must be < R) indicates which of the R-1 kernels 9 | % you are trying to index over 10 | % 11 | % output: 12 | % src - linear indices for all source points (c*sx*sy, all possible targets) 13 | % trg - linear indices for all the target points (c, all possible targets) 14 | 15 | function [src, trg] = grappa_get_indices(kernel, samp, pad, R, type) 16 | 17 | % Get dimensions 18 | dims = size(samp); 19 | 20 | % Make sure the under-sampling is in y-only 21 | % There are a few things here that require that assumption 22 | if R(1) > 1 23 | error('x-direction must be fully sampled'); 24 | end 25 | 26 | % Make sure the kernel is odd in x, and even in y 27 | if mod(kernel(1),2)==0 || mod(kernel(2),2)==1 28 | error('Kernel geometry is not allowed'); 29 | end 30 | 31 | % Make sure the type parameter makes sense 32 | % It should be between 1 and R-1 (inclusive) 33 | if type < 1 || type > R(2)-1 34 | error('Type parameter is inconsistent with R'); 35 | end 36 | 37 | % To get absolute kernel distances, multiply kernel and R 38 | kernel = kernel.*R; 39 | 40 | % Find the limits of all possible target points given padding 41 | kx = 1+pad(1):dims(2)-pad(1); 42 | ky = 1+pad(2):dims(3)-pad(2); 43 | 44 | %% Compute indices for a single coil 45 | 46 | % Find relative indices for kernel source points 47 | mask = false(dims(2:3)); 48 | mask(1:R(1):kernel(1), 1:R(2):kernel(2)) = true; 49 | k_idx = find(mask); 50 | 51 | % Find the index for the desired target point (depends on type parameter) 52 | % To simply things, we require than kernel size in x is odd 53 | % and that kernel size in y is even 54 | mask = false(dims(2:3)); 55 | mask((kernel(1)+1)/2, (kernel(2)/2-R(2)+1)+type) = true; 56 | k_trg = find(mask); 57 | 58 | % Subtract the target index from source indices 59 | % to get relative linear indices for all source points 60 | % relative to the target point (index 0, target position) 61 | k_idx = k_idx - k_trg; 62 | 63 | % Find all possible target indices 64 | mask = false(dims(2:3)); 65 | mask(kx,ky) = squeeze(samp(1,kx,ky)); 66 | trg = find(mask); 67 | 68 | % Find all source indices associated with the target points in trg 69 | src = bsxfun(@plus, k_idx, trg'); 70 | 71 | %% Now replicate indexing over all coils 72 | 73 | % Final shape of trg should be (#coils, all possible target points) 74 | trg = (trg'-1)*dims(1)+1; 75 | trg = bsxfun(@plus, trg, (0:dims(1)-1)'); 76 | 77 | % Final shape of src should be (#coils*sx*sy, all possible target points) 78 | src = (src-1)*dims(1)+1; 79 | src = bsxfun(@plus, src(:)', (0:dims(1)-1)'); 80 | src = reshape(src,[], size(trg,2)); 81 | -------------------------------------------------------------------------------- /solution/grappa_get_pad_size.m: -------------------------------------------------------------------------------- 1 | % grappa_get_pad_size.m 2 | % mchiew@fmrib.ox.ac.uk 3 | % 4 | % inputs: 5 | % kernel - [sx, sy] kernel size in each dimension 6 | % R - [Rx, Ry] undersampling factors 7 | % 8 | % output: 9 | % pad - [pad_x, pad_y] size of padding in each direction 10 | 11 | function pad = grappa_get_pad_size(kernel, R) 12 | 13 | % Compute size of padding needed in each direction 14 | pad = floor(R.*kernel/2); 15 | -------------------------------------------------------------------------------- /solution/grappa_pad_data.m: -------------------------------------------------------------------------------- 1 | % grappa_pad_data.m 2 | % mchiew@fmrib.ox.ac.uk 3 | % 4 | % inputs: 5 | % data - (c, kx, ky) complex k-space data 6 | % pad - [pad_x, pad_y] size of padding in each direction 7 | % 8 | % output: 9 | % pdata - (c, kx+pad, ky+pad) complex padded k-space data 10 | 11 | function pdata = grappa_pad_data(data, pad) 12 | 13 | % Zero-Pad 14 | % Apply zero-padding to kx, ky directions (don't pad coil dimension) 15 | pdata = padarray(data, [0 pad]); 16 | 17 | % Cyclic-Pad 18 | % Here we additionally copy data so that k-space has cyclic boundary conditions 19 | % This isn't absolutely necessary - if you like, just leave it zero-padded 20 | % First pad left boundary 21 | pdata(:, 1:pad(1),:) = pdata(:,1+size(pdata,2)-2*pad(1):size(pdata,2)-pad(1),:); 22 | 23 | % Next pad right boundary 24 | pdata(:, size(pdata,2)-pad(1)+1:size(pdata,2),:) = pdata(:,pad(1)+1:2*pad(1),:); 25 | 26 | % Third, pad top boundary 27 | pdata(:,:,1:pad(2)) = pdata(:,:,1+size(pdata,3)-2*pad(2):size(pdata,3)-pad(2)); 28 | 29 | % Finally, pad bottom boundary 30 | pdata(:,:,size(pdata,3)-pad(2)+1:size(pdata,3)) = pdata(:,:,pad(2)+1:2*pad(2)); 31 | -------------------------------------------------------------------------------- /solution/grappa_unpad_data.m: -------------------------------------------------------------------------------- 1 | % grappa_unpad_data.m 2 | % mchiew@fmrib.ox.ac.uk 3 | % 4 | % inputs: 5 | % pdata - (c, kx+pad_x, ky+pad_y) complex padded k-space data 6 | % pad - [pad_x, pad_y] size of padding in each direction 7 | % 8 | % output: 9 | % data - (c, kx, ky) complex k-space data 10 | 11 | function data = grappa_unpad_data(pdata, pad) 12 | 13 | % Subselect inner data absent the padded points 14 | data = pdata(:,pad(1)+1:size(pdata,2)-pad(1), pad(2)+1:size(pdata,3)-pad(2)); 15 | --------------------------------------------------------------------------------