-
Notifications
You must be signed in to change notification settings - Fork 13
/
nibabel_images.Rmd
450 lines (316 loc) · 10.1 KB
/
nibabel_images.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
---
jupyter:
jupytext:
notebook_metadata_filter: all,-language_info
split_at_heading: true
text_representation:
extension: .Rmd
format_name: rmarkdown
format_version: '1.2'
jupytext_version: 1.14.5
kernelspec:
display_name: Python 3 (ipykernel)
language: python
name: python3
orphan: true
---
# Nibabel images
A nibabel image object is the association of three things:
* an N-D array containing the image *data*;
* a (4, 4) *affine* matrix mapping array coordinates to coordinates in some
RAS+ world coordinate space (see
[coordinate_systems](http://nipy.org/nibabel/coordinate_systems.html));
* image metadata in the form of a *header*.
## The preliminaries
```{python}
# Numpy array library
import numpy as np
# Show values in array to 2 digits only.
# This does not affect calculations, just display.
np.set_printoptions(precision=2, suppress=True)
# Standard form of Nibabel import
import nibabel as nib
# Library to fetch example data.
import nipraxis
```
## The image object
First we load some libraries we are going to need for the examples:
There is an example 4D functional image:
```{python}
example_file = nipraxis.fetch_file('ds107_sub012_t1r2.nii')
example_file
```
We load the file to create a nibabel *image object*:
```{python}
import nibabel as nib
img = nib.load(example_file)
```
The object `img` is an instance of a nibabel image. In fact it is an
instance of a nibabel `nibabel.nifti1.Nifti1Image`:
```{python}
# Show img
img
```
As with any Python object, you can inspect `img` to see what attributes it has.
We recommend using IPython tab completion for this, but here are some examples
of interesting attributes:
`dataobj` is the object pointing to the image array data:
```{python}
img.dataobj
```
See the array proxies section below for more on why this is an array *proxy*.
`affine` is the affine array relating array coordinates from the image
data array to coordinates in some RAS+ world coordinate system
(see the coordinate systems page linked above).
```{python}
img.affine
```
`header` contains the metadata for this image. In this case it is
specifically NIfTI metadata:
```{python}
img.header
```
## The image header
The header of an image contains the image metadata. The information in
the header will differ between different image formats. For example, the
header information for a NIfTI1 format file differs from the header
information for a MINC format file.
Our image is a NIfTI1 format image, and it therefore has a NIfTI1 format
header:
```{python}
header = img.header
print(header)
```
The header of any image will normally have the following methods:
* `get_data_shape()` to get the output shape of the image data array:
```{python}
header.get_data_shape()
```
* `get_data_dtype()` to get the numpy data type in which the image data is
stored (or will be stored if you save the image):
```{python}
header.get_data_dtype()
```
* `get_zooms()` to get the voxel sizes in millimeters:
```{python}
header.get_zooms()
```
The last value of `header.get_zooms()` is the time between scans in
milliseconds; this is the equivalent of voxel size on the time axis.
## The image data array
The image data array is a little more complicated, because the image array can
be stored in the image object as a numpy array or stored on disk for you to
access later via an *array proxy*.
### Array proxies and proxy images
When you load an image from disk, as we did here, the data is likely to
be accessible via an array proxy. An array
[proxy](https://en.wikipedia.org/wiki/Proxy_pattern) is not the array itself
but something that represents the array, and can provide the array when we ask
for it.
Our image does have an array proxy, as we have already seen:
```{python}
img.dataobj
```
The array proxy allows us to create the image object without immediately
loading all the array data from disk.
Images with an array proxy object like this one are called *proxy
images* because the image data is not yet an array, but the array proxy
points to (proxies) the array data on disk.
You can test if the image has a array proxy like this:
```{python}
nib.is_proxy(img.dataobj)
```
### Array images
We can also create images from numpy arrays. For example:
```{python}
array_data = np.arange(24, dtype=np.int16).reshape((2, 3, 4))
affine = np.diag([1, 2, 3, 1])
array_img = nib.Nifti1Image(array_data, affine)
array_img
```
In this case the image array data is already a numpy array, and there is no
version of the array on disk. The `dataobj` property of the image is the array
itself rather than a proxy for the array:
```{python}
array_img.dataobj
```
```{python}
array_img.dataobj is array_data
```
`dataobj` is an array, not an array proxy, so:
```{python}
nib.is_proxy(array_img.dataobj)
```
### Getting the image data the easy way
For either type of image (array or proxy) you can always get the data with the
`get_fdata()` method.
For the array image, `get_fdata()` just returns the data array, if it's already
the required floating point type (default 64-bit float). If it isn't that type,
`get_fdata()` casts it to one:
```{python}
image_data = array_img.get_fdata()
image_data.shape
```
```{python}
image_data.dtype == np.dtype(np.float64)
```
The cast to floating point means the array is not the one attached to the
image:
```{python}
image_data is array_img.dataobj
```
Here's an image backed by a floating point array:
```{python}
farray_img = nib.Nifti1Image(image_data.astype(np.float64), affine)
farray_data = farray_img.get_fdata()
farray_data.dtype == np.dtype(np.float64)
```
There was no cast, so the array returned is exactly the array attached to the
image:
```{python}
farray_data is farray_img.dataobj
```
For the proxy image, the `get_fdata()` method fetches the array data from disk
using the proxy, and returns the array.
```{python}
image_data = img.get_fdata()
image_data.shape
```
The image `dataobj` property is still a proxy object:
```{python}
img.dataobj
```
### Proxies and caching
You may not want to keep loading the image data off disk every time you
call `get_fdata()` on a proxy image. By default, when you call
`get_fdata()` the first time on a proxy image, the image object keeps a
cached copy of the loaded array. The next time you call
`img.get_fdata()`, the image returns the array from cache rather than
loading it from disk again.
```{python}
data_again = img.get_fdata()
data_again
```
The returned data is the same (cached) copy we returned before:
```{python}
data_again is image_data
```
See :doc:`images_and_memory` for more details on managing image memory and
controlling the image cache.
### Image slicing
At times it is useful to manipulate an image's shape while keeping it in
the same coordinate system. The `slicer` attribute provides an
array-slicing interface to produce new images with an appropriately
adjusted header, such that the data at a given RAS+ location is
unchanged.
```{python}
img.shape
```
```{python}
cropped_img = img.slicer[16:-16, ...]
cropped_img.shape
```
The data is identical to cropping the data block directly:
```{python}
np.array_equal(cropped_img.get_fdata(), img.get_fdata()[16:-16, ...])
```
However, unused data did not need to be loaded into memory or scaled.
Additionally, the image affine was adjusted so that the X-translation is 16 voxels (16 * 3 = 48mm) less:
```{python}
cropped_img.affine
```
```{python}
img.affine - cropped_img.affine
```
Another use for the slicer object is to choose specific volumes from a time
series:
```{python}
vol0 = img.slicer[..., 0]
vol0.shape
```
Or a selection of volumes:
```{python}
img.slicer[..., :1].shape
```
```{python}
img.slicer[..., :2].shape
```
It is also possible to use an integer step when slicing, downsampling the image
without filtering. Note that this *will induce artifacts* in the frequency
spectrum ([aliasing](https://en.wikipedia.org/wiki/Aliasing)) along any axis
that is down-sampled.
```{python}
downsampled = vol0.slicer[::2, ::2, ::2]
downsampled.header.get_zooms()
```
Finally, an image can be flipped along an axis, maintaining an appropriate
affine matrix:
```{python}
nib.orientations.aff2axcodes(img.affine)
```
```{python}
ras = img.slicer[::-1]
nib.orientations.aff2axcodes(ras.affine)
```
```{python}
ras.affine
```
## Loading and saving
The `save` and `load` functions in nibabel should do all the work for you:
```{python}
nib.save(array_img, 'my_image.nii')
img_again = nib.load('my_image.nii')
img_again.shape
```
You can also use the `to_filename` method:
```{python}
array_img.to_filename('my_image_again.nii')
img_again = nib.load('my_image_again.nii')
img_again.shape
```
You can get and set the filename with `get_filename()` and `set_filename()`:
```{python}
img_again.set_filename('another_image.nii')
img_again.get_filename()
```
## Details of files and images
If an image can be loaded or saved on disk, the image will have an attribute
called `file_map`. `img.file_map` is a dictionary where the keys are the names
of the files that the image uses to load / save on disk, and the values are
`FileHolder` objects, that usually contain the filenames that the image has
been loaded from or saved to. In the case of a NiFTI1 single file, this is just
a single image file with a `.nii` or `.nii.gz` extension:
```{python}
list(img_again.file_map)
```
```{python}
img_again.file_map['image'].filename
```
Other file types need more than one file to make up the image. The NiFTI1 pair
type is one example. NIfTI pair images have one file containing the header
information and another containing the image array data:
```{python}
pair_img = nib.Nifti1Pair(array_data, np.eye(4))
nib.save(pair_img, 'my_pair_image.img')
sorted(pair_img.file_map)
```
```{python}
pair_img.file_map['header'].filename
```
```{python}
pair_img.file_map['image'].filename
```
The older Analyze format also has a separate header and image file:
```{python}
ana_img = nib.AnalyzeImage(array_data, np.eye(4))
sorted(ana_img.file_map)
```
It is the contents of the `file_map` that gets changed when you use
`set_filename` or `to_filename`:
```{python}
ana_img.set_filename('analyze_image.img')
ana_img.file_map['image'].filename
```
```{python}
ana_img.file_map['header'].filename
```