Esempio n. 1
0
    def from_file(path, scale):
        '''Read a monochrome 8 bit per pixel file into a new Image instance.

        Parameters
        ----------
        path : `string`
            path to a file
        scale : `float`
            pixel scale, in microns

        Returns
        -------
        `Image`
            a new image object

        Notes
        -----
        TODO: proper handling of images with more than 8bpp.

        '''
        from imageio import imread
        imgarr = imread(path, flatten=True, pilmode='F')
        s = imgarr.shape
        extx, exty = s[0] * scale // 2, s[1] * scale // 2
        ux, uy = m.arange(-extx, exty, scale), m.arange(-exty, exty, scale)
        return Convolvable(data=m.flip(imgarr, axis=0) / 255,
                           unit_x=ux,
                           unit_y=uy,
                           has_analytic_ft=False)
Esempio n. 2
0
    def __init__(self, phase, intensity=None, x=None, y=None, scale='px', phase_unit='nm', meta=None):
        if x is None:  # assume x, y given together
            x = m.arange(phase.shape[1])
            y = m.arange(phase.shape[0])
            scale = 'px'
            self.lateral_res = 1

        if meta:
            wvl = meta.get('wavelength', None)
            if wvl is None:
                wvl = meta.get('Wavelength')

            if wvl is not None:
                wvl *= 1e6  # m to um
        else:
            wvl = 1

        super().__init__(unit_x=x, unit_y=y, phase=phase,
                         wavelength=wvl, phase_unit=phase_unit,
                         spatial_unit=scale)

        self.xaxis_label = 'X'
        self.yaxis_label = 'Y'
        self.zaxis_label = 'Height'
        self.intensity = intensity
        self.meta = meta

        if scale != 'px':
            self.change_spatial_unit(to=scale, inplace=True)
Esempio n. 3
0
    def from_file(path, scale):
        """Read a monochrome 8 bit per pixel file into a new Image instance.

        Parameters
        ----------
        path : `string`
            path to a file
        scale : `float`
            pixel scale, in microns

        Returns
        -------
        `Convolvable`
            a new image object

        """
        from imageio import imread
        imgarr = imread(path)
        s = imgarr.shape
        extx, exty = (s[1] * scale) / 2, (s[0] * scale) / 2
        ux, uy = m.arange(-extx, extx, scale), m.arange(-exty, exty, scale)
        return Convolvable(data=m.flip(imgarr,
                                       axis=0).astype(config.precision),
                           unit_x=ux,
                           unit_y=uy,
                           has_analytic_ft=False)
Esempio n. 4
0
    def capture(self, convolvable):
        """Sample a convolvable, mimics capturing a photo of an oversampled representation of an image.

        Parameters
        ----------
        convolvable : `prysm.Convolvable`
            a convolvable object

        Returns
        -------
        `prysm.convolvable`
            a new convolvable object, as it would be sampled by the detector

        Raises
        ------
        ValueError
            if the convolvable would have to become supersampled by the detector;
            this would lead to an inaccurate result and is not supported

        """
        # we assume the pixels are bigger than the samples in the convolvable
        samples_per_pixel = self.pixel_size / convolvable.sample_spacing
        if samples_per_pixel < 1:
            raise ValueError('Pixels smaller than samples, bindown not possible.')
        else:
            samples_per_pixel = int(m.ceil(samples_per_pixel))

        data = bindown(convolvable.data, samples_per_pixel)
        s = data.shape
        extx, exty = s[0] * self.pixel_size // 2, s[1] * self.pixel_size // 2
        ux, uy = m.arange(-extx, exty, self.pixel_size), m.arange(-exty, exty, self.pixel_size)
        self.captures.append(Convolvable(data=data, unit_x=ux, unit_y=uy, has_analytic_ft=False))
        return self.captures[-1]
Esempio n. 5
0
    def __init__(self,
                 phase,
                 intensity=None,
                 x=None,
                 y=None,
                 scale='px',
                 phase_unit='nm',
                 meta=None):
        if x is None:  # assume x, y given together
            x = m.arange(phase.shape[1])
            y = m.arange(phase.shape[0])
            scale = 'px'
            self.lateral_res = 1

        super().__init__(unit_x=x,
                         unit_y=y,
                         phase=phase,
                         wavelength=meta.get('wavelength'),
                         phase_unit=phase_unit,
                         spatial_unit='m' if scale == 'px' else scale)

        self.xaxis_label = 'X'
        self.yaxis_label = 'Y'
        self.zaxis_label = 'Height'
        self.intensity = intensity
        self.meta = meta

        if scale != 'px':
            self.change_spatial_unit(to=scale, inplace=True)

        self._psd = None
        self._psdargs = {}
Esempio n. 6
0
    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)
Esempio n. 7
0
def gaussian(sigma=0.5, samples=128):
    """Generate a gaussian mask with a given sigma.

    Parameters
    ----------
    sigma : `float`
        width parameter of the gaussian, expressed in samples of the output array

    samples : `int`
        number of samples in square array

    Returns
    -------
    `numpy.ndarray`
        mask with gaussian shape

    """
    s = sigma

    x = m.arange(0, samples, 1, dtype=config.precision)
    y = x[:, m.newaxis]

    # // is floor division in python
    x0 = y0 = samples // 2
    return m.exp(-4 * m.log(2) * ((x - x0)**2 + (y - y0)**2) /
                 (s * samples)**2)
Esempio n. 8
0
def read_mtfmapper_sfr_single(file, pixel_pitch=None):
    """Read an MTF Mapper SFR (MTF) file generated by the -f flag with --single-roi.

    Notes
    -----
    This reads a "raw_sfr_values.txt" file, not an "edge_sfr_values.txt" file.

    Parameters
    ----------
    file : `str` or path_like or file_like
        contents of a file, path_like to the file, or file object
    pixel_pitch : `float`
        center-to-center pixel spacing, in microns

    Returns
    -------
    `numpy.ndarray`
        spatial_frequencies
    `numpy.ndarray`
        mtf

    """
    data = read_file_stream_or_path(file)
    floats = [float(d) for d in data.splitlines()[0].split(' ')[:-1]]
    edge_angle, *mtf = floats
    mtf = m.asarray(mtf)
    freqs = m.arange(len(mtf)) / 64
    if pixel_pitch is not None:  # convert cy/px to cy/mm
        freqs /= (pixel_pitch / 1e3)

    return freqs, mtf
Esempio n. 9
0
def prop_pupil_plane_to_psf_plane_units(wavefunction, input_sample_spacing,
                                        prop_dist, wavelength, Q):
    """Compute the ordinate axes for a pupil plane to PSF plane propagation.

    Parameters
    ----------
    wavefunction : `numpy.ndarray`
        the pupil wavefunction
    input_sample_spacing : `float`
        spacing between samples in the pupil plane
    prop_dist : `float`
        propagation distance along the z distance
    wavelength : `float`
        wavelength of light
    Q : `float`
        oversampling / padding factor

    Returns
    -------
    unit_x : `numpy.ndarray`
        x axis unit, 1D ndarray
    unit_y : `numpy.ndarray`
        y axis unit, 1D ndarray

    """
    s = wavefunction.shape
    samples_x, samples_y = s[1] * Q, s[0] * Q
    sample_spacing_x = pupil_sample_to_psf_sample(
        pupil_sample=input_sample_spacing,  # factor of
        samples=samples_x,  # 1e3 corrects
        wavelength=wavelength,  # for unit
        efl=prop_dist) / 1e3  # translation
    sample_spacing_y = pupil_sample_to_psf_sample(
        pupil_sample=input_sample_spacing,  # factor of
        samples=samples_y,  # 1e3 corrects
        wavelength=wavelength,  # for unit
        efl=prop_dist) / 1e3  # translation
    unit_x = m.arange(-1 * int(m.ceil(samples_x / 2)),
                      int(m.floor(samples_x / 2))) * sample_spacing_x
    unit_y = m.arange(-1 * int(m.ceil(samples_y / 2)),
                      int(m.floor(samples_y / 2))) * sample_spacing_y
    return unit_x, unit_y
Esempio n. 10
0
    def from_zygo_dat(path, multi_intensity_action='first', scale='mm'):
        """Create a new interferogram from a zygo dat file.

        Parameters
        ----------
        path : path_like
            path to a zygo dat file
        multi_intensity_action : str, optional
            see `io.read_zygo_dat`
        scale : `str`, optional, {'um', 'mm'}
            what xy scale to label the data with, microns or mm

        Returns
        -------
        `Interferogram`
            new Interferogram instance

        """
        if str(path).endswith('datx'):
            # datx file, use datx reader
            zydat = read_zygo_datx(path)
            res = zydat['meta']['Lateral Resolution']
        else:
            # dat file, use dat file reader
            zydat = read_zygo_dat(path, multi_intensity_action=multi_intensity_action)
            res = zydat['meta']['lateral_resolution']  # meters

        phase = zydat['phase']
        if res == 0.0:
            res = 1
            scale = 'px'

        if scale != 'px':
            _scale = 'm'
        else:
            _scale = 'px'

        i = Interferogram(phase=phase, intensity=zydat['intensity'],
                          x=m.arange(phase.shape[1]) * res, y=m.arange(phase.shape[0]) * res,
                          scale=_scale, meta=zydat['meta'])
        return i.change_spatial_unit(to=scale.lower(), inplace=True)
Esempio n. 11
0
def read_trioptics_mtfvfvf(file, filename=None):
    """Read MTF vs Field vs Focus data from a Trioptics .txt dump.

    Parameters
    ----------
    file : `str` or path_like or file_like
        file to read from, if string of file body, must provide filename
    filename : `str`, optional
        name of file; used to select tan/sag if file is given as contents

    Returns
    -------
    `MTFvFvF`
        MTF vs Field vs Focus object

    """
    if filename is None:
        with open(file, 'r') as fid:
            lines = fid.readlines()
    else:
        lines = file.splitlines()
        file = filename

    if str(file)[-7:-4] == 'Tan':
        azimuth = 'Tan'
    else:
        azimuth = 'Sag'

    imghts, objangs, focusposes, mtfs = [], [], [], []
    for meta, data in zip(lines[0::2],
                          lines[1::2]):  # iterate 2 lines at a time
        metavalues = meta.split()
        imght, objang, focuspos, freqpitch = metavalues[1::2]
        mtf_raw = data.split()[1:]  # first element is "MTF"
        mtf = m.asarray(mtf_raw, dtype=config.precision)
        imghts.append(imght)
        objangs.append(objang)
        focusposes.append(focuspos)
        mtfs.append(mtf)

    focuses = m.unique(m.asarray(focusposes, dtype=config.precision))
    focuses = (focuses - m.mean(focuses)) * 1e3
    imghts = m.unique(m.asarray(imghts, dtype=config.precision))
    freqs = m.arange(len(mtfs[0]), dtype=config.precision) * float(freqpitch)
    data = m.swapaxes(
        m.asarray(mtfs).reshape(len(focuses), len(imghts), len(freqs)), 0, 1)
    return {
        'data': data,
        'focus': focuses,
        'field': imghts,
        'freq': freqs,
        'azimuth': azimuth
    }
Esempio n. 12
0
    def change_spatial_unit(self, to, inplace=True):
        """Change the units used to describe the spatial dimensions.

        Parameters
        ----------
        to : `str`
            new unit, a member of `OpticalPhase`.units.keys()
        inplace : `bool`, optional
            whether to change self.unit_x and self.unit_y.
            If False, returns updated phase, if True, returns self

        Returns
        -------
        `new_ux` : `np.ndarray`
            new ordinate x axis
        `new_uy` : `np.ndarray`
            new ordinate y axis
        OR
        `self` : `OpticalPhase`
            self

        """
        if to.lower() != 'px':
            fctr = self.unit_changes['_'.join(
                [self.spatial_unit, self.units[to]])](self.wavelength)
            new_ux = self.unit_x / fctr
            new_uy = self.unit_y / fctr
        else:
            sy, sx = self.shape
            new_ux = m.arange(sx, dtype=config.precision)
            new_uy = m.arange(sy, dtype=config.precision)
        if inplace:
            self.unit_x = new_ux
            self.unit_y = new_uy
            self.spatial_unit = to
            return self
        else:
            return new_ux, new_uy
Esempio n. 13
0
def psd(height, sample_spacing):
    """Compute the power spectral density of a signal.

    Parameters
    ----------
    height : `numpy.ndarray`
        height or phase data
    sample_spacing : `float`
        spacing of samples in the input data

    Returns
    -------
    unit_x : `numpy.ndarray`
        ordinate x frequency axis
    unit_y : `numpy.ndarray`
        ordinate y frequency axis
    psd : `numpy.ndarray`
        power spectral density

    """
    s = height.shape

    window = window_2d_welch(
        m.arange(s[1]) * sample_spacing,
        m.arange(s[0]) * sample_spacing)
    window = m.ones(height.shape)
    psd = prop_pupil_plane_to_psf_plane(height * window, norm='ortho')
    ux = forward_ft_unit(sample_spacing, int(round(height.shape[1], 0)))
    uy = forward_ft_unit(sample_spacing, int(round(height.shape[0], 0)))

    psd /= height.size

    # adjustment for 2D welch window bias, there is room for improvement to this
    # approximate value from:
    # Power Spectral Density Specification and Analysis of Large Optical Surfaces
    # E. Sidick, JPL
    psd /= 0.925
    return ux, uy, psd
Esempio n. 14
0
File: lens.py Progetto: chllym/prysm
    def _uniformly_spaced_fields(self, num_pts):
        ''' Changes the `fields` property to n evenly spaced points from 0~1.

        Args:
            num_pts (`int`): number of points.

        Returns:
            self.

        '''
        _ = m.arange(0, num_pts, dtype=config.precision)
        flds = _ / _.max()
        self.fields = flds
        return self
Esempio n. 15
0
def synthesize_surface_from_psd(psd, nu_x, nu_y):
    """Synthesize a surface height map from PSD data.

    Parameters
    ----------
    psd : `numpy.ndarray`
        PSD data, units nm²/(cy/mm)²
    nu_x : `numpy.ndarray`
        x spatial frequency, cy/mm
    nu_y : `numpy.ndarray`
        y spatial frequency, cy_mm

    """
    # generate a random phase to be matched to the PSD
    randnums = m.rand(*psd.shape)
    randfft = m.fft2(randnums)
    phase = m.angle(randfft)


    # calculate the output window
    # the 0th element of nu_y has the greatest frequency in magnitude because of
    # the convention to put the nyquist sample at -fs instead of +fs for even-size arrays
    fs = -2 * nu_y[0]
    dx = dy = 1 / fs
    ny, nx = psd.shape
    x, y = m.arange(nx) * dx, m.arange(ny) * dy

    # calculate the area of the output window, "S2" in GH_FFT notation
    A = x[-1] * y[-1]

    # use ifft to compute the PSD
    signal = m.exp(1j * phase) * m.sqrt(A * psd)

    coef = 1 / dx / dy
    out = m.ifftshift(m.ifft2(m.fftshift(signal))) * coef
    out = out.real
    return x, y, out
Esempio n. 16
0
def ecdf(x):
    """Compute the empirical cumulative distribution function of a dataset.

    Parameters
    ----------
    x : `iterable`
        Data

    Returns
    -------
    xs : `numpy.ndarray`
        sorted data
    ys : `numpy.ndarray`
        cumulative distribution function of the data

    """
    xs = m.sort(x)
    ys = m.arange(1, len(xs) + 1) / float(len(xs))
    return xs, ys
Esempio n. 17
0
    def top_n(self, n=5):
        """Identify the top n terms in the wavefront.

        Parameters
        ----------
        n : `int`, optional
            identify the top n terms.

        Returns
        -------
        `list`
            list of tuples (magnitude, index, term)

        """
        coefs = m.asarray(self.coefs)
        coefs_work = abs(coefs)
        oidxs = m.arange(len(coefs), dtype=int) + self.base  # "original indexes"
        idxs = m.argpartition(coefs_work, -n)[-n:]  # argpartition does some magic to identify the top n (unsorted)
        idxs = idxs[m.argsort(coefs_work[idxs])[::-1]]  # use argsort to sort them in ascending order and reverse
        big_terms = coefs[idxs]  # finally, take the values from the
        big_idxs = oidxs[idxs]
        names = m.asarray(self.names, dtype=str)[big_idxs - self.base]
        return list(zip(big_terms, big_idxs, names))
Esempio n. 18
0
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
Esempio n. 19
0
def make_window(signal, sample_spacing, which='welch'):
    """Generate a window function to be used in PSD analysis.

    Parameters
    ----------
    signal : `numpy.ndarray`
        signal or phase data
    sample_spacing : `float`
        spacing of samples in the input data
    which : `str,` {'welch', 'hann', 'auto'}, optional
        which window to produce.  If auto, attempts to guess the appropriate
        window based on the input signal

    Notes
    -----
    For 2D welch, see:
    Power Spectral Density Specification and Analysis of Large Optical Surfaces
    E. Sidick, JPL

    Returns
    -------
    `numpy.ndarray`
        window array

    """
    s = signal.shape

    if which is None:
        # attempt to guess best window
        ysamples = int(round(s[0] * 0.02, 0))
        xsamples = int(round(s[1] * 0.02, 0))
        corner1 = signal[:ysamples, :xsamples] == 0
        corner2 = signal[-ysamples:, :xsamples] == 0
        corner3 = signal[:ysamples, -xsamples:] == 0
        corner4 = signal[-ysamples:, -xsamples:] == 0
        if corner1.all() and corner2.all() and corner3.all() and corner4.all():
            # four corners all "black" -- circular data, Welch window is best
            # looks wrong but 2D welch takes x, y while indices are y, x
            y = m.arange(s[1]) * sample_spacing
            x = m.arange(s[0]) * sample_spacing
            return window_2d_welch(y, x)
        else:
            # if not circular, square data; use Hanning window
            y = m.hanning(s[0])
            x = m.hanning(s[1])
            return m.outer(y, x)
    else:
        if type(which) is str:
            # known window type
            wl = which.lower()
            if wl == 'welch':
                y = m.arange(s[1]) * sample_spacing
                x = m.arange(s[0]) * sample_spacing
                return window_2d_welch(y, x)
            elif wl in ('hann', 'hanning'):
                y = m.hanning(s[0])
                x = m.hanning(s[1])
                return m.outer(y, x)
            else:
                raise ValueError('unknown window type')
        else:
            return which  # window provided as ndarray
Esempio n. 20
0
    def plot2d(self,
               axlim=25,
               power=1,
               interp_method='lanczos',
               pix_grid=None,
               fig=None,
               ax=None,
               show_axlabels=True,
               show_colorbar=True,
               circle_ee=None):
        """Create a 2D plot of the PSF.

        Parameters
        ----------
        axlim : `float`
            limits of axis, symmetric. xlim=(-axlim,axlim), ylim=(-axlim, axlim)
        power : `float`
            power to stretch the data by for plotting
        interp_method : `string`
            method used to interpolate the image between samples of the PSF
        pix_grid : `float`
            if not None, overlays gridlines with spacing equal to pix_grid.
            Intended to show the collection into camera pixels while still in
            the oversampled domain
        fig : `matplotlib.figure.Figure`, optional:
            Figure containing the plot
        ax : `matplotlib.axes.Axis`, optional:
            Axis containing the plot
        show_axlabels : `bool`
            whether or not to show the axis labels
        show_colorbar : `bool`
            whether or not to show the colorbar
        circle_ee : `float`, optional
            relative encircled energy to draw a circle at, in addition to
            diffraction limited airy radius (1.22*λ*F#).  First airy zero occurs
            at circle_ee=0.8377850436212378

        Returns
        -------
        fig : `matplotlib.figure.Figure`, optional
            Figure containing the plot
        ax : `matplotlib.axes.Axis`, optional
            Axis containing the plot

        """
        label_str = 'Normalized Intensity [a.u.]'
        lims = (0, 1)

        left, right = self.unit_x[0], self.unit_x[-1]
        bottom, top = self.unit_y[0], self.unit_y[-1]

        fig, ax = share_fig_ax(fig, ax)

        im = ax.imshow(self.data,
                       extent=[left, right, bottom, top],
                       origin='lower',
                       cmap='Greys_r',
                       norm=colors.PowerNorm(1 / power),
                       interpolation=interp_method,
                       clim=lims)
        if show_colorbar:
            cb = fig.colorbar(im, label=label_str, ax=ax, fraction=0.046)
            cb.outline.set_edgecolor('k')
            cb.outline.set_linewidth(0.5)
        if show_axlabels:
            ax.set(xlabel=r'Image Plane $x$ [$\mu m$]',
                   ylabel=r'Image Plane $y$ [$\mu m$]')

        ax.set(xlim=(-axlim, axlim), ylim=(-axlim, axlim))

        if pix_grid is not None:
            # if pixel grid is desired, add it
            mult = m.floor(axlim / pix_grid)
            gmin, gmax = -mult * pix_grid, mult * pix_grid
            pts = m.arange(gmin, gmax, pix_grid)
            ax.set_yticks(pts, minor=True)
            ax.set_xticks(pts, minor=True)
            ax.yaxis.grid(True, which='minor', color='white', alpha=0.25)
            ax.xaxis.grid(True, which='minor', color='white', alpha=0.25)

        if circle_ee is not None:
            if self.fno is None:
                raise ValueError(
                    'F/# must be known to compute EE, set self.fno')
            elif self.wavelength is None:
                raise ValueError(
                    'wavelength must be known to compute EE, set self.wavelength'
                )

            radius = self.ee_radius(circle_ee)
            analytic = _inverse_analytic_encircled_energy(
                self.fno, self.wavelength, circle_ee)
            c_diff = patches.Circle((0, 0),
                                    analytic,
                                    fill=False,
                                    color='r',
                                    ls='--')
            c_true = patches.Circle((0, 0), radius, fill=False, color='r')
            ax.add_artist(c_diff)
            ax.add_artist(c_true)

        return fig, ax
Esempio n. 21
0
    def plot2d(self,
               axlim=25,
               interp_method='lanczos',
               pix_grid=None,
               fig=None,
               ax=None):
        '''Create a 2D color plot of the PSF.

        Parameters
        ----------
        axlim : `float`
            limits of axis, symmetric. xlim=(-axlim,axlim), ylim=(-axlim, axlim)
        interp_method : `str`
            method used to interpolate the image between samples of the PSF
        pix_grid : `float`
            if not None, overlays gridlines with spacing equal to pix_grid.
            Intended to show the collection into camera pixels while still in
            the oversampled domain
        fig : `matplotlib.figure.Figure`, optional
            Figure containing the plot
        ax : `matplotlib.axes.Axis`, optional:
            Axis containing the plot

        Returns
        -------
        fig : `matplotlib.figure.Figure`, optional
            Figure containing the plot
        ax : `matplotlib.axes.Axis`, optional:
            Axis containing the plot

        '''
        dat = m.empty((self.samples_x, self.samples_y, 3))
        dat[:, :, 0] = self.R
        dat[:, :, 1] = self.G
        dat[:, :, 2] = self.B

        left, right = self.unit_y[0], self.unit_y[-1]
        bottom, top = self.unit_x[0], self.unit_x[-1]

        fig, ax = share_fig_ax(fig, ax)

        ax.imshow(dat,
                  extent=[left, right, bottom, top],
                  interpolation=interp_method,
                  origin='lower')
        ax.set(xlabel=r'Image Plane X [$\mu m$]',
               ylabel=r'Image Plane Y [$\mu m$]',
               xlim=(-axlim, axlim),
               ylim=(-axlim, axlim))

        if pix_grid is not None:
            # if pixel grid is desired, add it
            mult = m.floor(axlim / pix_grid)
            gmin, gmax = -mult * pix_grid, mult * pix_grid
            pts = m.arange(gmin, gmax, pix_grid)
            ax.set_yticks(pts, minor=True)
            ax.set_xticks(pts, minor=True)
            ax.yaxis.grid(True, which='minor')
            ax.xaxis.grid(True, which='minor')

        return fig, ax
Esempio n. 22
0
    def plot2d_rgbgrid(self,
                       axlim=25,
                       interp_method='lanczos',
                       pix_grid=None,
                       fig=None,
                       ax=None):
        """Create a 2D color plot of the PSF and R,G,B components.

        Parameters
        ----------
        axlim : `float`
            limits of axis, symmetric. xlim=(-axlim,axlim), ylim=(-axlim, axlim)
        interp_method : `str`
            method used to interpolate the image between samples of the PSF
        pix_grid : float
            if not None, overlays gridlines with spacing equal to pix_grid.
            Intended to show the collection into camera pixels while still in
            the oversampled domain
        fig : `matplotlib.figure.Figure`, optional
            Figure containing the plot
        ax : `matplotlib.axes.Axis`, optional:
            Axis containing the plot

        fig : `matplotlib.figure.Figure`, optional
            Figure containing the plot
        ax : `matplotlib.axes.Axis`, optional:
            Axis containing the plot

        Notes
        -----
        Need to refine internal workings at some point.

        """
        # make the arrays for the RGB images
        dat = m.empty((self.samples_y, self.samples_x, 3))
        datr = m.zeros((self.samples_y, self.samples_x, 3))
        datg = m.zeros((self.samples_y, self.samples_x, 3))
        datb = m.zeros((self.samples_y, self.samples_x, 3))
        dat[:, :, 0] = self.R
        dat[:, :, 1] = self.G
        dat[:, :, 2] = self.B
        datr[:, :, 0] = self.R
        datg[:, :, 1] = self.G
        datb[:, :, 2] = self.B

        left, right = self.unit[0], self.unit[-1]
        ax_width = 2 * axlim

        # generate a figure and axes to plot in
        fig, ax = share_fig_ax(fig, ax)
        axr, axg, axb = make_rgb_axes(ax)

        ax.imshow(dat,
                  extent=[left, right, left, right],
                  interpolation=interp_method,
                  origin='lower')

        axr.imshow(datr,
                   extent=[left, right, left, right],
                   interpolation=interp_method,
                   origin='lower')
        axg.imshow(datg,
                   extent=[left, right, left, right],
                   interpolation=interp_method,
                   origin='lower')
        axb.imshow(datb,
                   extent=[left, right, left, right],
                   interpolation=interp_method,
                   origin='lower')

        for axs in (ax, axr, axg, axb):
            ax.set(xlim=(-axlim, axlim), ylim=(-axlim, axlim))
            if pix_grid is not None:
                # if pixel grid is desired, add it
                mult = m.m.floor(axlim / pix_grid)
                gmin, gmax = -mult * pix_grid, mult * pix_grid
                pts = m.arange(gmin, gmax, pix_grid)
                ax.set_yticks(pts, minor=True)
                ax.set_xticks(pts, minor=True)
                ax.yaxis.grid(True, which='minor')
                ax.xaxis.grid(True, which='minor')
        ax.set(xlabel=r'Image Plane X [$\mu m$]',
               ylabel=r'Image Plane Y [$\mu m$]')
        axr.text(-axlim + 0.1 * ax_width,
                 axlim - 0.2 * ax_width,
                 'R',
                 color='white')
        axg.text(-axlim + 0.1 * ax_width,
                 axlim - 0.2 * ax_width,
                 'G',
                 color='white')
        axb.text(-axlim + 0.1 * ax_width,
                 axlim - 0.2 * ax_width,
                 'B',
                 color='white')
        return fig, ax
Esempio n. 23
0
    def plot2d(self,
               freq,
               show_contours=True,
               cmap='inferno',
               clim=(0, 1),
               show_cb=True,
               fig=None,
               ax=None):
        """Plot the MTF FFD.

        Parameters
        ----------
        freq : `float`
            frequency to plot at
        show_contours : `bool`
            whether to plot contours
        cmap : `str`
            colormap to pass to `imshow`
        clim : `iterable`
            length 2 iterable with lower, upper bounds of colors
        show_cb : `bool`
            whether to show the colorbar or not
        fig : `matplotlib.figure.Figure`, optional
            figure containing the plot
        ax : `matplotlib.axes.Axis`
            axis containing the plot

        Returns
        -------
        fig : `matplotlib.figure.Figure`, optional
            figure containing the plot
        axis : `matplotlib.axes.Axis`
            axis containing the plot

        """
        idx = m.searchsorted(self.freq, freq)
        extx = (self.field_x[0], self.field_x[-1])
        exty = (self.field_y[0], self.field_y[-1])
        ext = [*extx, *exty]
        fig, ax = share_fig_ax(fig, ax)
        im = ax.imshow(self.data[:, :, idx],
                       extent=ext,
                       origin='lower',
                       interpolation='gaussian',
                       cmap=cmap,
                       clim=clim)

        if show_contours is True:
            if clim[0] < 0:
                contours = list(m.arange(clim[0], clim[1] + 0.1, 0.1))
            else:
                contours = [
                    0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0
                ]
            cs = ax.contour(self.data[:, :, idx],
                            contours,
                            colors='0.15',
                            linewidths=0.75,
                            extent=ext)
            ax.clabel(cs, fmt='%1.1f', rightside_up=True)

        ax.set(xlabel='Image Plane X [mm]', ylabel='Image Plane Y [mm]')
        if show_cb:
            fig.colorbar(im,
                         label=f'MTF @ {freq} cy/mm',
                         ax=ax,
                         fraction=0.046)
        return fig, ax
Esempio n. 24
0
    def plot2d(self,
               axlim=25,
               power=1,
               clim=(None, None),
               interp_method='lanczos',
               pix_grid=None,
               cmap=config.image_colormap,
               fig=None,
               ax=None,
               show_axlabels=True,
               show_colorbar=True,
               circle_ee=None,
               circle_ee_lw=None):
        """Create a 2D plot of the PSF.

        Parameters
        ----------
        axlim : `float`
            limits of axis, symmetric. xlim=(-axlim,axlim), ylim=(-axlim, axlim)
        power : `float`
            power to stretch the data by for plotting
        clim : iterable
            limits to use for log color scaling.  If power != 1 and
            clim != (None, None), clim (log axes) takes precedence
        interp_method : `string`
            method used to interpolate the image between samples of the PSF
        pix_grid : `float`
            if not None, overlays gridlines with spacing equal to pix_grid.
            Intended to show the collection into camera pixels while still in
            the oversampled domain
        cmap : `str`, optional
            colormap, passed directly to matplotlib
        fig : `matplotlib.figure.Figure`, optional:
            Figure containing the plot
        ax : `matplotlib.axes.Axis`, optional:
            Axis containing the plot
        show_axlabels : `bool`
            whether or not to show the axis labels
        show_colorbar : `bool`
            whether or not to show the colorbar
        circle_ee : `float`, optional
            relative encircled energy to draw a circle at, in addition to
            diffraction limited airy radius (1.22*λ*F#).  First airy zero occurs
            at circle_ee=0.8377850436212378
        circle_ee_lw : `float`, optional
            linewidth passed to matplotlib for the encircled energy circles

        Returns
        -------
        fig : `matplotlib.figure.Figure`, optional
            Figure containing the plot
        ax : `matplotlib.axes.Axis`, optional
            Axis containing the plot

        """
        from matplotlib import colors, patches

        label_str = 'Normalized Intensity [a.u.]'

        left, right = self.unit_x[0], self.unit_x[-1]
        bottom, top = self.unit_y[0], self.unit_y[-1]

        fig, ax = share_fig_ax(fig, ax)

        plt_opts = {
            'extent': [left, right, bottom, top],
            'origin': 'lower',
            'cmap': cmap,
            'interpolation': interp_method,
        }
        cb_opts = {}
        if power is not 1:
            plt_opts['norm'] = colors.PowerNorm(1 / power)
            plt_opts['clim'] = (0, 1)
        elif clim[1] is not None:
            plt_opts['norm'] = colors.LogNorm(*clim)
            cb_opts = {'extend': 'both'}

        im = ax.imshow(self.data, **plt_opts)
        if show_colorbar:
            cb = fig.colorbar(im,
                              label=label_str,
                              ax=ax,
                              fraction=0.046,
                              **cb_opts)
            cb.outline.set_edgecolor('k')
            cb.outline.set_linewidth(0.5)
        if show_axlabels:
            ax.set(xlabel='Image Plane x [μm]', ylabel='Image Plane y [μm]')

        ax.set(xlim=(-axlim, axlim), ylim=(-axlim, axlim))

        if pix_grid is not None:
            # if pixel grid is desired, add it
            mult = m.floor(axlim / pix_grid)
            gmin, gmax = -mult * pix_grid, mult * pix_grid
            pts = m.arange(gmin, gmax, pix_grid)
            ax.set_yticks(pts, minor=True)
            ax.set_xticks(pts, minor=True)
            ax.yaxis.grid(True, which='minor', color='white', alpha=0.25)
            ax.xaxis.grid(True, which='minor', color='white', alpha=0.25)

        if circle_ee is not None:
            if self.fno is None:
                raise ValueError(
                    'F/# must be known to compute EE, set self.fno')
            elif self.wavelength is None:
                raise ValueError(
                    'wavelength must be known to compute EE, set self.wavelength'
                )

            radius = self.ee_radius(circle_ee)
            analytic = _inverse_analytic_encircled_energy(
                self.fno, self.wavelength, circle_ee)

            c_diff = patches.Circle((0, 0),
                                    analytic,
                                    fill=False,
                                    color='r',
                                    ls='--',
                                    lw=circle_ee_lw)
            c_true = patches.Circle((0, 0),
                                    radius,
                                    fill=False,
                                    color='r',
                                    lw=circle_ee_lw)
            ax.add_artist(c_diff)
            ax.add_artist(c_true)
            ax.legend([c_diff, c_true], ['Diff. Lim.', 'Actual'], ncol=2)

        return fig, ax
Esempio n. 25
0
    def trace_focus(self, algorithm='avg'):
        """Find the focus position in each field.

        This is, in effect, the "field curvature" for this azimuth.

        Parameters
        ----------
        algorithm : `str`
            algorithm to use to trace focus, currently only supports '0.5', see
            notes for a description of this technique

        Returns
        -------
        field : `numpy.ndarray`
            array of field values, mm
        focus : `numpy.ndarray`
            array of focus values, microns

        Notes
        -----
        Algorithm '0.5' uses the frequency that has its peak closest to 0.5
        on-axis to estimate the focus coresponding to the minimum RMS WFE
        condition.  This is based on the following assumptions:
            - Any combination of third, fifth, and seventh order spherical
                aberration will produce a focus shift that depends on
                frequency, and this dependence can be well fit by an
                equation of the form y(x) = ax^2 + bx + c.  If this is true,
                then the frequency which peaks at 0.5 will be near the
                vertex of the quadratic, which converges to the min RMS WFE
                condition.
            - Coma, while it enhances depth of field, does not shift the
                focus peak.
            - Astigmatism and field curvature are the dominant cause of any
                shift in best focus with field.
            - Chromatic aberrations do not influence the thru-focus MTF peak
                in a way that varies with field.

        Raises
        ------
        ValueError
            if an unsupported algorithm is entered

        """
        if algorithm == '0.5':
            # locate the frequency index on axis
            idx_axis = m.searchsorted(self.field, 0)
            idx_freq = abs(self.data[:, idx_axis, :].max(axis=0) -
                           0.5).argmin(axis=0)
            focus_idx = self.data[:, m.arange(self.data.shape[1]),
                                  idx_freq].argmax(axis=0)
            return self.field, self.focus[focus_idx],
        elif algorithm.lower() in ('avg', 'average'):
            if self.freq[0] == 0:
                # if the zero frequency is included, exclude it from our calculations
                avg_idxs = self.data.argmax(axis=0)[:, 1:].mean(axis=1)
            else:
                avg_idxs = self.data.argmax(axis=0).mean(axis=1)

            # account for fractional indexes
            focus_out = avg_idxs.copy()
            for i, idx in enumerate(avg_idxs):
                li, ri = int(m.floor(idx)), int(m.ceil(idx))
                lf, rf = self.focus[li], self.focus[ri]
                diff = rf - lf
                part = idx % 1
                focus_out[i] = lf + diff * part

            return self.field, focus_out
        else:
            raise ValueError('0.5 is only algorithm supported')