Пример #1
0
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
Пример #2
0
    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)
Пример #3
0
    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)
Пример #4
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')
Пример #5
0
    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)
Пример #6
0
    def total_integrated_scatter(self, wavelength, incident_angle=0):
        """Calculate the total integrated scatter (TIS) for an angle or angles.

        Parameters
        ----------
        wavelength : `float`
            wavelength of light in microns.
        incident_angle : `float` or `numpy.ndarray`
            incident angle(s) of light.

        Returns
        -------
        `float` or `numpy.ndarray`
            TIS value.

        """
        if self.spatial_unit != 'μm':
            raise ValueError('Use microns for spatial unit when evaluating TIS.')

        upper_limit = 1 / wavelength
        kernel = 4 * m.pi * m.cos(m.radians(incident_angle))
        kernel *= self.bandlimited_rms(upper_limit, None) / wavelength
        return 1 - m.exp(-kernel**2)
Пример #7
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