def run_order_odd(method, tol): """ Test cossin distributions including odd orders using Gaussian peaks. tol = list of tolerances for order=0, order=1, order=2, ... """ sigma = 5.0 # peak SD step = 6 * sigma # distance between peak centers for n in range(0, len(tol)): # order = n size = int((n + 2) * step) # coordinates: x = np.arange(float(size)) y = size - np.arange(2 * float(size) + 1)[:, None] # radius r = np.sqrt(x**2 + y**2) # cos, sin r[size, 0] = np.inf c = y / r s = x / r r[size, 0] = 0 # Gaussian peak with one cossin angular term def peak(i): m = i # cos power k = (n - m) & ~1 # sin power (round down to even) return c ** m * s ** k * \ np.exp(-(r - (i + 1) * step) ** 2 / (2 * sigma**2)) # quadrant with all peaks Q = peak(0) for i in range(1, n + 1): Q += peak(i) for rmax in ['MIN', 'all']: param = '-> rmax = {}, order={}, method = {}'.\ format(rmax, n, method) res = Distributions((int(size), 0), rmax, n, odd=True, method=method).image(Q) cossin = res.cossin() # extract values at peak centers cs = np.array( [cossin[:, int((i + 1) * step)] for i in range(n + 1)]) assert_allclose(cs, np.identity(n + 1), atol=tol[n], err_msg=param) # test that Ibeta, and thus harmonics, work in principle res.Ibeta()
def run_order(method, tol): """ Test cossin distributions for even orders using Gaussian peaks. tol = list of tolerances for order=0, order=2, order=4, ... """ sigma = 5.0 # peak SD step = 6 * sigma # distance between peak centers for n in range(len(tol)): # order = 2n size = int((n + 2) * step) # squared coordinates: x2 = np.arange(float(size))**2 y2 = x2[:, None] r2 = x2 + y2 # cos^2, sin^2 c2 = np.divide(y2, r2, out=np.zeros_like(r2), where=r2 != 0) s2 = 1 - c2 # radius r = np.sqrt(r2) # Gaussian peak with one cossin angular term. def peak(i): return c2 ** (n - i) * s2 ** i * \ np.exp(-(r - (i + 1) * step) ** 2 / (2 * sigma**2)) # quadrant with all peaks Q = peak(0) for i in range(1, n + 1): Q += peak(i) for rmax in ['MIN', 'all']: param = '-> rmax = {}, order={}, method = {}'.\ format(rmax, 2 * n, method) res = Distributions('ul', rmax, 2 * n, method=method).image(Q) cossin = res.cossin() # extract values at peak centers cs = np.array([cossin[:, int((i + 1) * step)] for i in range(n + 1)]) assert_allclose(cs, np.identity(n + 1), atol=tol[n], err_msg=param) # test that Ibeta, and thus harmonics, work in principle res.Ibeta()
def _profiles(IM, origin, rmax, order, odd, weights, verbose): """ Get radial profiles of cos^n theta terms from the input image. """ # the Distributions object is cached to speed up further calculations, # plus its cos^n theta matrices are used later to construct the transformed # image global _prm, _weights, _dst, _ibs, _trf, _tri_prm, _tri old_valid = None if _dst is None else _dst.valid if verbose: print('Extracting radial profiles...') prm = [IM.shape, origin, rmax, order, odd] if _prm != prm or _weights is not weights: _prm = prm _weights = weights _dst = Distributions(origin=origin, rmax=rmax, order=order, odd=odd, weights=weights, use_sin=False, method='linear') if verbose: print('(new Distributions object created)') # reset image basis _ibs = None else: if verbose: print('(reusing cached Distributions object)') c = _dst(IM).cos() if not np.array_equal(_dst.valid, old_valid): # reset transforms _trf = None _tri_prm = None _tri = None return c
def run_method_odd(method, rmax, tolP0, tolP1, tolP2, tolI, tolbeta1, tolbeta2, weq=True): """ Test harmonics and Ibeta for various combinations of origins and weights for default order=2, but with odd=True. method = method name rmax = 'MIN' or 'all' tol... = (atol, rmstol) for ... tolbeta = atol for beta weq = compare symbolic and array weights """ # image size n = 81 # width m = 91 # height # origin coordinates xc = [0, 30, n // 2, 50, n - 1] yc = [0, 25, m // 2, 65, m - 1] # peak SD sigma = 2.0 # Gaussian peak. def peak(i, r): return np.exp(-(r - i * step)**2 / (2 * sigma**2)) # Test image. def image(): # coordinates: x = np.arange(float(n)) - x0 y = y0 - np.arange(float(m))[:, None] # radius: r = np.sqrt(x**2 + y**2) # cos, |sin| r[y0, x0] = np.inf c = y / r s = np.abs(x) / r s[y0, x0] = 1 r[y0, x0] = 0 # image: 4 peaks with different anisotropies IM = s**2 * peak(1, r) + \ c**2 * peak(2, r) + \ (1/2 + c) * peak(3, r) + \ peak(4, r) return IM, s # image, sin theta # Reference distribution for test image. def ref_distr(): r = np.arange(R + 1) P0 = 2/3 * peak(1, r) + \ 1/3 * peak(2, r) + \ 1/2 * peak(3, r) + \ peak(4, r) P1 = peak(3, r) P2 = -2/3 * peak(1, r) + \ 2/3 * peak(2, r) I = 4 * np.pi * r**2 * P0 beta1 = P1 / P0 beta2 = P2 / P0 return r, P0, P1, P2, I, beta1, beta2 for y0, x0 in itertools.product(yc, xc): param = ' @ y0 = {}, x0 = {}, rmax = {}, method = {}'.\ format(y0, x0, rmax, method) # determine largest radius extracted from image if rmax == 'MIN': R = min(max(x0, n - 1 - x0), max(y0, m - 1 - y0)) elif rmax == 'MAX': # exclude situations when the outer ring has insufficient vertical # span for reliable even/odd separation if y0 in [0, m - 1] and x0 not in [0, n - 1] or \ x0 == n // 2 and abs(y0 - m // 2) not in [0, m // 2]: continue R = max(max(x0, n - 1 - x0), max(y0, m - 1 - y0)) step = (R - 5 * sigma) / 4 # distance between peak centers refr, refP0, refP1, refP2, refI, refbeta1, refbeta2 = ref_distr() f = 1 / (4 * np.pi * (1 + refr**2)) # rescaling factor for I IM, ws = image() w1 = np.ones_like(IM) IMcopy = IM.copy() w1copy = w1.copy() wscopy = ws.copy() weights = [(False, None, None), (True, None, None), (False, '1', w1), (False, 'sin', ws), (True, '1', w1)] P0, P1, P2, r, I, beta1, beta2 = {}, {}, {}, {}, {}, {}, {} for use_sin, wname, warray in weights: weight_param = param + \ ', sin = {}, weights = {}'.format(use_sin, wname) key = (use_sin, wname) distr = Distributions((y0, x0), rmax, odd=True, use_sin=use_sin, weights=warray, method=method) res = distr(IM) P0[key], P1[key], P2[key] = res.harmonics() r[key], I[key], beta1[key], beta2[key] = res.rIbeta() def assert_cmp(msg, a, ref, tol): atol, rmstol = tol assert_allclose(a, ref, atol=atol, err_msg=msg + weight_param) rms = np.sqrt(np.mean((a - ref)**2)) assert rms < rmstol, \ '\n' + msg + weight_param + \ '\nRMS error = {} > {}'.format(rms, rmstol) assert_cmp('-> P0', P0[key], refP0, tolP0) assert_cmp('-> P1', P1[key], refP1, tolP1) assert_cmp('-> P2', P2[key], refP2, tolP2) assert_equal(r[key], refr, err_msg='-> r' + weight_param) assert_cmp('-> I', f * I[key], f * refI, tolI) # beta values at peak centers b1 = [round(beta1[key][int(i * step)], 5) for i in (1, 2, 3, 4)] assert_allclose(b1, [0, 0, 2, 0], atol=tolbeta1, err_msg='-> beta1' + weight_param) b2 = [round(beta2[key][int(i * step)], 5) for i in (1, 2, 3, 4)] assert_allclose(b2, [-1, 2, 0, 0], atol=tolbeta2, err_msg='-> beta2' + weight_param) assert_equal(IM, IMcopy, err_msg='-> IM corrupted' + weight_param) assert_equal(w1, w1copy, err_msg='-> weights corrupted' + weight_param) assert_equal(ws, wscopy, err_msg='-> weights corrupted' + weight_param) if not weq: continue # check that results for symbolic and explicit weights match for key1, key2 in [((False, '1'), (False, None)), ((False, 'sin'), (True, None)), ((True, '1'), (False, 'sin'))]: pair_param = param + ', sin + weights {} != {}'.format(key1, key2) assert_allclose(P0[key1], P0[key2], err_msg='-> P0' + pair_param) assert_allclose(P1[key1], P1[key2], err_msg='-> P1' + pair_param) assert_allclose(P2[key1], P2[key2], err_msg='-> P2' + pair_param) assert_allclose(I[key1], I[key2], err_msg='-> I' + pair_param) assert_allclose(beta1[key1], beta1[key2], err_msg='-> beta1' + pair_param) assert_allclose(beta2[key1], beta2[key2], err_msg='-> beta2' + pair_param)
def run_method(method, rmax, tolP0, tolP2, tolI, tolbeta, weq=True): """ Test harmonics and Ibeta for various combinations of origins and weights for default order=2, odd=False. method = method name rmax = 'MIN' or 'all' tol... = (atol, rmstol) for ... tolbeta = atol for beta weq = compare symbolic and array weights """ # image size n = 81 # width m = 71 # height # origin coordinates xc = [0, 20, n // 2, 60, n - 1] yc = [0, 25, m // 2, 45, m - 1] # peak SD sigma = 2.0 # Gaussian peak. def peak(i, r): return np.exp(-(r - i * step)**2 / (2 * sigma**2)) # Test image. def image(): # squared coordinates: x2 = (np.arange(float(n)) - x0)**2 y2 = (np.arange(float(m))[:, None] - y0)**2 r2 = x2 + y2 # radius: r = np.sqrt(r2) # cos^2, sin^2: c2 = np.divide(y2, r2, out=np.zeros_like(r2), where=r2 != 0) s2 = 1 - c2 # image: 3 peaks with different anisotropies IM = s2 * peak(1, r) + \ peak(2, r) + \ c2 * peak(3, r) return IM, np.sqrt(s2) # image, sin theta # Reference distribution for test image. def ref_distr(): r = np.arange(R + 1) P0 = 2/3 * peak(1, r) + \ peak(2, r) + \ 1/3 * peak(3, r) P2 = -2/3 * peak(1, r) + \ 2/3 * peak(3, r) I = 4 * np.pi * r**2 * P0 beta = P2 / P0 return r, P0, P2, I, beta for y0, x0 in itertools.product(yc, xc): param = ' @ y0 = {}, x0 = {}, rmax = {}, method = {}'.\ format(y0, x0, rmax, method) # determine largest radius extracted from image if rmax == 'MIN': R = min(max(x0, n - 1 - x0), max(y0, m - 1 - y0)) elif rmax == 'all': R = max([ int(np.sqrt((x - x0)**2 + (y - y0)**2)) for x in (0, n - 1) for y in (0, m - 1) ]) step = (R - 5 * sigma) / 3 # distance between peak centers refr, refP0, refP2, refI, refbeta = ref_distr() f = 1 / (4 * np.pi * (1 + refr**2)) # rescaling factor for I IM, ws = image() w1 = np.ones_like(IM) IMcopy = IM.copy() w1copy = w1.copy() wscopy = ws.copy() weights = [(False, None, None), (True, None, None), (False, '1', w1), (False, 'sin', ws), (True, '1', w1)] P0, P2, r, I, beta = {}, {}, {}, {}, {} for use_sin, wname, warray in weights: weight_param = param + \ ', sin = {}, weights = {}'.format(use_sin, wname) key = (use_sin, wname) distr = Distributions((y0, x0), rmax, use_sin=use_sin, weights=warray, method=method) res = distr(IM) P0[key], P2[key] = res.harmonics() r[key], I[key], beta[key] = res.rIbeta() def assert_cmp(msg, a, ref, tol): atol, rmstol = tol assert_allclose(a, ref, atol=atol, err_msg=msg + weight_param) rms = np.sqrt(np.mean((a - ref)**2)) assert rms < rmstol, \ '\n' + msg + weight_param + \ '\nRMS error = {} > {}'.format(rms, rmstol) assert_cmp('-> P0', P0[key], refP0, tolP0) assert_cmp('-> P2', P2[key], refP2, tolP2) assert_equal(r[key], refr, err_msg='-> r' + weight_param) assert_cmp('-> I', f * I[key], f * refI, tolI) # beta values at peak centers b = [round(beta[key][int(i * step)], 5) for i in (1, 2, 3)] assert_allclose(b, [-1, 0, 2], atol=tolbeta, err_msg='-> beta' + weight_param) assert_equal(IM, IMcopy, err_msg='-> IM corrupted' + weight_param) assert_equal(w1, w1copy, err_msg='-> weights corrupted' + weight_param) assert_equal(ws, wscopy, err_msg='-> weights corrupted' + weight_param) if not weq: continue # check that results for symbolic and explicit weights match for key1, key2 in [((False, '1'), (False, None)), ((False, 'sin'), (True, None)), ((True, '1'), (False, 'sin'))]: pair_param = param + ', sin + weights {} != {}'.format(key1, key2) assert_allclose(P0[key1], P0[key2], err_msg='-> P0' + pair_param) assert_allclose(P2[key1], P2[key2], err_msg='-> P2' + pair_param) assert_allclose(I[key1], I[key2], err_msg='-> I' + pair_param) assert_allclose(beta[key1], beta[key2], err_msg='-> beta' + pair_param)
def rbasex_transform(IM, origin='center', rmax='MIN', order=2, odd=False, weights=None, direction='inverse', reg=None, out='same', basis_dir=None, verbose=False): r""" This function takes the input image and outputs its forward or inverse Abel transform as an image and its radial distributions. The **origin**, **rmax**, **order**, **odd** and **weights** parameters are passed to :class:`abel.tools.vmi.Distributions`, so see its documentation for their detailed descriptions. Parameters ---------- IM : m × n numpy array the image to be transformed origin : tuple of int or str image origin, explicit in the (row, column) format, or as a location string (by default, the image center) rmax : int or string largest radius to include in the transform (by default, the largest radius with at least one full quadrant of data) order : int highest angular order present in the data, ≥ 0 (by default, 2) odd : bool include odd angular orders (by default is `False`, but is enabled automatically if **order** is odd) weights : m × n numpy array, optional weighting factors for each pixel. The array shape must match the image shape. Parts of the image can be excluded from analysis by assigning zero weights to their pixels. By default is `None`, which applies equal weight to all pixels. direction : str: ``'forward'`` or ``'inverse'`` type of Abel transform to be performed (by default, inverse) reg : None or str or tuple (str, float), optional regularization to use for inverse Abel transform. ``None`` (default) means no regularization, a string selects a non-parameterized regularization method, and parameterized methods are selected by a tuple (`method`, `strength`). Available methods are: ``('L2', strength)``: Tikhonov :math:`L_2` regularization with `strength` as the square of the Tikhonov factor. This is the same as “Tikhonov regularization” used in BASEX, with almost identical effects on the radial distributions. ``('diff', strength)``: Tikhonov regularization with the difference operator (approximation of the derivative) multiplied by the square root of `strength` as the Tikhonov matrix. This tends to produce less blurring, but more negative overshoots than ``'L2'``. ``('SVD', strength)``: truncated SVD (singular value decomposition) with N = `strength` × **rmax** largest singular values removed for each angular order. This mimics the approach proposed (but in fact not used) in pBasex. `Not recommended` due to generally poor results. ``'pos'``: non-parameterized method, finds the best (in the least-squares sense) solution with non-negative :math:`\cos^n\theta \sin^m\theta` terms (see :meth:`~abel.tools.vmi.Distributions.Results.cossin`). For **order** = 0, 1, and 2 (with **odd** = `False`) this is equivalent to :math:`I(r, \theta) \geqslant 0`; for higher orders this assumption is stronger than :math:`I \geqslant 0` and corresponds to no interference between different multiphoton channels. Not implemented for odd orders > 1. Notice that this method is nonlinear, which also means that it is considerably slower than the linear methods and the transform operator cannot be cached. In all cases, `strength` = 0 provides no regularization. For the Tikhonov methods, `strength` ~ 100 is a reasonable value for megapixel images. For truncated SVD, `strength` must be < 1; `strength` ~ 0.1 is a reasonable value; `strength` ~ 0.5 can produce noticeable ringing artifacts. See the :ref:`full description <rBasexmathreg>` and examples there. out : str or None shape of the output image: ``'same'`` (default): same shape and origin as the input ``'fold'`` (fastest): Q0 (upper right) quadrant (for ``odd=False``) or right half (for ``odd=True``) up to **rmax**, but limited to the largest input-image quadrant (or half) ``'unfold'``: like ``'fold'``, but symmetrically “unfolded” to all 4 quadrants ``'full'``: all pixels with radii up to **rmax** ``'full-unique'``: the unique part of ``'full'``: Q0 (upper right) quadrant for ``odd=False``, right half for ``odd=True`` ``None``: no image (**recon** will be ``None``). Can be useful to avoid unnecessary calculations when only the transformed radial distributions (**distr**) are needed. basis_dir : str, optional path to the directory for saving / loading the basis set (useful only for the inverse transform without regularization; time savings in other cases are small and might be negated by the disk-access overhead). If ``None`` (default), the basis set will not be loaded from or saved to disk. verbose : bool print information about processing (for debugging), disabled by default Returns ------- recon : 2D numpy array or None the transformed image. Is centered and might have different dimensions than the input image. distr : Distributions.Results object the object from which various distributions for the transformed image can be retrieved, see :class:`abel.tools.vmi.Distributions.Results` """ if order == 0: odd = False # (to eliminate additional checks) elif order % 2: odd = True # enable automatically for odd orders # extract radial profiles from input image p = _profiles(IM, origin, rmax, order, odd, weights, verbose) # (caches Distributions as _dst) Rmax = _dst.rmax # get appropriate transform matrices A = get_bs_cached(Rmax, order, odd, direction, reg, _dst.valid, basis_dir, verbose) # transform radial profiles if reg == 'pos': if verbose: print('Solving NNLS equations...') N = len(p) p = np.hstack(p) cs = nnls(A, p)[0] cs = np.split(cs, N) if odd: # (1 ± cos) / 2 → cos^0, cos^1 c = [cs[0] + cs[1], cs[0] - cs[1]] else: # cossin → cos transform C = np.flip(invpascal(N, 'upper')) c = C.dot(cs) else: if verbose: print('Applying radial transforms...') c = [An.dot(pn) for An, pn in zip(A, p)] # construct output (transformed) distributions distr = Distributions.Results(np.arange(Rmax + 1), np.array(c), order, odd, _dst.valid) if out is None: return None, distr # output size if out == 'same': height = _dst.shape[0] if odd else _dst.VER + 1 width = _dst.HOR + 1 row = _dst.row if odd else 0 elif out in ['fold', 'unfold']: height = _dst.Qheight width = _dst.Qwidth row = _dst.row if odd else 0 elif out in ['full', 'full-unique']: height = 2 * Rmax + 1 if odd else Rmax + 1 width = Rmax + 1 row = Rmax if odd else 0 else: raise ValueError('Wrong output shape "{}"'.format(out)) # construct output image from transformed radial profiles if verbose: print('Constructing output image...') # bottom right quadrant or right half recon = _image(height, width, row, c, verbose) if odd: if out not in ['fold', 'full-unique']: # combine with left half (mirrored without central column) recon = np.hstack((recon[:, :0:-1], recon)) else: # even only recon = recon[::-1] # flip to Q0 if out not in ['fold', 'full-unique']: # assemble full image recon = put_image_quadrants((recon, recon, recon, recon), (2 * height - 1, 2 * width - 1)) if out == 'same': # crop as needed row = 0 if odd else _dst.VER - _dst.row col = _dst.HOR - _dst.col H, W = IM.shape recon = recon[row:row + H, col:col + W] return recon, distr
def __init__(self, n=[301, 501], shape='half', rmax='MIN', order=2, weight=['none', 'sin', 'sin+array'], method='all', repeat=1, t_min=0.1): self.n = _ensure_list(n) if shape == 'Q': origin = 'll' self.shape = 'One quadrant' elif shape == 'half': origin = 'cl' self.shape = 'Half image' elif shape == 'full': origin = 'cc' self.shape = 'Full image' else: raise ValueError('Incorrect shape "{}"'.format(shape)) self.rmaxs = rmaxs = _ensure_list(rmax) self.order = order weights = _ensure_list(weight) if 'all' in weights: weights = ['none', 'sin', 'array', 'sin+array'] self.weights = weights methods = _ensure_list(method) if 'all' in methods: methods = ['nearest', 'linear', 'remap'] self.methods = methods # create the timing function time = Timent(skip=-1, repeat=repeat, duration=t_min).time # dictionary for the results self.results = { m: {r: {w: [] for w in weights} for r in rmaxs} for m in methods } from abel.tools.vmi import Ibeta, Distributions # make sure that everything is loaded # (otherwise the 1st timing call is very slow) Ibeta(np.array([[0]])) # Loop over all image sizes for ni in self.n: ni = int(ni) # create image and weight array rows = (ni + 1) // 2 if shape == 'Q' else ni cols = (ni + 1) // 2 if shape in ['Q', 'half'] else ni IM = np.random.randn(rows, cols) warray = np.random.randn(rows, cols) # benchmark each combination of parameters for method in methods: for rmax in rmaxs: for weight in weights: if weight == 'none': w = {'use_sin': False, 'weights': None} elif weight == 'sin': w = {'use_sin': True, 'weights': None} elif weight == 'array': w = {'use_sin': False, 'weights': warray} elif weight == 'sin+array': w = {'use_sin': True, 'weights': warray} else: raise ValueError( 'Incorrect weight "{}"'.format(weight)) # single-image t1 = time(Ibeta, IM, origin, rmax, order, method=method, **w) # cached distr = Distributions(origin, rmax, order, method=method, **w) distr(IM) # trigger precalculations def distrIMIbeta(IM): return distr(IM).Ibeta() tn = time(distrIMIbeta, IM) # save results self.results[method][rmax][weight].append( (t1 * 1000, tn * 1000))