class EllipsePixelRegion(PixelRegion): """ An ellipse in pixel coordinates. Parameters ---------- center : `~regions.PixCoord` The position of the center of the ellipse. width : `float` The width of the ellipse (before rotation) in pixels height : `float` The height of the ellipse (before rotation) in pixels angle : `~astropy.units.Quantity`, optional The rotation angle of the ellipse, measured anti-clockwise. If set to zero (the default), the width axis is lined up with the x axis. meta : `~regions.RegionMeta` object, optional A dictionary which stores the meta attributes of this region. visual : `~regions.RegionVisual` object, optional A dictionary which stores the visual meta attributes of this region. Examples -------- .. plot:: :include-source: import numpy as np from astropy.modeling.models import Ellipse2D from astropy.coordinates import Angle from regions import PixCoord, EllipsePixelRegion import matplotlib.pyplot as plt x0, y0 = 15, 10 a, b = 8, 5 theta = Angle(30, 'deg') e = Ellipse2D(amplitude=100., x_0=x0, y_0=y0, a=a, b=b, theta=theta.radian) y, x = np.mgrid[0:20, 0:30] fig, ax = plt.subplots(1, 1) ax.imshow(e(x, y), origin='lower', interpolation='none', cmap='Greys_r') center = PixCoord(x=x0, y=y0) reg = EllipsePixelRegion(center=center, width=2*a, height=2*b, angle=theta) patch = reg.as_artist(facecolor='none', edgecolor='red', lw=2) ax.add_patch(patch) """ _params = ('center', 'width', 'height', 'angle') center = ScalarPix('center') width = ScalarLength('width') height = ScalarLength('height') angle = QuantityLength('angle') def __init__(self, center, width, height, angle=0. * u.deg, meta=None, visual=None): self.center = center self.width = width self.height = height self.angle = angle self.meta = meta or RegionMeta() self.visual = visual or RegionVisual() @property def area(self): """Region area (float)""" return math.pi / 4 * self.width * self.height def contains(self, pixcoord): pixcoord = PixCoord._validate(pixcoord, name='pixcoord') cos_angle = np.cos(self.angle) sin_angle = np.sin(self.angle) dx = pixcoord.x - self.center.x dy = pixcoord.y - self.center.y in_ell = ((2 * (cos_angle * dx + sin_angle * dy) / self.width) ** 2 + (2 * (sin_angle * dx - cos_angle * dy) / self.height) ** 2 <= 1.) if self.meta.get('include', True): return in_ell else: return np.logical_not(in_ell) def to_sky(self, wcs): # TODO: write a pixel_to_skycoord_scale_angle center = pixel_to_skycoord(self.center.x, self.center.y, wcs) _, scale, north_angle = skycoord_to_pixel_scale_angle(center, wcs) height = Angle(self.height / scale, 'deg') width = Angle(self.width / scale, 'deg') return EllipseSkyRegion(center, width, height, angle=self.angle - (north_angle - 90 * u.deg), meta=self.meta, visual=self.visual) @property def bounding_box(self): """ The minimal bounding box (`~regions.BoundingBox`) enclosing the exact elliptical region. """ # We use the solution described in http://stackoverflow.com/a/88020 # which is to use the parametric equation of an ellipse and to find # when dx/dt or dy/dt=0. cos_angle = np.cos(self.angle) sin_angle = np.sin(self.angle) tan_angle = np.tan(self.angle) t1 = np.arctan(-self.height * tan_angle / self.width) t2 = t1 + np.pi * u.rad dx1 = 0.5 * self.width * cos_angle * np.cos(t1) - 0.5 * self.height * sin_angle * np.sin(t1) dx2 = 0.5 * self.width * cos_angle * np.cos(t2) - 0.5 * self.height * sin_angle * np.sin(t2) if dx1 > dx2: dx1, dx2 = dx2, dx1 t1 = np.arctan(self.height / tan_angle / self.width) t2 = t1 + np.pi * u.rad dy1 = 0.5 * self.height * cos_angle * np.sin(t1) + 0.5 * self.width * sin_angle * np.cos(t1) dy2 = 0.5 * self.height * cos_angle * np.sin(t2) + 0.5 * self.width * sin_angle * np.cos(t2) if dy1 > dy2: dy1, dy2 = dy2, dy1 xmin = self.center.x + dx1 xmax = self.center.x + dx2 ymin = self.center.y + dy1 ymax = self.center.y + dy2 return BoundingBox.from_float(xmin, xmax, ymin, ymax) def to_mask(self, mode='center', subpixels=5): # NOTE: assumes this class represents a single circle self._validate_mode(mode, subpixels) if mode == 'center': mode = 'subpixels' subpixels = 1 # Find bounding box and mask size bbox = self.bounding_box ny, nx = bbox.shape # Find position of pixel edges and recenter so that ellipse is at origin xmin = float(bbox.ixmin) - 0.5 - self.center.x xmax = float(bbox.ixmax) - 0.5 - self.center.x ymin = float(bbox.iymin) - 0.5 - self.center.y ymax = float(bbox.iymax) - 0.5 - self.center.y if mode == 'subpixels': use_exact = 0 else: use_exact = 1 fraction = elliptical_overlap_grid( xmin, xmax, ymin, ymax, nx, ny, 0.5 * self.width, 0.5 * self.height, self.angle.to(u.rad).value, use_exact, subpixels, ) return RegionMask(fraction, bbox=bbox) def as_artist(self, origin=(0, 0), **kwargs): """ Matplotlib patch object for this region (`matplotlib.patches.Ellipse`). Parameters ---------- origin : array_like, optional The ``(x, y)`` pixel position of the origin of the displayed image. Default is (0, 0). kwargs : `dict` All keywords that a `~matplotlib.patches.Ellipse` object accepts Returns ------- patch : `~matplotlib.patches.Ellipse` Matplotlib ellipse patch """ from matplotlib.patches import Ellipse xy = self.center.x - origin[0], self.center.y - origin[1] width = self.width height = self.height # From the docstring: MPL expects "rotation in degrees (anti-clockwise)" angle = self.angle.to('deg').value mpl_params = self.mpl_properties_default('patch') mpl_params.update(kwargs) return Ellipse(xy=xy, width=width, height=height, angle=angle, **mpl_params) def _update_from_mpl_selector(self, *args, **kwargs): xmin, xmax, ymin, ymax = self._mpl_selector.extents self.center = PixCoord(x=0.5 * (xmin + xmax), y=0.5 * (ymin + ymax)) self.width = (xmax - xmin) self.height = (ymax - ymin) self.angle = 0. * u.deg if self._mpl_selector_callback is not None: self._mpl_selector_callback(self) def as_mpl_selector(self, ax, active=True, sync=True, callback=None, **kwargs): """ Matplotlib editable widget for this region (`matplotlib.widgets.EllipseSelector`) Parameters ---------- ax : `~matplotlib.axes.Axes` The Matplotlib axes to add the selector to. active : bool, optional Whether the selector should be active by default. sync : bool, optional If `True` (the default), the region will be kept in sync with the selector. Otherwise, the selector will be initialized with the values from the region but the two will then be disconnected. callback : func, optional If specified, this function will be called every time the region is updated. This only has an effect if ``sync`` is `True`. If a callback is set, it is called for the first time once the selector has been created. kwargs Additional keyword arguments are passed to matplotlib.widgets.EllipseSelector` Returns ------- selector : `matplotlib.widgets.EllipseSelector` The Matplotlib selector. Notes ----- Once a selector has been created, you will need to keep a reference to it until you no longer need it. In addition, you can enable/disable the selector at any point by calling ``selector.set_active(True)`` or ``selector.set_active(False)``. """ from matplotlib.widgets import EllipseSelector if hasattr(self, '_mpl_selector'): raise Exception("Cannot attach more than one selector to a region.") if self.angle.value != 0: raise NotImplementedError("Cannot create matplotlib selector for rotated ellipse.") if sync: sync_callback = self._update_from_mpl_selector else: def sync_callback(*args, **kwargs): pass self._mpl_selector = EllipseSelector(ax, sync_callback, interactive=True, rectprops={'edgecolor': self.visual.get('color', 'black'), 'facecolor': 'none', 'linewidth': self.visual.get('linewidth', 1), 'linestyle': self.visual.get('linestyle', 'solid')}) self._mpl_selector.extents = (self.center.x - self.width / 2, self.center.x + self.width / 2, self.center.y - self.height / 2, self.center.y + self.height / 2) self._mpl_selector.set_active(active) self._mpl_selector_callback = callback if sync and self._mpl_selector_callback is not None: self._mpl_selector_callback(self) return self._mpl_selector def rotate(self, center, angle): """Make a rotated region. Rotates counter-clockwise for positive ``angle``. Parameters ---------- center : `PixCoord` Rotation center point angle : `~astropy.coordinates.Angle` Rotation angle Returns ------- region : `EllipsePixelRegion` Rotated region (an independent copy) """ center = self.center.rotate(center, angle) angle = self.angle + angle return self.copy(center=center, angle=angle)
class FittingDataPlot2D(DataPlotEditorBase): nplots = 2 layout = 'horizontal' range_rect = Any(transient=True) peaks_ellipses = List([],transient=True) frangex = Tuple((0.0, 0.0),transient=True) frangey = Tuple((0.0, 0.0),transient=True) peaks = List([],transient=True) range_selector = Any(transient=True) # Instance(SpanSelector) peaks_selector = Any(transient=True) # (SpanSelector) editing = Enum('Peaks', ['Range', 'Peaks']) has_frange = Property(Bool) has_peaks = Property(Bool) def _get_has_frange(self): if (self.frangex[1]-self.frangex[0])>10 and (self.frangey[1]-self.frangey[0])>10: return True else: return False def _get_has_peaks(self): if len(self.peaks): return True else: return False def clear_patches(self, frange=True, peaks=True): if frange: try: self.range_rect.remove() self.range_rect = None except: pass if peaks: for ellipse in self.peaks_ellipses: try: ellipse.remove() self.peaks_ellipses.remove(ellipse) except: pass def clear_selections(self): self.clear_patches() self.peaks = [] self.frangex = (0.0,0.0) self.frangey = (0.0, 0.0) def draw_patches(self, frange=True, peaks=True): if all([frange, len(self.axs), len(self.frangex), len(self.frangey)]): self.range_rect = self.draw_rectangle(self.frangex, self.frangey, color='g', alpha=0.15) if all([peaks, len(self.axs), len(self.peaks)]): for p in self.peaks: self.peaks_ellipses.append(self.draw_ellipse(*p, color='r', alpha=0.4)) if self.figure is not None: if self.figure.canvas is not None: self.figure.canvas.draw() def mpl_setup(self): self.add_subplots(self.nplots) self.configure_selector(peaks=True) # self.figure.canvas.draw() # self.activate_selector() def draw_rectangle(self,xs,ys,alpha=0.2,color='g'): xy = (min(xs),min(ys)) width = np.abs(np.diff(xs)) height = np.abs(np.diff(ys)) re = Rectangle(xy, width, height, angle=0.0) ax = self.axs[0] ax.add_artist(re) re.set_alpha(alpha=alpha) re.set_facecolor(color) return re def draw_ellipse(self,xmid,ymid,width,height,alpha=0.4,color='r'): el = Ellipse(xy=(xmid, ymid), width=width, height=height, angle=0) ax = self.axs[0] ax.add_artist(el) #el.set_clip_box(ax.bbox) el.set_alpha(alpha=alpha) el.set_facecolor(color) return el def rect_onselect(self, eclick, erelease): xs = [eclick.xdata, erelease.xdata] ys = [eclick.ydata, erelease.ydata] #print xs, ys self.frangex = (min(xs), max(xs)) self.frangey = (min(ys), max(ys)) self.clear_patches(peaks=False) self.draw_patches(peaks=False) def ellipse_onselect(self, eclick, erelease): xs = [eclick.xdata, erelease.xdata] ys = [eclick.ydata, erelease.ydata] xmid, ymid = np.mean(xs), np.mean(ys) width, height = np.abs(np.diff(xs)), np.abs(np.diff(ys)) #print [xmid, ymid, width, height] self.peaks.append([xmid, ymid, width, height]) self.clear_patches(frange=False) self.draw_patches(frange=False) def configure_selector(self, frange=False, peaks=False): self.range_selector = RectangleSelector(self.axs[0], self.rect_onselect, drawtype='box', useblit=True, button=[1, 3], # don't use middle button minspanx=15, minspany=15, spancoords='pixels', #interactive=True, rectprops=dict(alpha=0.5, facecolor='g')) self.peaks_selector = EllipseSelector(self.axs[0], self.ellipse_onselect, drawtype='line', button=[1, 3], # don't use middle button spancoords='pixels', useblit=True, minspanx=10, minspany=10, rectprops=dict(alpha=0.5, facecolor='red')) self.peaks_selector.set_active(peaks) self.range_selector.set_active(frange) def activate_selector(self, frange=False, peaks=False): if self.peaks_selector is not None: self.peaks_selector.set_active(peaks) if self.range_selector is not None: self.range_selector.set_active(frange) def _editing_changed(self, new): if new == 'Range': self.configure_selector(frange=True, peaks=False) elif new == 'Peaks': self.configure_selector(frange=False, peaks=True)