@@ -145,6 +145,16 @@ def _calculate_alpha(energy, distance_micron, ratio_delta_beta):
145145 ) * ratio_delta_beta
146146
147147
148+ # the scaling is different here and doesn't follow the original formula
149+ def _calculate_alpha_savu (energy , distance_micron , ratio_delta_beta ):
150+ return (
151+ ((1240.0 / energy ) * 10.0 ** (- 9 ))
152+ * distance_micron
153+ * ratio_delta_beta
154+ * math .pi
155+ )
156+
157+
148158def _shift_bit_length (x : int ) -> int :
149159 return 1 << (x - 1 ).bit_length ()
150160
@@ -264,10 +274,64 @@ def paganin_filter_savu_legacy(
264274 cp.ndarray
265275 The 3D array of Paganin phase-filtered projection images.
266276 """
277+ # Check the input data is valid
278+ if tomo .ndim != 3 :
279+ raise ValueError (
280+ f"Invalid number of dimensions in data: { tomo .ndim } ,"
281+ " please provide a stack of 2D projections."
282+ )
283+
284+ dz_orig , dy_orig , dx_orig = tomo .shape
267285
268- # the scaling is different here and doesn't follow the original formula
269- # ((1240.0 / energy) * 10.0 ** (-9)) * distance_micron * ratio_delta_beta * math.pi
286+ # Perform padding to the power of 2 as FFT is O(n*log(n)) complexity
287+ # TODO: adding other options of padding?
288+ padded_tomo , pad_tup = _pad_projections_to_second_power (tomo )
270289
271- return paganin_filter (
272- tomo , pixel_size , distance * 1e-03 , energy , ratio_delta_beta / (4 * 2 * np .pi )
290+ dz , dy , dx = padded_tomo .shape
291+
292+ # 3D FFT of tomo data
293+ padded_tomo = cp .asarray (padded_tomo , dtype = cp .complex64 )
294+ fft_tomo = fft2 (padded_tomo , axes = (- 2 , - 1 ), overwrite_x = True )
295+
296+ # calculate alpha constant as Savu does
297+ micron = 10 ** (- 6 )
298+ KeV = 1000.0
299+ alpha = _calculate_alpha_savu (energy * KeV , distance * micron , ratio_delta_beta )
300+
301+ # Compute the reciprocal grid
302+ # NOTE: pixel_size is ALREADY in microns, why it rescaled to micron again in Savu?
303+ indx = _reciprocal_coord (pixel_size * micron , dy )
304+ indy = _reciprocal_coord (pixel_size * micron , dx )
305+
306+ # Build Lorentzian-type filter
307+ phase_filter = fftshift (
308+ 1.0 / (1.0 + alpha * (cp .add .outer (cp .square (indx ), cp .square (indy ))))
273309 )
310+
311+ phase_filter = phase_filter / phase_filter .max () # normalisation
312+
313+ # Filter projections
314+ fft_tomo *= phase_filter
315+
316+ # Apply filter and take inverse FFT
317+ ifft_filtered_tomo = ifft2 (fft_tomo , axes = (- 2 , - 1 ), overwrite_x = True ).real
318+
319+ # slicing indices for cropping
320+ slc_indices = (
321+ slice (pad_tup [0 ][0 ], pad_tup [0 ][0 ] + dz_orig , 1 ),
322+ slice (pad_tup [1 ][0 ], pad_tup [1 ][0 ] + dy_orig , 1 ),
323+ slice (pad_tup [2 ][0 ], pad_tup [2 ][0 ] + dx_orig , 1 ),
324+ )
325+
326+ # crop the padded filtered data:
327+ tomo = ifft_filtered_tomo [slc_indices ].astype (cp .float32 )
328+
329+ # taking the negative log
330+ _log_kernel = cp .ElementwiseKernel (
331+ "C tomo" ,
332+ "C out" ,
333+ "out = -log(tomo)" ,
334+ name = "log_kernel" ,
335+ )
336+
337+ return _log_kernel (tomo )
0 commit comments