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 analytic_ft(self, unit_x, unit_y): ''' Analytic fourier transform of a pixel aperture Args: unit_x (numpy.ndarray): sample points in x axis. unit_y (numpy.ndarray): sample points in y axis. Returns: `numpy.ndarray`: 2D numpy array containing the analytic fourier transform. ''' xq, yq = np.meshgrid(unit_x, unit_y) return (cos(2 * xq * self.width_x / 1e3) * cos(2 * yq * self.width_y / 1e3)).astype(config.precision)
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 __init__(self, angle=8, background='white', sample_spacing=2, samples=384): ''' Creates a new TitledSquare instance. Args: 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. Returns: `TiltedSquare`: new TiltedSquare instance. ''' radius = 0.3 if background.lower() == 'white': arr = np.ones((samples, samples)) fill_with = 0 else: arr = np.zeros((samples, samples)) fill_with = 1 # TODO: optimize by working with index numbers directly and avoid # creation of X,Y arrays for performance. x = np.linspace(-0.5, 0.5, samples) y = np.linspace(-0.5, 0.5, samples) xx, yy = np.meshgrid(x, y) # TODO: convert inline operation to use of rotation matrix angle = np.radians(angle) xp = xx * cos(angle) - yy * sin(angle) yp = xx * sin(angle) + yy * cos(angle) mask = (abs(xp) < radius) * (abs(yp) < radius) arr[mask] = fill_with super().__init__(data=arr, sample_spacing=sample_spacing, synthetic=True)
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)
def analytic_ft(self, unit_x, unit_y): """Analytic fourier transform of a pixel aperture. Parameters ---------- unit_x : `numpy.ndarray` sample points in x axis unit_y : `numpy.ndarray` sample points in y axis Returns ------- `numpy.ndarray` 2D numpy array containing the analytic fourier transform """ xq, yq = m.meshgrid(unit_x, unit_y) return (m.cos(2 * xq * self.width_x) * m.cos(2 * yq * self.width_y)).astype(config.precision)
def __init__(self, num_spokes, sinusoidal=True, background='black', sample_spacing=2, samples=384): ''' Produces a Siemen's Star. Args: num_spokes (`int`): number of spokes in the star. sinusoidal (`bool`): if True, generates a sinusoidal Siemen' star. If false, generates a bar/block siemen's star. background ('string'): "black" or "white". sample_spacing (`float`): Spacing of samples, in microns. samples (`int`): number of samples per dimension in the synthetic image. ''' relative_width = 0.9 self.num_spokes = num_spokes # generate a coordinate grid x = np.linspace(-1, 1, samples) y = np.linspace(-1, 1, samples) xx, yy = np.meshgrid(x, y) rv, pv = cart_to_polar(xx, yy) # generate the siemen's star as a (rho,phi) polynomial arr = cos(num_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, sample_spacing=sample_spacing, synthetic=True)
def cie_1976_wavelength_annotations(wavelengths, fig=None, ax=None): ''' Draws lines normal to the spectral locust on a CIE 1976 diagram and writes the text for each wavelength. Args: wavelengths (`iterable`): set of wavelengths to annotate. fig (`matplotlib.figure.Figure`): figure to draw on. ax (`matplotlib.axes.Axis`): axis to draw in. Returns: `tuple` containing: `matplotlib.figure.Figure`: figure containing the annotations. `matplotlib.axes.Axis`: axis containing the annotations. Notes: see SE: https://stackoverflow.com/questions/26768934/annotation-along-a-curve-in-matplotlib ''' # some tick parameters tick_length = 0.025 text_offset = 0.06 # convert wavelength to u' v' coordinates wavelengths = np.asarray(wavelengths) idx = np.arange(1, len(wavelengths) - 1, dtype=int) wvl_lbl = wavelengths[idx] uv = XYZ_to_uvprime(wavelength_to_XYZ(wavelengths)) u, v = uv[..., 0][idx], uv[..., 1][idx] u_last, v_last = uv[..., 0][idx - 1], uv[..., 1][idx - 1] u_next, v_next = uv[..., 0][idx + 1], uv[..., 1][idx + 1] angle = atan2(v_next - v_last, u_next - u_last) + pi / 2 cos_ang, sin_ang = cos(angle), sin(angle) u1, v1 = u + tick_length * cos_ang, v + tick_length * sin_ang u2, v2 = u + text_offset * cos_ang, v + text_offset * sin_ang fig, ax = share_fig_ax(fig, ax) tick_lines = LineCollection(np.c_[u, v, u1, v1].reshape(-1, 2, 2), color='0.25', lw=1.25) ax.add_collection(tick_lines) for i in range(len(idx)): ax.text(u2[i], v2[i], str(wvl_lbl[i]), va="center", ha="center", clip_on=True) return fig, ax
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 polar_to_cart(rho, phi): ''' Returns the (x,y) coordinates of the (rho,phi) input points. Args: rho (`float` or `numpy.ndarray`): radial coordinate. phi (`float` or `numpy.ndarray`): azimuthal cordinate. Returns: `tuple` containing: `float` or `numpy.ndarray`: x coordinate. `float` or `numpy.ndarray`: y coordinate. ''' x = rho * cos(phi) y = rho * sin(phi) return x, y
def polar_to_cart(rho, phi): '''Return the (x,y) coordinates of the (rho,phi) input points. Parameters ---------- rho : `numpy.ndarray` or number radial coordinate phi : `numpy.ndarray` or number azimuthal coordinate Returns ------- x : `numpy.ndarray` or number x coordinate y : `numpy.ndarray` or number y coordinate ''' x = rho * m.cos(phi) y = rho * m.sin(phi) return x, y
def generate_vertices(num_sides, radius=1): ''' Generates a list of vertices for a convex regular polygon with the given number of sides and radius. Args: 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 * pi / num_sides pts = [] for point in range(num_sides): x = radius * sin(point * angle) y = radius * cos(point * angle) pts.append((int(x), int(y))) return np.asarray(pts)
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 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)
def Z42(rho, phi): return (84 * rho**9 - 168 * rho**7 + 105 * rho**5 - 20 * rho**3) * cos(3 * phi)
def Z40(rho, phi): return (28 * rho**8 - 42 * rho**6 + 15 * rho**4) * cos(4 * phi)
def Z38(rho, phi): return (7 * rho**7 - 6 * rho**5) * cos(5 * phi)
def Z36(rho, phi): return rho**6 * cos(6 * phi)
def Z33(rho, phi): return (5 * rho - 60 * rho**3 + 210 * rho**5 - 280 * rho**7 + 126 * rho**9)\ * cos(phi)
def Z31(rho, phi): return (10 * rho**2 - 30 * rho**4 + 21 * rho**6) * cos(2 * phi)
def Z30(rho, phi): return (10 * rho**3 - 30 * rho**5 + 21 * rho**7) * cos(3 * phi)
def Z27(rho, phi): return (6 * rho**6 - 5 * rho**4) * cos(4 * phi)
def Z25(rho, phi): return rho**5 * cos(5 * phi)
def Z22(rho, phi): return (-4 * rho + 30 * rho**3 - 60 * rho**5 + 35 * rho**7) * cos(phi)
def Z1(rho, phi): return rho * cos(phi)
def Z44(rho, phi): return (210 * rho**10 - 504 * rho**8 + 420 * rho**6 - 140 * rho**4 + 15 * rho**2) \ * cos(2 * phi)
def Z46(rho, phi): return (462 * rho**11 - 1260 * rho**9 + 1260 * rho**7 - 560 * rho**5 + 105 * rho**3 - 6 * rho) \ * cos(phi)
def Z18(rho, phi): return (5 * rho**5 - 4 * rho**3) * cos(3 * phi)
def Z4(rho, phi): return rho**2 * cos(2 * phi)
def Z20(rho, phi): return (6 * rho**2 - 20 * rho**4 + 15 * rho**6) * cos(2 * phi)