Commit c131e3e7 authored by Paul McCarthy's avatar Paul McCarthy 🚵
Browse files

Merge branch 'master' into 'master'

Made MATLAB consistent with python, added plotting to partial fourier

See merge request fsl/pytreat-practicals-2020!27
parents 1c0fd89d a47d89cc
......@@ -18,7 +18,7 @@ subplot(2,1,2);
plot(pulseq(:,3));
ylabel('Gradient');
% Integrate ODE
%% Integrate ODE
T1 = 1500;
T2 = 50;
t0 = 0;
......@@ -27,20 +27,20 @@ dt = 0.005;
M0 = [0; 0; 1];
[t, M] = ode45(@(t,M)bloch_ode(t, M, T1, T2), linspace(t0, t1, (t1-t0)/dt), M0);
% Plot Results
%% Plot Results
% create figure
figure();hold on;
% plot x, y and z components of Magnetisation
plot(t, M(:,1));
plot(t, M(:,2));
plot(t, M(:,3));
plot(t, M(:,1), 'linewidth', 2);
plot(t, M(:,2), 'linewidth', 2);
plot(t, M(:,3), 'linewidth', 2);
% add legend and grid
legend({'Mx','My','Mz'});
grid on;
% define the bloch equation
%% define the bloch equation
function dM = bloch_ode(t, M, T1, T2)
% get effective B-field at time t
B = B_eff(t);
......@@ -51,7 +51,7 @@ function dM = bloch_ode(t, M, T1, T2)
M(1)*B(2) - M(2)*B(1) - (M(3)-1)/T1];
end
% define effective B-field
%% define effective B-field
function b = B_eff(t)
% Do nothing for 0.25 ms
if t < 0.25
......
......@@ -64,7 +64,8 @@
"In this section:\n",
"- np.random.randn\n",
"- np.fft\n",
"- 0-based indexing"
"- 0-based indexing\n",
"- image plotting"
]
},
{
......@@ -80,7 +81,11 @@
"y = np.fft.fftshift(np.fft.fft2(img), axes=0) + n\n",
"\n",
"# set bottom 24/96 lines to 0\n",
"y[72:,:] = 0"
"y[72:,:] = 0\n",
"\n",
"# show sampling\n",
"_, ax = plt.subplots()\n",
"ax.imshow(np.log(np.abs(np.fft.fftshift(y, axes=1))))"
]
},
{
......@@ -96,7 +101,8 @@
"In this section:\n",
"- np.pad\n",
"- np.hanning\n",
"- reshaping 1D array to 2D array using np.newaxis (or None)"
"- reshaping 1D array to 2D array using np.newaxis (or None)\n",
"- subplots with titles"
]
},
{
......@@ -117,7 +123,14 @@
"low = np.fft.ifft2(np.fft.ifftshift(y*filt, axes=0))\n",
"\n",
"# get phase image\n",
"phs = np.exp(1j*np.angle(low))"
"phs = np.exp(1j*np.angle(low))\n",
"\n",
"# show phase estimate alongside true phase\n",
"_, ax = plt.subplots(1,2)\n",
"ax[0].imshow(np.angle(img))\n",
"ax[0].set_title('True image phase')\n",
"ax[1].imshow(np.angle(phs))\n",
"ax[1].set_title('Estimated phase')"
]
},
{
......@@ -190,7 +203,7 @@
"\n",
"In this section:\n",
"- print formatted strings to standard output\n",
"- plotting, with min/max scales\n",
"- 2D subplots with min/max scales, figure size\n",
"- np.sum sums over all elements by default"
]
},
......@@ -212,15 +225,17 @@
"print(f'RMSE for POCS recon: {err_pocs}')\n",
"\n",
"# plot both recons side-by-side\n",
"_, ax = plt.subplots(1,2,figsize=(16,16))\n",
"_, ax = plt.subplots(2,2,figsize=(16,16))\n",
"\n",
"# plot zero-filled\n",
"ax[0].imshow(np.abs(zf), vmin=0, vmax=1)\n",
"ax[0].set_title('Zero-filled')\n",
"ax[0,0].imshow(np.abs(zf), vmin=0, vmax=1)\n",
"ax[0,0].set_title('Zero-filled')\n",
"ax[1,0].plot(np.abs(zf[:,47]))\n",
"\n",
"# plot POCS\n",
"ax[1].imshow(est, vmin=0, vmax=1)\n",
"ax[1].set_title('POCS recon')"
"ax[0,1].imshow(est, vmin=0, vmax=1)\n",
"ax[0,1].set_title('POCS recon')\n",
"ax[1,1].plot(np.abs(est[:,47]))"
]
}
],
......
%% Cell type:markdown id: tags:
Imports
%% Cell type:code id: tags:
``` python
import h5py
import matplotlib.pyplot as plt
import numpy as np
```
%% Cell type:markdown id: tags:
# Load data
Load complex image data from MATLAB mat-file (v7.3 or later), which is actually an HDF5 format
Complex data is loaded as a (real, imag) tuple, so it neds to be explicitly converted to complex double
In this section:
- using h5py module
- np.transpose
- 1j as imaginary constant
%% Cell type:code id: tags:
``` python
# get hdf5 object for the mat-file
h = h5py.File('data.mat','r')
# get img variable from the mat-file
dat = h.get('img')
# turn array of (real, imag) tuples into an array of complex doubles
# transpose to keep data in same orientation as MATLAB
img = np.transpose(dat['real'] + 1j*dat['imag'])
```
%% Cell type:markdown id: tags:
# 6/8 Partial Fourier sampling
Fourier transform the image to get k-space data, and add complex Gaussian noise
To simulate 6/8 Partial Fourier sampling, zero out the bottom 1/4 of k-space
In this section:
- np.random.randn
- np.fft
- 0-based indexing
- image plotting
%% Cell type:code id: tags:
``` python
# generate normally-distributed complex noise
n = np.random.randn(96,96) + 1j*np.random.randn(96,96)
# Fourier transform the image and add noise
y = np.fft.fftshift(np.fft.fft2(img), axes=0) + n
# set bottom 24/96 lines to 0
y[72:,:] = 0
# show sampling
_, ax = plt.subplots()
ax.imshow(np.log(np.abs(np.fft.fftshift(y, axes=1))))
```
%% Cell type:markdown id: tags:
# Estimate phase
Filter the k-space data and extract a low-resolution phase estimate
Filtering can help reduce ringing in the phase image
In this section:
- np.pad
- np.hanning
- reshaping 1D array to 2D array using np.newaxis (or None)
- subplots with titles
%% Cell type:code id: tags:
``` python
# create zero-padded hanning filter for ky-filtering
filt = np.pad(np.hanning(48),24,'constant')
# reshape 1D array into 2D array
filt = filt[:,np.newaxis]
# or
# filt = filt[:,None]
# generate low-res image with inverse Fourier transform
low = np.fft.ifft2(np.fft.ifftshift(y*filt, axes=0))
# get phase image
phs = np.exp(1j*np.angle(low))
# show phase estimate alongside true phase
_, ax = plt.subplots(1,2)
ax[0].imshow(np.angle(img))
ax[0].set_title('True image phase')
ax[1].imshow(np.angle(phs))
ax[1].set_title('Estimated phase')
```
%% Cell type:markdown id: tags:
# POCS reconstruction
Perform the projection-onto-convex-sets (POCS) partial Fourier reconstruction method.
POCS is an iterative scheme estimates the reconstructed image as any element in the intersection of the following two (convex) sets:
1. Set of images consistent with the measured data
2. Set of images that are non-negative real
This requires prior knowledge of the image phase (hence the estimate above), and it works because although we have less than a full k-space of measurements, we're now only estimating half the number of free parameters (real values only, instead of real + imag), and we're no longer under-determined. Equivalently, consider the fact that real-valued images have conjugate symmetric k-spaces, so we only require half of k-space to reconstruct our image.
In this section:
- np.zeros
- range() builtin
- point-wise multiplication (*)
- np.fft operations default to last axis, not first
- np.maximum vs np.max
%% Cell type:code id: tags:
``` python
# initialise image estimate to be zeros
est = np.zeros((96,96))
# set the number of iterations
iters = 10
# each iteration cycles between projections
for i in range(iters):
# projection onto data-consistent set:
# use a-priori phase to get complex image
est = est*phs
# Fourier transform to get k-space
est = np.fft.fftshift(np.fft.fft2(est), axes=0)
# replace data with measured lines
est[:72,:] = y[:72,:]
# inverse Fourier transform to get back to image space
est = np.fft.ifft2(np.fft.ifftshift(est, axes=0))
# projection onto non-negative reals:
# remove a-priori phase
est = est*np.conj(phs)
# get real part
est = np.real(est)
# ensure output is non-negative
est = np.maximum(est, 0)
```
%% Cell type:markdown id: tags:
# Display error and plot reconstruction
The POCS reconstruction is compared to a zero-filled reconstruction (i.e., where the missing data is zeroed prior to inverse Fourier Transform)
In this section:
- print formatted strings to standard output
- plotting, with min/max scales
- 2D subplots with min/max scales, figure size
- np.sum sums over all elements by default
%% Cell type:code id: tags:
``` python
# compute zero-filled recon
zf = np.fft.ifft2(np.fft.ifftshift(y, axes=0))
# compute rmse for zero-filled and POCS recon
err_zf = np.sqrt(np.sum(np.abs(zf - img)**2))
err_pocs = np.sqrt(np.sum(np.abs(est*phs - img)**2))
# print errors
print(f'RMSE for zero-filled recon: {err_zf}')
print(f'RMSE for POCS recon: {err_pocs}')
# plot both recons side-by-side
_, ax = plt.subplots(1,2,figsize=(16,16))
_, ax = plt.subplots(2,2,figsize=(16,16))
# plot zero-filled
ax[0].imshow(np.abs(zf), vmin=0, vmax=1)
ax[0].set_title('Zero-filled')
ax[0,0].imshow(np.abs(zf), vmin=0, vmax=1)
ax[0,0].set_title('Zero-filled')
ax[1,0].plot(np.abs(zf[:,47]))
# plot POCS
ax[1].imshow(est, vmin=0, vmax=1)
ax[1].set_title('POCS recon')
ax[0,1].imshow(est, vmin=0, vmax=1)
ax[0,1].set_title('POCS recon')
ax[1,1].plot(np.abs(est[:,47]))
```
......
......@@ -15,6 +15,10 @@ y = fftshift(fft2(img),1) + n;
% set bottom 24/96 lines to 0
y(73:end,:) = 0;
% show sampling
figure();
imshow(log(abs(fftshift(y,2))), [], 'colormap', jet)
%% Estimate phase
% create zero-padded hanning filter for ky-filtering
filt = padarray(hann(48),24);
......@@ -25,6 +29,16 @@ low = ifft2(ifftshift(y.*filt,1));
% get phase image
phs = exp(1j*angle(low));
% show phase estimate alongside true phase
figure();
subplot(1,2,1);
imshow(angle(img), [-pi,pi], 'colormap', hsv);
title('True image phase');
subplot(1,2,2);
imshow(angle(phs), [-pi,pi], 'colormap', hsv)
title('Estimated phase');
%% POCS reconstruction
% initialise image estimate to be zeros
est = zeros(96);
......@@ -74,11 +88,15 @@ fprintf(1, 'RMSE for POCS recon: %f\n', err_pocs);
figure();
% plot zero-filled
subplot(1,2,1);
subplot(2,2,1);
imshow(abs(zf), [0 1]);
title('Zero-Filled');
subplot(2,2,3);
plot(abs(zf(:,48)), 'linewidth', 2);
% plot POCS
subplot(1,2,2);
subplot(2,2,2);
imshow(est, [0 1]);
title('POCS recon');
subplot(2,2,4);
plot(abs(est(:,48)), 'linewidth', 2);
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment