Exercise3 Group7
Exercise3 Group7
where pf ,m is the normalized historgram count and can be interpreted as the probability of a
specific intensity combination (f,m). Whereas H F , M is the joint entropy, that tells how
predictable a intensity combination is. The lower the joint entropy the more predictable is the
intensity combinations, thereby the histogram will consist of fewer histogram bins with more
information and a thus more aligned images.
Problems with local minimas for the joint entropy can be found when for example the moving
image only contains background.
E ( w )=H F , M − H F − H M ,
where H F and H M are the marginal entropies of the fixed and moving image, respectively. They
are defined as follows:
B B
H F=− ∑ pf log ( p f ) and H M =− ∑ p m log ( p m )
f =1 m=1
In this report rigid image registration will be implemented using mutual information. Two MRI
images are used, where one is rotated around one axis. The joint histograms and mutual
information are computed. Followed by a evaluation of the mutal information when rotating the
image in a range. At last the joint histogram for the rotated moving image and fixed image is
compared with the original one.
import numpy as np
from numpy import linalg as la
import matplotlib.pyplot as plt
import matplotlib as mpl
from mpl_toolkits import mplot3d
#%matplotlib tk
#%matplotlib notebook
%matplotlib inline
plt.ion()
np.set_printoptions( suppress=True )
import nibabel as nib
import scipy
from scipy.ndimage import map_coordinates
T1_fileName = 'IXI014-HH-1236-T1.nii.gz'
T2_fileName = 'IXI014-HH-1236-T2_rotated.nii.gz'
T1 = nib.load( T1_fileName )
T2 = nib.load( T2_fileName )
T1_data = T1.get_fdata()
T2_data = T2.get_fdata()
Below is code to define a simple interactive viewer class that can be used to visualize 2D cross-
sections of a 3D array along three orthogonal directions. It takes a 3D volume as input and
shows the location a "linked cursor" in all three cross-sections.
The initial location of the cursor is in the middle of the volume in each case. It can be changed by
clicking on one of the cross-sections. The viewer also displays the voxel index v of the cursor.
class Viewer:
def __init__(self, data ):
self.fig, self.ax = plt.subplots()
self.data = data
self.dims = self.data.shape
self.position = np.round( np.array( self.dims ) / 2 ).astype(
int )
self.draw()
self.fig.canvas.mpl_connect( 'button_press_event', self )
self.fig.show()
#
if ( x > (self.dims[0]-1) ) and ( y <= (self.dims[1]-1) ):
return # lower-right quadrant
#
if x < self.dims[0]:
self.position[ 0 ] = x
else:
self.position[ 1 ] = x - self.dims[0]
if y < self.dims[1]:
self.position[ 1 ] = y
else:
self.position[ 2 ] = y -self.dims[1]
self.draw()
self.ax.clear()
self.ax.imshow( xySlice.T,
extent=( 0, dims[0]-1,
0, dims[1]-1 ),
**kwargs )
self.ax.imshow( xzSlice.T,
extent=( 0, dims[0]-1,
dims[1], dims[1]+dims[2]-1 ),
**kwargs )
self.ax.imshow( yzSlice.T, extent=( dims[0], dims[0]+dims[1]-
1,
dims[1], dims[1]+dims[2]-1
),
**kwargs )
color = 'g'
self.ax.plot( (0, dims[0]-1), (position[1], position[1]),
color )
self.ax.plot( (0, dims[0]+dims[1]-1), (dims[1]+position[2],
dims[1]+position[2]), color )
self.ax.plot( (position[0], position[0]), (0, dims[1]+dims[2]-
1), color )
self.ax.plot( (dims[0]+position[1], dims[0]+position[1]),
(dims[1]+1, dims[1]+dims[2]-1), color )
self.ax.axis( False )
self.fig.canvas.draw()
Task 1: Resample the T2-weighted scan to the image grid of the T1-
weighted scan
In the following a resampling of the T2-weighted scan is perfomed. This is done by finding the
voxel-to-voxel transformation between v T 1 and v T 2:
( )
vT 2
1
−1
( )
v
=M T 2 ⋅ M T 1 ⋅ T 1 .
1
At the location v T 2, cubic B-spline interpolation is used to determine the intensity in the T2-
weighted scan, and store it at index v T 1 in the newly created image.
# Defining the affine voxel-to-world matrices for the T1- and T2-
weighted scan
M_T1 = T1.affine
M_T2 = T2.affine
# Defining the inverse of the affine voxel-to-world matrix for the T2-
weighted scan
M_T2_inv = np.linalg.inv(M_T2)
Below the resampled T2-weighted volume and the T1-weighted and resampled T2-weighted
volume overlaid is visualized. This is done to give an insight into how the T2-weighted image is
rotated. It has been given that the T2-weighted volume is only rotated around a single axis.
# Normalizing data
image_T1 = T1_data / T1_data.max()
image_T2 = T2_data / T2_data.max()
image_moved = T2_data_resampled / T2_data_resampled.max()
#Plots
Viewer( image_moved )
plt.title('T2 in T1 space')
C:\Users\sofie\AppData\Local\Temp\ipykernel_12224\1359666277.py:9:
UserWarning: FigureCanvasAgg is non-interactive, and thus cannot be
shown
self.fig.show()
( )
h1 , 1 .. . h1 ,B
H= ⋮ .. . ⋮
h B ,1 .. . hB ,B
of the T1-weighted scan and the resampled T2-weighted we found in Task 1, using B=32 bins.
We do this by creating a function that takes two datasets as inputs and uses 32 bins as standard
to compute and plot a 2D histogram. The function then uses this 2D histogram to create a 3D
bar plot.
dz = hist.ravel()
# Plotting
fig, ax = plt.subplots(subplot_kw=dict(projection='3d'))
ax.bar3d(xpos, ypos, zpos, dx, dy, dz, zsort='average')
ax.set_xlabel(f'T1 Intensity')
ax.set_ylabel(f'T2 Intensity')
ax.set_zlabel('Count')
plt.show()
We ignore all voxels with an intensity lower than 10, so that the background voxels will not
dominate the image and use the function defined above to plot our 3D bar plot.
# Normalize
T1_norm = T1_data / T1_data.max()
T2_norm = T2_data_resampled / T2_data_resampled.max()
# Joint histogram
B = 32 # Number of bins
# Filtering out voxels where the intensity of both the T1- and T2-
weighted image is below 10
T1_filtered = T1_data[mask]
T2_filtered = T2_data_resampled[mask]
# Plot 3D histogram
create_joint_histogram(T1_filtered, T2_filtered, B)
The histogram above shows the intensity combinations of the two image histograms depicted as
many entries with small counts, that then add up. Generally, if the two images are well aligned,
most of these intensity combinations will be collected in few of the bins of the histogram plot.
However, if the images are poorly aligned, the histogram will be more spread out over various
bins. For our plot, we see that the two images seem somewhat aligned, but we will now work to
try and improve alignment by use of the mutual information technique.
with
B B
H F , M =− ∑ ∑ pf ,m log ( pf ,m ) ,
f =1 m=1
B
H F=− ∑ pf log ( p f ) ,
f =1
and
B
H M =− ∑ p m log ( p m ) .
m =1
The function uses the joint histogram computed as in the previous task (i.e., using B=32 bins
and ignoring all voxels with intensity lower than 10). It computes the mutual information
between the T1-weighted image and the resampled T2-weighted image as well as the mutual
information-based registration (here denoted nMI) in order to avoid pathological solutions.
# Joint histogram
hist, _, _ = np.histogram2d(image1[mask].ravel(),
image2[mask].ravel(), bins=bins)
From Task 1, we found that the T2-weighted volume in the T1-coordinates was rotated around
the y-axis. Though, in the following the T2-weighted volume is rotated in the original T2-
coordinates and therefore rotated around the z-axis, thus the rotation angles around the other
axes are clamped to zero.
def rotation_matrix_y(theta_y):
"""Rotation matrix around the y-axis."""
return np.array([[np.cos(theta_y), -np.sin(theta_y), 0],
[0, 1, 0],
[np.sin(theta_y), np.cos(theta_y), 0]])
def rotation_matrix_z(theta_z):
"""Rotation matrix around the z-axis."""
return np.array([[np.cos(theta_z), -np.sin(theta_z), 0],
[np.sin(theta_z), np.cos(theta_z), 0],
[0, 0, 1]])
We define a function that maximizes the MI and minimizes nMI, to find the best rotational axis.
Based on the mutual information from trying all angles between -20 and 55 in 5 degree
intervals, the best alignment of T1 and T2 is is achieved with 45 degree rotation of T2.
# Plotting
plt.figure()
plt.plot(angles, nMIs, 'o-')
plt.title('E(w) = H_FM - H_F - H_M')
plt.grid(True)
plt.xlabel('Angle')
plt.ylabel('E(w)')
print(f'\nBest Angle: {best_nMI_angle} degrees with Mutual
Information: {best_nMI:.3f}')
C:\Users\jaco8\AppData\Local\Temp\ipykernel_43176\1359666277.py:9:
UserWarning: Matplotlib is currently using
module://matplotlib_inline.backend_inline, which is a non-GUI backend,
so cannot show the figure.
self.fig.show()
bar_thickness = 50
dx = dy = bar_thickness * np.ones_like(zpos)
dz = hist.ravel()
ax.set_xlabel('T1 Intensity')
ax.set_ylabel('T2 Intensity')
ax.set_zlabel('Count')
# Normalizes data
T2_norm_2 = best_resampled_data / best_resampled_data.max()
# Apply mask
T1_filtered_2 = T1_data[mask_2]
T2_filtered_2 = best_resampled_data[mask_2]
Conclusion
We have now seen how using mutual information-based registration can help align two
different modalities for medical imaging. Both computing the mutual information and using the
energy function the results showed that the best rotation angle was 45 degrees. This was
confirmed by looking at the rotated T2-image overlayed the T1 and by the spread of values in the
histogram which showed what was introduced in the introduction.