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, 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, 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 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')
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)
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)
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