1+ """
2+ MRI affine transformation demo using Heat.
3+
4+ This example loads a sample MRI volume, extracts a middle slice,
5+ applies various affine transformations (rotation, scaling,
6+ translation, shear), and visualizes the results.
7+
8+ MRI sample data © 2018 Adam Wolf, used under the MIT License.
9+ See heat/datasets/mri_sample_LICENSE.txt for details.
10+ """
11+
112import numpy as np
213import matplotlib .pyplot as plt
314import nibabel as nib
415import heat as ht
516from heat .ndimage .affine import affine_transform
6- """
7- MRI sample data © 2018 Adam Wolf, used under MIT License.
8- See heat/datasets/mri_sample_LICENSE.txt for details.
9- """
1017
1118
1219# ============================================================
1320# Load MRI volume
1421# ============================================================
22+
1523def load_mri (path ):
24+ """
25+ Load an MRI volume from a NIfTI file.
26+
27+ Parameters
28+ ----------
29+ path : str
30+ Path to the .nii or .nii.gz MRI file.
31+
32+ Returns
33+ -------
34+ numpy.ndarray
35+ MRI volume as a float32 array of shape (D, H, W),
36+ normalized to the range [0, 255].
37+ """
1638 nii = nib .load (path )
17- vol = nii .get_fdata ().astype (np .float32 ) # (D,H,W)
39+ vol = nii .get_fdata ().astype (np .float32 )
1840 vol = vol / np .max (vol ) * 255
1941 return vol
2042
2143
2244# ============================================================
2345# Slice helper
2446# ============================================================
47+
2548def middle_slice (volume ):
49+ """
50+ Extract the middle axial slice from a 3D volume.
51+
52+ Parameters
53+ ----------
54+ volume : numpy.ndarray
55+ 3D array of shape (D, H, W).
56+
57+ Returns
58+ -------
59+ numpy.ndarray
60+ 2D slice of shape (H, W).
61+ """
2662 mid = volume .shape [0 ] // 2
2763 return volume [mid , :, :]
2864
2965
3066# ============================================================
3167# Convert numpy MRI slice → Heat array
3268# ============================================================
69+
3370def to_heat_slice (slice2d ):
71+ """
72+ Convert a 2D NumPy array into a Heat array.
73+
74+ Parameters
75+ ----------
76+ slice2d : numpy.ndarray
77+ 2D image slice.
78+
79+ Returns
80+ -------
81+ ht.DNDarray
82+ Heat array representation of the slice.
83+ """
3484 return ht .array (slice2d , dtype = ht .float32 )
3585
3686
3787# ============================================================
3888# Plot grid
3989# ============================================================
90+
4091def show_results (titles , images , save_path = None ):
92+ """
93+ Display a grid of images with titles.
94+
95+ Parameters
96+ ----------
97+ titles : list of str
98+ Titles for each subplot.
99+ images : list of numpy.ndarray
100+ Images to display.
101+ save_path : str, optional
102+ If given, the figure is saved to this path.
103+ """
41104 n = len (images )
42105 cols = 3
43106 rows = (n + cols - 1 ) // cols
@@ -62,107 +125,121 @@ def show_results(titles, images, save_path=None):
62125# ============================================================
63126# MAIN
64127# ============================================================
128+
65129def main ():
130+ """
131+ Run the MRI affine transformation demo.
132+
133+ This function:
134+ 1. Loads a sample MRI volume
135+ 2. Extracts the middle slice
136+ 3. Applies multiple affine transformations using Heat
137+ 4. Visualizes and saves the results
138+ """
66139 # -------- LOAD MRI --------
67- vol = load_mri ("/Users/marka.k/1900_Image_transformations/heat/heat/datasets/flair.nii.gz" )
140+ vol = load_mri (
141+ "/Users/marka.k/1900_Image_transformations/heat/heat/datasets/flair.nii.gz"
142+ )
68143 print ("Loaded MRI:" , vol .shape )
69144
70- orig = middle_slice (vol ) # (H,W)
145+ orig = middle_slice (vol )
71146 x = to_heat_slice (orig )
72147
73148 H , W = orig .shape
74149
75150 # ========================================================
76- # Define transforms (all center-aware)
151+ # Define transforms (center-aware)
77152 # ========================================================
78153
79- cx , cy = W / 2 , H / 2
80-
81- # Helper to shift center for rotation/scale
82154 def recenter (M ):
83- """
84- Input: 2x3 affine matrix.
85- Output: 2x3 affine matrix recentered around the image center.
86- """
87-
88- cx , cy = W / 2 , H / 2
89-
90- # Convert 2×3 → 3×3 homogeneous
91- M3 = np .array ([
92- [M [0 ,0 ], M [0 ,1 ], M [0 ,2 ]],
93- [M [1 ,0 ], M [1 ,1 ], M [1 ,2 ]],
94- [0 , 0 , 1 ]
95- ], dtype = np .float32 )
96-
97- # Center shift matrices
98- T1 = np .array ([
99- [1 , 0 , - cx ],
100- [0 , 1 , - cy ],
101- [0 , 0 , 1 ]
102- ], np .float32 )
103-
104- T2 = np .array ([
105- [1 , 0 , cx ],
106- [0 , 1 , cy ],
107- [0 , 0 , 1 ]
108- ], np .float32 )
109-
110- # Recenter: T2 * M * T1
111- M_centered = T2 @ M3 @ T1
112-
113- # Return as 2×3
114- return np .array ([
115- [M_centered [0 ,0 ], M_centered [0 ,1 ], M_centered [0 ,2 ]],
116- [M_centered [1 ,0 ], M_centered [1 ,1 ], M_centered [1 ,2 ]],
117- ], dtype = np .float32 )
155+ """
156+ Recenter a 2D affine matrix around the image center.
157+
158+ Parameters
159+ ----------
160+ M : numpy.ndarray
161+ Affine matrix of shape (2, 3).
162+
163+ Returns
164+ -------
165+ numpy.ndarray
166+ Recentered affine matrix of shape (2, 3).
167+ """
168+ cx , cy = W / 2 , H / 2
169+
170+ M3 = np .array (
171+ [
172+ [M [0 , 0 ], M [0 , 1 ], M [0 , 2 ]],
173+ [M [1 , 0 ], M [1 , 1 ], M [1 , 2 ]],
174+ [0 , 0 , 1 ],
175+ ],
176+ dtype = np .float32 ,
177+ )
178+
179+ T1 = np .array (
180+ [[1 , 0 , - cx ], [0 , 1 , - cy ], [0 , 0 , 1 ]],
181+ dtype = np .float32 ,
182+ )
183+
184+ T2 = np .array (
185+ [[1 , 0 , cx ], [0 , 1 , cy ], [0 , 0 , 1 ]],
186+ dtype = np .float32 ,
187+ )
188+
189+ M_centered = T2 @ M3 @ T1
190+
191+ return np .array (
192+ [
193+ [M_centered [0 , 0 ], M_centered [0 , 1 ], M_centered [0 , 2 ]],
194+ [M_centered [1 , 0 ], M_centered [1 , 1 ], M_centered [1 , 2 ]],
195+ ],
196+ dtype = np .float32 ,
197+ )
118198
119199 # ROTATION
120200 angle = np .radians (20 )
121- M_rot = np .array ([
122- [np .cos (angle ), - np .sin (angle ), 0 ],
123- [np .sin (angle ), np .cos (angle ), 0 ]
124- ], np .float32 )
125- M_rot = recenter (M_rot )
201+ M_rot = recenter (
202+ np .array (
203+ [[np .cos (angle ), - np .sin (angle ), 0 ],
204+ [np .sin (angle ), np .cos (angle ), 0 ]],
205+ dtype = np .float32 ,
206+ )
207+ )
126208
127209 # SCALE
128210 s = 1.2
129- M_scale = np .array ([
130- [s , 0 , 0 ],
131- [0 , s , 0 ]
132- ], np .float32 )
133- M_scale = recenter (M_scale )
134-
135- # TRANSLATE
136- M_trans = np .array ([
137- [1 , 0 , 20 ],
138- [0 , 1 , - 20 ]
139- ], np .float32 )
211+ M_scale = recenter (
212+ np .array ([[s , 0 , 0 ], [0 , s , 0 ]], dtype = np .float32 )
213+ )
214+
215+ # TRANSLATION
216+ M_trans = np .array ([[1 , 0 , 20 ], [0 , 1 , - 20 ]], dtype = np .float32 )
140217
141218 # SHEAR
142219 sh = 0.3
143- M_shear = np .array ([
144- [1 , sh , 0 ],
145- [0 , 1 , 0 ]
146- ], np .float32 )
147- M_shear = recenter (M_shear )
220+ M_shear = recenter (
221+ np .array ([[1 , sh , 0 ], [0 , 1 , 0 ]], dtype = np .float32 )
222+ )
148223
149- # 3D ROTATION ABOUT Z AXIS APPLIED TO 2D SLICE ( equivalent)
224+ # ROTATION (Z-axis equivalent)
150225 angle = np .radians (35 )
151- M_rotZ = np .array ([
152- [np .cos (angle ), - np .sin (angle ), 0 ],
153- [np .sin (angle ), np .cos (angle ), 0 ]
154- ], np .float32 )
155- M_rotZ = recenter (M_rotZ )
226+ M_rotZ = recenter (
227+ np .array (
228+ [[np .cos (angle ), - np .sin (angle ), 0 ],
229+ [np .sin (angle ), np .cos (angle ), 0 ]],
230+ dtype = np .float32 ,
231+ )
232+ )
156233
157234 # ========================================================
158235 # Run transformations
159236 # ========================================================
160237
161- out_rot = affine_transform (x , M_rot , order = 1 ).numpy ()
162- out_scale = affine_transform (x , M_scale , order = 1 ).numpy ()
163- out_trans = affine_transform (x , M_trans , order = 1 ).numpy ()
164- out_shear = affine_transform (x , M_shear , order = 1 ).numpy ()
165- out_rotZ = affine_transform (x , M_rotZ , order = 1 ).numpy ()
238+ out_rot = affine_transform (x , M_rot , order = 1 ).numpy ()
239+ out_scale = affine_transform (x , M_scale , order = 1 ).numpy ()
240+ out_trans = affine_transform (x , M_trans , order = 1 ).numpy ()
241+ out_shear = affine_transform (x , M_shear , order = 1 ).numpy ()
242+ out_rotZ = affine_transform (x , M_rotZ , order = 1 ).numpy ()
166243
167244 # ========================================================
168245 # Show + save
@@ -172,15 +249,14 @@ def recenter(M):
172249 "Original (middle slice)" ,
173250 "Rotate 20°" ,
174251 "Scale ×1.2" ,
175- "Translate (+20, - 20)" ,
252+ "Translate (+20, − 20)" ,
176253 "Shear (0.3)" ,
177- "3D Rotation (Z-axis 35°)" ,
254+ "Rotation (Z-axis 35°)" ,
178255 ]
179256
180257 imgs = [orig , out_rot , out_scale , out_trans , out_shear , out_rotZ ]
181258
182259 save_path = "mri_affine_demo.png"
183-
184260 show_results (titles , imgs , save_path = save_path )
185261
186262 print (f"\n Saved result figure → { save_path } " )
0 commit comments