def remove_zinger(tomo, dif, size=3, ncore=None, nchunk=None): """ Remove high intensity bright spots from 3D tomographic data. Parameters ---------- tomo : ndarray 3D tomographic data. dif : float Expected difference value between outlier measurement and the median filtered raw measurement. size : int, optional Size of the median filter. ncore : int, optional Number of cores that will be assigned to jobs. nchunk : int, optional Chunk size for each core. Returns ------- ndarray Corrected 3D tomographic data. """ arr = mp.distribute_jobs(tomo, func=_remove_zinger, args=(dif, size), axis=0, ncore=ncore, nchunk=nchunk) return arr
def remove_stripe2(tomo, nblock=0, alpha=1.5, ncore=None, nchunk=None): """ Remove horizontal stripes from sinogram using Titarenko's approach :cite:`Miqueles:14`. Parameters ---------- tomo : ndarray 3D tomographic data. nblock : int, optional Number of blocks. alpha : int, optional Damping factor. ncore : int, optional Number of cores that will be assigned to jobs. nchunk : int, optional Chunk size for each core. Returns ------- ndarray Corrected 3D tomographic data. """ arr = mp.distribute_jobs(tomo, func=_remove_stripe2, args=(nblock, alpha), axis=1, ncore=ncore, nchunk=nchunk) return arr
def median_filter(arr, size=3, axis=0, ncore=None, nchunk=None): """ Apply median filter to 3D array along specified axis. Parameters ---------- arr : ndarray Arbitrary 3D array. size : int, optional The size of the filter. axis : int, optional Axis along which median filtering is performed. ncore : int, optional Number of cores that will be assigned to jobs. nchunk : int, optional Chunk size for each core. Returns ------- ndarray Median filtered 3D array. """ arr = as_float32(arr) arr = mp.distribute_jobs( arr, func=_median_filter, args=(size, axis), axis=axis, ncore=ncore, nchunk=nchunk) return arr
def correct_air(tomo, air=10, ncore=None, nchunk=None): """ Weight sinogram such that the left and right image boundaries (i.e., typically the air region around the object) are set to one and all intermediate values are scaled linearly. Parameters ---------- tomo : ndarray 3D tomographic data. air : int, optional Number of pixels at each boundary to calculate the scaling factor. ncore : int, optional Number of cores that will be assigned to jobs. nchunk : int, optional Chunk size for each core. Returns ------- ndarray Corrected 3D tomographic data. """ tomo = as_float32(tomo) air = as_int32(air) arr = mp.distribute_jobs(tomo, func=_correct_air, args=(air, ), axis=0, ncore=ncore, nchunk=nchunk) return arr
def retrieve_phase( tomo, psize=1e-4, dist=50, energy=20, alpha=1e-4, pad=True, ind=None): """ Perform single-step phase retrieval from phase-contrast measurements :cite:`Paganin:02`. Parameters ---------- tomo : ndarray 3D tomographic data. psize : float, optional Detector pixel size in cm. dist : float, optional Propagation distance of the wavefront in cm. energy : float, optional Energy of incident wave in keV. alpha : float, optional Regularization parameter. pad : bool, optional If True, extend the size of the projections by padding with zeros. ind : array of int, optional Projection indices at which the phase retrieval is applied. Returns ------- ndarray Approximated 3D tomographic phase data. """ if type(tomo) == str and tomo == 'SHARED': tomo = mp.shared_data else: arr = mp.distribute_jobs( tomo, func=retrieve_phase, args=(psize, dist, energy, alpha, pad), axis=0) return arr dx, dy, dz = tomo.shape if ind is None: ind = np.arange(0, dx) # Compute the filter. H, xshift, yshift, prj = _paganin_filter( tomo, psize, dist, energy, alpha, pad) for m in ind: proj = tomo[m, :, :] if pad: prj[xshift:dy + xshift, yshift:dz + yshift] = proj fproj = np.fft.fft2(prj) filtproj = np.multiply(H, fproj) tmp = np.real(np.fft.ifft2(filtproj)) / np.max(H) proj = tmp[xshift:dy + xshift, yshift:dz + yshift] elif not pad: fproj = np.fft.fft2(proj) filtproj = np.multiply(H, fproj) proj = np.real(np.fft.ifft2(filtproj)) / np.max(H) tomo[m, :, :] = proj
def median_filter(tomo, size=3, axis=0, ind=None): """ Apply median filter to a 3D array along a specified axis. Parameters ---------- tomo : ndarray Arbitrary 3D array. size : int, optional The size of the filter. axis : int, optional Axis along which median filtering is performed. ind : array of int, optional Indices at which the filtering is applied. Returns ------- ndarray Median filtered 3D array. """ if type(tomo) == str and tomo == 'SHARED': tomo = mp.shared_data else: arr = mp.distribute_jobs( tomo, func=median_filter, axis=axis, args=(size, axis)) return arr dx, dy, dz = tomo.shape if ind is None: if axis == 0: ind = np.arange(0, dx) elif axis == 1: ind = np.arange(0, dy) elif axis == 2: ind = np.arange(0, dz) if axis == 0: for m in ind: tomo[m, :, :] = filters.median_filter( tomo[m, :, :], (size, size)) elif axis == 1: for m in ind: tomo[:, m, :] = filters.median_filter( tomo[:, m, :], (size, size)) elif axis == 2: for m in ind: tomo[:, :, m] = filters.median_filter( tomo[:, :, m], (size, size))
def normalize(tomo, flat, dark, cutoff=None, ind=None): """ Normalize raw projection data using the flat and dark field projections. Parameters ---------- tomo : ndarray 3D tomographic data. flat : ndarray 3D flat field data. dark : ndarray 3D dark field data. cutoff : float, optional Permitted maximum vaue for the normalized data. ind : array of int, optional Projection indices at which the normalization is applied. Returns ------- ndarray Normalized 3D tomographic data. """ if type(tomo) == str and tomo == 'SHARED': tomo = mp.shared_data else: arr = mp.distribute_jobs( tomo, func=normalize, axis=0, args=(flat, dark, cutoff)) return arr dx, dy, dz = tomo.shape if ind is None: ind = np.arange(0, dx) # Calculate average flat and dark fields for normalization. flat = flat.mean(axis=0) dark = dark.mean(axis=0) # Avoid zero division in normalization denom = flat - dark denom[denom == 0] = 1e-6 for m in ind: proj = tomo[m, :, :] proj = np.divide(proj - dark, denom) if cutoff is not None: proj[proj > cutoff] = cutoff tomo[m, :, :] = proj
def retrieve_phase(tomo, psize=1e-4, dist=50, energy=20, alpha=1e-3, pad=True, ncore=None, nchunk=None): """ Perform single-step phase retrieval from phase-contrast measurements :cite:`Paganin:02`. Parameters ---------- tomo : ndarray 3D tomographic data. psize : float, optional Detector pixel size in cm. dist : float, optional Propagation distance of the wavefront in cm. energy : float, optional Energy of incident wave in keV. alpha : float, optional Regularization parameter. pad : bool, optional If True, extend the size of the projections by padding with zeros. ncore : int, optional Number of cores that will be assigned to jobs. nchunk : int, optional Chunk size for each core. Returns ------- ndarray Approximated 3D tomographic phase data. """ # Compute the filter. H, xshift, yshift, prj = _paganin_filter(tomo, psize, dist, energy, alpha, pad) arr = mp.distribute_jobs(tomo, func=_retrieve_phase, args=(H, xshift, yshift, prj, pad), axis=0, ncore=ncore, nchunk=nchunk) return arr
def project(obj, theta, center=None, ncore=None, nchunk=None): """ Project x-rays through a given 3D object. Parameters ---------- obj : ndarray Voxelized 3D object. theta : array Projection angles in radian. center: array, optional Location of rotation axis. ncore : int, optional Number of cores that will be assigned to jobs. nchunk : int, optional Chunk size for each core. Returns ------- ndarray 3D tomographic data. """ obj = as_float32(obj) # Estimate data dimensions. ox, oy, oz = obj.shape dx = len(theta) dy = ox dz = np.ceil(np.sqrt(oy * oy + oz * oz)).astype('int') tomo = np.zeros((dx, dy, dz), dtype='float32') if center is None: center = np.ones(dy, dtype='float32') * dz / 2. elif np.array(center).size == 1: center = np.ones(dy, dtype='float32') * center theta = as_float32(theta) center = as_float32(center) _init_shared(obj) arr = mp.distribute_jobs(tomo, func=_project, args=(theta, center), axis=0, ncore=ncore, nchunk=nchunk) return arr
def remove_stripe1(tomo, level=None, wname='db5', sigma=2, pad=True, ncore=None, nchunk=None): """ Remove horizontal stripes from sinogram using the Fourier-Wavelet (FW) based method :cite:`Munch:09`. Parameters ---------- tomo : ndarray 3D tomographic data. level : int, optional Number of discrete wavelet transform levels. wname : str, optional Type of the wavelet filter. 'haar', 'db5', sym5', etc. sigma : float, optional Damping parameter in Fourier space. pad : bool, optional If True, extend the size of the sinogram by padding with zeros. ncore : int, optional Number of cores that will be assigned to jobs. nchunk : int, optional Chunk size for each core. Returns ------- ndarray Corrected 3D tomographic data. """ if level is None: size = np.max(tomo.shape) level = int(np.ceil(np.log2(size))) arr = mp.distribute_jobs(tomo, func=_remove_stripe1, args=(level, wname, sigma, pad), axis=1, ncore=ncore, nchunk=nchunk) return arr
def gaussian_filter(arr, sigma, order=0, axis=0, ncore=None, nchunk=None): """ Apply Gaussian filter to 3D array along specified axis. Parameters ---------- arr : ndarray Arbitrary 3D array. sigma : scalar or sequence of scalars Standard deviation for Gaussian kernel. The standard deviations of the Gaussian filter are given for each axis as a sequence, or as a single number, in which case it is equal for all axes. order : {0, 1, 2, 3} or sequence from same set, optional Order of the filter along each axis is given as a sequence of integers, or as a single number. An order of 0 corresponds to convolution with a Gaussian kernel. An order of 1, 2, or 3 corresponds to convolution with the first, second or third derivatives of a Gaussian. Higher order derivatives are not implemented axis : int, optional Axis along which median filtering is performed. ncore : int, optional Number of cores that will be assigned to jobs. nchunk : int, optional Chunk size for each core. Returns ------- ndarray 3D array of same shape as input. """ arr = as_float32(arr) arr = mp.distribute_jobs( arr, func=_gaussian_filter, args=(sigma, order, axis), axis=axis, ncore=ncore, nchunk=nchunk) return arr
def remove_zinger(tomo, dif=1000, size=3, ind=None): """ Remove high intensity bright spots from tomographic data. Parameters ---------- tomo : ndarray 3D tomographic data. dif : float, optional Expected difference value between outlier measurements and the median filtered raw measurements. size : int, optional Size of the median filter. ind : array of int, optional Projection indices at which the zinger removal is applied. Returns ------- ndarray Corrected 3D tomographic data. """ if type(tomo) == str and tomo == 'SHARED': tomo = mp.shared_data else: arr = mp.distribute_jobs( tomo, func=remove_zinger, axis=0, args=(dif, size)) return arr dx, dy, dz = tomo.shape if ind is None: ind = np.arange(0, dx) mask = np.zeros((1, dy, dz)) for m in ind: tmp = filters.median_filter(tomo[m, :, :], (size, size)) mask = ((tomo[m, :, :] - tmp) >= dif).astype(int) tomo[m, :, :] = tmp * mask + tomo[m, :, :] * (1 - mask)
def normalize(tomo, flat, dark, cutoff=None, ncore=None, nchunk=None): """ Normalize raw projection data using the flat and dark field projections. Parameters ---------- tomo : ndarray 3D tomographic data. flat : ndarray 3D flat field data. dark : ndarray 3D dark field data. cutoff : float, optional Permitted maximum vaue for the normalized data. ncore : int, optional Number of cores that will be assigned to jobs. nchunk : int, optional Chunk size for each core. Returns ------- ndarray Normalized 3D tomographic data. """ tomo = as_float32(tomo) flat = as_float32(flat) dark = as_float32(dark) # Calculate average flat and dark fields for normalization. flat = flat.mean(axis=0) dark = dark.mean(axis=0) arr = mp.distribute_jobs(tomo, func=_normalize, args=(flat, dark, cutoff), axis=0, ncore=ncore, nchunk=nchunk) return arr
def pml_hybrid(tomo, theta, center=None, emission=True, recon=None, num_gridx=None, num_gridy=None, num_iter=1, reg_par=None, ncore=None, nchunk=None): """ Reconstruct object from projection data using penalized maximum likelihood algorithm with weighted linear and quadratic penalties :cite:`Chang:04`. Parameters ---------- tomo : ndarray 3D tomographic data. theta : array Projection angles in radian. center: array, optional Location of rotation axis. emission : bool, optional Determines whether data is emission or transmission type. recon : ndarray, optional Initial values of the reconstruction object. num_gridx, num_gridy : int, optional Number of pixels along x- and y-axes in the reconstruction grid. num_iter : int, optional Number of algorithm iterations performed. reg_par : list, optional Regularization hyperparameters as an array, (beta, delta). num_block : int, optional Number of data blocks for intermediate updating the object. ind_block : array of int, optional Order of projections to be used for updating. ncore : int, optional Number of cores that will be assigned to jobs. nchunk : int, optional Chunk size for each core. Returns ------- ndarray Reconstructed 3D object. """ tomo = as_float32(tomo) theta = as_float32(theta) dx, dy, dz = tomo.shape if center is None: center = np.ones(dy, dtype='float32') * dz / 2. elif np.array(center).size == 1: center = np.ones(dy, dtype='float32') * center if num_gridx is None: num_gridx = dz if num_gridy is None: num_gridy = dz if emission is False: tomo = -np.log(tomo) if recon is None: recon = 1e-6 * np.ones((dy, num_gridx, num_gridy), dtype='float32') if reg_par is None: reg_par = np.ones(10, dtype="float32") center = as_float32(center) recon = as_float32(recon) num_gridx = as_int32(num_gridx) num_gridy = as_int32(num_gridy) num_iter = as_int32(num_iter) reg_par = as_float32(reg_par) _init_shared(tomo) arr = mp.distribute_jobs(recon, func=_pml_hybrid, args=(theta, center, num_gridx, num_gridy, num_iter, reg_par), axis=0, ncore=ncore, nchunk=nchunk) return arr
def art(tomo, theta, center=None, emission=True, recon=None, num_gridx=None, num_gridy=None, num_iter=1, ncore=None, nchunk=None): """ Reconstruct object from projection data using algebraic reconstruction technique (ART) :cite:`Kak:98`. Parameters ---------- tomo : ndarray 3D tomographic data. theta : array Projection angles in radian. center: array, optional Location of rotation axis. emission : bool, optional Determines whether data is emission or transmission type. recon : ndarray, optional Initial values of the reconstruction object. num_gridx, num_gridy : int, optional Number of pixels along x- and y-axes in the reconstruction grid. num_iter : int, optional Number of algorithm iterations performed. ncore : int, optional Number of cores that will be assigned to jobs. nchunk : int, optional Chunk size for each core. Returns ------- ndarray Reconstructed 3D object. """ tomo = as_float32(tomo) theta = as_float32(theta) dx, dy, dz = tomo.shape if center is None: center = np.ones(dy, dtype='float32') * dz / 2. elif np.array(center).size == 1: center = np.ones(dy, dtype='float32') * center if num_gridx is None: num_gridx = dz if num_gridy is None: num_gridy = dz if emission is False: tomo = -np.log(tomo) if recon is None: recon = 1e-6 * np.ones((dy, num_gridx, num_gridy), dtype='float32') center = as_float32(center) recon = as_float32(recon) num_gridx = as_int32(num_gridx) num_gridy = as_int32(num_gridy) num_iter = as_int32(num_iter) _init_shared(tomo) arr = mp.distribute_jobs(recon, func=_art, args=(theta, center, num_gridx, num_gridy, num_iter), axis=0, ncore=ncore, nchunk=nchunk) return arr
def osem(tomo, theta, center=None, emission=True, recon=None, num_gridx=None, num_gridy=None, num_iter=1, num_block=1, ind_block=None, ncore=None, nchunk=None): """ Reconstruct object from projection data using ordered-subset expectation-maximization (OS-EM) :cite:`Hudson:94`. Parameters ---------- tomo : ndarray 3D tomographic data. theta : array Projection angles in radian. center: array, optional Location of rotation axis. emission : bool, optional Determines whether data is emission or transmission type. recon : ndarray, optional Initial values of the reconstruction object. num_gridx, num_gridy : int, optional Number of pixels along x- and y-axes in the reconstruction grid. num_iter : int, optional Number of algorithm iterations performed. num_block : int, optional Number of data blocks for intermediate updating the object. ind_block : array of int, optional Order of projections to be used for updating. ncore : int, optional Number of cores that will be assigned to jobs. nchunk : int, optional Chunk size for each core. Returns ------- ndarray Reconstructed 3D object. """ tomo = as_float32(tomo) theta = as_float32(theta) dx, dy, dz = tomo.shape if center is None: center = np.ones(dy, dtype='float32') * dz / 2. elif np.array(center).size == 1: center = np.ones(dy, dtype='float32') * center if num_gridx is None: num_gridx = dz if num_gridy is None: num_gridy = dz if emission is False: tomo = -np.log(tomo) if recon is None: recon = 1e-6 * np.ones((dy, num_gridx, num_gridy), dtype='float32') if ind_block is None: ind_block = np.arange(0, dx).astype("float32") center = as_float32(center) recon = as_float32(recon) num_gridx = as_int32(num_gridx) num_gridy = as_int32(num_gridy) num_iter = as_int32(num_iter) num_block = as_int32(num_block) ind_block = as_float32(ind_block) _init_shared(tomo) arr = mp.distribute_jobs(recon, func=_osem, args=(theta, center, num_gridx, num_gridy, num_iter, num_block, ind_block), axis=0, ncore=ncore, nchunk=nchunk) return arr
def gridrec(tomo, theta, center=None, emission=True, num_gridx=None, num_gridy=None, filter_name='shepp', ncore=None, nchunk=None): """ Reconstruct object from projection data using gridrec algorithm :cite:`Dowd:99`. Parameters ---------- tomo : ndarray 3D tomographic data. theta : array Projection angles in radian. center: array, optional Location of rotation axis. emission : bool, optional Determines whether data is emission or transmission type. num_gridx, num_gridy : int, optional Number of pixels along x- and y-axes in the reconstruction grid. filter_name : str, optional Filter name for weighting. 'shepp', 'hann', 'hamming', 'ramlak', 'cosine' or 'none'. ncore : int, optional Number of cores that will be assigned to jobs. nchunk : int, optional Chunk size for each core. Returns ------- ndarray Reconstructed 3D object. """ tomo = as_float32(tomo) theta = as_float32(theta) dx, dy, dz = tomo.shape # Gridrec accepts even number of slices. is_odd = False if tomo.shape[1] % 2 != 0: is_odd = True lasttomo = np.expand_dims(tomo[:, -1, :], 1) tomo = np.append(tomo, lasttomo, 1) dy += 1 if center is None: center = np.ones(dy, dtype='float32') * dz / 2. elif np.array(center).size == 1: center = np.ones(dy, dtype='float32') * center if num_gridx is None: num_gridx = dz if num_gridy is None: num_gridy = dz if emission is False: tomo = -np.log(tomo) recon = 1e-6 * np.ones((dy, num_gridx, num_gridy), dtype='float32') filter_name = np.array(filter_name, dtype=(str, 16)) center = as_float32(center) num_gridx = as_int32(num_gridx) num_gridy = as_int32(num_gridy) # Chunk size can't be smaller than two for gridrec. if ncore is None: ncore = multiprocessing.cpu_count() if dx < ncore: ncore = dx if nchunk is None: nchunk = (dy - 1) // ncore + 1 if nchunk < 2: nchunk = 2 _init_shared(tomo) arr = mp.distribute_jobs(recon, func=_gridrec, args=(theta, center, num_gridx, num_gridy, filter_name), axis=0, ncore=ncore, nchunk=nchunk) # Dump last slice if original number of sice was even. if is_odd: arr = arr[0:-1, :, :] return arr
def fbp(tomo, theta, center=None, emission=True, num_gridx=None, num_gridy=None, filter_name='shepp', ncore=None, nchunk=None): """ Reconstruct object from projection data using filtered back projection (FBP). Warning ------- Filter not implemented yet. Parameters ---------- tomo : ndarray 3D tomographic data. theta : array Projection angles in radian. center: array, optional Location of rotation axis. emission : bool, optional Determines whether data is emission or transmission type. num_gridx, num_gridy : int, optional Number of pixels along x- and y-axes in the reconstruction grid. filter_name : str, optional Filter name for weighting. 'shepp', 'hann', 'hamming', 'ramlak', ncore : int, optional Number of cores that will be assigned to jobs. nchunk : int, optional Chunk size for each core. Returns ------- ndarray Reconstructed 3D object. """ tomo = as_float32(tomo) theta = as_float32(theta) dx, dy, dz = tomo.shape if center is None: center = np.ones(dy, dtype='float32') * dz / 2. elif np.array(center).size == 1: center = np.ones(dy, dtype='float32') * center if num_gridx is None: num_gridx = dz if num_gridy is None: num_gridy = dz if emission is False: tomo = -np.log(tomo) recon = 1e-6 * np.ones((dy, num_gridx, num_gridy), dtype='float32') filter_name = np.array(filter_name, dtype=(str, 16)) center = as_float32(center) num_gridx = as_int32(num_gridx) num_gridy = as_int32(num_gridy) _init_shared(tomo) arr = mp.distribute_jobs(recon, func=_fbp, args=(theta, center, num_gridx, num_gridy, filter_name), axis=0, ncore=ncore, nchunk=nchunk) return arr
def remove_stripe( tomo, level=None, wname='db5', sigma=2, pad=True, ind=None): """ Remove horizontal stripes from sinogram using the Fourier-Wavelet (FW) based method :cite:`Munch:09`. Parameters ---------- tomo : ndarray 3D tomographic data. level : int, optional Number of discrete wavelet transform levels. wname : str, optional Type of the wavelet filter. 'haar', 'db5', sym5', etc. sigma : float, optional Damping parameter in Fourier space. pad : bool, optional If True, extend the size of the sinogram by padding with zeros. ind : array of int, optional Sinogram indices at which the stripe removal is applied. Returns ------- ndarray Corrected 3D tomographic data. """ if type(tomo) == str and tomo == 'SHARED': tomo = mp.shared_data else: arr = mp.distribute_jobs( tomo, func=remove_stripe, axis=1, args=(level, wname, sigma, pad)) return arr dx, dy, dz = tomo.shape if ind is None: ind = np.arange(0, dy) if level is None: size = np.max(tomo.shape) level = int(np.ceil(np.log2(size))) # pad temp image. nx = dx if pad: nx = dx + dx / 8 xshift = int((nx - dx) / 2.) sli = np.zeros((nx, dz), dtype='float32') for n in ind: sli[xshift:dx + xshift, :] = tomo[:, n, :] # Wavelet decomposition. cH = [] cV = [] cD = [] for m in range(level): sli, (cHt, cVt, cDt) = pywt.dwt2(sli, wname) cH.append(cHt) cV.append(cVt) cD.append(cDt) # FFT transform of horizontal frequency bands. for m in range(level): # FFT fcV = np.fft.fftshift(np.fft.fft(cV[m], axis=0)) my, mx = fcV.shape # Damping of ring artifact information. y_hat = (np.arange(-my, my, 2, dtype='float') + 1) / 2 damp = 1 - np.exp(-np.power(y_hat, 2) / (2 * np.power(sigma, 2))) fcV = np.multiply(fcV, np.transpose(np.tile(damp, (mx, 1)))) # Inverse FFT. cV[m] = np.real(np.fft.ifft(np.fft.ifftshift(fcV), axis=0)) # Wavelet reconstruction. for m in range(level)[::-1]: sli = sli[0:cH[m].shape[0], 0:cH[m].shape[1]] sli = pywt.idwt2((sli, (cH[m], cV[m], cD[m])), wname) tomo[:, n, :] = sli[xshift:dx + xshift, 0:dz]