def rotated_ellipse(width_major, width_minor, major_axis_angle=0, samples=128): """Generate a binary mask for an ellipse, centered at the origin. The major axis will notionally extend to the limits of the array, but this will not be the case for rotated cases. Parameters ---------- width_major : `float` width of the ellipse in its major axis width_minor : `float` width of the ellipse in its minor axis major_axis_angle : `float` angle of the major axis w.r.t. the x axis, degrees samples : `int` number of samples Returns ------- `numpy.ndarray` An ndarray of shape (samples,samples) of value 0 outside the ellipse, and value 1 inside the ellipse Notes ----- The formula applied is: ((x-h)cos(A)+(y-k)sin(A))^2 ((x-h)sin(A)+(y-k)cos(A))^2 ______________________________ + ______________________________ 1 a^2 b^2 where x and y are the x and y dimensions, A is the rotation angle of the major axis, h and k are the centers of the the ellipse, and a and b are the major and minor axis widths. In this implementation, h=k=0 and the formula simplifies to: (x*cos(A)+y*sin(A))^2 (x*sin(A)+y*cos(A))^2 ______________________________ + ______________________________ 1 a^2 b^2 see SO: https://math.stackexchange.com/questions/426150/what-is-the-general-equation-of-the-ellipse-that-is-not-in-the-origin-and-rotate Raises ------ ValueError Description """ if width_minor > width_major: raise ValueError( 'By definition, major axis must be larger than minor.') arr = m.ones((samples, samples)) lim = width_major x, y = m.linspace(-lim, lim, samples), m.linspace(-lim, lim, samples) xv, yv = m.meshgrid(x, y) A = m.radians(-major_axis_angle) a, b = width_major, width_minor major_axis_term = ((xv * m.cos(A) + yv * m.sin(A))**2) / a**2 minor_axis_term = ((xv * m.sin(A) - yv * m.cos(A))**2) / b**2 arr[major_axis_term + minor_axis_term > 1] = 0 return arr
def __init__(self, width, sample_spacing=None, samples=0): """Create a Pinhole instance. Parameters ---------- width : `float` the width of the pinhole sample_spacing : `float` spacing of samples in the synthetic image samples : `int` number of samples per dimension in the synthetic image Notes ----- Default of 0 samples allows quick creation for convolutions without generating the image; use samples > 0 for an actual image. """ self.width = width # produce coordinate arrays if samples > 0: ext = samples / 2 * sample_spacing x, y = m.linspace(-ext, ext, samples), m.linspace(-ext, ext, samples) xv, yv = m.meshgrid(x, y) w = width / 2 # paint a circle on a black background arr = m.zeros((samples, samples)) arr[m.sqrt(xv**2 + yv**2) < w] = 1 else: arr, x, y = None, None, None super().__init__(data=arr, unit_x=x, unit_y=y, has_analytic_ft=True)
def resample_2d_complex(array, sample_pts, query_pts): '''Resamples a 2D complex array. Works by interpolating the magnitude and phase independently and merging the results into a complex value. Parameters ---------- array : `numpy.ndarray` complex 2D array sample_pts : `tuple` pair of `numpy.ndarray` objects that contain the x and y sample locations, each array should be 1D query_pts : `tuple` points to interpolate onto, also 1D for each array Returns ------- `numpy.ndarray` array resampled onto query_pts via bivariate spline ''' xq, yq = m.meshgrid(*query_pts) mag = abs(array) phase = m.angle(array) magfunc = interpolate.RegularGridInterpolator(sample_pts, mag) phasefunc = interpolate.RegularGridInterpolator(sample_pts, phase) interp_mag = magfunc((yq, xq)) interp_phase = phasefunc((yq, xq)) return interp_mag * m.exp(1j * interp_phase)
def __init__(self, fno, wavelength, extent=None, samples=None): """Create a new AiryDisk. Parameters ---------- fno : `float` F/# associated with the PSF wavelength : `float` wavelength of light, in microns extent : `float` cartesian window half-width, e.g. 10 will make an RoI 20x20 microns wide samples : `int` number of samples across full width """ if samples is not None: x = m.linspace(-extent, extent, samples) y = m.linspace(-extent, extent, samples) xx, yy = m.meshgrid(x, y) rho, phi = cart_to_polar(xx, yy) data = airydisk(rho, fno, wavelength) else: x, y, data = None, None, None self.fno = fno self.wavelength = wavelength super().__init__(data, x, y) self.has_analytic_ft = True
def render_synthetic_surface(size, samples, rms=None, mask='circle', psd_fcn=abc_psd, **psd_fcn_kwargs): """Render a synthetic surface with a given RMS value given a PSD function. Parameters ---------- size : `float` diameter of the output surface, mm samples : `int` number of samples across the output surface rms : `float` desired RMS value of the output, if rms=None, no normalization is done mask : `str`, optional mask defining the clear aperture psd_fcn : `callable` function used to generate the PSD **psd_fcn_kwargs: keyword arguments passed to psd_fcn in addition to nu if psd_fcn == abc_psd, kwargs are a, b, c elif psd_Fcn == ab_psd kwargs are a, b kwargs will be user-defined for user PSD functions Returns ------- x : `numpy.ndarray` x coordinates, mm y: `numpy.ndarray` y coordinates, mm z : `numpy.ndarray` height data, nm """ # compute the grid and PSD sample_spacing = size / (samples - 1) nu_x = nu_y = forward_ft_unit(sample_spacing, samples) center = samples // 2 # some bullshit here to gloss over zeros for ab_psd nu_x[center] = nu_x[center+1] / 10 nu_y[center] = nu_y[center+1] / 10 nu_xx, nu_yy = m.meshgrid(nu_x, nu_y) nu_r, _ = cart_to_polar(nu_xx, nu_yy) psd = psd_fcn(nu_r, **psd_fcn_kwargs) # synthesize a surface from the PSD x, y, z = synthesize_surface_from_psd(psd, nu_x, nu_y) # mask mask = mcache(mask, samples) z[mask == 0] = m.nan # possibly scale RMS if rms is not None: z_rms = globals()['rms'](z) # rms function is shadowed by rms kwarg scale_factor = rms / z_rms z *= scale_factor return x, y, z
def __init__(self, spokes, sinusoidal=True, background='black', sample_spacing=2, samples=256): """Produces a Siemen's Star. Parameters ---------- spokes : `int` number of spokes in the star. sinusoidal : `bool` if True, generates a sinusoidal Siemen' star, else, generates a bar/block siemen's star background : 'string', {'black', 'white'} background color sample_spacing : `float` spacing of samples, in microns samples : `int` number of samples per dimension in the synthetic image Raises ------ ValueError background other than black or white """ relative_width = 0.9 self.spokes = spokes # generate a coordinate grid x = m.linspace(-1, 1, samples) y = m.linspace(-1, 1, samples) xx, yy = m.meshgrid(x, y) rv, pv = cart_to_polar(xx, yy) ext = sample_spacing * samples / 2 ux, uy = m.arange(-ext, ext, sample_spacing), m.arange(-ext, ext, sample_spacing) # generate the siemen's star as a (rho,phi) polynomial arr = m.cos(spokes / 2 * pv) if not sinusoidal: # make binary arr[arr < 0] = -1 arr[arr > 0] = 1 # scale to (0,1) and clip into a disk arr = (arr + 1) / 2 if background.lower() in ('b', 'black'): arr[rv > relative_width] = 0 elif background.lower() in ('w', 'white'): arr[rv > relative_width] = 1 else: raise ValueError('invalid background color') super().__init__(data=arr, unit_x=ux, unit_y=uy, has_analytic_ft=False)
def fit_plane(x, y, z): xx, yy = m.meshgrid(x, y) pts = m.isfinite(z) xx_, yy_ = xx[pts].flatten(), yy[pts].flatten() flat = m.ones(xx_.shape) coefs = m.lstsq(m.stack([xx_, yy_, flat]).T, z[pts].flatten(), rcond=None)[0] plane_fit = coefs[0] * xx + coefs[1] * yy + coefs[2] return plane_fit
def fit_sphere(z): x, y = m.linspace(-1, 1, z.shape[1]), m.linspace(-1, 1, z.shape[0]) xx, yy = m.meshgrid(x, y) pts = m.isfinite(z) xx_, yy_ = xx[pts].flatten(), yy[pts].flatten() rho, phi = cart_to_polar(xx_, yy_) focus = defocus(rho, phi) coefs = m.lstsq(m.stack([focus, m.ones(focus.shape)]).T, z[pts].flatten(), rcond=None)[0] rho, phi = cart_to_polar(xx, yy) sphere = defocus(rho, phi) * coefs[0] return sphere
def encircled_energy(self, radius): """Compute the encircled energy of the PSF. Parameters ---------- radius : `float` or iterable radius or radii to evaluate encircled energy at Returns ------- encircled energy if radius is a float, returns a float, else returns a list. Notes ----- implementation of "Simplified Method for Calculating Encircled Energy," Baliga, J. V. and Cohn, B. D., doi: 10.1117/12.944334 """ from .otf import MTF if hasattr(radius, '__iter__'): # user wants multiple points # um to mm, cy/mm assumed in Fourier plane radius_is_array = True else: radius_is_array = False # compute MTF from the PSF if self._mtf is None: self._mtf = MTF.from_psf(self) nx, ny = m.meshgrid(self._mtf.unit_x, self._mtf.unit_y) self._nu_p = m.sqrt(nx**2 + ny**2) # this is meaninglessly small and will avoid division by 0 self._nu_p[self._nu_p == 0] = 1e-99 self._dnx, self._dny = ny[1, 0] - ny[0, 0], nx[0, 1] - nx[0, 0] if radius_is_array: out = [] for r in radius: if r not in self._ee: self._ee[r] = _encircled_energy_core( self._mtf.data, r / 1e3, self._nu_p, self._dnx, self._dny) out.append(self._ee[r]) return m.asarray(out) else: if radius not in self._ee: self._ee[radius] = _encircled_energy_core( self._mtf.data, radius / 1e3, self._nu_p, self._dnx, self._dny) return self._ee[radius]
def fit_plane(x, y, z): pts = m.isfinite(z) if len(z.shape) > 1: x, y = m.meshgrid(x, y) xx, yy = x[pts].flatten(), y[pts].flatten() else: xx, yy = x, y flat = m.ones(xx.shape) coefs = m.lstsq(m.stack([xx, yy, flat]).T, z[pts].flatten(), rcond=None)[0] plane_fit = coefs[0] * x + coefs[1] * y + coefs[2] return plane_fit
def __init__(self, angle=4, contrast=0.9, crossed=False, sample_spacing=2, samples=256): """Create a new TitledSquare instance. Parameters ---------- angle : `float` angle in degrees to tilt w.r.t. the y axis contrast : `float` difference between minimum and maximum values in the image crossed : `bool`, optional whether to make a single edge (crossed=False) or pair of crossed edges (crossed=True) aka a "BMW target" sample_spacing : `float` spacing of samples samples : `int` number of samples """ diff = (1 - contrast) / 2 arr = m.full((samples, samples), 1 - diff) x = m.linspace(-0.5, 0.5, samples) y = m.linspace(-0.5, 0.5, samples) xx, yy = m.meshgrid(x, y) sf = samples * sample_spacing angle = m.radians(angle) xp = xx * m.cos(angle) - yy * m.sin(angle) # yp = xx * m.sin(angle) + yy * m.cos(angle) # do not need this mask = xp > 0 # single edge if crossed: mask = xp > 0 # set of 4 edges upperright = mask & m.rot90(mask) lowerleft = m.rot90(upperright, 2) mask = upperright | lowerleft arr[mask] = diff self.contrast = contrast self.black = diff self.white = 1 - diff super().__init__(data=arr, unit_x=x * sf, unit_y=y * sf, has_analytic_ft=False)
def __init__(self, psfs, weights=None): '''Create a new `MultispectralPSF` instance. Parameters ---------- psfs : iterable iterable of PSFs weights : iterable iterable of weights associated with each PSF ''' if weights is None: weights = [1] * len(psfs) # find the most densely sampled PSF min_spacing = 1e99 ref_idx = None ref_unit_x = None ref_unit_y = None ref_samples_x = None ref_samples_y = None for idx, psf in enumerate(psfs): if psf.sample_spacing < min_spacing: min_spacing = psf.sample_spacing ref_idx = idx ref_unit_x = psf.unit_x ref_unit_y = psf.unit_y ref_samples_x = psf.samples_x ref_samples_y = psf.samples_y merge_data = m.zeros((ref_samples_x, ref_samples_y, len(psfs))) for idx, psf in enumerate(psfs): # don't do anything to our reference PSF if idx is ref_idx: merge_data[:, :, idx] = psf.data * weights[idx] else: xv, yv = m.meshgrid(ref_unit_x, ref_unit_y) interpf = interpolate.RegularGridInterpolator( (psf.unit_x, psf.unit_y), psf.data) merge_data[:, :, idx] = interpf( (xv, yv), method='linear') * weights[idx] self.weights = weights super().__init__(merge_data.sum(axis=2), min_spacing) self._renorm()
def polychromatic(psfs, spectral_weights=None, interp_method='linear'): """Create a new PSF instance from an ensemble of monochromatic PSFs given spectral weights. The new PSF is the polychromatic PSF, assuming the wavelengths are sufficiently different that they do not interfere and the mode of imaging is incoherent. """ if spectral_weights is None: spectral_weights = [1] * len(psfs) # find the most densely sampled PSF min_spacing = 1e99 ref_idx = None ref_unit_x = None ref_unit_y = None ref_samples_x = None ref_samples_y = None for idx, psf in enumerate(psfs): if psf.sample_spacing < min_spacing: min_spacing = psf.sample_spacing ref_idx = idx ref_unit_x = psf.unit_x ref_unit_y = psf.unit_y ref_samples_x = psf.samples_x ref_samples_y = psf.samples_y merge_data = m.zeros((ref_samples_x, ref_samples_y, len(psfs))) for idx, psf in enumerate(psfs): # don't do anything to the reference PSF besides spectral scaling if idx is ref_idx: merge_data[:, :, idx] = psf.data * spectral_weights[idx] else: xv, yv = m.meshgrid(ref_unit_x, ref_unit_y) interpf = interpolate.RegularGridInterpolator( (psf.unit_y, psf.unit_x), psf.data) merge_data[:, :, idx] = interpf( (yv, xv), method=interp_method) * spectral_weights[idx] psf = PSF(data=merge_data.sum(axis=2), unit_x=ref_unit_x, unit_y=ref_unit_y) psf.spectral_weights = spectral_weights psf._renorm() return psf
def analytic_ft(self, unit_x, unit_y): """Analytic fourier transform of a pixel aperture. Parameters ---------- unit_x : `numpy.ndarray` sample points in x axis unit_y : `numpy.ndarray` sample points in y axis Returns ------- `numpy.ndarray` 2D numpy array containing the analytic fourier transform """ xq, yq = m.meshgrid(unit_x, unit_y) return abs(pixelaperture_analytic_otf(self.width_x, self.width_y, xq, yq))
def fit(data, num_terms=16, rms_norm=False, round_at=6): ''' Fits a number of Zernike coefficients to provided data by minimizing the root sum square between each coefficient and the given data. The data should be uniformly sampled in an x,y grid. Args: data (`numpy.ndarray`): data to fit to. num_terms (`int`): number of terms to fit, fits terms 0~num_terms. rms_norm (`bool`): if true, normalize coefficients to unit RMS value. round_at (`int`): decimal place to round values at. Returns: numpy.ndarray: an array of coefficients matching the input data. ''' if num_terms > len(zernmap): raise ValueError(f'number of terms must be less than {len(zernmap)}') # precompute the valid indexes in the original data pts = m.isfinite(data) # set up an x/y rho/phi grid to evaluate Zernikes on x, y = m.linspace(-1, 1, data.shape[1]), m.linspace(-1, 1, data.shape[0]) xx, yy = m.meshgrid(x, y) rho = m.sqrt(xx**2 + yy**2)[pts].flatten() phi = m.arctan2(xx, yy)[pts].flatten() # compute each Zernike term zernikes = [] for i in range(num_terms): func = z.zernikes[zernmap[i]] base_zern = func(rho, phi) if rms_norm: base_zern *= func.norm zernikes.append(base_zern) zerns = m.asarray(zernikes).T # use least squares to compute the coefficients coefs = m.lstsq(zerns, data[pts].flatten(), rcond=None)[0] return coefs.round(round_at)
def __init__(self, angle=4, background='white', sample_spacing=2, samples=256): """Create a new TitledSquare instance. Parameters ---------- angle : `float` angle in degrees to tilt w.r.t. the x axis background : `string` white or black; the square will be the opposite color of the background sample_spacing : `float` spacing of samples samples : `int` number of samples """ radius = 0.3 if background.lower() == 'white': arr = m.ones((samples, samples)) fill_with = 0 else: arr = m.zeros((samples, samples)) fill_with = 1 # TODO: optimize by working with index numbers directly and avoid # creation of X,Y arrays for performance. x = m.linspace(-0.5, 0.5, samples) y = m.linspace(-0.5, 0.5, samples) xx, yy = m.meshgrid(x, y) sf = samples * sample_spacing # TODO: convert inline operation to use of rotation matrix angle = m.radians(angle) xp = xx * m.cos(angle) - yy * m.sin(angle) yp = xx * m.sin(angle) + yy * m.cos(angle) mask = (abs(xp) < radius) * (abs(yp) < radius) arr[mask] = fill_with super().__init__(data=arr, unit_x=x * sf, unit_y=y * sf, has_analytic_ft=False)
def analytic_ft(self, unit_x, unit_y): """Analytic fourier transform of a pixel aperture. Parameters ---------- unit_x : `numpy.ndarray` sample points in x axis unit_y : `numpy.ndarray` sample points in y axis Returns ------- `numpy.ndarray` 2D numpy array containing the analytic fourier transform """ xq, yq = m.meshgrid(unit_x, unit_y) return (m.sinc(xq * self.width_x) * m.sinc(yq * self.width_y)).astype( config.precision)
def analytic_ft(self, unit_x, unit_y): """Analytic fourier transform of an airy disk. Parameters ---------- unit_x : `numpy.ndarray` sample points in x axis unit_y : `numpy.ndarray` sample points in y axis Returns ------- `numpy.ndarray` 2D numpy array containing the analytic fourier transform """ from .otf import diffraction_limited_mtf r, p = cart_to_polar(*m.meshgrid(unit_x, unit_y)) return diffraction_limited_mtf(self.fno, self.wavelength, r * 1e3) # um to mm
def __init__(self, r_psf, g_psf, b_psf): '''Create a new `RGBPSF` instance. Parameters ---------- r_psf : `PSF` PSF for the red channel g_psf : `PSF` PSF for the green channel b_psf : `PSF` PSF for the blue channel ''' if m.array_equal(r_psf.unit_x, g_psf.unit_x) and \ m.array_equal(g_psf.unit_x, b_psf.unit_x) and \ m.array_equal(r_psf.unit_y, g_psf.unit_y) and \ m.array_equal(g_psf.unit_y, b_psf.unit_y): # do not need to interpolate the arrays self.R = r_psf.data self.G = g_psf.data self.B = b_psf.data else: # need to interpolate the arrays. Blue tends to be most densely # sampled, use it to define our grid self.B = b_psf.data xv, yv = m.meshgrid(b_psf.unit_x, b_psf.unit_y) interpf_r = interpolate.RegularGridInterpolator( (r_psf.unit_y, r_psf.unit_x), r_psf.data) interpf_g = interpolate.RegularGridInterpolator( (g_psf.unit_y, g_psf.unit_x), g_psf.data) self.R = interpf_r((yv, xv), method='linear') self.G = interpf_g((yv, xv), method='linear') self.sample_spacing = b_psf.sample_spacing self.samples_x = b_psf.samples_x self.samples_y = b_psf.samples_y self.unit_x = b_psf.unit_x self.unit_y = b_psf.unit_y self.center_x = b_psf.center_x self.center_y = b_psf.center_y
def uniform_cart_to_polar(x, y, data): """Interpolate data uniformly sampled in cartesian coordinates to polar coordinates. Parameters ---------- x : `numpy.ndarray` sorted 1D array of x sample pts y : `numpy.ndarray` sorted 1D array of y sample pts data : `numpy.ndarray` data sampled over the (x,y) coordinates Returns ------- rho : `numpy.ndarray` samples for interpolated values phi : `numpy.ndarray` samples for interpolated values f(rho,phi) : `numpy.ndarray` data uniformly sampled in (rho,phi) """ # create a set of polar coordinates to interpolate onto xmin, xmax = min(x), max(x) ymin, ymax = min(y), max(y) _max = max(abs(m.asarray([xmin, xmax, ymin, ymax]))) rho = m.linspace(0, _max, len(x)) phi = m.linspace(0, 2 * m.pi, len(y)) rv, pv = m.meshgrid(rho, phi) # map points to x, y and make a grid for the original samples xv, yv = polar_to_cart(rv, pv) # interpolate the function onto the new points f = interpolate.RegularGridInterpolator((y, x), data, bounds_error=False, fill_value=0) return rho, phi, f((yv, xv), method='linear')
def resample_2d(array, sample_pts, query_pts): """Resample 2D array to be sampled along queried points. Parameters ---------- array : `numpy.ndarray` 2D array sample_pts : `tuple` pair of `numpy.ndarray` objects that contain the x and y sample locations, each array should be 1D query_pts : `tuple` points to interpolate onto, also 1D for each array Returns ------- `numpy.ndarray` array resampled onto query_pts via bivariate spline """ xq, yq = m.meshgrid(*query_pts) interpf = interpolate.RectBivariateSpline(*sample_pts, array) return interpf.ev(yq, xq)
def bandreject_filter(array, sample_spacing, wllow, wlhigh): sy, sx = array.shape # compute the bandpass in sample coordinates ux, uy = forward_ft_unit(sample_spacing, sx), forward_ft_unit(sample_spacing, sy) fhigh, flow = 1 / wllow, 1 / wlhigh # make an ordinate array in frequency space and use it to make a mask uxx, uyy = m.meshgrid(ux, uy) highpass = ((uxx < -fhigh) | (uxx > fhigh)) | ((uyy < -fhigh) | (uyy > fhigh)) lowpass = ((uxx > -flow) & (uxx < flow)) & ((uyy > -flow) & (uyy < flow)) mask = highpass | lowpass # adjust NaNs and FFT work = array.copy() work[~m.isfinite(work)] = 0 fourier = m.fftshift(m.fft2(m.ifftshift(work))) fourier[mask] = 0 out = m.fftshift(m.ifft2(m.ifftshift(fourier))) return out.real
def analytic_ft(self, unit_x, unit_y): """Analytic fourier transform of a slit. Parameters ---------- unit_x : `numpy.ndarray` sample points in x frequency axis unit_y : `numpy.ndarray` sample points in y frequency axis Returns ------- `numpy.ndarray` 2D numpy array containing the analytic fourier transform """ xq, yq = m.meshgrid(unit_x, unit_y) # factor of pi corrects for jinc being modulo pi # factor of 2 converts radius to diameter rho = m.sqrt(xq**2 + yq**2) * self.width * 2 * m.pi return m.jinc(rho).astype(config.precision)
def window_2d_welch(x, y, alpha=8): """Return a 2D welch window for a given alpha. Parameters ---------- x : `numpy.ndarray` x values, 1D array y : `numpy.ndarray` y values, 1D array alpha : `float` alpha (edge roll) parameter Returns ------- `numpy.ndarray` window """ xx, yy = m.meshgrid(x, y) r, _ = cart_to_polar(xx, yy) rmax = m.sqrt(x.max()**2 + y.max()**2) window = 1 - abs(r/rmax)**alpha return window
def analytic_ft(self, unit_x, unit_y): """Analytic fourier transform of a slit. Parameters ---------- unit_x : `numpy.ndarray` sample points in x frequency axis unit_y : `numpy.ndarray` sample points in y frequency axis Returns ------- `numpy.ndarray` 2D numpy array containing the analytic fourier transform """ xq, yq = m.meshgrid(unit_x, unit_y) if self.width_x > 0 and self.width_y > 0: return (m.sinc(xq * self.width_x) + m.sinc(yq * self.width_y)).astype(config.precision) elif self.width_x > 0 and self.width_y is 0: return m.sinc(xq * self.width_x).astype(config.precision) else: return m.sinc(yq * self.width_y).astype(config.precision)
def make_xy_grid(samples_x, samples_y=None): """Create an x, y grid from -1, 1 with n number of samples. Parameters ---------- samples_x : `int` number of samples in x direction samples_y : `int` number of samples in y direction, if None, copied from sample_x Returns ------- xx : `numpy.ndarray` x meshgrid yy : `numpy.ndarray` y meshgrid """ if samples_y is None: samples_y = samples_x x = m.linspace(-1, 1, samples_x, dtype=config.precision) y = m.linspace(-1, 1, samples_y, dtype=config.precision) xx, yy = m.meshgrid(x, y) return xx, yy
def generate_mask(vertices, num_samples=128): """Create a filled convex polygon mask based on the given vertices. Parameters ---------- vertices : `iterable` ensemble of vertice (x,y) coordinates, in array units num_samples : `int` number of points in the output array along each dimension Returns ------- `numpy.ndarray` polygon mask """ vertices = m.asarray(vertices) unit = m.arange(num_samples) xxyy = m.stack(m.meshgrid(unit, unit), axis=2) # use delaunay to fill from the vertices and produce a mask triangles = Delaunay(vertices, qhull_options='Qj Qf') mask = ~(triangles.find_simplex(xxyy) < 0) return mask
def inverted_circle(samples=128, radius=1): """ Create an inverted circular mask (obscuration). Parameters ---------- samples : `int`, optional number of samples in the square output array Returns ------ `numpy.ndarray` binary ndarray representation of the mask """ if radius is 0: return m.ones((samples, samples)) else: x = m.linspace(-1, 1, samples) y = x xx, yy = m.meshgrid(x, y) rho, phi = cart_to_polar(xx, yy) mask = m.ones(rho.shape) mask[rho < radius] = 0 return mask
def bandlimited_rms(ux, uy, psd, wllow=None, wlhigh=None, flow=None, fhigh=None): """Calculate the bandlimited RMS of a signal from its PSD. Parameters ---------- ux : `numpy.ndarray` x spatial frequencies uy : `numpy.ndarray` y spatial frequencies psd : `numpy.ndarray` power spectral density wllow : `float` short spatial scale wlhigh : `float` long spatial scale flow : `float` low frequency fhigh : `float` high frequency Returns ------- `float` band-limited RMS value. """ if wllow is not None or wlhigh is not None: # spatial period given if wllow is None: flow = 0 else: fhigh = 1 / wllow if wlhigh is None: fhigh = max(ux[-1], uy[-1]) else: flow = 1 / wlhigh elif flow is not None or fhigh is not None: # spatial frequency given if flow is None: flow = 0 if fhigh is None: fhigh = max(ux[-1], uy[-1]) else: raise ValueError('must specify either period (wavelength) or frequency') ux2, uy2 = m.meshgrid(ux, uy) r, p = cart_to_polar(ux2, uy2) if flow is None: warnings.warn('no lower limit given, using 0 for low frequency') flow = 0 if fhigh is None: warnings.warn('no upper limit given, using limit imposed by data.') fhigh = r.max() work = psd.copy() work[r < flow] = 0 work[r > fhigh] = 0 first = m.trapz(work, uy, axis=0) second = m.trapz(first, ux, axis=0) return m.sqrt(second)
def window_2d_welch(x, y, alpha=8): xx, yy = m.meshgrid(x, y) r, _ = cart_to_polar(xx, yy) rmax = m.sqrt(x.max()**2 + y.max()**2) window = 1 - abs(r / rmax)**alpha return window