Source code for data_slicer.cutline


import logging

import pyqtgraph as pg
from pyqtgraph import Qt as qt
from pyqtgraph import QtGui, Point
from pyqtgraph.functions import affineSlice

logger = logging.getLogger('ds.'+__name__)

[docs]class CustomizableLineSegmentROI(pg.LineSegmentROI) : """ Subclass of :class:`LineSegmentROI <pyqtgraph.LineSegmentROI>`. Implements a few features that were missing from the parent class, namely customizable hover pen. """ def __init__(self, *args, **kwargs) : super().__init__(*args, **kwargs) # Set a default pen self.set_hover_pen(color=(255, 255, 0))
[docs] def set_hover_pen(self, *args, **kwargs) : """ Set the pen used for drawing this widget when it is in the `hovered` state. Accepts function arguments to :func: `mkPen pyqtgraph.mkPen` """ self.hover_pen = pg.mkPen(*args, **kwargs)
def _makePen(self) : """ Overrided parent's :func: `_makePen <pyqtgrapch.graphicsItems.ROI.makePen()>` function to allow custom hover styles. """ if self.mouseHovering : return self.hover_pen else : return self.pen
[docs]class CustomizableHandle(pg.graphicsItems.ROI.Handle) : """ Same concept as :class:`CustomizableLineSegmentROI <data_slicer.cutline.CustomizableLineSegmentROI`>. Subclass to allow customization of hover pen. .. Note:: This class is unused because that would require setting these handles as the handles for `CustomizableLineSegmentROI`. Too much work for a relatively unimportant feature. """ def __init__(self, *args, **kwargs) : super().__init__(*args, **kwargs) # Set a default pen self.set_hover_pen(color=(255, 255, 0))
[docs] def set_hover_pen(self, *args, **kwargs) : """ Set the pen used for drawing this widget when it is in the *hovered* state. Accepts function arguments to :func:` pyqtgraph.mkPen` """ self.hover_pen = pg.mkPen(*args, **kwargs)
[docs] def hoverEvent(self, ev) : """ This just copies the code of the parent class with the difference of a variable hover pen. """ hover = False # Check if it is appropriate to change the state to `hover` if not ev.isExit() : if ev.acceptDrags(QtCore.Qt.LeftButton) : hover=True for btn in [QtCore.Qt.LeftButton, QtCore.Qt.RightButton, QtCore.Qt.MidButton] : if (int(self.acceptedMouseButtons() & btn) > 0 and ev.acceptClicks(btn)) : hover=True if hover : # This is the only changed line self.currentPen = self.hover_pen else : self.currentPen = self.pen self.update()
[docs]class Cutline(qt.QtCore.QObject) : """ Wrapper class allowing easy adding and removing of :class:`pyqtgraph.LineSegmentROI` to a :class:`pyqtgraph.PlotWidget`. It both has-a LineSegmentROI and has-a PlotWidget and handles interactions between the two. Needs to inherit from :class:`pyqtgraph.qt.QtCore.QObject` in order to have signals. **Signals** ================== ======================================================== sig_region_changed wraps the underlying :class:`LineSegmentROI <pyqtgraph.LineSegmentROI>`'s sigRegionChange. Emitted whenever the ROI is moved or changed. sig_initialized emitted when a new :class:`LineSegmentROI <pyqtgraph.LineSegmentROI>` has been created and assigned as this :class:`Cutline <data_slicer.cutline.Cutline>`'s `roi`. ================== ======================================================== """ sig_initialized = qt.QtCore.Signal() def __init__(self, plot_widget=None, orientation='horizontal', handles=(None, None), **kwargs) : super().__init__(**kwargs) if plot_widget : self.add_to_plot(plot_widget) self.orientation = orientation self.roi = None # Define default pens self.pen = pg.mkPen((255, 255, 0), width=3) self.hover_pen = pg.mkPen((255, 150, 10), width=3)
[docs] def add_to_plot(self, plot_widget) : """ Add this cutline to a :class:`PlotWidget <pyqtgraph.PlotWidget>`. This is effectively implemented by setting this :class:`Cutline <data_slicer.cutline.Cutline>`'s plot attribute to the given *plot_widget*. """ self.plot = plot_widget
# # Signal connection: whenever the viewRange changes, the cutline should # # be updated. Make sure to not accumulate connections by trying to # # disconnect first. # try : # self.plot.sigRangeChanged.disconnect(self.initialize) # except TypeError : # pass # self.plot.sig_axes_changed.connect(self.initialize) # self.plot.sigRangeChanged.connect(self.initialize)
[docs] def initialize(self, orientation=None) : """ Emits :signal:`sig_initialized`. """ logger.debug('initialize()') # Change the orientation if one is given if orientation : self.orientation = orientation # Remove the old LineSegmentROI if necessary self.plot.removeItem(self.roi) # Put a new LineSegmentROI in the center of the plot in the right # orientation lower_left, upper_right = self.calculate_endpoints() self.roi = CustomizableLineSegmentROI(positions=[lower_left, upper_right], pen='m') self.roi.setPen(self.pen) self.roi.set_hover_pen(self.hover_pen) # Set default handle style self.set_handle_style() self.plot.addItem(self.roi, ignoreBounds=True) # Reconnect signal handling # Wrap the LineSegmentROI's sigRegionChanged self.sig_region_changed = self.roi.sigRegionChanged logger.info('Emitting sig_initialized.') self.sig_initialized.emit()
[docs] def set_handle_style(self, radius=8, color=(200, 255, 200), width=2) : """ Set the size and pen of the handles. """ for h in self.roi.getHandles() : # Delete cached shapes h._shape = None h.radius = radius h.pen = pg.mkPen(color, width=width) # Need to redraw path h.buildPath()
[docs] def recenter(self) : """ Put the ROI in the center of the current plot. """ logger.info('Recentering ROI.') lower_left, upper_right = self.calculate_endpoints()
[docs] def calculate_endpoints(self) : """ Get sensible initial values for the endpoints of the :class:`LineSegmentROI <pyqtgraph.LineSegmentROI>` from the :class:`pyqtrgaph.PlotWidget`'s current view range. Depending on the state of `self.orientation` these endpoints correspond either to a vertical or horizontal line centered at the center of the plot and spanning exactly the whole plot range. Returns a tuple of len(2) lists: (lower_left, top_right) corresponding to the two endpoints. """ # Get the current range of the plot [[xmin, xmax], [ymin, ymax]] = self.plot.get_limits() x = 0.5*(xmax+xmin) y = 0.5*(ymax+ymin) # Set the start and endpoint depending on the orientation if self.orientation == 'horizontal' : lower_left = [xmin, y] upper_right = [xmax, y] elif self.orientation == 'vertical' : lower_left = [x, ymin] upper_right = [x, ymax] logger.debug('lower_left: {}, upper_right: {}'.format(lower_left, upper_right)) return lower_left, upper_right
[docs] def flip_orientation(self) : """ Change the cutline's orientation from vertical to horitontal or vice-versa and re-initialize it in the new orientation. """ # Find out which orientation we're currently in and change # accordingly orientations = ['horizontal', 'vertical'] # `i` will be the index of the orientation we currently don't have i = (orientations.index(self.orientation) + 1) % 2 self.orientation = orientations[i] logger.info('New orientation: {}'.format(self.orientation)) self.initialize()
[docs] def get_array_region(self, *args, **kwargs) : """ Wrapper for the underlying ROI's :meth:`~data_slicer.cutline.Cutline.roi.getArrayRegion`. """ return self.roi.getArrayRegion(*args, **kwargs)