# - compatibility with Python 3
from __future__ import print_function # print('me') instead of print 'me'
from __future__ import division # 1/2 == 0.5, not 0
# - show figures inside the notebook
%matplotlib inline
# - import common modules
import numpy as np # the Python array package
import matplotlib.pyplot as plt # the Python plotting package
# - set gray colormap and nearest neighbor interpolation by default
plt.rcParams['image.cmap'] = 'gray'
plt.rcParams['image.interpolation'] = 'nearest'
# - import numpy.linalg with a shorter name
import numpy.linalg as npl
A comparison between two arrays returns the elementwise result of the comparison:
arr1 = np.array([[0, 1, 2],
[3, 4, 5]])
arr2 = np.array([[1, 1, 2],
[3, 4, 6]])
arr1 == arr2
Sometimes we want to know if two arrays are equal, in the sense that all the elements of the two arrays are equal to each other. For this we use np.all. np.all accepts an array as input, and returns True if all the elements are non-zero (evaluate to True if forced to a True / False value):
np.all([True, True, True])
np.all([True, False, True])
To ask whether all the elements in two arrays are equal, we can pass the result of the elementwise comparison to np.all:
np.all(arr1 == arr2)
arr3 = arr1.copy()
np.all(arr1 == arr3)
Sometimes we want to know if any of the values in an array are non-zero (evaluate to True if forced to a True / False value). Enter np.any:
np.any([False, False, False])
np.any([False, False, True])
np.any(arr1 == arr2)
np.any(arr1 != arr3)
We sometimes want to print numbers or strings with particular formatting. For example, we might want to print a floating point number to only 2 digits of precision. One good way to do this is using the Python curly brace string formatting. It works like this:
'Here is a {} floating point number'.format(3.33333)
The curly braces indicate that the number should go at that position in the string. Calling the format method with the number as an argument makes the number into a string and replaces the curly braces with the number represented as a string.
You can use multiple values in the formatting, and mix strings and numbers:
'Here is a number - {}; here is a string - {}'.format(2.0, 'Hello')
This system allows us to give formatting instructions for things like numbers, by using a : inside the curly braces, followed by the formatting instructions. Here is an example of printing a fixed precision floating point value (f) with exactly 4 digits after the decimal point:
'A formatted number - {:.4f}'.format(.2)
See the Python string formatting docs for more details and examples.
There are an infinite number of real - er - numbers, for example, there are an infinite number of real numbers between 0 and 1.
The computer stores real numbers as floating point numbers. The typical format the computer uses for floating point numbers uses 8 bytes (64 bits of memory) per number. That means the computer can only represent some of the numbers between 0 and 1 (for example).
One number that standard floating point numbers cannot represent exactly is 0.1. Here we print the value with 20 digits after the decimal point to show that:
print('{:.20f}'.format(0.1))
This means that when we do mathematical operations on floating point numbers, the exact result of the operation may be a number that the computer cannot represent exactly. In that case the software implementing the calculation can at best give us the nearest floating point number that it can represent:
print('{:1.20f}'.format(1 / 10))
For this reason, sometimes the results of doing floating point calculations are not exactly accurate. This is called floating point error. In the case above the floating point error is very small - 0.00000000000000000555.
Floating point error means that we have to be careful comparing results of floating point calculations, because the result may be off by some small amount, due to floating point error:
c = np.cos(1)
# We hope that the inverse cos should give back the original number, but
np.arccos(c) == 1
np.arccos(c)
In this case, we probably want to check that the result is equal within some reasonable error due to the limited precision of floating point. For example, np.allclose checks whether two numbers are almost equal within some default tolerance levels:
np.allclose(np.arccos(c), 1)
We often want to do some processing on an image, then save the processed image back to an image file on disk.
When we load an image from disk, we get back an image object. When we load a NIfTI .nii image, we get an image object of type Nifti1Image.
import nibabel as nib
img = nib.load('ds114_sub009_highres.nii')
type(img)
Maybe we were worried about some very high values in the image, and we wanted to clip them down to a more reasonable number:
data = img.get_data()
data.max()
We might consider clipping the top 5 percent of voxel values:
data = img.get_data()
top_95_thresh = np.percentile(data, 95)
top_95_thresh
new_data = data.copy()
new_data[new_data > top_95_thresh] = top_95_thresh
new_data.max()
We can make a new Nifti1Image by constructing directly, passing the new data, the image affine, and (optionally) a template header for the image:
clipped_img = nib.Nifti1Image(new_data, img.affine, img.header)
type(clipped_img)
The nib.Nifti1Image routine copies and adapts the passed header to the new image data shape, and affine.
# Show the original data array shape from the original header
img.header.get_data_shape()
# Here we construct a new empty header
empty_header = nib.Nifti1Header()
empty_header.get_data_shape()
If we make a new image with this header, the constructor routine fixes the header to have the correct shape for the data array:
another_img = nib.Nifti1Image(new_data, img.affine, empty_header)
another_img.header.get_data_shape()
We can save our new image with nib.save:
nib.save(clipped_img, 'clipped_image.nii')
This image has the clipped data:
nib.load('clipped_image.nii').get_data().max()
The NIfTI standard suggests putting the TR of a functional image, into the voxel dimension field of the header.
You can get the voxel (plus TR) dimensions with the get_zooms method of the header object:
func_img = nib.load('ds114_sub009_t2r1.nii')
header = func_img.header
header.get_zooms()
In this case, the image spatial voxel sizes are (4 by 4 by 4) millimeters, and the TR is 2.5 seconds.
In fact these values come from the NIfTI header field called pixdim:
print(header)
Unfortunately, it is common for people writing NIfTI images not to write this information correctly into the header, so we have to be careful, and very suspicious, if the TR value appears to be 0 or 1.