@@ -125,3 +125,85 @@ def dseg_label(in_seg, label, newpath=None):
125125 new .set_data_dtype (np .uint8 )
126126 new .to_filename (out_file )
127127 return out_file
128+
129+
130+ def resample_by_spacing (in_file , zooms , order = 3 , clip = True ):
131+ """Regrid the input image to match the new zooms."""
132+ from pathlib import Path
133+ import numpy as np
134+ import nibabel as nb
135+ from scipy .ndimage import map_coordinates
136+
137+ if isinstance (in_file , (str , Path )):
138+ in_file = nb .load (in_file )
139+
140+ # Prepare output x-forms
141+ sform , scode = in_file .get_sform (coded = True )
142+ qform , qcode = in_file .get_qform (coded = True )
143+
144+ hdr = in_file .header .copy ()
145+ dtype = hdr .get_data_dtype ()
146+ data = np .asanyarray (in_file .dataobj )
147+ zooms = np .array (zooms )
148+
149+ # Calculate the factors to normalize voxel size to the specific zooms
150+ pre_zooms = np .array (in_file .header .get_zooms ()[:3 ])
151+
152+ # Calculate an affine aligned with cardinal axes, for simplicity
153+ card = nb .affines .from_matvec (np .diag (pre_zooms ))
154+ extent = card [:3 , :3 ].dot (np .array (in_file .shape [:3 ]))
155+ card [:3 , 3 ] = - 0.5 * extent
156+
157+ # Cover the FoV with the new grid
158+ new_size = np .ceil (extent / zooms ).astype (int )
159+ offset = (extent - np .diag (zooms ).dot (new_size )) * 0.5
160+ new_card = nb .affines .from_matvec (np .diag (zooms ), card [:3 , 3 ] + offset )
161+
162+ # Calculate the new indexes
163+ new_grid = np .array (
164+ np .meshgrid (
165+ np .arange (new_size [0 ]),
166+ np .arange (new_size [1 ]),
167+ np .arange (new_size [2 ]),
168+ indexing = "ij" ,
169+ )
170+ ).reshape ((3 , - 1 ))
171+
172+ # Calculate the locations of the new samples, w.r.t. the original grid
173+ ijk = np .linalg .inv (card ).dot (
174+ new_card .dot (np .vstack ((new_grid , np .ones ((1 , new_grid .shape [1 ])))))
175+ )
176+
177+ # Resample data in the new grid
178+ resampled = map_coordinates (
179+ data ,
180+ ijk [:3 , :],
181+ output = dtype ,
182+ order = order ,
183+ mode = "constant" ,
184+ cval = 0 ,
185+ prefilter = True ,
186+ ).reshape (new_size )
187+ if clip :
188+ resampled = np .clip (resampled , a_min = data .min (), a_max = data .max ())
189+
190+ # Set new zooms
191+ hdr .set_zooms (zooms )
192+
193+ # Get the original image's affine
194+ affine = in_file .affine .copy ()
195+ # Determine rotations w.r.t. cardinal axis and eccentricity
196+ rot = affine .dot (np .linalg .inv (card ))
197+ # Apply to the new cardinal, so that the resampling is consistent
198+ new_affine = rot .dot (new_card )
199+
200+ if qcode != 0 :
201+ hdr .set_qform (new_affine .dot (np .linalg .inv (affine ).dot (qform )), code = int (qcode ))
202+ if scode != 0 :
203+ hdr .set_sform (new_affine .dot (np .linalg .inv (affine ).dot (sform )), code = int (scode ))
204+ if (scode , qcode ) == (0 , 0 ):
205+ hdr .set_qform (new_affine , code = 1 )
206+ hdr .set_sform (new_affine , code = 1 )
207+
208+ # Create a new x-form affine, aligned with cardinal axes, 1mm3 and centered.
209+ return nb .Nifti1Image (resampled , new_affine , hdr )
0 commit comments