示例#1
0
文件: quad.py 项目: gjvargas/cytoflow
class QuadSelection(cytoflow.views.ScatterplotView):
    """Plots, and lets the user interact with, a quadrant gate.
    
    Attributes
    ----------
    op : Instance(Range2DOp)
        The instance of Range2DOp that we're viewing / editing
        
    huefacet : Str
        The conditioning variable to plot multiple colors
        
    subset : Str
        The string passed to `Experiment.query()` to subset the data before
        plotting
        
    interactive : Bool
        is this view interactive?  Ie, can the user set the threshold with a 
        mouse click?
        
    Notes
    -----
    We inherit `xfacet` and `yfacet` from `cytoflow.views.ScatterplotView`, but
    they must both be unset!
        
    Examples
    --------
    
    In an IPython notebook with `%matplotlib notebook`
    
    >>> q = flow.QuadOp(name = "Quad",
    ...                 xchannel = "V2-A",
    ...                 ychannel = "Y2-A"))
    >>> qv = q.default_view()
    >>> qv.interactive = True
    >>> qv.plot(ex2) 
    """
    
    id = Constant('edu.mit.synbio.cytoflow.views.quad')
    friendly_id = Constant("Quadrant Selection")
    
    op = Instance(IOperation)
    name = DelegatesTo('op')
    xchannel = DelegatesTo('op')
    ychannel = DelegatesTo('op')
    interactive = Bool(False, transient = True)
    
    # internal state.
    _ax = Any(transient = True)
    _hline = Instance(Line2D, transient = True)
    _vline = Instance(Line2D, transient = True)
    _cursor = Instance(Cursor, transient = True)
        
    def plot(self, experiment, **kwargs):
        """Plot the underlying scatterplot and then plot the selection on top of it."""
        
        if not experiment:
            raise util.CytoflowOpError("No experiment specified")
        
        if not experiment:
            raise util.CytoflowViewError("No experiment specified")
        
        if self.xfacet:
            raise util.CytoflowViewError("RangeSelection.xfacet must be empty or `Undefined`")
        
        if self.yfacet:
            raise util.CytoflowViewError("RangeSelection.yfacet must be empty or `Undefined`")
        
        super(QuadSelection, self).plot(experiment, **kwargs)
        self._ax = plt.gca()
        self._draw_lines()
        self._interactive()

    @on_trait_change('op.xthreshold, op.ythreshold', post_init = True)
    def _draw_lines(self):
        if not self._ax:
            return
        
        if self._hline and self._hline in self._ax.lines:
            self._hline.remove()
            
        if self._vline and self._vline in self._ax.lines:
            self._vline.remove()
            
        if self.op.xthreshold and self.op.ythreshold:
            self._hline = plt.axhline(self.op.ythreshold, 
                                      linewidth = 3, 
                                      color = 'blue')
            self._vline = plt.axvline(self.op.xthreshold,
                                      linewidth = 3,
                                      color = 'blue')

            plt.draw_if_interactive()

    @on_trait_change('interactive', post_init = True)
    def _interactive(self):
        if self._ax and self.interactive:
            self._cursor = Cursor(self._ax,
                                  horizOn = True,
                                  vertOn = True,
                                  color = 'blue') 
            self._cursor.connect_event('button_press_event', self._onclick)
        elif self._cursor:
            self._cursor.disconnect_events()
            self._cursor = None
            
    def _onclick(self, event):
        """Update the threshold location"""
        self.op.xthreshold = event.xdata
        self.op.ythreshold = event.ydata    
示例#2
0
class CrossSection(object):
    """
    Class to manage the axes, artists and properties associated with
    showing a 2D image, a cross-hair cursor and two parasite axes which
    provide horizontal and vertical cross sections of image.

    You will likely need to call `CrossSection.init_artists(init_image)` after
    creating this object.

    Parameters
    ----------

    fig : matplotlib.figure.Figure
        The figure object to build the class on, will clear
        current contents

    cmap : str,  colormap, or None
        color map to use.  Defaults to gray

    norm : Normalize or None
       Normalization function to use

    limit_func : callable, optional
        function that takes in the image and returns clim values
    auto_redraw : bool, optional
    interpolation : str, optional
        Interpolation method to use. List of valid options can be found in
        CrossSection2DView.interpolation
    """
    def __init__(self,
                 fig,
                 cmap=None,
                 norm=None,
                 limit_func=None,
                 auto_redraw=True,
                 interpolation=None):

        self._cursor_position_cbs = []
        self._interpolation = interpolation
        # used to determine if setting properties should force a re-draw
        self._auto_redraw = auto_redraw
        # clean defaults
        if limit_func is None:
            limit_func = lambda image: (image.min(), image.max())
        if cmap is None:
            cmap = 'gray'
        # stash the color map
        self._cmap = cmap
        # let norm pass through as None, mpl defaults to linear which is fine
        if norm is None:
            norm = Normalize()
        self._norm = norm
        # save a copy of the limit function, we will need it later
        self._limit_func = limit_func

        # this is used by the widget logic
        self._active = True
        self._dirty = True
        self._cb_dirty = True

        # work on setting up the mpl axes

        self._fig = fig
        # blow away what ever is currently on the figure
        fig.clf()
        # Configure the figure in our own image
        #
        #     	  +----------------------+
        #	      |   H cross section    |
        #     	  +----------------------+
        #   +---+ +----------------------+
        #   | V | |                      |
        #   |   | |                      |
        #   | x | |                      |
        #   | s | |      Main Axes       |
        #   | e | |                      |
        #   | c | |                      |
        #   | t | |                      |
        #   | i | |                      |
        #   | o | |                      |
        #   | n | |                      |
        #   +---+ +----------------------+

        # make the main axes
        self._im_ax = fig.add_subplot(1, 1, 1)
        self._im_ax.set_aspect('equal')
        self._im_ax.xaxis.set_major_locator(NullLocator())
        self._im_ax.yaxis.set_major_locator(NullLocator())
        self._imdata = None
        self._im = self._im_ax.imshow([[]],
                                      cmap=self._cmap,
                                      norm=self._norm,
                                      interpolation=self._interpolation,
                                      aspect='equal',
                                      vmin=0,
                                      vmax=1)

        # make it dividable
        divider = make_axes_locatable(self._im_ax)

        # set up all the other axes
        # (set up the horizontal and vertical cuts)
        self._ax_h = divider.append_axes('top',
                                         .5,
                                         pad=0.1,
                                         sharex=self._im_ax)
        self._ax_h.yaxis.set_major_locator(LinearLocator(numticks=2))
        self._ax_v = divider.append_axes('left',
                                         .5,
                                         pad=0.1,
                                         sharey=self._im_ax)
        self._ax_v.xaxis.set_major_locator(LinearLocator(numticks=2))
        self._ax_cb = divider.append_axes('right', .2, pad=.5)
        # add the color bar
        self._cb = fig.colorbar(self._im, cax=self._ax_cb)

        # add the cursor place holder
        self._cur = None

        # turn off auto-scale for the horizontal cut
        self._ax_h.autoscale(enable=False)

        # turn off auto-scale scale for the vertical cut
        self._ax_v.autoscale(enable=False)

        # create line artists
        self._ln_v, = self._ax_v.plot([], [],
                                      'k-',
                                      animated=True,
                                      visible=False)

        self._ln_h, = self._ax_h.plot([], [],
                                      'k-',
                                      animated=True,
                                      visible=False)

        # backgrounds for blitting
        self._ax_v_bk = None
        self._ax_h_bk = None

        # stash last-drawn row/col to skip if possible
        self._row = None
        self._col = None

        # make attributes for callback ids
        self._move_cid = None
        self._click_cid = None
        self._clear_cid = None

    def add_cursor_position_cb(self, callback):
        """ Add a callback for the cursor position in the main axes

        Parameters
        ----------
        callback : callable(cc, rr)
            Function that gets called when the cursor position moves to a new
            row or column on main axes
        """
        self._cursor_position_cbs.append(callback)

    # set up the call back for the updating the side axes
    def _move_cb(self, event):
        if not self._active:
            return
        if event is None:
            x = self._col
            y = self._row
            self._col = None
            self._row = None
        else:
            # short circuit on other axes
            if event.inaxes is not self._im_ax:
                return
            x, y = event.xdata, event.ydata
        numrows, numcols = self._imdata.shape
        if x is not None and y is not None:
            self._ln_h.set_visible(True)
            self._ln_v.set_visible(True)
            col = int(x + 0.5)
            row = int(y + 0.5)
            if row != self._row or col != self._col:
                if 0 <= col < numcols and 0 <= row < numrows:
                    self._col = col
                    self._row = row
                    for cb in self._cursor_position_cbs:
                        cb(col, row)
                    for data, ax, bkg, art, set_fun in zip(
                        (self._imdata[row, :], self._imdata[:, col]),
                        (self._ax_h, self._ax_v),
                        (self._ax_h_bk, self._ax_v_bk),
                        (self._ln_h, self._ln_v),
                        (self._ln_h.set_ydata, self._ln_v.set_xdata)):
                        self._fig.canvas.restore_region(bkg)
                        set_fun(data)
                        ax.draw_artist(art)
                        self._fig.canvas.blit(ax.bbox)

    def _click_cb(self, event):
        if event.inaxes is not self._im_ax:
            return
        self.active = not self.active
        if self.active:
            self._cur.onmove(event)
            self._move_cb(event)

    @auto_redraw
    def _connect_callbacks(self):
        """
        Connects all of the callbacks for the motion and click events
        """
        self._disconnect_callbacks()
        self._cur = Cursor(self._im_ax, useblit=True, color='red', linewidth=2)
        self._move_cid = self._fig.canvas.mpl_connect('motion_notify_event',
                                                      self._move_cb)

        self._click_cid = self._fig.canvas.mpl_connect('button_press_event',
                                                       self._click_cb)

        self._clear_cid = self._fig.canvas.mpl_connect('draw_event',
                                                       self._clear)
        self._fig.tight_layout()
        self._fig.canvas.draw_idle()

    def _disconnect_callbacks(self):
        """
        Disconnects all of the callbacks
        """
        if self._fig.canvas is None:
            # no canvas -> can't do anything about the call backs which
            # should not exist
            self._move_cid = None
            self._clear_cid = None
            self._click_cid = None
            return

        for atr in ('_move_cid', '_clear_cid', '_click_cid'):
            cid = getattr(self, atr, None)
            if cid is not None:
                self._fig.canvas.mpl_disconnect(cid)
                setattr(self, atr, None)

        # clean up the cursor
        if self._cur is not None:
            self._cur.disconnect_events()
            del self._cur
            self._cur = None

    @auto_redraw
    def _init_artists(self, init_image):
        """
        Update the CrossSection with a new base-image.  This function
        takes care of setting up all of the details about the image size
        in the limits/artist extent of the image and the secondary data
        in the cross-section parasite plots.

        Parameters
        ----------
        init_image : ndarray
           An image to serve as the new 'base' image.
        """

        im_shape = init_image.shape

        # first deal with the image axis
        # update the image, `update_artists` takes care of
        # updating the actual artist
        self._imdata = init_image

        # update the extent of the image artist
        self._im.set_extent([-0.5, im_shape[1] + .5, im_shape[0] + .5, -0.5])

        # update the limits of the image axes to match the exent
        self._im_ax.set_xlim([-.05, im_shape[1] + .5])
        self._im_ax.set_ylim([im_shape[0] + .5, -0.5])

        # update the format coords printer
        numrows, numcols = im_shape

        # note, this is a closure over numrows and numcols
        def format_coord(x, y):
            # adjust xy -> col, row
            col = int(x + 0.5)
            row = int(y + 0.5)
            # make sure the point falls in the array
            if col >= 0 and col < numcols and row >= 0 and row < numrows:
                # if it does, grab the value
                z = self._imdata[row, col]
                return "X: {x:d} Y: {y:d} I: {i:.2f}".format(x=col, y=row, i=z)
            else:
                return "X: {x:d} Y: {y:d}".format(x=col, y=row)

        # replace the current format_coord function
        self._im_ax.format_coord = format_coord

        # net deal with the parasite axes and artist
        self._ln_v.set_data(np.zeros(im_shape[0]), np.arange(im_shape[0]))
        self._ax_v.set_ylim([0, im_shape[0]])

        self._ln_h.set_data(np.arange(im_shape[1]), np.zeros(im_shape[1]))
        self._ax_h.set_xlim([0, im_shape[1]])

        # if we have a cavas, then connect/set up junk
        if self._fig.canvas is not None:
            self._connect_callbacks()
        # mark as dirty
        self._dirty = True

    def _clear(self, event):
        self._ax_v_bk = self._fig.canvas.copy_from_bbox(self._ax_v.bbox)
        self._ax_h_bk = self._fig.canvas.copy_from_bbox(self._ax_h.bbox)
        self._ln_h.set_visible(False)
        self._ln_v.set_visible(False)
        # this involves reaching in and touching the guts of the
        # cursor widget.  The problem is that the mpl widget
        # skips updating it's saved background if the widget is inactive
        if self._cur:
            self._cur.background = self._cur.canvas.copy_from_bbox(
                self._cur.canvas.figure.bbox)

    @property
    def interpolation(self):
        return self._interpolation

    @property
    def active(self):
        return self._active

    @active.setter
    def active(self, val):
        self._active = val
        self._cur.active = val

    @auto_redraw
    def update_interpolation(self, interpolation):
        """
        Set the interpolation method

        """
        self._dirty = True
        self._im.set_interpolation(interpolation)

    @auto_redraw
    def update_cmap(self, cmap):
        """
        Set the color map used
        """
        # TODO: this should stash new value, not apply it
        self._cmap = cmap
        self._dirty = True

    @auto_redraw
    def update_image(self, image):
        """
        Set the image data

        The input data does not necessarily have to be the same shape as the
        original image
        """
        image = np.asarray(image)
        if self._imdata is None or self._imdata.shape != image.shape:
            self._init_artists(image)
        self._imdata = image
        self._move_cb(None)
        self._dirty = True

    @auto_redraw
    def update_norm(self, norm):
        """
        Update the way that matplotlib normalizes the image
        """
        self._norm = norm
        self._dirty = True
        self._cb_dirty = True

    @auto_redraw
    def update_limit_func(self, limit_func):
        """
        Set the function to use to determine the color scale
        """
        # set the new function to use for computing the color limits
        self._limit_func = limit_func
        self._dirty = True

    def _update_artists(self):
        """
        Updates the figure by re-drawing
        """
        # if the figure is not dirty, short-circuit
        if not (self._dirty or self._cb_dirty):
            return

        # this is a tuple which is the max/min used in the color mapping.
        # these values are also used to set the limits on the value
        # axes of the parasite axes
        # value_limits
        vlim = self._limit_func(self._imdata)
        # set the color bar limits
        self._im.set_clim(vlim)
        self._norm.vmin, self._norm.vmax = vlim
        # set the cross section axes limits
        self._ax_v.set_xlim(*vlim[::-1])
        self._ax_h.set_ylim(*vlim)
        # set the imshow data
        self._im.set_cmap(self._cmap)
        self._im.set_norm(self._norm)
        if self._imdata is None:
            return
        self._im.set_data(self._imdata)
        # TODO if cb_dirty, remake the colorbar, I think this is
        # why changing the norm does not play well
        self._dirty = False
        self._cb_dirty = False

    def _draw(self):
        self._fig.canvas.draw_idle()

    @auto_redraw
    def autoscale_horizontal(self, enable):
        self._ax_h.autoscale(enable=enable)

    @auto_redraw
    def autoscale_vertical(self, enable):
        self._ax_v.autoscale(enable=False)
示例#3
0
class ThresholdSelection(cytoflow.views.HistogramView):
    """
    Plots, and lets the user interact with, a threshold on the X axis.
    
    TODO - beautify!
    
    Attributes
    ----------
    op : Instance(ThresholdOp)
        the ThresholdOp we're working on.
        
    huefacet : Str
        The conditioning variable to show multiple colors on this plot

    subset : Str    
        the string passed to Experiment.subset() defining the subset we plot

    interactive : Bool
        is this view interactive?
        
    Notes
    -----
    We inherit `xfacet` and `yfacet` from `cytoflow.views.HistogramView`, but
    they must both be unset!
        
    Examples
    --------
    In an IPython notebook with `%matplotlib notebook`
    
    >>> t = flow.ThresholdOp(name = "Threshold",
    ...                      channel = "Y2-A")
    >>> tv = t.default_view()
    >>> tv.plot(ex2)
    >>> tv.interactive = True
    >>> # .... draw a threshold on the plot
    >>> ex3 = thresh.apply(ex2)
    """
    
    id = Constant('edu.mit.synbio.cytoflow.views.threshold')
    friendly_id = Constant("Threshold Selection")
    
    op = Instance(IOperation)
    name = DelegatesTo('op')
    channel = DelegatesTo('op')
    interactive = Bool(False, transient = True)

    # internal state
    _ax = Any(transient = True)
    _line = Instance(Line2D, transient = True)
    _cursor = Instance(Cursor, transient = True)
    
    def plot(self, experiment, **kwargs):
        """Plot the histogram and then plot the threshold on top of it."""
        
        if not experiment:
            raise util.CytoflowViewError("No experiment specified")
        
        if self.xfacet:
            raise util.CytoflowViewError("ThresholdSelection.xfacet must be empty")
        
        if self.yfacet:
            raise util.CytoflowViewError("ThresholdSelection.yfacet must be empty")
        
        super(ThresholdSelection, self).plot(experiment, **kwargs)
        self._ax = plt.gca()        
        self._draw_threshold()
        self._interactive()
    
    @on_trait_change('op.threshold', post_init = True)
    def _draw_threshold(self):
        if not self._ax or not self.op.threshold:
            return
        
        if self._line:
            # when used in the GUI, _draw_threshold gets called *twice* without
            # the plot being updated inbetween: and then the line can't be 
            # removed from the plot, because it was never added.  so check
            # explicitly first.  this is likely to be an issue in other
            # interactive plots, too.
            if self._line and self._line in self._ax.lines:
                self._line.remove()
 
            self._line = None
        
        if self.op.threshold:    
            self._line = plt.axvline(self.op.threshold, linewidth=3, color='blue')
            
        plt.draw_if_interactive()
        
    @on_trait_change('interactive', post_init = True)
    def _interactive(self):
        if self._ax and self.interactive:
            self._cursor = Cursor(self._ax, 
                                  horizOn=False,
                                  vertOn=True,
                                  color='blue')
            self._cursor.connect_event('button_press_event', self._onclick)
            
        elif self._cursor:
            self._cursor.disconnect_events()
            self._cursor = None
            
    def _onclick(self, event):
        """Update the threshold location"""
        self.op.threshold = event.xdata
示例#4
0
class PolygonSelection(HasStrictTraits):
    """Plots, and lets the user interact with, a 2D selection.
    
    Attributes
    ----------
    polygon : Instance(numpy.ndarray)
        The polygon vertices
        
    view : Instance(IView)
        the IView that this view is wrapping.  I suggest that if it's another
        ISelectionView, that its `interactive` property remain False. >.>
        
    interactive : Bool
        is this view interactive?  Ie, can the user set the polygon verticies
        with mouse clicks?
    """
    
    id = "edu.mit.synbio.cytoflow.views.polygon"
    friendly_id = "Polygon Selection"
    
    view = Instance(IView, transient = True)
    interactive = Bool(False, transient = True)
    
    vertices = List((Float, Float))
    
    # internal state.
    _cursor = Instance(Cursor, transient = True)
    _path = Instance(mpl.path.Path, transient = True)
    _patch = Instance(mpl.patches.PathPatch, transient = True)
    _line = Instance(mpl.lines.Line2D, transient = True)
    _drawing = Bool(transient = True)
        
    def plot(self, experiment, **kwargs):
        """Plot self.view, and then plot the selection on top of it."""
        self.view.plot(experiment, **kwargs)
        self._draw_poly()

    def is_valid(self, experiment):
        """If the decorated view is valid, we are too."""
        return self.view.is_valid(experiment)
    
    @on_trait_change('vertices')
    def _draw_poly(self):
        ca = plt.gca()
         
        if self._patch and self._patch in ca.patches:
            self._patch.remove()
            
        if self._drawing or not self.vertices or len(self.vertices) < 3 \
           or any([len(x) != 2 for x in self.vertices]):
            return
             

        patch_vert = np.concatenate((np.array(self.vertices), 
                                    np.array((0,0), ndmin = 2)))
                                    
        self._patch = \
            mpl.patches.PathPatch(mpl.path.Path(patch_vert, closed = True),
                                  edgecolor="black",
                                  linewidth = 1.5,
                                  fill = False)
            
        ca.add_patch(self._patch)
        plt.gcf().canvas.draw()
    
    @on_trait_change('interactive')
    def _interactive(self):
        if self.interactive:
            ax = plt.gca()
            self._cursor = Cursor(ax, horizOn = False, vertOn = False)            
            self._cursor.connect_event('button_press_event', self._onclick)
            self._cursor.connect_event('motion_notify_event', self._onmove)
        else:
            self._cursor.disconnect_events()
            self._cursor = None       
    
    def _onclick(self, event): 
        """Update selection traits"""      
        if(self._cursor.ignore(event)):
            return
        
        if event.dblclick:
            self._drawing = False
            self.vertices = map(tuple, self._path.vertices)
            self._path = None
            return
                
        ca = plt.gca()
                
        self._drawing = True
        if self._patch and self._patch in ca.patches:
            self._patch.remove()
            
        if self._path:
            vertices = np.concatenate((self._path.vertices,
                                      np.array((event.xdata, event.ydata), ndmin = 2)))
        else:
            vertices = np.array((event.xdata, event.ydata), ndmin = 2)
            
        self._path = mpl.path.Path(vertices, closed = False)
        self._patch = mpl.patches.PathPatch(self._path, 
                                            edgecolor = "black",
                                            fill = False)

        ca.add_patch(self._patch)
        plt.gcf().canvas.draw()
        
    def _onmove(self, event):       
         
        if(self._cursor.ignore(event) 
           or not self._drawing
           or not self._path
           or self._path.vertices.shape[0] == 0
           or not event.xdata
           or not event.ydata):
            return
        
        ca = plt.gca()
         
        if not ca:
            return
         
        if self._line and self._line in ca.lines:
            self._line.remove()
            
        xdata = [self._path.vertices[-1, 0], event.xdata]
        ydata = [self._path.vertices[-1, 1], event.ydata]
        self._line = mpl.lines.Line2D(xdata, ydata, linewidth = 1, color = "black")
        
        ca.add_line(self._line)
        plt.gcf().canvas.draw()
示例#5
0
文件: maskeditor.py 项目: awacha/cct
class MaskEditor(ToolWindow, DoubleFileChooserDialog):
    def __init__(self, *args, **kwargs):
        self.mask = None
        self._undo_stack = []
        self._im = None
        self._selector = None
        self._cursor = None
        self.exposureloader = None
        self.plot2d = None
        ToolWindow.__init__(self, *args, **kwargs)
        DoubleFileChooserDialog.__init__(
            self, self.widget, 'Open mask file...', 'Save mask file...', [('Mask files', '*.mat'), ('All files', '*')],
            self.instrument.config['path']['directories']['mask'],
            os.path.abspath(self.instrument.config['path']['directories']['mask']),
        )

    def init_gui(self, *args, **kwargs):
        self.exposureloader = ExposureLoader(self.instrument)
        self.builder.get_object('loadexposure_expander').add(self.exposureloader)
        self.exposureloader.connect('open', self.on_loadexposure)
        self.plot2d = PlotImageWidget()
        self.builder.get_object('plotbox').pack_start(self.plot2d.widget, True, True, 0)
        self.builder.get_object('toolbar').set_sensitive(False)

    def on_loadexposure(self, exposureloader: ExposureLoader, im: Exposure):
        if self.mask is None:
            self.mask = im.mask
        self._im = im
        self.plot2d.set_image(im.intensity)
        self.plot2d.set_mask(self.mask)
        self.builder.get_object('toolbar').set_sensitive(True)

    def on_new(self, button):
        if self._im is None or self.mask is None:
            return False
        self.mask = np.ones_like(self.mask)
        self.plot2d.set_mask(self.mask)
        self.set_last_filename(None)

    def on_open(self, button):
        filename = self.get_open_filename()
        if filename is not None:
            mask = loadmat(filename)
            self.mask = mask[[k for k in mask.keys() if not k.startswith('__')][0]]
            self.plot2d.set_mask(self.mask)

    def on_save(self, button):
        filename = self.get_last_filename()
        if filename is None:
            return self.on_saveas(button)
        maskname = os.path.splitext(os.path.split(filename)[1])[0]
        savemat(filename, {maskname: self.mask})

    def on_saveas(self, button):
        filename = self.get_save_filename(None)
        if filename is not None:
            self.on_save(button)

    def suggest_filename(self):
        return 'mask_dist_{0.year:d}{0.month:02d}{0.day:02d}.mat'.format(datetime.date.today())

    def on_selectcircle_toggled(self, button):
        if button.get_active():
            self.set_sensitive(False, 'Ellipse selection not ready',
                               ['new_button', 'save_button', 'saveas_button', 'open_button', 'undo_button',
                                'selectrectangle_button', 'selectpolygon_button', 'pixelhunting_button',
                                'loadexposure_expander', 'close_button', self.plot2d.toolbar,
                                self.plot2d.settings_expander])
            while self.plot2d.toolbar.mode != '':
                # turn off zoom, pan, etc. modes.
                self.plot2d.toolbar.zoom()
            self._selector = EllipseSelector(self.plot2d.axis,
                                             self.on_ellipse_selected,
                                             rectprops={'facecolor': 'white', 'edgecolor': 'none', 'alpha': 0.7,
                                                        'fill': True, 'zorder': 10},
                                             button=[1, ],
                                             interactive=False, lineprops={'zorder': 10})
            self._selector.state.add('square')
            self._selector.state.add('center')
        else:
            assert isinstance(self._selector, EllipseSelector)
            self._selector.set_active(False)
            self._selector.set_visible(False)
            self._selector = None
            self.plot2d.replot(keepzoom=False)
            self.set_sensitive(True)

    def on_ellipse_selected(self, pos1, pos2):
        # pos1 and pos2 are mouse button press and release events, with xdata and ydata carrying
        # the two opposite corners of the bounding box of the circle. These are NOT the exact
        # button presses and releases!
        row = np.arange(self.mask.shape[0])[:, np.newaxis]
        column = np.arange(self.mask.shape[1])[np.newaxis, :]
        row0 = 0.5 * (pos1.ydata + pos2.ydata)
        col0 = 0.5 * (pos1.xdata + pos2.xdata)
        r2 = ((pos2.xdata - pos1.xdata) ** 2 + (pos2.ydata - pos1.ydata) ** 2) / 8
        tobemasked = (row - row0) ** 2 + (column - col0) ** 2 <= r2
        self._undo_stack.append(self.mask)
        if self.builder.get_object('mask_button').get_active():
            self.mask &= ~tobemasked
        elif self.builder.get_object('unmask_button').get_active():
            self.mask |= tobemasked
        elif self.builder.get_object('invertmask_button').get_active():
            self.mask[tobemasked] = ~self.mask[tobemasked]
        else:
            pass
        self.builder.get_object('selectcircle_button').set_active(False)
        self.plot2d.set_mask(self.mask)

    def on_selectrectangle_toggled(self, button):
        if button.get_active():
            self.set_sensitive(False, 'Rectangle selection not ready',
                               ['new_button', 'save_button', 'saveas_button', 'open_button', 'undo_button',
                                'selectcircle_button', 'selectpolygon_button', 'pixelhunting_button',
                                'loadexposure_expander', 'close_button', self.plot2d.toolbar,
                                self.plot2d.settings_expander])
            while self.plot2d.toolbar.mode != '':
                # turn off zoom, pan, etc. modes.
                self.plot2d.toolbar.zoom()
            self._selector = RectangleSelector(self.plot2d.axis,
                                               self.on_rectangle_selected,
                                               rectprops={'facecolor': 'white', 'edgecolor': 'none', 'alpha': 0.7,
                                                          'fill': True, 'zorder': 10},
                                               button=[1, ],
                                               interactive=False, lineprops={'zorder': 10})
        else:
            self._selector.set_active(False)
            self._selector.set_visible(False)
            self._selector = None
            self.plot2d.replot(keepzoom=False)
            self.set_sensitive(True)

    def on_rectangle_selected(self, pos1, pos2):
        # pos1 and pos2 are mouse button press and release events, with xdata and ydata
        # carrying the two opposite corners of the bounding box of the rectangle. These
        # are NOT the exact button presses and releases!
        row = np.arange(self.mask.shape[0])[:, np.newaxis]
        column = np.arange(self.mask.shape[1])[np.newaxis, :]
        tobemasked = ((row >= min(pos1.ydata, pos2.ydata)) & (row <= max(pos1.ydata, pos2.ydata)) &
                      (column >= min(pos1.xdata, pos2.xdata)) & (column <= max(pos1.xdata, pos2.xdata)))
        self._undo_stack.append(self.mask)
        if self.builder.get_object('mask_button').get_active():
            self.mask = self.mask & (~tobemasked)
        elif self.builder.get_object('unmask_button').get_active():
            self.mask = self.mask | tobemasked
        elif self.builder.get_object('invertmask_button').get_active():
            self.mask[tobemasked] = ~self.mask[tobemasked]
        else:
            pass
        self.builder.get_object('selectrectangle_button').set_active(False)
        self.plot2d.set_mask(self.mask)

    def on_selectpolygon_toggled(self, button):
        if button.get_active():
            self.set_sensitive(False, 'Polygon selection not ready',
                               ['new_button', 'save_button', 'saveas_button', 'open_button', 'undo_button',
                                'selectrectangle_button', 'selectcircle_button', 'pixelhunting_button',
                                'loadexposure_expander', 'close_button', self.plot2d.toolbar,
                                self.plot2d.settings_expander])
            while self.plot2d.toolbar.mode != '':
                # turn off zoom, pan, etc. modes.
                self.plot2d.toolbar.zoom()
            self._selector = LassoSelector(self.plot2d.axis,
                                           self.on_polygon_selected,
                                           lineprops={'color': 'white', 'zorder': 10},
                                           button=[1, ],
                                           )
        else:
            self._selector.set_active(False)
            self._selector.set_visible(False)
            self._selector = None
            self.plot2d.replot(keepzoom=False)
            self.set_sensitive(True)

    def on_polygon_selected(self, vertices):
        path = Path(vertices)
        col, row = np.meshgrid(np.arange(self.mask.shape[1]),
                               np.arange(self.mask.shape[0]))
        points = np.vstack((col.flatten(), row.flatten())).T
        tobemasked = path.contains_points(points).reshape(self.mask.shape)
        self._undo_stack.append(self.mask)
        if self.builder.get_object('mask_button').get_active():
            self.mask = self.mask & (~tobemasked)
        elif self.builder.get_object('unmask_button').get_active():
            self.mask = self.mask | tobemasked
        elif self.builder.get_object('invertmask_button').get_active():
            self.mask[tobemasked] = ~self.mask[tobemasked]
        else:
            pass
        self.plot2d.set_mask(self.mask)
        self.builder.get_object('selectpolygon_button').set_active(False)

    def on_mask_toggled(self, button):
        pass

    def on_unmask_toggled(self, button):
        pass

    def on_invertmask_toggled(self, button):
        pass

    def on_pixelhunting_toggled(self, button):
        if button.get_active():
            self._cursor = Cursor(self.plot2d.axis, useblit=False, color='white', lw=1)
            self._cursor.connect_event('button_press_event', self.on_cursorclick)
            while self.plot2d.toolbar.mode != '':
                # turn off zoom, pan, etc. modes.
                self.plot2d.toolbar.zoom()
        else:
            self._cursor.disconnect_events()
            self._cursor = None
            self._undo_stack.append(self.mask)
            self.plot2d.replot(keepzoom=False)

    def on_cursorclick(self, event):
        if (event.inaxes == self.plot2d.axis) and (self.plot2d.toolbar.mode == ''):
            self.mask[round(event.ydata), round(event.xdata)] ^= True
            self._cursor.disconnect_events()
            self._cursor = None
            self.plot2d.replot(keepzoom=True)
            self.on_pixelhunting_toggled(self.builder.get_object('pixelhunting_button'))

    def cleanup(self):
        super().cleanup()
        self._undo_stack = []

    def on_undo(self, button):
        try:
            self.mask = self._undo_stack.pop()
        except IndexError:
            return
        self.plot2d.set_mask(self.mask)
示例#6
0
class ThresholdSelection(cytoflow.views.HistogramView):
    """
    Plots, and lets the user interact with, a threshold on the X axis.
    
    TODO - beautify!
    
    Attributes
    ----------
    op : Instance(ThresholdOp)
        the ThresholdOp we're working on.
        
    huefacet : Str
        The conditioning variable to show multiple colors on this plot

    subset : Str    
        the string passed to Experiment.subset() defining the subset we plot

    interactive : Bool
        is this view interactive?
        
    Notes
    -----
    We inherit `xfacet` and `yfacet` from `cytoflow.views.HistogramView`, but
    they must both be unset!
        
    Examples
    --------
    In an IPython notebook with `%matplotlib notebook`
    
    >>> t = flow.ThresholdOp(name = "Threshold",
    ...                      channel = "Y2-A")
    >>> tv = t.default_view()
    >>> tv.plot(ex2)
    >>> tv.interactive = True
    >>> # .... draw a threshold on the plot
    >>> ex3 = thresh.apply(ex2)
    """

    id = Constant('edu.mit.synbio.cytoflow.views.threshold')
    friendly_id = Constant("Threshold Selection")

    op = Instance(IOperation)
    name = DelegatesTo('op')
    channel = DelegatesTo('op')
    threshold = DelegatesTo('op')
    interactive = Bool(False, transient=True)

    # internal state
    _ax = Any(transient=True)
    _line = Instance(Line2D, transient=True)
    _cursor = Instance(Cursor, transient=True)

    def plot(self, experiment, **kwargs):
        """Plot the histogram and then plot the threshold on top of it."""

        if not experiment:
            raise util.CytoflowViewError("No experiment specified")

        if self.xfacet:
            raise util.CytoflowViewError(
                "ThresholdSelection.xfacet must be empty")

        if self.yfacet:
            raise util.CytoflowViewError(
                "ThresholdSelection.yfacet must be empty")

        super(ThresholdSelection, self).plot(experiment, **kwargs)
        self._ax = plt.gca()
        self._draw_threshold()
        self._interactive()

    @on_trait_change('threshold', post_init=True)
    def _draw_threshold(self):
        if not self._ax or not self.threshold:
            return

        if self._line:
            # when used in the GUI, _draw_threshold gets called *twice* without
            # the plot being updated inbetween: and then the line can't be
            # removed from the plot, because it was never added.  so check
            # explicitly first.  this is likely to be an issue in other
            # interactive plots, too.
            if self._line and self._line in self._ax.lines:
                self._line.remove()

            self._line = None

        if self.threshold:
            self._line = plt.axvline(self.threshold, linewidth=3, color='blue')

        plt.draw()

    @on_trait_change('interactive', post_init=True)
    def _interactive(self):
        if self._ax and self.interactive:
            self._cursor = Cursor(self._ax,
                                  horizOn=False,
                                  vertOn=True,
                                  color='blue',
                                  useblit=True)
            self._cursor.connect_event('button_press_event', self._onclick)

        elif self._cursor:
            self._cursor.disconnect_events()
            self._cursor = None

    def _onclick(self, event):
        """Update the threshold location"""
        # sometimes the axes aren't set up and we don't get xdata (??)
        if event.xdata:
            self.threshold = event.xdata
示例#7
0
class CrossSection(object):
    """
    Class to manage the axes, artists and properties associated with
    showing a 2D image, a cross-hair cursor and two parasite axes which
    provide horizontal and vertical cross sections of image.

    You will likely need to call `CrossSection.init_artists(init_image)` after
    creating this object.

    Parameters
    ----------

    fig : matplotlib.figure.Figure
        The figure object to build the class on, will clear
        current contents

    init_image : 2d ndarray
        The initial image

    cmap : str,  colormap, or None
        color map to use.  Defaults to gray

    norm : Normalize or None
       Normalization function to use

    limit_func : callable, optional
        function that takes in the image and returns clim values
    auto_redraw : bool, optional
    interpolation : str, optional
        Interpolation method to use. List of valid options can be found in
        CrossSection2DView.interpolation

    Properties
    ----------
    interpolation : str
        The stringly-typed pixel interpolation. See _INTERPOLATION attribute
        of this cross_section_2d module
    cmap : str
        The colormap to use for rendering the image



    """
    def __init__(self, fig, cmap=None, norm=None,
                 limit_func=None, auto_redraw=True, interpolation=None):

        self._cursor_position_cbs = []
        if interpolation is None:
            interpolation = _INTERPOLATION[0]
        self._interpolation = interpolation
        # used to determine if setting properties should force a re-draw
        self._auto_redraw = auto_redraw
        # clean defaults
        if limit_func is None:
            limit_func = fullrange_limit_factory()
        if cmap is None:
            cmap = 'gray'
        # stash the color map
        self._cmap = cmap
        # let norm pass through as None, mpl defaults to linear which is fine
        if norm is None:
            norm = Normalize()
        self._norm = norm
        # save a copy of the limit function, we will need it later
        self._limit_func = limit_func

        # this is used by the widget logic
        self._active = True
        self._dirty = True
        self._cb_dirty = True

        # work on setting up the mpl axes

        self._fig = fig
        # blow away what ever is currently on the figure
        fig.clf()
        # Configure the figure in our own image
        #
        #     	  +----------------------+
        #	      |   H cross section    |
        #     	  +----------------------+
        #   +---+ +----------------------+
        #   | V | |                      |
        #   |   | |                      |
        #   | x | |                      |
        #   | s | |      Main Axes       |
        #   | e | |                      |
        #   | c | |                      |
        #   | t | |                      |
        #   | i | |                      |
        #   | o | |                      |
        #   | n | |                      |
        #   +---+ +----------------------+

        # make the main axes
        self._im_ax = fig.add_subplot(1, 1, 1)
        self._im_ax.set_aspect('equal')
        self._im_ax.xaxis.set_major_locator(NullLocator())
        self._im_ax.yaxis.set_major_locator(NullLocator())
        self._imdata = None
        self._im = self._im_ax.imshow([[]], cmap=self._cmap, norm=self._norm,
                        interpolation=self._interpolation,
                                      aspect='equal', vmin=0,
                                      vmax=1)

        # make it dividable
        divider = make_axes_locatable(self._im_ax)

        # set up all the other axes
        # (set up the horizontal and vertical cuts)
        self._ax_h = divider.append_axes('top', .5, pad=0.1,
                                         sharex=self._im_ax)
        self._ax_h.yaxis.set_major_locator(LinearLocator(numticks=2))
        self._ax_v = divider.append_axes('left', .5, pad=0.1,
                                         sharey=self._im_ax)
        self._ax_v.xaxis.set_major_locator(LinearLocator(numticks=2))
        self._ax_cb = divider.append_axes('right', .2, pad=.5)
        # add the color bar
        self._cb = fig.colorbar(self._im, cax=self._ax_cb)

        # add the cursor place holder
        self._cur = None

        # turn off auto-scale for the horizontal cut
        self._ax_h.autoscale(enable=False)

        # turn off auto-scale scale for the vertical cut
        self._ax_v.autoscale(enable=False)

        # create line artists
        self._ln_v, = self._ax_v.plot([],
                                      [], 'k-',
                                      animated=True,
                                      visible=False)

        self._ln_h, = self._ax_h.plot([],
                                      [], 'k-',
                                      animated=True,
                                      visible=False)

        # backgrounds for blitting
        self._ax_v_bk = None
        self._ax_h_bk = None

        # stash last-drawn row/col to skip if possible
        self._row = None
        self._col = None

        # make attributes for callback ids
        self._move_cid = None
        self._click_cid = None
        self._clear_cid = None

    def add_cursor_position_cb(self, callback):
        """ Add a callback for the cursor position in the main axes

        Parameters
        ----------
        callback : callable(cc, rr)
            Function that gets called when the cursor position moves to a new
            row or column on main axes
        """
        self._cursor_position_cbs.append(fun)

    # set up the call back for the updating the side axes
    def _move_cb(self, event):
        if not self._active:
            return
        if event is None:
            x = self._col
            y = self._row
            self._col = None
            self._row = None
        else:
            # short circuit on other axes
            if event.inaxes is not self._im_ax:
                return
            x, y = event.xdata, event.ydata
        numrows, numcols = self._imdata.shape
        if x is not None and y is not None:
            self._ln_h.set_visible(True)
            self._ln_v.set_visible(True)
            col = int(x + 0.5)
            row = int(y + 0.5)
            if row != self._row or col != self._col:
                if 0 <= col < numcols and 0 <= row < numrows:
                    self._col = col
                    self._row = row
                    for cb in self._cursor_position_cbs:
                        cb(col, row)
                    for data, ax, bkg, art, set_fun in zip(
                            (self._imdata[row, :], self._imdata[:, col]),
                            (self._ax_h, self._ax_v),
                            (self._ax_h_bk, self._ax_v_bk),
                            (self._ln_h, self._ln_v),
                            (self._ln_h.set_ydata, self._ln_v.set_xdata)):
                        self._fig.canvas.restore_region(bkg)
                        set_fun(data)
                        ax.draw_artist(art)
                        self._fig.canvas.blit(ax.bbox)


    def _click_cb(self, event):
        if event.inaxes is not self._im_ax:
            return
        self.active = not self.active
        if self.active:
            self._cur.onmove(event)
            self._move_cb(event)

    @auto_redraw
    def _connect_callbacks(self):
        """
        Connects all of the callbacks for the motion and click events
        """
        self._disconnect_callbacks()
        self._cur = Cursor(self._im_ax, useblit=True, color='red', linewidth=2)
        self._move_cid = self._fig.canvas.mpl_connect('motion_notify_event',
                                                     self._move_cb)

        self._click_cid = self._fig.canvas.mpl_connect('button_press_event',
                                                      self._click_cb)

        self._clear_cid = self._fig.canvas.mpl_connect('draw_event',
                                                       self._clear)
        self._fig.tight_layout()
        self._fig.canvas.draw()

    def _disconnect_callbacks(self):
        """
        Disconnects all of the callbacks
        """
        if self._fig.canvas is None:
            # no canvas -> can't do anything about the call backs which
            # should not exist
            self._move_cid = None
            self._clear_cid = None
            self._click_cid = None
            return

        for atr in ('_move_cid', '_clear_cid', '_click_cid'):
            cid = getattr(self, atr, None)
            if cid is not None:
                self._fig.canvas.mpl_disconnect(cid)
                setattr(self, atr, None)

        # clean up the cursor
        if self._cur is not None:
            self._cur.disconnect_events()
            del self._cur
            self._cur = None

    @auto_redraw
    def _init_artists(self, init_image):
        """
        Update the CrossSection with a new base-image.  This function
        takes care of setting up all of the details about the image size
        in the limits/artist extent of the image and the secondary data
        in the cross-section parasite plots.

        Parameters
        ----------
        init_image : ndarray
           An image to serve as the new 'base' image.
        """

        im_shape = init_image.shape

        # first deal with the image axis
        # update the image, `update_artists` takes care of
        # updating the actual artist
        self._imdata = init_image

        # update the extent of the image artist
        self._im.set_extent([-0.5, im_shape[1] + .5,
                             im_shape[0] + .5, -0.5])

        # update the limits of the image axes to match the exent
        self._im_ax.set_xlim([-.05, im_shape[1] + .5])
        self._im_ax.set_ylim([im_shape[0] + .5, -0.5])

        # update the format coords printer
        numrows, numcols = im_shape

        # note, this is a closure over numrows and numcols
        def format_coord(x, y):
            # adjust xy -> col, row
            col = int(x + 0.5)
            row = int(y + 0.5)
            # make sure the point falls in the array
            if col >= 0 and col < numcols and row >= 0 and row < numrows:
                # if it does, grab the value
                z = self._imdata[row, col]
                return "X: {x:d} Y: {y:d} I: {i:.2f}".format(x=col, y=row, i=z)
            else:
                return "X: {x:d} Y: {y:d}".format(x=col, y=row)

        # replace the current format_coord function
        self._im_ax.format_coord = format_coord

        # net deal with the parasite axes and artist
        self._ln_v.set_data(np.zeros(im_shape[0]),
                            np.arange(im_shape[0]))
        self._ax_v.set_ylim([0, im_shape[0]])

        self._ln_h.set_data(np.arange(im_shape[1]),
                            np.zeros(im_shape[1]))
        self._ax_h.set_xlim([0, im_shape[1]])

        # if we have a cavas, then connect/set up junk
        if self._fig.canvas is not None:
            self._connect_callbacks()
        # mark as dirty
        self._dirty = True

    def _clear(self, event):
        self._ax_v_bk = self._fig.canvas.copy_from_bbox(self._ax_v.bbox)
        self._ax_h_bk = self._fig.canvas.copy_from_bbox(self._ax_h.bbox)
        self._ln_h.set_visible(False)
        self._ln_v.set_visible(False)
        # this involves reaching in and touching the guts of the
        # cursor widget.  The problem is that the mpl widget
        # skips updating it's saved background if the widget is inactive
        if self._cur:
            self._cur.background = self._cur.canvas.copy_from_bbox(
                self._cur.canvas.figure.bbox)

    @property
    def interpolation(self):
        return self._interpolation

    @property
    def active(self):
        return self._active

    @active.setter
    def active(self, val):
        self._active = val
        self._cur.active = val

    @auto_redraw
    def update_interpolation(self, interpolation):
        """
        Set the interpolation method

        """
        self._dirty = True
        self._im.set_interpolation(interpolation)

    @auto_redraw
    def update_cmap(self, cmap):
        """
        Set the color map used
        """
        # TODO: this should stash new value, not apply it
        self._cmap = cmap
        self._dirty = True

    @auto_redraw
    def update_image(self, image):
        """
        Set the image data

        The input data does not necessarily have to be the same shape as the
        original image
        """
        if self._imdata is None or self._imdata.shape != image.shape:
            self._init_artists(image)
        self._imdata = image
        self._move_cb(None)
        self._dirty = True

    @auto_redraw
    def update_norm(self, norm):
        """
        Update the way that matplotlib normalizes the image
        """
        self._norm = norm
        self._dirty = True
        self._cb_dirty = True

    @auto_redraw
    def update_limit_func(self, limit_func):
        """
        Set the function to use to determine the color scale
        """
        # set the new function to use for computing the color limits
        self._limit_func = limit_func
        self._dirty = True

    def _update_artists(self):
        """
        Updates the figure by re-drawing
        """
        # if the figure is not dirty, short-circuit
        if not (self._dirty or self._cb_dirty):
            return

        # this is a tuple which is the max/min used in the color mapping.
        # these values are also used to set the limits on the value
        # axes of the parasite axes
        # value_limits
        vlim = self._limit_func(self._imdata)
        # set the color bar limits
        self._im.set_clim(vlim)
        self._norm.vmin, self._norm.vmax = vlim
        # set the cross section axes limits
        self._ax_v.set_xlim(*vlim[::-1])
        self._ax_h.set_ylim(*vlim)
        # set the imshow data
        self._im.set_data(self._imdata)
        self._im.set_cmap(self._cmap)
        self._im.set_norm(self._norm)

        # TODO if cb_dirty, remake the colorbar, I think this is
        # why changing the norm does not play well
        self._dirty = False
        self._cb_dirty = False

    def _draw(self):
        self._fig.canvas.draw()

    @auto_redraw
    def autoscale_horizontal(self, enable):
        self._ax_h.autoscale(enable=enable)

    @auto_redraw
    def autoscale_vertical(self, enable):
        self._ax_v.autoscale(enable=False)
示例#8
0
class PolygonSelection(cytoflow.views.ScatterplotView):
    """Plots, and lets the user interact with, a 2D polygon selection.
    
    Attributes
    ----------
    op : Instance(PolygonOp)
        The operation on which this selection view is operating
        
    huefacet : Str
        The conditioning variable to show multiple colors on this plot
        
    subset : Str
        The string for subsetting the plot

    interactive : Bool
        is this view interactive?  Ie, can the user set the polygon verticies
        with mouse clicks?
        
    Notes
    -----
    We inherit `xfacet` and `yfacet` from `cytoflow.views.ScatterPlotView`, but
    they must both be unset!
        
    Examples
    --------

    In an IPython notebook with `%matplotlib notebook`
    
    >>> s = flow.ScatterplotView(xchannel = "V2-A",
    ...                          ychannel = "Y2-A")
    >>> poly = s.default_view()
    >>> poly.plot(ex2)
    >>> poly.interactive = True
    """

    id = Constant('edu.mit.synbio.cytoflow.views.polygon')
    friendly_id = Constant("Polygon Selection")

    op = Instance(IOperation)
    name = DelegatesTo('op')
    xchannel = DelegatesTo('op')
    ychannel = DelegatesTo('op')
    interactive = Bool(False, transient=True)

    # internal state.
    _ax = Any(transient=True)
    _cursor = Instance(Cursor, transient=True)
    _path = Instance(mpl.path.Path, transient=True)
    _patch = Instance(mpl.patches.PathPatch, transient=True)
    _line = Instance(mpl.lines.Line2D, transient=True)
    _drawing = Bool(transient=True)
    _last_draw_time = Float(0.0, transient=True)
    _last_click_time = Float(0.0, transient=True)

    def plot(self, experiment, **kwargs):
        """Plot self.view, and then plot the selection on top of it."""

        if not experiment:
            raise util.CytoflowViewError("No experiment specified")

        if self.xfacet:
            raise util.CytoflowViewError(
                "RangeSelection.xfacet must be empty or `Undefined`")

        if self.yfacet:
            raise util.CytoflowViewError(
                "RangeSelection.yfacet must be empty or `Undefined`")

        super(PolygonSelection, self).plot(experiment, **kwargs)
        self._ax = plt.gca()
        self._draw_poly()
        self._interactive()

    @on_trait_change('op.vertices', post_init=True)
    def _draw_poly(self):
        if not self._ax:
            return

        if self._patch and self._patch in self._ax.patches:
            self._patch.remove()

        if self._drawing or not self.op.vertices or len(self.op.vertices) < 3 \
                         or any([len(x) != 2 for x in self.op.vertices]):
            return

        patch_vert = np.concatenate(
            (np.array(self.op.vertices), np.array((0, 0), ndmin=2)))

        self._patch = \
            mpl.patches.PathPatch(mpl.path.Path(patch_vert, closed = True),
                                  edgecolor="black",
                                  linewidth = 1.5,
                                  fill = False)

        self._ax.add_patch(self._patch)
        plt.draw_if_interactive()

    @on_trait_change('interactive', post_init=True)
    def _interactive(self):
        if self._ax and self.interactive:
            self._cursor = Cursor(self._ax, horizOn=False, vertOn=False)
            self._cursor.connect_event('button_press_event', self._onclick)
            self._cursor.connect_event('motion_notify_event', self._onmove)
        elif self._cursor:
            self._cursor.disconnect_events()
            self._cursor = None

    def _onclick(self, event):
        """Update selection traits"""
        if not self._ax:
            return

        if (self._cursor.ignore(event)):
            return

        # we have to check the wall clock time because the IPython notebook
        # doesn't seem to register double-clicks
        if event.dblclick or (time.clock() - self._last_click_time < 0.5):
            self._drawing = False
            self.op.vertices = map(tuple, self._path.vertices)
            self.op._xscale = plt.gca().get_xscale()
            self.op._yscale = plt.gca().get_yscale()
            self._path = None
            return

        self._last_click_time = time.clock()

        self._drawing = True
        if self._patch and self._patch in self._ax.patches:
            self._patch.remove()

        if self._path:
            vertices = np.concatenate((self._path.vertices,
                                       np.array((event.xdata, event.ydata),
                                                ndmin=2)))
        else:
            vertices = np.array((event.xdata, event.ydata), ndmin=2)

        self._path = mpl.path.Path(vertices, closed=False)
        self._patch = mpl.patches.PathPatch(self._path,
                                            edgecolor="black",
                                            fill=False)

        self._ax.add_patch(self._patch)
        plt.draw_if_interactive()

    def _onmove(self, event):

        if not self._ax:
            return

        if (self._cursor.ignore(event) or not self._drawing or not self._path
                or self._path.vertices.shape[0] == 0 or not event.xdata
                or not event.ydata):
            return

        # only draw 5 times/sec
        if (time.clock() - self._last_draw_time < 0.2):
            return

        self._last_draw_time = time.clock()

        if self._line and self._line in self._ax.lines:
            self._line.remove()

        xdata = [self._path.vertices[-1, 0], event.xdata]
        ydata = [self._path.vertices[-1, 1], event.ydata]
        self._line = mpl.lines.Line2D(xdata, ydata, linewidth=1, color="black")

        self._ax.add_line(self._line)
        plt.gcf().canvas.draw()
示例#9
0
class QuadSelection(Op2DView, ScatterplotView):
    """Plots, and lets the user interact with, a quadrant gate.
    
    Attributes
    ----------
    interactive : Bool
        is this view interactive?  Ie, can the user set the threshold with a 
        mouse click?
        
    Notes
    -----
    We inherit :attr:`xfacet` and :attr:`yfacet` from 
    :class:`cytoflow.views.ScatterplotView`, but they must both be unset!
        
    Examples
    --------
    
    In an Jupyter notebook with `%matplotlib notebook`
    
    >>> q = flow.QuadOp(name = "Quad",
    ...                 xchannel = "V2-A",
    ...                 ychannel = "Y2-A"))
    >>> qv = q.default_view()
    >>> qv.interactive = True
    >>> qv.plot(ex2) 
    """

    id = Constant('edu.mit.synbio.cytoflow.views.quad')
    friendly_id = Constant("Quadrant Selection")

    xfacet = Constant(None)
    yfacet = Constant(None)

    # override the Op2DView
    xscale = util.ScaleEnum
    yscale = util.ScaleEnum

    xthreshold = DelegatesTo('op')
    ythreshold = DelegatesTo('op')

    interactive = Bool(False, transient=True)

    # internal state.
    _ax = Any(transient=True)
    _hline = Instance(Line2D, transient=True)
    _vline = Instance(Line2D, transient=True)
    _cursor = Instance(Cursor, transient=True)

    def plot(self, experiment, **kwargs):
        """
        Plot the underlying scatterplot and then plot the selection on top of it.
        
        Parameters
        ----------
        
        """

        if experiment is None:
            raise util.CytoflowViewError('experiment',
                                         "No experiment specified")

        super().plot(experiment, **kwargs)
        self._ax = plt.gca()
        self._draw_lines()
        self._interactive()

    @on_trait_change('xthreshold, ythreshold', post_init=True)
    def _draw_lines(self):
        if not self._ax:
            return

        if self._hline and self._hline in self._ax.lines:
            self._hline.remove()

        if self._vline and self._vline in self._ax.lines:
            self._vline.remove()

        if self.xthreshold and self.ythreshold:
            self._hline = plt.axhline(self.ythreshold,
                                      linewidth=3,
                                      color='blue')
            self._vline = plt.axvline(self.xthreshold,
                                      linewidth=3,
                                      color='blue')

            plt.draw()

    @on_trait_change('interactive', post_init=True)
    def _interactive(self):
        if self._ax and self.interactive:
            self._cursor = Cursor(self._ax,
                                  horizOn=True,
                                  vertOn=True,
                                  color='blue',
                                  useblit=True)
            self._cursor.connect_event('button_press_event', self._onclick)
        elif self._cursor:
            self._cursor.disconnect_events()
            self._cursor = None

    def _onclick(self, event):
        """Update the threshold location"""
        self.xthreshold = event.xdata
        self.ythreshold = event.ydata
示例#10
0
class PolygonSelection(cytoflow.views.ScatterplotView):
    """Plots, and lets the user interact with, a 2D polygon selection.
    
    Attributes
    ----------
    op : Instance(PolygonOp)
        The operation on which this selection view is operating
        
    huefacet : Str
        The conditioning variable to show multiple colors on this plot
        
    subset : Str
        The string for subsetting the plot

    interactive : Bool
        is this view interactive?  Ie, can the user set the polygon verticies
        with mouse clicks?
        
    Notes
    -----
    We inherit `xfacet` and `yfacet` from `cytoflow.views.ScatterPlotView`, but
    they must both be unset!
        
    Examples
    --------

    In an IPython notebook with `%matplotlib notebook`
    
    >>> s = flow.ScatterplotView(xchannel = "V2-A",
    ...                          ychannel = "Y2-A")
    >>> poly = s.default_view()
    >>> poly.plot(ex2)
    >>> poly.interactive = True
    """
    
    id = Constant('edu.mit.synbio.cytoflow.views.polygon')
    friendly_id = Constant("Polygon Selection")
    
    op = Instance(IOperation)
    name = DelegatesTo('op')
    xchannel = DelegatesTo('op')
    ychannel = DelegatesTo('op')
    interactive = Bool(False, transient = True)

    # internal state.
    _ax = Any(transient = True)
    _cursor = Instance(Cursor, transient = True)
    _path = Instance(mpl.path.Path, transient = True)
    _patch = Instance(mpl.patches.PathPatch, transient = True)
    _line = Instance(mpl.lines.Line2D, transient = True)
    _drawing = Bool(transient = True)
    _last_draw_time = Float(0.0, transient = True)
    _last_click_time = Float(0.0, transient = True)
        
    def plot(self, experiment, **kwargs):
        """Plot self.view, and then plot the selection on top of it."""
        
        if not experiment:
            raise util.CytoflowViewError("No experiment specified")
        
        if self.xfacet:
            raise util.CytoflowViewError("RangeSelection.xfacet must be empty or `Undefined`")
        
        if self.yfacet:
            raise util.CytoflowViewError("RangeSelection.yfacet must be empty or `Undefined`")
        
        super(PolygonSelection, self).plot(experiment, **kwargs)
        self._ax = plt.gca()
        self._draw_poly()
        self._interactive()
    
    @on_trait_change('op.vertices', post_init = True)
    def _draw_poly(self):
        if not self._ax:
            return
         
        if self._patch and self._patch in self._ax.patches:
            self._patch.remove()
            
        if self._drawing or not self.op.vertices or len(self.op.vertices) < 3 \
                         or any([len(x) != 2 for x in self.op.vertices]):
            return
             
        patch_vert = np.concatenate((np.array(self.op.vertices), 
                                    np.array((0,0), ndmin = 2)))
                                    
        self._patch = \
            mpl.patches.PathPatch(mpl.path.Path(patch_vert, closed = True),
                                  edgecolor="black",
                                  linewidth = 1.5,
                                  fill = False)
            
        self._ax.add_patch(self._patch)
        plt.draw_if_interactive()
    
    @on_trait_change('interactive', post_init = True)
    def _interactive(self):
        if self._ax and self.interactive:
            self._cursor = Cursor(self._ax, horizOn = False, vertOn = False)            
            self._cursor.connect_event('button_press_event', self._onclick)
            self._cursor.connect_event('motion_notify_event', self._onmove)
        elif self._cursor:
            self._cursor.disconnect_events()
            self._cursor = None       
    
    def _onclick(self, event): 
        """Update selection traits"""      
        if not self._ax:
            return
        
        if(self._cursor.ignore(event)):
            return
        
        # we have to check the wall clock time because the IPython notebook
        # doesn't seem to register double-clicks
        if event.dblclick or (time.clock() - self._last_click_time < 0.5):
            self._drawing = False
            self.op.vertices = map(tuple, self._path.vertices)
            self.op._xscale = plt.gca().get_xscale()
            self.op._yscale = plt.gca().get_yscale()
            self._path = None
            return
        
        self._last_click_time = time.clock()
                
        self._drawing = True
        if self._patch and self._patch in self._ax.patches:
            self._patch.remove()
            
        if self._path:
            vertices = np.concatenate((self._path.vertices,
                                      np.array((event.xdata, event.ydata), ndmin = 2)))
        else:
            vertices = np.array((event.xdata, event.ydata), ndmin = 2)

        self._path = mpl.path.Path(vertices, closed = False)
        self._patch = mpl.patches.PathPatch(self._path, 
                                            edgecolor = "black",
                                            fill = False)

        self._ax.add_patch(self._patch)
        plt.draw_if_interactive()
        
    def _onmove(self, event):       
         
        if not self._ax:
            return
         
        if(self._cursor.ignore(event) 
           or not self._drawing
           or not self._path
           or self._path.vertices.shape[0] == 0
           or not event.xdata
           or not event.ydata):
            return

        # only draw 5 times/sec
        if(time.clock() - self._last_draw_time < 0.2):
            return
        
        self._last_draw_time = time.clock()
         
        if self._line and self._line in self._ax.lines:
            self._line.remove()
            
        xdata = [self._path.vertices[-1, 0], event.xdata]
        ydata = [self._path.vertices[-1, 1], event.ydata]
        self._line = mpl.lines.Line2D(xdata, ydata, linewidth = 1, color = "black")
        
        self._ax.add_line(self._line)
        plt.gcf().canvas.draw()
示例#11
0
class QuadSelection(cytoflow.views.ScatterplotView):
    """Plots, and lets the user interact with, a quadrant gate.
    
    Attributes
    ----------
    op : Instance(Range2DOp)
        The instance of Range2DOp that we're viewing / editing
        
    huefacet : Str
        The conditioning variable to plot multiple colors
        
    subset : Str
        The string passed to `Experiment.query()` to subset the data before
        plotting
        
    interactive : Bool
        is this view interactive?  Ie, can the user set the threshold with a 
        mouse click?
        
    Notes
    -----
    We inherit `xfacet` and `yfacet` from `cytoflow.views.ScatterplotView`, but
    they must both be unset!
        
    Examples
    --------
    
    In an IPython notebook with `%matplotlib notebook`
    
    >>> q = flow.QuadOp(name = "Quad",
    ...                 xchannel = "V2-A",
    ...                 ychannel = "Y2-A"))
    >>> qv = q.default_view()
    >>> qv.interactive = True
    >>> qv.plot(ex2) 
    """

    id = Constant('edu.mit.synbio.cytoflow.views.quad')
    friendly_id = Constant("Quadrant Selection")

    op = Instance(IOperation)
    name = DelegatesTo('op')
    xchannel = DelegatesTo('op')
    ychannel = DelegatesTo('op')
    interactive = Bool(False, transient=True)

    # internal state.
    _ax = Any(transient=True)
    _hline = Instance(Line2D, transient=True)
    _vline = Instance(Line2D, transient=True)
    _cursor = Instance(Cursor, transient=True)

    def plot(self, experiment, **kwargs):
        """Plot the underlying scatterplot and then plot the selection on top of it."""

        if not experiment:
            raise util.CytoflowOpError("No experiment specified")

        if not experiment:
            raise util.CytoflowViewError("No experiment specified")

        if self.xfacet:
            raise util.CytoflowViewError(
                "RangeSelection.xfacet must be empty or `Undefined`")

        if self.yfacet:
            raise util.CytoflowViewError(
                "RangeSelection.yfacet must be empty or `Undefined`")

        super(QuadSelection, self).plot(experiment, **kwargs)
        self._ax = plt.gca()
        self._draw_lines()
        self._interactive()

    @on_trait_change('op.xthreshold, op.ythreshold', post_init=True)
    def _draw_lines(self):
        if not self._ax:
            return

        if self._hline and self._hline in self._ax.lines:
            self._hline.remove()

        if self._vline and self._vline in self._ax.lines:
            self._vline.remove()

        if self.op.xthreshold and self.op.ythreshold:
            self._hline = plt.axhline(self.op.ythreshold,
                                      linewidth=3,
                                      color='blue')
            self._vline = plt.axvline(self.op.xthreshold,
                                      linewidth=3,
                                      color='blue')

            plt.draw_if_interactive()

    @on_trait_change('interactive', post_init=True)
    def _interactive(self):
        if self._ax and self.interactive:
            self._cursor = Cursor(self._ax,
                                  horizOn=True,
                                  vertOn=True,
                                  color='blue')
            self._cursor.connect_event('button_press_event', self._onclick)
        elif self._cursor:
            self._cursor.disconnect_events()
            self._cursor = None

    def _onclick(self, event):
        """Update the threshold location"""
        self.op.xthreshold = event.xdata
        self.op.ythreshold = event.ydata