def make_sinogram(mat, angles, pad_rate=0.5, pad_mode="edge"): """ Create a sinogram (series of 1D projections) from a 2D image based on the Fourier slice theorem (Ref. [1]). Parameters ---------- mat : array_like Square array. angles : array_like 1D array. List of angles (in radian) for projecting. pad_rate : float To apply padding before the FFT. The padding width equals to (pad_rate * image_width). pad_mode : str Padding method. Full list can be found at numpy.pad documentation. References ---------- .. [1] https://doi.org/10.1071/PH560198 """ (nrow0, ncol0) = mat.shape if nrow0 != ncol0: raise ValueError("Width and height of the image are not the same") if np.max(np.abs(angles)) > np.pi: print("!!! Warning !!!\nMaking sure that the angles are converted to " "Radian and in the range of [0; Pi]") pad = int(pad_rate * nrow0) mat_pad = np.pad(mat, pad, mode=pad_mode) if mat_pad.shape[0] % 2 == 0: mat_pad = np.pad(mat_pad, ((0, 1), (0, 1)), mode="edge") (nrow, ncol) = mat_pad.shape xcenter = (ncol - 1.0) * 0.5 ycenter = (nrow - 1.0) * 0.5 r_max = np.floor(max(xcenter, ycenter)) r_list = np.linspace(-r_max, r_max, ncol) theta_list = -np.asarray(angles) r_mat, theta_mat = np.meshgrid(r_list, theta_list) x_mat = np.float32( np.clip(xcenter + r_mat * np.cos(theta_mat), 0, ncol - 1)) y_mat = np.float32( np.clip(ycenter + r_mat * np.sin(theta_mat), 0, nrow - 1)) mat_fft = fft.fftshift(fft.fft2(fft.ifftshift(mat_pad))) mat_real = np.real(mat_fft) mat_imag = np.imag(mat_fft) sino_real = util.mapping(mat_real, x_mat, y_mat, order=5, mode="reflect") sino_imag = util.mapping(mat_imag, x_mat, y_mat, order=5, mode="reflect") sinogram = np.real( fft.fftshift(fft.ifft(fft.ifftshift(sino_real + 1j * sino_imag, axes=1)), axes=1)) return sinogram[:, pad:ncol0 + pad]
def remove_ring_based_wavelet_fft(mat, level=5, size=1, wavelet_name="db9", sort=False): """ Remove ring artifacts in a reconstructed image by combining the polar transform and the wavelet-fft-based method (Ref. [1]). Parameters ---------- mat : array_like Square array. Reconstructed image level : int Wavelet decomposition level. size : int Damping parameter. Larger is stronger. wavelet_name : str Name of a wavelet. Search pywavelets API for a full list. sort : bool, optional Apply sorting (Ref. [2]) if True. Returns ------- array_like Ring-removed image. References ---------- .. [1] https://doi.org/10.1364/OE.17.008567 .. [2] https://doi.org/10.1364/OE.26.028396 """ (nrow, ncol) = mat.shape if nrow != ncol: raise ValueError( "Width and height of the reconstructed image are not the same") mask = util.make_circle_mask(ncol, 1.0) (x_mat, y_mat) = util.rectangular_from_polar(ncol, ncol, ncol, ncol) (r_mat, theta_mat) = util.polar_from_rectangular(ncol, ncol, ncol, ncol) polar_mat = util.mapping(mat, x_mat, y_mat) polar_mat = remo.remove_stripe_based_wavelet_fft(polar_mat, level, size, wavelet_name, sort=sort) mat_rec = util.mapping(polar_mat, r_mat, theta_mat) return mat_rec * mask
def remove_ring_based_fft(mat, u=20, n=8, v=1, sort=False): """ Remove ring artifacts in the reconstructed image by combining the polar transform and the fft-based method. Parameters ---------- mat : array_like Square array. Reconstructed image u : int Cutoff frequency. n : int Filter order. v : int Number of rows (* 2) to be applied the filter. sort : bool, optional Apply sorting (Ref. [2]) if True. Returns ------- array_like Ring-removed image. References ---------- .. [1] https://doi.org/10.1063/1.1149043 .. [2] https://doi.org/10.1364/OE.26.028396 """ (nrow, ncol) = mat.shape if nrow != ncol: raise ValueError( "Width and height of the reconstructed image are not the same") mask = util.make_circle_mask(ncol, 1.0) (x_mat, y_mat) = util.rectangular_from_polar(ncol, ncol, ncol, ncol) (r_mat, theta_mat) = util.polar_from_rectangular(ncol, ncol, ncol, ncol) polar_mat = util.mapping(mat, x_mat, y_mat) polar_mat = remo.remove_stripe_based_fft(polar_mat, u, n, v, sort=sort) mat_rec = util.mapping(polar_mat, r_mat, theta_mat) return mat_rec * mask
def generate_tilted_sinogram_chunk(data, start_index, stop_index, angle, **option): """ Generate a chunk of tilted sinograms of a 3D tomographic dataset or a hdf/nxs object. Parameters ---------- data : array_like or hdf object 3D array. start_index : int Starting index of sinograms. stop_index : int Stopping index of sinograms. angle : float Tilted angle in degree. option : list or tuple of int To extract subset data along axis 0 from a hdf object. E.g option = (start, stop, step) Returns ------- array_like 3D array. Chunk of tilted sinograms. """ if data.ndim != 3: raise ValueError("Input must be a 3D data !!!") (depth, height, width) = data.shape if len(option) != 0: opt = option[list(option.keys())[0]] start, stop, step = opt else: start, stop, step = 0, depth, 1 list_idx = range(start, stop, step) x_cen = (width - 1.0) / 2 y_cen = (height - 1.0) / 2 angle = angle * np.pi / 180.0 x_list = np.arange(0, width) - x_cen y_list = np.arange(start_index, stop_index + 1) - y_cen x_mat, y_mat = np.meshgrid(x_list, y_list) x_mat1 = x_mat * np.cos(angle) - y_mat * np.sin(angle) y_mat1 = x_mat * np.sin(angle) + y_mat * np.cos(angle) x_mat1 = np.clip(x_cen + x_mat1, 0, width - 1) y_mat1 = np.clip(y_cen + y_mat1, 0, height - 1) y_min = np.int16(np.floor(np.amin(y_mat1))) y_max = np.int16(np.ceil(np.amax(y_mat1))) + 1 y_mat1 = y_mat1 - y_min sino_chunk = np.asarray([ util.mapping(data[i, y_min:y_max, :], x_mat1, y_mat1) for i in list_idx ]) return sino_chunk
def generate_tilted_profile_chunk(mat, start_index, stop_index, angle): """ Generate a chunk of tilted horizontal intensity-profiles of an image. Parameters ---------- mat : array_like 2D array. start_index : int Starting index of lines. stop_index : int Stopping index of lines. angle : float Tilted angle in degree. Returns ------- array_like 2D array. """ if mat.ndim != 2: raise ValueError("Input must be a 2D data !!!") (height, width) = mat.shape x_cen = (width - 1.0) / 2 y_cen = (height - 1.0) / 2 angle = angle * np.pi / 180.0 x_list = np.arange(0, width) - x_cen y_list = np.arange(start_index, stop_index + 1) - y_cen x_mat, y_mat = np.meshgrid(x_list, y_list) x_mat1 = x_mat * np.cos(angle) - y_mat * np.sin(angle) y_mat1 = x_mat * np.sin(angle) + y_mat * np.cos(angle) x_mat1 = np.clip(x_cen + x_mat1, 0, width - 1) y_mat1 = np.clip(y_cen + y_mat1, 0, height - 1) y_min = np.int16(np.floor(np.amin(y_mat1))) y_max = np.int16(np.ceil(np.amax(y_mat1))) + 1 y_mat1 = y_mat1 - y_min profile_chunk = util.mapping(mat[y_min:y_max, :], x_mat1, y_mat1) return profile_chunk
def unwarp_sinogram_chunk(data, start_index, stop_index, xcenter, ycenter, list_fact, **option): """ Unwarp chunk of sinograms [:, start_index: stop_index, :] of a 3D tomographic dataset or a hdf/nxs object. Parameters ---------- data : array_like or hdf object 3D array. start_index : int Starting index of sinograms. stop_index : int Stopping index of sinograms. xcenter : float Center of distortion in x-direction. ycenter : float Center of distortion in y-direction. list_fact : list of float Polynomial coefficients of the backward model. option : list or tuple of int To extract subset data along axis 0 from a hdf object. E.g option = [start, stop, step] Returns ------- array_like 3D array. Distortion corrected. """ if data.ndim != 3: raise ValueError("Input must be a 3D data !!!") (depth, height, width) = data.shape if len(option) != 0: opt = option[list(option.keys())[0]] start, stop, step = opt else: start, stop, step = 0, depth, 1 list_idx = range(start, stop, step) if stop_index == -1: stop_index = height xu_list = np.arange(0, width) - xcenter yu1 = start_index - ycenter ru_list = np.sqrt(xu_list**2 + yu1**2) flist = np.sum(np.asarray( [factor * ru_list**i for i, factor in enumerate(list_fact)]), axis=0) yd_list1 = np.clip(ycenter + flist * yu1, 0, height - 1) yu2 = stop_index - ycenter ru_list = np.sqrt(xu_list**2 + yu2**2) flist = np.sum(np.asarray( [factor * ru_list**i for i, factor in enumerate(list_fact)]), axis=0) yd_list2 = np.clip(ycenter + flist * yu2, 0, height - 1) yd_min = np.int16(np.floor(np.amin(yd_list1))) yd_max = np.int16(np.ceil(np.amax(yd_list2))) yu_list = np.arange(start_index, stop_index) - ycenter xu_mat, yu_mat = np.meshgrid(xu_list, yu_list) ru_mat = np.sqrt(xu_mat**2 + yu_mat**2) fact_mat = np.sum(np.asarray( [factor * ru_mat**i for i, factor in enumerate(list_fact)]), axis=0) xd_mat = np.float32(np.clip(xcenter + fact_mat * xu_mat, 0, width - 1)) yd_mat = np.float32(np.clip(ycenter + fact_mat * yu_mat, 0, height - 1)) - yd_min sino_chunk = np.asarray([ util.mapping(data[i, yd_min:yd_max, :], xd_mat, yd_mat) for i in list_idx ]) return sino_chunk
def dfi_reconstruction(sinogram, center, angles=None, ratio=1.0, filter_name="hann", pad_rate=0.25, pad_mode="edge", apply_log=True): """ Apply the DFI (direct Fourier inversion) reconstruction method to a sinogram image (Ref. [1]_). The method is a practical and direct implementation of the Fourier slice theorem (Ref. [2]_). Parameters ---------- sinogram : array_like 2D array. Sinogram image. center : float Center of rotation. angles : array_like 1D array. List of angles (in radian) corresponding to the sinogram. ratio : float To apply a circle mask to the reconstructed image. filter_name : {None, "hann", "bartlett", "blackman", "hamming", "nuttall",\\ "parzen", "triang"} Apply a smoothing filter. pad_rate : float To apply padding before the FFT. The padding width equals to (pad_rate * image_width). pad_mode : str Padding method. Full list can be found at numpy.pad documentation. apply_log : bool Apply the logarithm function to the sinogram before reconstruction. Returns ------- array_like Square array. Reconstructed image. References ---------- .. [1] https://doi.org/10.1364/OE.418448 .. [2] https://doi.org/10.1071/PH560198 """ if apply_log is True: sinogram = -np.log(sinogram) (nrow, ncol) = sinogram.shape if ncol % 2 == 0: sinogram = np.pad(sinogram, ((0, 0), (0, 1)), mode="edge") ncol1 = sinogram.shape[1] xshift = (ncol1 - 1) / 2.0 - center sinogram = shift(sinogram, (0, xshift), mode='nearest') if angles is not None: t_ang = np.sum(np.abs(np.diff(angles * 180.0 / np.pi))) if abs(t_ang - 360) < 10: nrow = nrow // 2 + 1 sinogram = (sinogram[:nrow] + np.fliplr(sinogram[-nrow:])) / 2 step = np.mean(np.abs(np.diff(angles))) b_ang = angles[0] - (angles[0] // (2 * np.pi)) * (2 * np.pi) sino_360 = np.vstack((sinogram[:nrow - 1], np.fliplr(sinogram))) sinogram = shift(sino_360, (b_ang / step, 0), mode='wrap')[:nrow] if angles[-1] < angles[0]: sinogram = np.flipud(np.fliplr(sinogram)) num_pad = int(pad_rate * ncol1) sinogram = np.pad(sinogram, ((0, 0), (num_pad, num_pad)), mode=pad_mode) ncol2 = sinogram.shape[1] mask = util.make_circle_mask(ncol2, 1.0) (r_mat, theta_mat) = generate_mapping_coordinate(ncol2, nrow, ncol2, ncol2) sino_fft = fft.fftshift(fft.fft(fft.ifftshift(sinogram, axes=1)), axes=1) if filter_name is not None: window = make_smoothing_window(filter_name, ncol2) sino_fft = sino_fft * np.tile(window, (nrow, 1)) mat_real = np.real(sino_fft) mat_imag = np.imag(sino_fft) reg_real = util.mapping( mat_real, r_mat, theta_mat, order=5, mode="reflect") * mask reg_imag = util.mapping( mat_imag, r_mat, theta_mat, order=5, mode="reflect") * mask recon = np.real( fft.fftshift(fft.ifft2( fft.ifftshift(reg_real + 1j * reg_imag))))[num_pad:ncol + num_pad, num_pad:ncol + num_pad] if ratio is not None: if ratio == 0.0: ratio = min(center, ncol - center) / (0.5 * ncol) mask = util.make_circle_mask(ncol, ratio) recon = recon * mask return recon