Example #1
0
File: lens.py Project: chllym/prysm
    def plot_mtf_thrufocus(self,
                           field_index,
                           focus_range,
                           numpts,
                           freqs,
                           fig=None,
                           ax=None):
        focus, mtfs = self._make_mtf_thrufocus(field_index, focus_range,
                                               numpts)
        t = []
        s = []
        for mtf in mtfs:
            t.append(mtf.exact_polar(freqs, 0))
            s.append(mtf.exact_polar(freqs, 90))

        t, s = m.asarray(t), m.asarray(s)
        fig, ax = share_fig_ax(fig, ax)
        for idx, freq in enumerate(freqs):
            l, = ax.plot(focus, t[:, idx], lw=2, label=freq)
            ax.plot(focus, s[:, idx], lw=2, ls='--', c=l.get_color())
        ax.legend(title=r'$\nu$ [cy/mm]')
        ax.set(xlim=(focus[0], focus[-1]),
               xlabel=r'Defocus [$\mu m$]',
               ylim=(0, 1),
               ylabel='MTF [Rel. 1.0]',
               title='Through Focus MTF')

        return fig, ax
Example #2
0
def read_trioptics_mtf(file, metadata=False):
    """Read MTF data from a Trioptics data file.

    Parameters
    ----------
    file : `str` or path_like or file_like
        contents of a file, path_like to the file, or file object
    metadata : `bool`
        whether to also extract and return metadata

    Returns
    -------
    `dict`
        dictionary with keys focus, wavelength, freq, tan, sag
        if metadata=True, also has keys in the return of
        `io.parse_trioptics_metadata`.

    """
    data = read_file_stream_or_path(file)
    data = data[:len(data) // 10]

    # compile regex scanners to grab wavelength, focus, and frequency information
    # in addition to the T, S MTF data.
    # lastly, compile a scanner to cut the file after the end of the "MTF Sagittal" scanner
    focus_scanner = re.compile(r'Focus Position  : (\-?\d+\.\d+) mm')
    data_scanner = re.compile(r'\r\n(\d+\.?\d?)=09\r\n(\d+\.\d+)=09')
    sag_scanner = re.compile(
        r'Measurement Table: MTF vs. Frequency \( Sagittal \)')
    blockend_scanner = re.compile(r'  _____ =20')

    sagpos, cutoff = sag_scanner.search(data).end(), None
    for blockend in blockend_scanner.finditer(data):
        if blockend.end() > sagpos and cutoff is None:
            cutoff = blockend.end()

    # get focus and wavelength
    focus_pos = float(focus_scanner.search(data).group(1))

    # simultaneously grab frequency and MTF
    result = data_scanner.findall(data[:cutoff])
    freqs, mtfs = [], []
    for dat in result:
        freqs.append(float(dat[0]))
        mtfs.append(dat[1])

    breakpt = len(mtfs) // 2
    t = m.asarray(mtfs[:breakpt], dtype=config.precision)
    s = m.asarray(mtfs[breakpt:], dtype=config.precision)
    freqs = tuple(freqs[:breakpt])

    res = {
        'focus': focus_pos,
        'freq': freqs,
        'tan': t,
        'sag': s,
    }
    if metadata is True:
        return {**res, **parse_trioptics_metadata(data)}
    else:
        return res
Example #3
0
def read_trioptics_mtf_vs_field(file, metadata=False):
    """Read tangential and sagittal MTF data from a Trioptics .mht file.

    Parameters
    ----------
    file : `str` or path_like or file_like
        contents of a file, path_like to the file, or file object
    metadata : `bool`
        whether to also extract and return metadata

    Returns
    -------
    `dict`
        dictionary with keys of freq, field, tan, sag

    """
    data = read_file_stream_or_path(file)
    data = data[:len(data) //
                10]  # only search in a subset of the file for speed

    # compile a pattern that will search for the image heights in the file and extract
    fields_pattern = re.compile(
        f'MTF=09{os.linesep}(.*?){os.linesep}Legend=09', flags=re.DOTALL)
    fields = fields_pattern.findall(data)[0]  # two copies, only need 1st

    # make a pattern that will search for and extract the tan and sag MTF data.  The match will
    # return two copies; one for vs imght, one for vs angle.  Only keep half the matches.
    tan_pattern = re.compile(r'Tan(.*?)=97', flags=re.DOTALL)
    sag_pattern = re.compile(r'Sag(.*?)=97', flags=re.DOTALL)
    tan, sag = tan_pattern.findall(data), sag_pattern.findall(data)
    endpt = len(tan) // 2
    tan, sag = tan[:endpt], sag[:endpt]

    # now extract the freqs from the tan data
    freqs = m.asarray([float(s.split('(')[0][1:]) for s in tan])

    # lastly, extract the floating point tan and sag data
    # also take fields, to the 4th decimal place (nearest .1um)
    # reformat T/S to 2D arrays with indices of (freq, field)
    tan = m.asarray([s.split('=09')[1:-1] for s in tan],
                    dtype=config.precision)
    sag = m.asarray([s.split('=09')[1:-1] for s in sag],
                    dtype=config.precision)
    fields = m.asarray(fields.split('=09')[0:-1],
                       dtype=config.precision).round(4)
    res = {
        'freq': freqs,
        'field': fields,
        'tan': tan,
        'sag': sag,
    }
    if metadata is True:
        return {**res, **parse_trioptics_metadata(data)}
    else:
        return res
Example #4
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
    }
Example #5
0
    def barplot(self, orientation='h', buffer=1, zorder=3, fig=None, ax=None):
        """Create a barplot of coefficients and their names.

        Parameters
        ----------
        orientation : `str`, {'h', 'v', 'horizontal', 'vertical'}
            orientation of the plot
        buffer : `float`, optional
            buffer to use around the left and right (or top and bottom) bars
        zorder : `int`, optional
            zorder of the bars.  Use zorder > 3 to put bars in front of gridlines
        fig : `matplotlib.figure.Figure`
            Figure containing the plot
        ax : `matplotlib.axes.Axis`
            Axis containing the plot

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

        """
        from matplotlib import pyplot as plt
        fig, ax = share_fig_ax(fig, ax)

        coefs = m.asarray(self.coefs)
        idxs = m.asarray(range(len(coefs))) + self.base
        names = self.names
        lab = f'{self.zaxis_label} [{self.phase_unit}]'
        lims = (idxs[0] - buffer, idxs[-1] + buffer)
        if orientation.lower() in ('h', 'horizontal'):
            vmin, vmax = coefs.min(), coefs.max()
            drange = vmax - vmin
            offset = drange * 0.01

            ax.bar(idxs, self.coefs, zorder=zorder)
            plt.xticks(idxs, names, rotation=90)
            for i in idxs:
                ax.text(i, offset, str(i), ha='center')
            ax.set(ylabel=lab, xlim=lims)
        else:
            ax.barh(idxs, self.coefs, zorder=zorder)
            plt.yticks(idxs, names)
            for i in idxs:
                ax.text(0, i, str(i), ha='center')
            ax.set(xlabel=lab, ylim=lims)
        return fig, ax
Example #6
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
Example #7
0
def guarantee_array(variable):
    """Guarantee that a varaible is a numpy ndarray and supports -, *, +, and other operators.

    Parameters
    ----------
    variable : `number` or `numpy.ndarray`
        variable to coalesce

    Returns
    -------
    `object`
        an object that  supports * / and other operations with ndarrays

    Raises
    ------
    ValueError
        non-numeric type

    """
    if type(variable) in [
            float, m.ndarray, m.int32, m.int64, m.float32, m.float64,
            m.complex64, m.complex128
    ]:
        return variable
    elif type(variable) is int:
        return float(variable)
    elif type(variable) is list:
        return m.asarray(variable)
    else:
        raise ValueError(f'variable is of invalid type {type(variable)}')
Example #8
0
    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]
Example #9
0
File: otf.py Project: chllym/prysm
    def exact_xy(self, x, y=None):
        """Retrieve the MTF at the specified X-Y frequency pairs.

        Parameters
        ----------
        x : iterable
            X frequencies to retrieve the MTF at
        y : iterable
            Y frequencies to retrieve the MTF at

        Returns
        -------
        `list`
            MTF at the given points

        """
        self._make_interp_function_2d()

        # handle data along just x
        if y is None:
            if type(x) in (int, float):
                # single azimuth
                y = 0
            else:
                y = [0] * len(x)

        elif type(y) in (int, float):
            y = [y] * len(x)

        # handle data just along y
        if type(x) in (int, float):
            x = [x] * len(y)

        x, y = m.asarray(x), m.asarray(y)
        outs = []
        for x, y in zip(x, y):
            outs.append(float(self.interpf_2d((x, y), method='linear')))
        return m.asarray(outs)
Example #10
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))
Example #11
0
def diffraction_limited_mtf(fno, wavelength, frequencies=None, samples=128):
    """Give the diffraction limited MTF for a circular pupil and the given parameters.

    Parameters
    ----------
    fno : `float`
        f/# of the lens.
    wavelength : `float`
        wavelength of light, in microns.
    frequencies : `numpy.ndarray`
        spatial frequencies of interest, in cy/mm if frequencies are given, samples is ignored.
    samples : `int`
        number of points in the output array, if frequencies not given.

    Returns
    -------
    if frequencies not given:
        frequencies : `numpy.ndarray`
            array of ordinate data
        mtf : `numpy.ndarray`
            array of coordinate data
    else:
        mtf : `numpy.ndarray`
            array of MTF data

    Notes
    -----
    If frequencies are given, just returns the MTF.  If frequencies are not
    given, returns both the frequencies and the MTF.

    """
    extinction = 1 / (wavelength / 1000 * fno)
    if frequencies is None:
        normalized_frequency = m.linspace(0, 1, samples)
    else:
        normalized_frequency = m.asarray(frequencies) / extinction
        try:
            normalized_frequency[normalized_frequency > 1] = 1  # clamp values
        except TypeError:  # single freq
            if normalized_frequency > 1:
                normalized_frequency = 1

    mtf = _difflim_mtf_core(normalized_frequency)

    if frequencies is None:
        return normalized_frequency * extinction, mtf
    else:
        return mtf
Example #12
0
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)
Example #13
0
    def from_trioptics_files(paths, azimuths, upsample=10, ret=('tan', 'sag')):
        """Convert a set of trioptics files to MTF FFD object(s).

        Parameters
        ----------
        paths : path_like
            paths to trioptics files
        azimuths : iterable of `strs`
            azimuths, one per path
        ret : tuple, optional
            strings representing outputs, {'tan', 'sag'} are the only currently implemented options

        Returns
        -------
        `MTFFFD`
            MTF FFD object

        Raises
        ------
        NotImplemented
            return option is not available

        """
        azimuths = m.radians(m.asarray(azimuths, dtype=m.float64))
        freqs, ts, ss = [], [], []
        for path, angle in zip(paths, azimuths):
            d = read_trioptics_mtf_vs_field(path)
            imght, freq, t, s = d['field'], d['freq'], d['tan'], d['sag']
            freqs.append(freq)
            ts.append(t)
            ss.append(s)

        xx, yy, tan, sag = radial_mtf_to_mtfffd_data(ts,
                                                     ss,
                                                     imght,
                                                     azimuths,
                                                     upsample=10)
        if ret == ('tan', 'sag'):
            return MTFFFD(tan, xx, yy, freq), MTFFFD(sag, xx, yy, freq)
        else:
            raise NotImplementedError('other returns not implemented')
Example #14
0
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')
Example #15
0
File: otf.py Project: chllym/prysm
    def exact_polar(self, freqs, azimuths=None):
        """Retrieve the MTF at the specified frequency-azimuth pairs.

        Parameters
        ----------
        freqs : iterable
            radial frequencies to retrieve MTF for
        azimuths : iterable
            corresponding azimuths to retrieve MTF for

        Returns
        -------
        `list`
            MTF at the given points

        """
        self._make_interp_function_2d()

        # handle user-unspecified azimuth
        if azimuths is None:
            if type(freqs) in (int, float):
                # single azimuth
                azimuths = 0
            else:
                azimuths = [0] * len(freqs)
        # handle single azimuth, multiple freqs
        elif type(azimuths) in (int, float):
            azimuths = [azimuths] * len(freqs)

        azimuths = m.radians(azimuths)
        # handle single value case
        if type(freqs) in (int, float):
            x, y = polar_to_cart(freqs, azimuths)
            return float(self.interpf_2d((x, y), method='linear'))

        outs = []
        for freq, az in zip(freqs, azimuths):
            x, y = polar_to_cart(freq, az)
            outs.append(float(self.interpf_2d((x, y), method='linear')))
        return m.asarray(outs)
Example #16
0
    def __init__(self, *args, **kwargs):
        """Initialize a new Zernike instance."""
        if args is not None:
            if len(args) is 0:
                self.coefs = m.zeros(len(self._map), dtype=config.precision)
            else:
                self.coefs = m.asarray([*args[0]], dtype=config.precision)

        self.normalize = False
        pass_args = {}

        self.base = config.zernike_base
        try:
            bb = kwargs['base']
            if bb > 1:
                raise ValueError('It violates convention to use a base greater than 1.')
            elif bb < 0:
                raise ValueError('It is nonsensical to use a negative base.')
            self.base = bb
        except KeyError:
            # user did not specify base
            pass

        if kwargs is not None:
            for key, value in kwargs.items():
                if key[0].lower() == 'z':
                    idx = int(key[1:])  # strip 'Z' from index
                    self.coefs[idx - self.base] = value
                elif key in ('norm'):
                    self.normalize = value
                elif key.lower() == 'base':
                    self.base = value
                else:
                    pass_args[key] = value

        super().__init__(**pass_args)
Example #17
0
def generate_vertices(num_sides, radius=1):
    """Generate a list of vertices for a convex regular polygon with the given number of sides and radius.

    Parameters
    ----------
    num_sides : `int`
        number of sides to the polygon
    radius : `float`
        radius of the polygon

    Returns
    -------
    `numpy.ndarray`
        array with first column X points, second column Y points

    """
    angle = 2 * m.pi / num_sides
    pts = []
    for point in range(num_sides):
        x = radius * m.sin(point * angle)
        y = radius * m.cos(point * angle)
        pts.append((int(x), int(y)))

    return m.asarray(pts)
Example #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
Example #19
0
def zernikefit(data, x=None, y=None, rho=None, phi=None, terms=16, norm=False, residual=False, round_at=6, map_='fringe'):
    """Fits a number of Zernike coefficients to provided data.

    Works by minimizing the mean square error  between each coefficient and the
    given data.  The data should be uniformly sampled in an x,y grid.

    Parameters
    ----------
    data : `numpy.ndarray`
        data to fit to.
    x : `numpy.ndarray`, optional
        x coordinates, same shape as data
    y : `numpy.ndarray`, optional
        y coordinates, same shape as data
    rho : `numpy.ndarray`, optional
        radial coordinates, same shape as data
    phi : `numpy.ndarray`, optional
        azimuthal coordinates, same shape as data
    terms : `int`, optional
        number of terms to fit, fits terms 0~terms
    norm : `bool`, optional
        if True, normalize coefficients to unit RMS value
    residual : `bool`, optional
        if True, return a tuple of (coefficients, residual)
    round_at : `int`, optional
        decimal place to round values at.
    map_ : `str`, optional, {'fringe', 'noll'}
        which ordering of Zernikes to use

    Returns
    -------
    coefficients : `numpy.ndarray`
        an array of coefficients matching the input data.
    residual : `float`
        RMS error between the input data and the fit.

    Raises
    ------
    ValueError
        too many terms requested.

    """
    map_ = maps[map_]
    if terms > len(fringemap):
        raise ValueError(f'number of terms must be less than {len(fringemap)}')

    data = data.T  # transpose to mimic transpose of zernikes

    # precompute the valid indexes in the original data
    pts = m.isfinite(data)

    if x is None and rho is None:
        # set up an x/y rho/phi grid to evaluate Zernikes on
        rho, phi = make_rho_phi_grid(*reversed(data.shape))
        rho = rho[pts].flatten()
        phi = phi[pts].flatten()
    elif rho is None:
        rho, phi = cart_to_polar(x, y)
        rho, phi = rho[pts].flatten(), phi[pts].flatten()

    # compute each Zernike term
    zerns_raw = []
    for i in range(terms):
        func = zernikes[map_[i]]
        base_zern = func(rho, phi)
        if norm:
            base_zern *= func.norm
        zerns_raw.append(base_zern)
    zerns = m.asarray(zerns_raw).T

    # use least squares to compute the coefficients
    meas_pts = data[pts].flatten()
    coefs = m.lstsq(zerns, meas_pts, rcond=None)[0]
    if round_at is not None:
        coefs = coefs.round(round_at)

    if residual is True:
        components = []
        for zern, coef in zip(zerns_raw, coefs):
            components.append(coef * zern)

        _fit = m.asarray(components)
        _fit = _fit.sum(axis=0)
        rmserr = rms(data[pts].flatten() - _fit)
        return coefs, rmserr
    else:
        return coefs
Example #20
0
 def names(self):
     """Names of the terms in self."""
     # need to call through class variable to avoid insertion of self as arg
     idxs = m.asarray(range(len(self.coefs))) + self.base
     return [self.__class__._namer(i, base=self.base) for i in idxs]  # NOQA
Example #21
0
def radial_mtf_to_mtfffd_data(tan, sag, imagehts, azimuths, upsample):
    """Take radial MTF data and map it to inputs to the MTFFFD constructor.

    Performs upsampling/interpolation in cartesian coordinates

    Parameters
    ----------
    tan : `np.ndarray`
        tangential data
    sag : `np.ndarray`
        sagittal data
    imagehts : `np.ndarray`
        array of image heights
    azimuths : iterable
        azimuths corresponding to the first dimension of the tan/sag arrays
    upsample : `float`
        upsampling factor

    Returns
    -------
    out_x : `np.ndarray`
        x coordinates of the output data
    out_y : `np.ndarray`
        y coordinates of the output data
    tan : `np.ndarray`
        tangential data
    sag : `np.ndarray`
        sagittal data

    """
    azimuths = m.asarray(azimuths)
    imagehts = m.asarray(imagehts)

    if imagehts[0] > imagehts[-1]:
        # distortion profiled, values "reversed"
        # just flip imagehts, since spacing matters and not exact values
        imagehts = imagehts[::-1]
    amin, amax = min(azimuths), max(azimuths)
    imin, imax = min(imagehts), max(imagehts)
    aq = m.linspace(amin, amax, int(len(azimuths) * upsample))
    iq = m.linspace(imin, imax, int(
        len(imagehts) * 4))  # hard-code 4x linear upsample, change later
    aa, ii = m.meshgrid(aq, iq, indexing='ij')

    # for each frequency, build an interpolating function and upsample
    up_t = m.empty((len(aq), tan.shape[1], len(iq)))
    up_s = m.empty((len(aq), sag.shape[1], len(iq)))
    for idx in range(tan.shape[1]):
        t, s = tan[:, idx, :], sag[:, idx, :]
        interpft = RGI((azimuths, imagehts), t, method='linear')
        interpfs = RGI((azimuths, imagehts), s, method='linear')
        up_t[:, idx, :] = interpft((aa, ii))
        up_s[:, idx, :] = interpfs((aa, ii))

    # compute the locations of the samples on a cartesian grid
    xd, yd = m.outer(m.cos(m.radians(aq)),
                     iq), m.outer(m.sin(m.radians(aq)), iq)
    samples = m.stack([xd.ravel(), yd.ravel()], axis=1)

    # for the output cartesian grid, figure out the x-y coverage and build a regular grid
    absamin = min(abs(azimuths))
    closest_to_90 = azimuths[m.argmin(azimuths - 90)]
    xfctr = m.cos(m.radians(absamin))
    yfctr = m.cos(m.radians(closest_to_90))
    xmin, xmax = imin * xfctr, imax * xfctr
    ymin, ymax = imin * yfctr, imax * yfctr
    xq, yq = m.linspace(xmin, xmax, len(iq)), m.linspace(ymin, ymax, len(iq))
    xx, yy = m.meshgrid(xq, yq)

    outt, outs = [], []
    # for each frequency, interpolate onto the cartesian grid
    for idx in range(up_t.shape[1]):
        datt = griddata(samples,
                        up_t[:, idx, :].ravel(), (xx, yy),
                        method='linear')
        dats = griddata(samples,
                        up_s[:, idx, :].ravel(), (xx, yy),
                        method='linear')
        outt.append(datt.reshape(xx.shape))
        outs.append(dats.reshape(xx.shape))

    outt, outs = m.rollaxis(m.asarray(outt), 0,
                            3), m.rollaxis(m.asarray(outs), 0, 3)
    return xq, yq, outt, outs