Skip to content

Commit dca2985

Browse files
committed
adds missing text (normal and spoiler sandpaper style) for the cropping and registration sections, including figs, regs and other minor cosmetic changes #4
1 parent df159ee commit dca2985

6 files changed

Lines changed: 90 additions & 8 deletions

episodes/data/Untitled.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#Insight Transform File V1.0
2+
#Transform 0
3+
Transform: MatrixOffsetTransformBase_double_3_3
4+
Parameters: 0.9999131459879729 0 0.013179547808429644 0 0.9999999999999986 0 -0.013179547808429644 0 0.9999131459879729 109.26849697249529 -159.80707395498393 -3.780654544846636
5+
FixedParameters: 0 0 0
401 KB
Loading
-339 KB
Loading
292 KB
Loading
196 KB
Loading

episodes/itksnap.Rmd

Lines changed: 85 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -369,13 +369,11 @@ nii3ct.shape
369369
```
370370

371371
3. Affine transform, mapping from voxel space to world space
372-
```callout
373372
There are two common ways of specifying the affine transformation mapping from voxel coordinates
374373
to world coordinates in the nifti header, called the `sform` and the `qform'.
375374
Another transformation is that neither the sform or qform is used the mapping is performed based just on the voxel dimensions.
376375
The **sform** directly stores the affine transformation in the header
377376
whereas the **qform** stores the affine transformation using [quaternions](https://en.wikipedia.org/wiki/Quaternion).
378-
```
379377

380378
:::::::::::::::::::::::::::::::::::::: spoiler
381379

@@ -483,12 +481,17 @@ print(nii3ct.get_qform())
483481
# [ 0. 0. 0. 1. ]]
484482
```
485483

486-
```callout
487-
However, the nifti format does not require that the sform and qform specify the same
488-
matrix, and it is not well defined what should be done when they are both provided and are
489-
different from each other.
484+
485+
486+
:::::::::::::::::::::::::::::::::::::: spoiler
487+
488+
### Advincing on using `sform`
489+
However, the nifti format does not require that the sform and qform specify the same matrix.
490+
It is also not well defined what should be done when they are both provided and are different from each other.
490491
Often the `qform` is simply ignored and the `sform` is used, so the general advice is to only use the `sform` and set the `qform` to unknown to avoid any confusion.
491-
```
492+
493+
::::::::::::::::::::::::::::::::::::::
494+
492495

493496
### Coordinate system
494497
#### LPS
@@ -703,6 +706,16 @@ print(nii3ct_float32.header)
703706

704707
### Cropping data
705708
Cropping data might often contain background voxels which can be useful in some instances but might take up valuable RAM memory, especially for large images.
709+
We are going to crop the image from slice 103 to slice 381 in the x dimension (Sagittal slices) and from slice 160 to 340 in the y dimension (Coronal slices).
710+
Use ITK-SNAP to confirm that cropping the image to these slices will only remove slices that do not contain the patient.
711+
712+
:::::::::::::::::::::::::::::::::::::: spoiler
713+
714+
### Notes on `Nifti1Image` object
715+
It is not possible to change the image data in a Nifti1Image object, e.g. to use a smaller array corresponding to a cropped image.
716+
Therefore, if you want to modify the image data you need to create a new array with the modified data, then create a new Nifti1Image object using this new array which is used to save the modified image to disk.
717+
718+
::::::::::::::::::::::::::::::::::::::
706719

707720
* Read the image data from disk using `get_fdata` and then create a new array containing a copy of the desired slices:
708721
```python
@@ -728,6 +741,24 @@ image_data_cropped = image_data[103:381,160:340,:].copy()
728741
#x[sagittal], y[coronal], z[axial] #voxel units
729742
```
730743

744+
:::::::::::::::::::::::::::::::::::::: spoiler
745+
746+
### Notes on copying data
747+
It is important to create a copy of the data (using the copy function, as show above).
748+
Otherwise the new image will reference the data in the original image, and if you make any changes to the values in the new image they will also be changed in the original image.
749+
750+
::::::::::::::::::::::::::::::::::::::
751+
752+
753+
754+
When creating a new Nifti1Image object you can provide an affine transform and/or a Nifti1Header object (e.g. nii_1.header).
755+
When you just provide an affine, a default header is created with the sform set according to the provided affine transform and the qform set to unknown.
756+
When you provide a header, the values in it will be copied into the header for the new image, except that the dim values will be updated based on the image data provided.
757+
If you provide an affine as well as a header, and the affine is different to the sform or qform in the header they will be set to the provided affine and unknown respectively.
758+
If the affine is not provided, it is not set from the header but left empty.
759+
So, if any of your downstream processing will make use of the affine, make sure you manually set it from the header either when creating the Nifti1Image object or by calling the set_sform or set_qform functions for the object (which update the affine as well as setting the sform/qform).
760+
Here we will provide a Nifti1Header object as we want the header for the new image to based on the uncropped image and will use the sform from the header as the affine for the new Nifti1Image.
761+
731762
```python
732763
nii3ct_float32_cropped = nib.nifti1.Nifti1Image(image_data_cropped, nii3ct_float32.get_sform(), nii3ct_float32.header)
733764
nii3ct_float32_cropped.shape
@@ -783,17 +814,30 @@ print(nii3ct_float32_cropped.header)
783814
#magic : b'n+1'
784815
```
785816

817+
The cropped image can now be saved using Nibabel’s save function.
818+
786819
```python
787820
nib.save(nii3ct_float32_cropped, "data/ct_for_pet_cropped.nii.gz")
788821
```
789822

790-
* Setting world coordinates of voxel (103, 160, 0) in the uncropped image.
823+
Now load the cropped image into ITK-SNAP.
824+
You will see that the image has been cropped in the x and y dimensions as expected, but it is shifted relative to the uncropped image:
825+
826+
![Figure. ITK-SNAP: Cropped image in ITK-SNAP](fig/itk-snap_ct_for_pet_cropped_nii_gz.png)
827+
828+
This is because we did not update the origin/offset in the nifti header to account for the slices we removed.
829+
The origin gives the world coordinates of the voxel (0, 0, 0).
830+
So, if we want the images to remain aligned, we need to set the origin for the cropped image to be the same as the world coordinates of voxel (103, 160, 0) in the uncropped image.
831+
This can be calculated using the affine from the uncropped image, and used to create a new affine matrix with this as the origin (the origin is the top 3 values in the 4th column of the matrix).
832+
The new affine matrix can then be used to set the sform (which also sets the affine) for the cropped image, which can then be saved.
833+
791834
```python
792835
cropped_origin = nii3ct_float32.affine@np.array([103,160,0,1])
793836
cropped_origin
794837
#array([ 148.92578125, 123.23828125, -553.5 , 1. ])
795838
```
796839

840+
797841
```python
798842
aff_mat_cropped = nii3ct_float32_cropped.get_sform()
799843
print(aff_mat_cropped)
@@ -812,6 +856,7 @@ print(aff_mat_cropped)
812856
# [ 0. 0. 0. 1. ]]
813857
```
814858

859+
Note, NiBabel provides handy functionality for slicing nifti images and updating the affine transforms and header accordingly using the slicer attribute:
815860

816861
```python
817862
nii3ct_float32_cropped.set_sform(aff_mat_cropped)
@@ -836,6 +881,34 @@ nii3ct_float32_cropped_with_nibabel_slicer.affine
836881
# [ 0. , 0. , 0. , 1. ]])
837882
```
838883

884+
But you wouldn’t have learnt so much if we’d simply done that to start with :)
885+
886+
If you load the cropped and aligned image into ITK-SNAP using overlay semi-transparent feature, you will see that it is now aligned with the original image but has fewer slices in the x and y dimensions.
887+
888+
![Figure. ITK-SNAP: Cropped image in ITK-SNAP](fig/itk-snap-overlay-semi-transparent-cropped-affine.png)
889+
890+
891+
If we try to perform a registration between these images as they are, it will likely have difficulties as the images are so far out of alignment to start with.
892+
One way to roughly align the images prior to performing a registration is to align the centres of the images by modifying the origin for the 2nd subject such that the image centres for both images have the same world coordinates.
893+
894+
This can be done by following these steps:
895+
* Load `ct_for_pet_cropped.nii.gz`
896+
* Calculate the centre of this image in voxel coordinates
897+
* Calculate the centre of this image in world coordinates
898+
* Calculate the centre of the cropped image from subject 1 in voxel coordinates
899+
* Calculate the centre of the cropped image from subject 1 in world coordinates
900+
* Calculate the translation (in world coordinates) required to align the images: Centre image 1 = centre image 2 + translation
901+
902+
Add this translation to the origin for image 2
903+
Save the image for image 2 using the header with the updated origin
904+
Try and write code to implement these steps yourself, based on what you have learnt so far.
905+
If you get stuck ask me or one of the PGTAs for help.
906+
907+
If you have implemented this correctly when you load the aligned image from image 2 into ITK-SNAP is should appear roughly aligned with the images from image 1.
908+
You can use `Color Map` feature tab and the scroll bar to see the differences between two images.
909+
910+
![Figure. ITK-SNAP main window](fig/itk-snap-calculating-registrations.png)
911+
839912
### Cropped and aligned image
840913
Automated image registrations can be prone to be failure if there are very large initial differences between the images. A manual alignment is subjective, but can provide a good enough starting guess that keeps the objective, automated registration more reliable.
841914
* Manually aligning images
@@ -855,4 +928,8 @@ You can use [niftyreg](https://nipype.readthedocs.io/en/latest/api/generated/nip
855928
* [Tutorial: Getting Started with ITK-SnAP](http://www.itksnap.org/docs/viewtutorial.php)
856929
* [ITK-SNAP 3.x Training Class Final Program](http://itksnap.org/files/handout_201409.pdf)
857930

931+
* AI-based medical image registration:
932+
* https://github.com/Project-MONAI/tutorials/tree/main/2d_registration
933+
* https://github.com/Project-MONAI/tutorials/tree/main/3d_registration
934+
* https://github.com/Project-MONAI/tutorials/tree/main/3d_regression
858935

0 commit comments

Comments
 (0)