def _unequal_spacing_conv_core(psf1, psf2): '''Interpolates psf2 before using fft-based convolution Args: psf1 (prysm.PSF): PSF. This one defines the sampling of the output. psf2 (prysm.PSF): PSF. This one will have its frequency response truncated. Returns: PSF: a new `PSF` that is the convolution of psf1 and psf2. ''' # map psf1 into the fourier domain ft1 = fft2(fftshift(psf1.data)) unit1x = forward_ft_unit(psf1.sample_spacing, psf1.samples_x) unit1y = forward_ft_unit(psf1.sample_spacing, psf1.samples_y) # map psf2 into the fourier domain ft2 = fft2(fftshift(psf2.data)) unit2x = forward_ft_unit(psf2.sample_spacing, psf2.samples_x) unit2y = forward_ft_unit(psf2.sample_spacing, psf2.samples_y) ft3 = ifftshift(resample_2d_complex(fftshift(ft2), (unit2y, unit2x), (unit1y, unit1x))) psf3 = PSF(data=abs(ifftshift(ifft2(ft1 * ft3))), sample_spacing=psf1.sample_spacing) return psf3._renorm()
def mtf(): x, y = forward_ft_unit(1 / 1e3, 128), forward_ft_unit(1 / 1e3, 128) xx, yy = np.meshgrid(x, y) dat = np.sin(xx) return otf.MTF( data=dat, unit_x=x) # do not pass unit_y, simultaneous test for unit_y=None
def conv(self, psf2): '''Convolves this PSF with another Args: psf2 (`PSF`): PSf to convolve with this one. Returns: PSF: A new `PSF` that is the convolution of these two PSFs. Notes: output PSF has equal sampling to whichever PSF has a lower nyquist frequency. ''' try: psf_ft = fftshift(fft2(self.data)) psf_unit_x = forward_ft_unit(self.sample_spacing, self.samples_x) psf_unit_y = forward_ft_unit(self.sample_spacing, self.samples_y) psf2_ft = psf2.analytic_ft(psf_unit_x, psf_unit_y) psf3 = PSF(data=abs(ifft2(psf_ft * psf2_ft)), sample_spacing=self.sample_spacing) return psf3._renorm() except AttributeError: # no analytic FT on the PSF/subclass print('No analytic FT, falling back to numerical approach.') return convpsf(self, psf2)
def test_otf_calc_correct(): x, y = forward_ft_unit(1 / 1e3, 128), forward_ft_unit(1 / 1e3, 128) xx, yy = np.meshgrid(x, y) dat = np.sin(xx) otf_ = otf.otf_from_psf(dat, x[1] - x[0]) center = tuple(s // 2 for s in otf_.shape) assert otf_.data[center] == 1 + 0j
def from_psf(psf): ''' Generates an MTF from a PSF. Args: psf (:class:`PSF`): PSF to compute an MTF from. Returns: :class:`MTF`: A new MTF instance. ''' dat = abs(fftshift(fft2(psf.data))) unit_x = forward_ft_unit(psf.sample_spacing, psf.samples_x) unit_y = forward_ft_unit(psf.sample_spacing, psf.samples_y) return MTF(dat / dat[psf.center_x, psf.center_y], unit_x, unit_y)
def show_fourier(self, interp_method='lanczos', fig=None, ax=None): ''' Displays the fourier transform of the image. Args: interp_method (`string`): method used to interpolate the data for display. fig (`matplotlib.figure`): figure to plot in. ax (`matplotlib.axis`): axis to plot in. Returns: `tuple` containing: `matplotlib.figure`: figure containing the plot. `matplotlib.axis`: axis containing the plot. ''' dat = abs(fftshift(fft2(pad2d(self.data)))) dat /= dat.max() unit_x = forward_ft_unit(self.sample_spacing, self.samples_x) unit_y = forward_ft_unit(self.sample_spacing, self.samples_y) xmin, xmax = unit_x[0], unit_x[-1] ymin, ymax = unit_y[0], unit_y[-1] fig, ax = share_fig_ax(fig, ax) im = ax.imshow(dat**0.1, extent=[xmin, xmax, ymin, ymax], cmap='Greys_r', interpolation=interp_method, origin='lower') fig.colorbar(im) ax.set(xlabel='Spatial Frequency X [cy/mm]', ylabel='Spatial Frequency Y [cy/mm]', title='Normalized FT of image, to 0.1 power') return fig, ax
def mtf(): x, y = forward_ft_unit(1/1e3, 128), forward_ft_unit(1/1e3, 128) xx, yy = np.meshgrid(x, y) dat = np.sin(xx) return otf.MTF(data=dat, x=x, y=y)
def __init__(self, ifn, Nact=50, sep=10, shift=(0, 0), rot=(0, 0, 0), upsample=1, spline_order=3, mask=None): """Create a new DM model. This model is based on convolution of a 'poke lattice' with the influence function. It has the following idiosyncracies: 1. The poke lattice is always "FFT centered" on the array, i.e. centered on the sample which would contain the DC frequency bin after an FFT. 2. The rotation is applied in the same sampling as ifn 3. Shifts and resizing are applied using a Fourier method and not subject to quantization Parameters ---------- ifn : numpy.ndarray influence function; assumes the same for all actuators and must be the same shape as (x,y). Assumed centered on N//2th sample of x, y. Assumed to be well-conditioned for use in convolution, i.e. compact compared to the array holding it Nact : int or tuple of int, length 2 (X, Y) actuator counts sep : int or tuple of int, length 2 (X, Y) actuator separation, samples of influence function shift : tuple of float, length 2 (X, Y) shift of the actuator grid to (x, y), units of x influence function sampling. E.g., influence function on 0.1 mm grid, shift=1 = 0.1 mm shift. Positive numbers describe (rightward, downward) shifts in image coordinates (origin lower left). rot : tuple of int, length <= 3 (Z, Y, X) rotations; see coordinates.make_rotation_matrix upsample : float upsampling factor used in determining output resolution, if it is different to the resolution of ifn. mask : numpy.ndarray boolean ndarray of shape Nact used to suppress/delete/exclude actuators; 1=keep, 0=suppress """ if isinstance(Nact, int): Nact = (Nact, Nact) if isinstance(sep, int): sep = (sep, sep) x, y = make_xy_grid(ifn.shape, dx=1) # stash inputs and some computed values on self self.ifn = ifn self.Ifn = fft.fft2(ifn) self.Nact = Nact self.sep = sep self.shift = shift self.obliquity = truenp.cos(truenp.radians(truenp.linalg.norm(rot))) self.rot = rot self.upsample = upsample # prepare the poke array and supplimentary integer arrays needed to # copy it into the working array out = prepare_actuator_lattice(ifn.shape, Nact, sep, mask, dtype=x.dtype) self.mask = out['mask'] self.actuators = out['actuators'] self.actuators_work = np.zeros_like(self.actuators) self.poke_arr = out['poke_arr'] self.ixx = out['ixx'] self.iyy = out['iyy'] # rotation data self.rotmat = make_rotation_matrix(rot) XY = apply_rotation_matrix(self.rotmat, x, y) XY2 = xyXY_to_pixels((x, y), XY) self.XY = XY self.XY2 = XY2 self.needs_rot = True if np.allclose(rot, [0, 0, 0]): self.needs_rot = False # shift data if shift[0] != 0 or shift[1] != 0: # caps = Fourier variable (x -> X, y -> Y) # make 2pi/px phase ramps in 1D (much faster) # then broadcast them to 2D when they're used as transfer functions # in a Fourier convolution Y, X = [forward_ft_unit(1, s, shift=False) for s in x.shape] Xramp = np.exp(X * (-2j * np.pi * shift[0])) Yramp = np.exp(Y * (-2j * np.pi * shift[1])) shpx = x.shape shpy = tuple(reversed(x.shape)) Xramp = np.broadcast_to(Xramp, shpx) Yramp = np.broadcast_to(Yramp, shpy).T self.Xramp = Xramp self.Yramp = Yramp self.tf = [self.Ifn * self.Xramp * self.Yramp] else: self.tf = [self.Ifn]