コード例 #1
0
ファイル: pit.py プロジェクト: xy2259/data_slicer
    def prepare_data(self, data, axes=3 * [None]):
        """ Load the specified data and prepare the corresponding z range. 
        Then display the newly loaded data.

        *Parameters*
        ====  ==================================================================
        data  3d array; the data to display
        axes  len(3) list or array of 1d-arrays or None; the units along the 
              x, y and z axes respectively. If any of those is *None*, pixels 
              are used.
        ====  ==================================================================
        """
        logger.debug('prepare_data()')

        self.data = TracedVariable(data, name='data')
        self.axes = np.array(axes)

        # Retain a copy of the original data and axes so that we can reset later
        # NOTE: this effectively doubles the used memory!
        self.original_data = copy(self.data.get_value())
        self.original_axes = copy(self.axes)

        self.prepare_axes()
        self.on_z_dim_change()

        # Connect signal handling so changes in data are immediately reflected
        self.z.sig_value_changed.connect( \
            lambda : self.main_window.update_main_plot(emit=False))
        self.data.sig_value_changed.connect(self.on_data_change)

        self.main_window.update_main_plot()
        self.main_window.set_axes()
コード例 #2
0
    def __init__(self, *args, data=None, **kwargs):
        """
        Set up the :class: `GLViewWidget <pyqtgraph.opengl.GLViewWidget>` 
        as this widget's central widget.
        """
        super().__init__(*args, **kwargs)

        # Initialize instance variables
        self.data = TracedVariable(None, name='data')
        self.cmap = load_cmap(DEFAULT_CMAP)
        self.lut = self.cmap.getLookupTable()
        self.gloptions = 'translucent'

        # Create a GLViewWidget and put it into the layout of this view
        self.layout = QtGui.QGridLayout()
        self.setLayout(self.layout)
        self.glview = gl.GLViewWidget()

        # Add the scalebar
        self._initialize_sub_widgets()

        # Put all widgets in place
        self.align()

        # Add some coordinate axes to the view
        self.coordinate_axes = gl.GLAxisItem()
        self.glview.addItem(self.coordinate_axes)
        self.set_coordinate_axes(True)

        # Try to set the default data
        if data is not None:
            self.set_data(data)
コード例 #3
0
    def __init__(self, pos=(0,0)) :
        # Store the positions in TracedVariables
        self.hpos = TracedVariable(pos[1], name='hpos')
        self.vpos = TracedVariable(pos[0], name='vpos')

        # Initialize the InfiniteLines
        self.hline = pg.InfiniteLine(pos[1], movable=True, angle=0)
        self.vline = pg.InfiniteLine(pos[0], movable=True, angle=90)

        # Set the color
        self.set_color(BASE_LINECOLOR, HOVER_COLOR)

        # Register some callbacks
        self.hpos.sig_value_changed.connect(self.update_position_h)
        self.vpos.sig_value_changed.connect(self.update_position_v)

        self.hline.sigDragged.connect(self.on_dragged_h)
        self.vline.sigDragged.connect(self.on_dragged_v)
コード例 #4
0
    def __init__(self, image=None, parent=None, background='default', 
                 name=None, **kwargs) :
        """ Allows setting of the image upon initialization. 
        
        **Parameters**

        ==========  ============================================================
        image       np.ndarray or pyqtgraph.ImageItem instance; the image to be
                    displayed.
        parent      QtWidget instance; parent widget of this widget.
        background  str; confer PyQt documentation
        name        str; allows giving a name for debug purposes
        ==========  ============================================================
        """
        # Initialize instance variables
        # np.array, raw image data
        self.image_data = None
        # pg.ImageItem of *image_data*
        self.image_item = None
        self.image_kwargs = {}
        self.xlim = None
        self.ylim = None
        self.xscale = None
        self.yscale = None
        self.transform_factors = []
        self.transposed = TracedVariable(False, name='transposed')
        self.crosshair_cursor_visible = False

        super().__init__(parent=parent, background=background, 
                         viewBox=DSViewBox(imageplot=self), **kwargs) 

        self.name = name

        # Show top and tight axes by default, but without ticklabels
        self.showAxis('top')
        self.showAxis('right')
        self.getAxis('top').setStyle(showValues=False)
        self.getAxis('right').setStyle(showValues=False)

        if image is not None :
            self.set_image(image)

        self.sig_axes_changed.connect(self.fix_viewrange)
コード例 #5
0
ファイル: imageplot.py プロジェクト: xy2259/data_slicer
    def __init__(self, pos=(0, 0)):
        # Store the positions in TracedVariables
        self.hpos = TracedVariable(pos[1], name='hpos')
        self.vpos = TracedVariable(pos[0], name='vpos')

        # Initialize the InfiniteLines
        self.hline = pg.InfiniteLine(pos[1], movable=True, angle=0)
        self.vline = pg.InfiniteLine(pos[0], movable=True, angle=90)

        # Set the color
        for line in [self.hline, self.vline]:
            line.setPen((255, 255, 0, 255))
            line.setHoverPen(HOVER_COLOR)

        # Register some callbacks
        self.hpos.sig_value_changed.connect(self.update_position_h)
        self.vpos.sig_value_changed.connect(self.update_position_v)

        self.hline.sigDragged.connect(self.on_dragged_h)
        self.vline.sigDragged.connect(self.on_dragged_v)
コード例 #6
0
ファイル: imageplot.py プロジェクト: xy2259/data_slicer
class Crosshair():
    """ Crosshair made up of two InfiniteLines. """
    def __init__(self, pos=(0, 0)):
        # Store the positions in TracedVariables
        self.hpos = TracedVariable(pos[1], name='hpos')
        self.vpos = TracedVariable(pos[0], name='vpos')

        # Initialize the InfiniteLines
        self.hline = pg.InfiniteLine(pos[1], movable=True, angle=0)
        self.vline = pg.InfiniteLine(pos[0], movable=True, angle=90)

        # Set the color
        for line in [self.hline, self.vline]:
            line.setPen((255, 255, 0, 255))
            line.setHoverPen(HOVER_COLOR)

        # Register some callbacks
        self.hpos.sig_value_changed.connect(self.update_position_h)
        self.vpos.sig_value_changed.connect(self.update_position_v)

        self.hline.sigDragged.connect(self.on_dragged_h)
        self.vline.sigDragged.connect(self.on_dragged_v)

    def add_to(self, widget):
        """ Add this crosshair to a Qt widget. """
        for line in [self.hline, self.vline]:
            line.setZValue(1)
            widget.addItem(line)

    def update_position_h(self):
        """ Callback for the :signal: `sig_value_changed 
        <data_slicer.utilities.TracedVariable.sig_value_changed>`. Whenever the 
        value of this TracedVariable is updated (possibly from outside this 
        Crosshair object), put the crosshair to the appropriate position.
        """
        self.hline.setValue(self.hpos.get_value())

    def update_position_v(self):
        """ Confer update_position_h. """
        self.vline.setValue(self.vpos.get_value())

    def on_dragged_h(self):
        """ Callback for dragging of InfiniteLines. Their visual position 
        should be reflected in the TracedVariables self.hpos and self.vpos.
        """
        self.hpos.set_value(self.hline.value())

    def on_dragged_v(self):
        """ Callback for dragging of InfiniteLines. Their visual position 
        should be reflected in the TracedVariables self.hpos and self.vpos.
        """
        self.vpos.set_value(self.vline.value())

    def set_bounds(self, xmin, xmax, ymin, ymax):
        """ Set the area in which the infinitelines can be dragged. """
        self.hline.setBounds([ymin, ymax])
        self.vline.setBounds([xmin, xmax])
コード例 #7
0
ファイル: imageplot.py プロジェクト: xy2259/data_slicer
    def __init__(self,
                 parent=None,
                 background='default',
                 name=None,
                 orientation='vertical',
                 slider_width=1,
                 **kwargs):
        """ Initialize the slider and set up the visual tweaks to make a 
        PlotWidget look more like a scalebar.

        ===========  ============================================================
        parent       QtWidget instance; parent widget of this widget
        background   str; confer PyQt documentation
        name         str; allows giving a name for debug purposes
        orientation  str, `horizontal` or `vertical`; orientation of the cursor
        ===========  ============================================================
        """
        super().__init__(parent=parent, background=background, **kwargs)

        if orientation not in ['horizontal', 'vertical']:
            raise ValueError('Only `horizontal` or `vertical` are allowed for '
                             'orientation.')
        self.orientation = orientation
        self.orientate()

        if name is not None:
            self.name = name

        # Hide the pyqtgraph auto-rescale button
        self.getPlotItem().buttonsHidden = True

        # Display the right (or top) axis without ticklabels
        self.showAxis(self.right_axis)
        self.getAxis(self.right_axis).setStyle(showValues=False)

        # The position of the slider is stored with a TracedVariable
        initial_pos = 0
        pos = TracedVariable(initial_pos, name='pos')
        self.register_traced_variable(pos)

        # Set up the slider
        self.slider_width = TracedVariable(slider_width,
                                           name='{}.slider_width'.format(
                                               self.name))
        self.slider = pg.InfiniteLine(initial_pos,
                                      movable=True,
                                      angle=self.angle)
        self.set_slider_pen(color=(255, 255, 0, 255), width=slider_width)

        # Add a marker. Args are (style, position (from 0-1), size #NOTE
        # seems broken
        #self.slider.addMarker('o', 0.5, 10)
        self.addItem(self.slider)

        # Disable mouse scrolling, panning and zooming for both axes
        self.setMouseEnabled(False, False)

        # Initialize range to [0, 1]
        self.set_bounds(initial_pos, initial_pos + 1)

        # Connect a slot (callback) to dragging and clicking events
        self.slider.sigDragged.connect(self.on_position_change)
コード例 #8
0
ファイル: imageplot.py プロジェクト: xy2259/data_slicer
class CursorPlot(pg.PlotWidget):
    """ Implements a simple, draggable scalebar represented by a line 
    (:class: `InfiniteLine <pyqtgraph.InfiniteLine>) on an axis (:class: 
    `PlotWidget <pyqtgraph.PlotWidget>).
    The current position of the slider is tracked with the :class: 
    `TracedVariable <data_slicer.utilities.TracedVariable>` self.pos and its 
    width with the `TracedVariable` self.slider_width.
    """
    name = 'Unnamed'
    hover_color = HOVER_COLOR
    # Whether to allow changing the slider width with arrow keys
    change_width_enabled = False

    def __init__(self,
                 parent=None,
                 background='default',
                 name=None,
                 orientation='vertical',
                 slider_width=1,
                 **kwargs):
        """ Initialize the slider and set up the visual tweaks to make a 
        PlotWidget look more like a scalebar.

        ===========  ============================================================
        parent       QtWidget instance; parent widget of this widget
        background   str; confer PyQt documentation
        name         str; allows giving a name for debug purposes
        orientation  str, `horizontal` or `vertical`; orientation of the cursor
        ===========  ============================================================
        """
        super().__init__(parent=parent, background=background, **kwargs)

        if orientation not in ['horizontal', 'vertical']:
            raise ValueError('Only `horizontal` or `vertical` are allowed for '
                             'orientation.')
        self.orientation = orientation
        self.orientate()

        if name is not None:
            self.name = name

        # Hide the pyqtgraph auto-rescale button
        self.getPlotItem().buttonsHidden = True

        # Display the right (or top) axis without ticklabels
        self.showAxis(self.right_axis)
        self.getAxis(self.right_axis).setStyle(showValues=False)

        # The position of the slider is stored with a TracedVariable
        initial_pos = 0
        pos = TracedVariable(initial_pos, name='pos')
        self.register_traced_variable(pos)

        # Set up the slider
        self.slider_width = TracedVariable(slider_width,
                                           name='{}.slider_width'.format(
                                               self.name))
        self.slider = pg.InfiniteLine(initial_pos,
                                      movable=True,
                                      angle=self.angle)
        self.set_slider_pen(color=(255, 255, 0, 255), width=slider_width)

        # Add a marker. Args are (style, position (from 0-1), size #NOTE
        # seems broken
        #self.slider.addMarker('o', 0.5, 10)
        self.addItem(self.slider)

        # Disable mouse scrolling, panning and zooming for both axes
        self.setMouseEnabled(False, False)

        # Initialize range to [0, 1]
        self.set_bounds(initial_pos, initial_pos + 1)

        # Connect a slot (callback) to dragging and clicking events
        self.slider.sigDragged.connect(self.on_position_change)
        # sigMouseReleased seems to not work (maybe because sigDragged is used)
        #self.sigMouseReleased.connect(self.onClick)
        # The inherited mouseReleaseEvent is probably used for sigDragged
        # already. Anyhow, overwriting it here leads to inconsistent behaviour.
        #self.mouseReleaseEvent = self.onClick

    def orientate(self):
        """ Define all aspects that are dependent on the orientation. """
        if self.orientation == 'vertical':
            self.right_axis = 'right'
            self.secondary_axis = 'top'
            self.secondary_axis_grid = (1, 1)
            self.angle = 90
            self.slider_axis_index = 0
        else:
            self.right_axis = 'top'
            self.secondary_axis = 'right'
            self.secondary_axis_grid = (2, 2)
            self.angle = 0
            self.slider_axis_index = 1

    def register_traced_variable(self, traced_variable):
        """ Set self.pos to the given TracedVariable instance and connect the 
        relevant slots to the signals. This can be used to share a 
        TracedVariable among widgets.
        """
        self.pos = traced_variable
        self.pos.sig_value_changed.connect(self.set_position)
        self.pos.sig_allowed_values_changed.connect(
            self.on_allowed_values_change)

    def on_position_change(self):
        """ Callback for the :signal: `sigDragged 
        <pyqtgraph.InfiniteLine.sigDragged>`. Set the value of the 
        TracedVariable instance self.pos to the current slider position. 
        """
        current_pos = self.slider.value()
        # NOTE pos.set_value emits signal sig_value_changed which may lead to
        # duplicate processing of the position change.
        self.pos.set_value(current_pos)

    def on_allowed_values_change(self):
        """ Callback for the :signal: `sig_allowed_values_changed
        <pyqtgraph.utilities.TracedVariable.sig_allowed_values_changed>`. 
        With a change of the allowed values in the TracedVariable, we should 
        update our bounds accordingly.
        The number of allowed values can also give us a hint for a reasonable 
        maximal width for the slider.
        """
        # If the allowed values were reset, just exit
        if self.pos.allowed_values is None: return

        lower = self.pos.min_allowed
        upper = self.pos.max_allowed
        self.set_bounds(lower, upper)

        # Define a max width of the slider and the resulting set of allowed
        # widths
        max_width = int(len(self.pos.allowed_values) / 2)
        allowed_widths = [2 * i + 1 for i in range(max_width + 1)]
        self.slider_width.set_allowed_values(allowed_widths)

    def set_position(self):
        """ Callback for the :signal: `sig_value_changed 
        <data_slicer.utilities.TracedVariable.sig_value_changed>`. Whenever the 
        value of this TracedVariable is updated (possibly from outside this 
        Scalebar object), put the slider to the appropriate position.
        """
        new_pos = self.pos.get_value()
        self.slider.setValue(new_pos)

    def set_bounds(self, lower, upper):
        """ Set both, the displayed area of the axis as well as the the range 
        in which the slider (InfiniteLine) can be dragged to the interval 
        [lower, upper].
        """
        if self.orientation == 'vertical':
            self.setXRange(lower, upper, padding=0.01)
        else:
            self.setYRange(lower, upper, padding=0.01)
        self.slider.setBounds([lower, upper])

        # When the bounds update, the mousewheelspeed should change accordingly
        # TODO This should be in a slot to self.pos.sig_value_changed now
        self.wheel_frames = 1
        # Ensure wheel_frames is at least as big as a step in the allowed
        # values. NOTE This assumes allowed_values to be evenly spaced.
        av = self.pos.allowed_values
        if av is not None and self.wheel_frames <= 1:
            self.wheel_frames = av[1] - av[0]

    def set_secondary_axis(self, min_val, max_val):
        """ Create (or replace) a second x-axis on the top which ranges from 
        :param: `min_val` to :param: `max_val`.
        This is the right axis in case of the horizontal orientation.
        """
        # Get a handle on the underlying plotItem
        plotItem = self.plotItem

        # Remove the old top-axis
        plotItem.layout.removeItem(plotItem.getAxis(self.secondary_axis))
        # Create the new axis and set its range
        new_axis = pg.AxisItem(orientation=self.secondary_axis)
        new_axis.setRange(min_val, max_val)
        # Attach it internally to the plotItem and its layout (The arguments
        # `*(1, 1)` or `*(2, 2)` refers to the axis' position in the GridLayout)
        plotItem.axes[self.secondary_axis]['item'] = new_axis
        plotItem.layout.addItem(new_axis, *self.secondary_axis_grid)

    def set_slider_pen(self, color=None, width=None, hover_color=None):
        """ Define the color and thickness of the slider (`InfiniteLine 
        object <pyqtgraph.InfiniteLine>`) and store these attribute in :attr: 
        `self.slider_width` and :attr: `self.cursor_color`).
        """
        # Default to the current values if none are given
        if color is None:
            color = self.cursor_color
        else:
            self.cursor_color = color

        if width is None:
            #            width = self.slider_width.get_value()
            width = self.pen_width
        else:
            self.pen_width = width

        if hover_color is None:
            hover_color = self.hover_color
        else:
            self.hover_color = hover_color

        self.slider.setPen(color=color, width=width)
        # Keep the hoverPen-size consistent
        self.slider.setHoverPen(color=hover_color, width=width)

    def increase_width(self, step=1):
        """ Increase (or decrease) `self.slider_width` by `step` units of odd 
        numbers (such that the line always has a well defined center at the 
        value it is positioned at).
        """
        old_width = self.slider_width.get_value()
        new_width = old_width + 2 * step
        if new_width < 0:
            new_width = 1
        self.slider_width.set_value(new_width)

        # Convert width in steps to width in pixels
        dmin, dmax = self.viewRange()[self.slider_axis_index]
        pmax = self.rect().getRect()[self.slider_axis_index + 2]
        pixel_per_step = pmax / (dmax - dmin)
        pen_width = new_width * pixel_per_step
        self.set_slider_pen(width=pen_width)

    def increase_pos(self, step=1):
        """ Increase (or decrease) `self.pos` by a reasonable amount. 
        I.e. move `step` steps along the list of allowed values.
        """
        allowed_values = self.pos.allowed_values
        old_index = indexof(self.pos.get_value(), allowed_values)
        new_index = int((old_index + step) % len(allowed_values))
        new_value = allowed_values[int(new_index)]
        self.pos.set_value(new_value)

    def keyPressEvent(self, event):
        """ Define responses to keyboard interactions. """
        key = event.key()
        logger.debug('{}.keyPressEvent(): key={}'.format(self.name, key))
        if key == qt.QtCore.Qt.Key_Right:
            self.increase_pos(1)
        elif key == qt.QtCore.Qt.Key_Left:
            self.increase_pos(-1)
        elif self.change_width_enabled and key == qt.QtCore.Qt.Key_Up:
            self.increase_width(1)
        elif self.change_width_enabled and key == qt.QtCore.Qt.Key_Down:
            self.increase_width(-1)
        else:
            event.ignore()
            return
        # If any if-statement matched, we accept the event
        event.accept()

    def wheelEvent(self, event):
        """ Override of the Qt wheelEvent method. Fired on mousewheel 
        scrolling inside the widget. 
        """
        # Get the relevant coordinate of the mouseWheel scroll
        delta = event.angleDelta().y()
        logger.debug('<{}>wheelEvent(); delta = {}'.format(self.name, delta))
        if delta > 0:
            sign = 1
        elif delta < 0:
            sign = -1
        else:
            # It seems that in some cases delta==0
            sign = 0
        increment = sign * self.wheel_frames
        logger.debug('<{}>wheelEvent(); increment = {}'.format(
            self.name, increment))
        self.increase_pos(increment)
コード例 #9
0
ファイル: traced_test.py プロジェクト: xy2259/data_slicer
import logging

from data_slicer.utilities import TracedVariable

from data_slicer import set_up_logging

print('Initializing TracedVariable')
tv1 = TracedVariable(5, name='tv1')

print('Setting value')
tv1.set_value(3)

print('Reading value')
foo = tv1.get_value()

print('Initializing second TracedVariable')
tv2 = TracedVariable(5, name='tv2')

print('Setting value')
tv2.set_value(3)

print('Reading value')
foo = tv2.get_value()
コード例 #10
0
class ThreeDWidget(QtGui.QWidget):
    """
    A widget that contains a :class:`GLViewWidget 
    <pyqtgraph.opengl.GLViewWidget>` that allows displaying 2D colormeshes in 
    a three dimensional scene.
    This class mostly functions as a base class for more refined variations.
    """
    def __init__(self, *args, data=None, **kwargs):
        """
        Set up the :class: `GLViewWidget <pyqtgraph.opengl.GLViewWidget>` 
        as this widget's central widget.
        """
        super().__init__(*args, **kwargs)

        # Initialize instance variables
        self.data = TracedVariable(None, name='data')
        self.cmap = load_cmap(DEFAULT_CMAP)
        self.lut = self.cmap.getLookupTable()
        self.gloptions = 'translucent'

        # Create a GLViewWidget and put it into the layout of this view
        self.layout = QtGui.QGridLayout()
        self.setLayout(self.layout)
        self.glview = gl.GLViewWidget()

        # Add the scalebar
        self._initialize_sub_widgets()

        # Put all widgets in place
        self.align()

        # Add some coordinate axes to the view
        self.coordinate_axes = gl.GLAxisItem()
        self.glview.addItem(self.coordinate_axes)
        self.set_coordinate_axes(True)

        # Try to set the default data
        if data is not None:
            self.set_data(data)

    def _initialize_sub_widgets(self):
        """ Create the slider subwidget. This method exists so it can be 
        overridden by subclasses.
        """
        self.slider_xy = Scalebar(name='xy-slider')
        self.slider_xy.pos.sig_value_changed.connect(self.update_xy)
        self.slider_xy.add_text('xy plane', relpos=(0.5, 0.5))

    def align(self):
        """ Put all sub-widgets and elements into the layout. """
        l = self.layout

        l.addWidget(self.glview, 0, 0, 5, 5)

        l.addWidget(self.slider_xy, 4, 2, 1, 1)

    def set_coordinate_axes(self, on=True):
        """ 
        Turn the visibility of the coordinate axes on or off. 

        **Parameters**

        ==  ====================================================================
        on  bool or one of ('on', 1); if not `True` or any of the values stated,
            turn the axes off. Otherwise turn them on.
        ==  ====================================================================
        """
        if on in [True, 'on', 1]:
            logger.debug('Turning coordinate axes ON')
            self.coordinate_axes.show()
        else:
            logger.debug('Turning coordinate axes OFF')
            self.coordinate_axes.hide()

    def set_data(self, data):
        """
        Set this widget's data in a :class:`TracedVariable 
        <data_slicer.utilities.TracedVariable>` instance to allow direct 
        updates whenever the data changes.

        **Parameters**

        ====  ==================================================================
        data  np.array of shape (x, y, z); the data cube to be displayed.
        ====  ==================================================================
        """
        self.data.set_value(data)
        self.levels = [data.min(), data.max()]
        self.xscale, self.yscale, self.zscale = [1 / s for s in data.shape]
        self._update_sliders()

        self._initialize_planes()

    def _initialize_planes(self):
        """ This wrapper exists to be overwritten by subclasses. """
        self.initialize_xy()

    def _update_sliders(self):
        """ Update the allowed values for of the plane slider(s). """
        data = self.data.get_value()
        self.slider_xy.pos.set_allowed_values(range(data.shape[2]))

    def get_slice(self, d, i, integrate=0, silent=True):
        """
        Wrap :func:`make_slice <data_slicer.utilities.make_slice>` to create 
        slices out of this widget's `self.data`.
        Confer respective documentation for details.
        """
        return make_slice(self.data.get_value(),
                          d,
                          i,
                          integrate=integrate,
                          silent=silent)

    def get_xy_slice(self, i, integrate=0):
        """ Shorthand to get an xy slice, i.e. *d=2*. """
        self.old_z = i
        return self.get_slice(2, i, integrate)

    def make_texture(self, cut):
        """ Wrapper for :func:`makeRGBA <pyqtgraph.makeRGBA>`."""
        return pg.makeRGBA(cut, levels=self.levels, lut=self.lut)[0]

    def initialize_xy(self):
        """ Create the xy plane. """
        # Get out if no data is present
        if self.data.get_value() is None: return
        # Remove any old planes
        if hasattr(self, 'xy'):
            self.glview.removeItem(self.xy)
        # Get the data and texture to create the GLImageItem object
        cut = self.get_xy_slice(self.slider_xy.pos.get_value())
        texture = self.make_texture(cut)
        self.xy = gl.GLImageItem(texture, glOptions=self.gloptions)
        # Scale and translate to origin and add to glview
        self.xy.scale(self.xscale, self.yscale, 1)
        self.xy.translate(T, T, T)
        # Put to position in accordance with slider
        self.old_z = 0  #self.slider_xy.pos.get_value()
        self.update_xy()
        # Add to GLView
        self.glview.addItem(self.xy)

    def update_xy(self):
        """ Update both texture and position of the xy plane. """
        if not hasattr(self, 'xy'): return

        # Get the current position
        z = self.slider_xy.pos.get_value()

        # Translate
        self.xy.translate(0, 0, self.zscale * (z - self.old_z))

        # Update the texture (this needs to happen after the translation
        # because self.get_xy_slice updates the value of self.old_z)
        cut = self.get_xy_slice(z)
        texture = self.make_texture(cut)
        self.xy.setData(texture)

    def set_cmap(self, cmap):
        """ Change the used colormap to a :class:`ds_cmap 
        <data_slicer.cmaps.ds_cmap>` instance.
        """
        if isinstance(cmap, str):
            self.cmap = load_cmap(cmap)
        elif isinstance(cmap, ds_cmap):
            self.cmap = cmap
        else:
            raise TypeError('*cmap* has to be a valid colormap name or a '
                            '*ds_cmap* instance')
        # Update the necessary elements
        self.lut = self.cmap.getLookupTable()
        self._on_cmap_change()

    def _on_cmap_change(self):
        """ Update all elements affected by the cmap change. """
        self.update_xy()
コード例 #11
0
ファイル: pit.py プロジェクト: xy2259/data_slicer
class PITDataHandler():
    """ Object that keeps track of a set of 3D data and allows 
    manipulations on it. In a Model-View-Controller framework this could be 
    seen as the Model, while :class: `MainWindow 
    <data_slicer.pit.MainWindow>` would be the View part.
    """
    # np.array that contains the 3D data
    data = None
    axes = np.array([[0, 1], [0, 1], [0, 1]])
    # Indices of *data* that are displayed in the main plot
    displayed_axes = (0, 1)
    # Index along the z axis at which to produce a slice
    z = TracedVariable(0, name='z')
    # Number of slices to integrate along z
    #    integrate_z = TracedVariable(value=0, name='integrate_z')
    # How often we have rolled the axes from the original setup
    _roll_state = 0

    def __init__(self, main_window):
        self.main_window = main_window

    def get_data(self):
        """ Convenience `getter` method. Allows writing `self.get_data()` 
        instead of ``self.data.get_value()``. 
        """
        return self.data.get_value()

    def set_data(self, data):
        """ Convenience `setter` method. Allows writing `self.set_data(d)` 
        instead of ``self.data.set_value(d)``. 
        """
        self.data.set_value(data)

    def prepare_data(self, data, axes=3 * [None]):
        """ Load the specified data and prepare the corresponding z range. 
        Then display the newly loaded data.

        *Parameters*
        ====  ==================================================================
        data  3d array; the data to display
        axes  len(3) list or array of 1d-arrays or None; the units along the 
              x, y and z axes respectively. If any of those is *None*, pixels 
              are used.
        ====  ==================================================================
        """
        logger.debug('prepare_data()')

        self.data = TracedVariable(data, name='data')
        self.axes = np.array(axes)

        # Retain a copy of the original data and axes so that we can reset later
        # NOTE: this effectively doubles the used memory!
        self.original_data = copy(self.data.get_value())
        self.original_axes = copy(self.axes)

        self.prepare_axes()
        self.on_z_dim_change()

        # Connect signal handling so changes in data are immediately reflected
        self.z.sig_value_changed.connect( \
            lambda : self.main_window.update_main_plot(emit=False))
        self.data.sig_value_changed.connect(self.on_data_change)

        self.main_window.update_main_plot()
        self.main_window.set_axes()

    def load(self, filename):
        """ Alias to :func: `open <data_slicer.pit.PITDataHandler.open>`. """
        self.open(filename)

    def open(self, filename):
        """ Open a file that's readable by :module: `dataloading 
        <data_slicer.dataloading>`.
        """
        D = dl.load_data(filename)
        self.prepare_data(D.data, D.axes)

    def update_z_range(self):
        """ When new data is loaded or the axes are rolled, the limits and 
        allowed values along the z dimension change.
        """
        # Determine the new ranges for z
        self.zmin = 0
        self.zmax = self.get_data().shape[2] - 1

        self.z.set_allowed_values(range(self.zmin, self.zmax + 1))


#        self.z.set_value(self.zmin)

    def reset_data(self):
        """ Put all data and metadata into its original state, as if it was 
        just loaded from file.
        """
        logger.debug('reset_data()')
        self.set_data(copy(self.original_data))
        self.axes = copy(self.original_axes)
        self.prepare_axes()
        # Roll back to the view we had before reset_data was called
        self._roll_axes(self._roll_state, update=False)

    def prepare_axes(self):
        """ Create a list containing the three original x-, y- and z-axes 
        and replace *None* with the amount of pixels along the given axis.
        """
        shapes = self.data.get_value().shape
        # Avoid undefined axes scales and replace them with len(1) sequences
        for i, axis in enumerate(self.axes):
            if axis is None:
                self.axes[i] = np.arange(shapes[i])

    def on_data_change(self):
        """ Update self.main_window.image_data and replot. """
        logger.debug('on_data_change()')
        self.update_image_data()
        self.main_window.redraw_plots()
        # Also need to recalculate the intensity plot
        self.on_z_dim_change()

    def on_z_dim_change(self):
        """ Called when either completely new data is loaded or the dimension 
        from which we look at the data changed (e.g. through :func: `roll_axes 
        <data_slicer.pit.PITDataHandler.roll_axes>`).
        Update the z range and the integrated intensity plot.
        """
        logger.debug('on_z_dim_change()')
        self.update_z_range()

        # Get a shorthand for the integrated intensity plot
        ip = self.main_window.integrated_plot
        # Remove the old integrated intensity curve
        try:
            old = ip.listDataItems()[0]
            ip.removeItem(old)
        except IndexError:
            pass

        # Calculate the integrated intensity and plot it
        self.calculate_integrated_intensity()
        ip.plot(self.integrated)

        # Also display the actual data values in the top axis
        zscale = self.axes[2]
        zmin = zscale[0]
        zmax = zscale[-1]
        ip.set_secondary_axis(zmin, zmax)

    def calculate_integrated_intensity(self):
        self.integrated = self.get_data().sum(0).sum(0)

    def update_image_data(self):
        """ Get the right (possibly integrated) slice out of *self.data*, 
        apply postprocessings and store it in *self.image_data*. 
        Skip this if the z value happens to be out of range, which can happen 
        if the image data changes and the z scale hasn't been updated yet.
        """
        logger.debug('update_image_data()')
        z = self.z.get_value()
        integrate_z = \
        int(self.main_window.integrated_plot.slider_width.get_value()/2)
        data = self.get_data()
        try:
            self.main_window.image_data = make_slice(data,
                                                     dim=2,
                                                     index=z,
                                                     integrate=integrate_z)
        except IndexError:
            logger.debug(
                ('update_image_data(): z index {} out of range for '
                 'data of length {}.').format(z, self.image_data.shape[0]))

    def roll_axes(self, i=1):
        """ Change the way we look at the data cube. While initially we see 
        an Y vs. X slice in the main plot, roll it to Z vs. Y. A second call 
        would roll it to X vs. Z and, finally, a third call brings us back to 
        the original situation.

        *Parameters*
        =  =====================================================================
        i  int; Number of dimensions to roll.
        =  =====================================================================
        """
        self._roll_axes(i, update=True)

    def _roll_axes(self, i=1, update=True):
        """ Backend for :func: `roll_axes <arpys.pit.PITDataHandler.roll_axes>`
        that allows suppressing updating the roll-state, which is useful for
        :func: `reset_data <arpys.pit.PITDataHandler.reset_data>`.
        """
        logger.debug('roll_axes()')
        data = self.get_data()
        res = np.roll([0, 1, 2], i)
        self.axes = np.roll(self.axes, -i)
        self.set_data(np.moveaxis(data, [0, 1, 2], res))
        # Setting the data triggers a call to self.redraw_plots()
        self.on_z_dim_change()
        self.main_window.set_axes()
        if update:
            self._roll_state = (self._roll_state + i) % NDIM

    def lineplot(self,
                 plot='main',
                 dim=0,
                 ax=None,
                 n=10,
                 offset=0.2,
                 lw=0.5,
                 color='k',
                 label_fmt='{:.2f}',
                 n_ticks=5,
                 **getlines_kwargs):
        """
        Create a matplotlib figure with *n* lines extracted out of one of the 
        visible plots. The lines are normalized to their global maximum and 
        shifted from each other by *offset*.
        See :func: `get_lines <data_slicer.utilities.get_lines>` for more 
        options on the extraction of the lines.
        This wraps the :class: `ImagePlot <data_slicer.imageplot.ImagePlot>`'s
        lineplot method.

        *Parameters*
        ===============  =======================================================
        plot             str; either "main" or "cut", specifies from which 
                         plot to extract the lines.
        dim              int; either 0 or 1, specifies in which direction to 
                         take the lines.
        ax               matplotlib.axes.Axes; the axes in which to plot. If 
                         *None*, create a new figure with a fresh axes.
        n                int; number of lines to extract.
        offset           float; spacing between neighboring lines.
        lw               float; linewidth of the plotted lines.
        color            any color argument understood by matplotlib; color 
                         of the plotted lines.
        label_fmt        str; a format string for the ticklabels.
        n_ticks          int; number of ticks to print.
        getlines_kwargs  other kwargs are passed to :func: `get_lines 
                         <data_slicer.utilities.get_lines>`
        ===============  =======================================================

        *Returns*
        ===========  ===========================================================
        lines2ds     list of Line2D objects; the drawn lines.
        xticks       list of float; locations of the 0 intensity value of 
                     each line.
        xtickvalues  list of float; if *momenta* were supplied, corresponding 
                     xtick values in units of *momenta*. Otherwise this is 
                     just a copy of *xticks*.
        xticklabels  list of str; *xtickvalues* formatted according to 
                     *label_fmt*.
        ===========  ===========================================================
        """
        # Get the specified data
        if plot == 'main':
            imageplot = self.main_window.main_plot
        elif plot == 'cut':
            imageplot = self.main_window.cut_plot
        else:
            raise ValueError('*plot* should be one of ("main", "cut").')

        # Create a mpl axis object if none was given
        if ax is None: fig, ax = plt.subplots(1)

        return imageplot.lineplot(ax=ax,
                                  dim=dim,
                                  n=n,
                                  offset=offset,
                                  lw=lw,
                                  color=color,
                                  label_fmt=label_fmt,
                                  n_ticks=n_ticks,
                                  **getlines_kwargs)

    def plot_all_slices(self,
                        dim=2,
                        integrate=0,
                        zs=None,
                        labels='default',
                        max_ppf=16,
                        max_nfigs=2):
        """ Wrapper for :func: `plot_cuts <data_slicer.utilities.plot_cuts>`.
        Plot all (or only the ones specified by `zs`) slices along dimension 
        `dim` on separate suplots onto matplotlib figures.

        *Parameters*
        =========  ============================================================
        dim        int; one of (0,1,2). Dimension along which to take the cuts.
        integrate  int or 'full'; number of slices to integrate around each 
                   extracted cut. If 'full', take the maximum number possible, 
                   depending on *zs* and whether the number of cuts is reduced 
                   due to otherwise exceeding *max_nfigs*.
        zs         1D np.array; selection of indices along dimension `dim`. Only 
                   the given indices will be plotted.
        labels     1D array/list of length z. Optional labels to assign to the 
                   different cuts. By default the values of the respective axis
                   are used. Set to *None* to suppress labels.
        max_ppf    int; maximum number of *p*lots *p*er *f*igure.
        max_nfigs  int; maximum number of figures that are created. If more would 
                   be necessary to display all plots, a warning is issued and 
                   only every N'th plot is created, where N is chosen such that 
                   the whole 'range' of plots is represented on the figures. 
        =========  ============================================================
        """
        data = self.get_data()
        if labels == 'default':
            # Use the values of the respective axis as default labels
            labels = self.axes[dim]

        # The default values for the colormap are taken from the main_window
        # settings
        gamma = self.main_window.gamma
        vmax = self.main_window.vmax * data.max()
        cmap = convert_ds_to_matplotlib(self.main_window.cmap,
                                        self.main_window.cmap_name)
        plot_cuts(data,
                  dim=dim,
                  integrate=integrate,
                  zs=zs,
                  labels=labels,
                  cmap=cmap,
                  vmax=vmax,
                  gamma=gamma,
                  max_ppf=max_ppf,
                  max_nfigs=max_nfigs)
コード例 #12
0
class ImagePlot(pg.PlotWidget) :
    """
    A PlotWidget which mostly contains a single 2D image (intensity 
    distribution) or a 3D array (distribution of RGB values) as well as all 
    the nice pyqtgraph axes panning/rescaling/zooming functionality.

    In addition, this allows one to use custom axes scales as opposed to 
    being limited to pixel coordinates.

    **Signals**

    =================  =========================================================
    sig_image_changed  emitted whenever the image is updated
    sig_axes_changed   emitted when the axes are updated
    sig_clicked        emitted when user clicks inside the imageplot
    =================  =========================================================
    """
    sig_image_changed = qt.QtCore.Signal()
    sig_axes_changed = qt.QtCore.Signal()
    sig_clicked = qt.QtCore.Signal(object)

    def __init__(self, image=None, parent=None, background='default', 
                 name=None, **kwargs) :
        """ Allows setting of the image upon initialization. 
        
        **Parameters**

        ==========  ============================================================
        image       np.ndarray or pyqtgraph.ImageItem instance; the image to be
                    displayed.
        parent      QtWidget instance; parent widget of this widget.
        background  str; confer PyQt documentation
        name        str; allows giving a name for debug purposes
        ==========  ============================================================
        """
        # Initialize instance variables
        # np.array, raw image data
        self.image_data = None
        # pg.ImageItem of *image_data*
        self.image_item = None
        self.image_kwargs = {}
        self.xlim = None
        self.ylim = None
        self.xscale = None
        self.yscale = None
        self.transform_factors = []
        self.transposed = TracedVariable(False, name='transposed')
        self.crosshair_cursor_visible = False

        super().__init__(parent=parent, background=background, 
                         viewBox=DSViewBox(imageplot=self), **kwargs) 

        self.name = name

        # Show top and tight axes by default, but without ticklabels
        self.showAxis('top')
        self.showAxis('right')
        self.getAxis('top').setStyle(showValues=False)
        self.getAxis('right').setStyle(showValues=False)

        if image is not None :
            self.set_image(image)

        self.sig_axes_changed.connect(self.fix_viewrange)

    def show_cursor(self, show=True) :
        """
        Toggle whether or not to show a crosshair cursor that tracks the 
        mouse movement.
        """
        if show :
            crosshair_cursor = Crosshair()
            crosshair_cursor.set_movable(False)
            crosshair_cursor.set_color((255, 255, 255, 255), 
                                       (255, 255, 255, 255))
            crosshair_cursor.add_to(self)
            self.scene().sigMouseMoved.connect(self.on_mouse_move)
            self.crosshair_cursor = crosshair_cursor
        else :
            try :
                self.scene().sigMouseMoved.disconnect(self.on_mouse_move)
            except TypeError :
                pass
            try :
                self.crosshair_cursor.remove_from(self)
            except AttributeError :
                pass
        self.crosshair_cursor_visible = show
        self.plotItem.vb.menu.toggle_cursor.setChecked(show)

    def toggle_cursor(self) :
        """ Change the visibility of the crosshair cursor. """
        self.show_cursor(not self.crosshair_cursor_visible)

    def on_mouse_move(self, pos) :
        """ Slot for mouse movement over the plot. Calculate the mouse 
        position in data coordinates and move the crosshair_cursor there.

        **Parameters**

        ===  ===================================================================
        pos  QPointF object; x and y position of the mouse as returned by 
             :signal:`sigMouseMoved 
             <data_slicer.imageplot.ImagePlot.sigMouseMoved>`.
        ===  ===================================================================
        """
        if self.plotItem.sceneBoundingRect().contains(pos) :
            data_point = self.plotItem.vb.mapSceneToView(pos)
            self.crosshair_cursor.move_to((data_point.x(), data_point.y()))

    def mousePressEvent(self, event) :
        """ Figure out where the click happened in data coordinates and make 
        the position available through the signal :signal:`sig_clicked 
        <data_slicer.imageplot.ImagePlot.sig_clicked>`.
        """
        if event.button() == qt.QtCore.Qt.LeftButton :
            vb = self.plotItem.vb
            last_click = vb.mapToView(vb.mapFromScene(event.localPos()))
            message = 'Last click at ( {:.4f} | {:.4f} )'
            self.sig_clicked.emit(message.format(last_click.x(), last_click.y()))
        super().mousePressEvent(event)

    def remove_image(self) :
        """ Removes the current image using the parent's :meth:`removeItem 
        pyqtgraph.PlotWidget.removeItem` function. 
        """
        if self.image_item is not None :
            self.removeItem(self.image_item)
        self.image_item = None

    def set_image(self, image, emit=True, *args, **kwargs) :
        """ Expects either np.arrays or pg.ImageItems as input and sets them 
        correctly to this PlotWidget's Image with `addItem`. Also makes sure 
        there is only one Image by deleting the previous image.

        Emits :signal:`sig_image_changed`

        **Parameters**

        ========  ==============================================================
        image     np.ndarray or pyqtgraph.ImageItem instance; the image to be
                  displayed.
        emit      bool; whether or not to emit :signal:`sig_image_changed`
        (kw)args  positional and keyword arguments that are passed on to 
                  :class:`pyqtgraph.ImageItem`
        ========  ==============================================================
        """
        # Convert array to ImageItem
        if isinstance(image, ndarray) :
            if 0 not in image.shape :
                image_item = ImageItem(image, *args, **kwargs)
            else :
                logger.debug(('<{}>.set_image(): image.shape is {}. Not '
                              'setting image.').format(self.name, image.shape))
                return
        else :
            image_item = image
        # Throw an exception if image is not an ImageItem
        if not isinstance(image_item, ImageItem) :
            message = '''`image` should be a np.array or pg.ImageItem instance,
            not {}'''.format(type(image))
            raise TypeError(message)

        # Transpose if necessary
        if self.transposed.get_value() :
            image_item = ImageItem(image_item.image.T, *args, **kwargs)

        # Replace the image
        self.remove_image()
        self.image_item = image_item
        self.image_data = image_item.image
        logger.debug('<{}>Setting image.'.format(self.name))
        self.addItem(image_item)
        # Reset limits if necessary
        if self.xscale is not None and self.yscale is not None :
            axes_shape = (len(self.xscale), len(self.yscale))
            if axes_shape != self.image_data.shape :
                self.xlim = None
                self.ylim = None
        self._set_axes_scales(emit=emit)

        if emit :
            logger.info('<{}>Emitting sig_image_changed.'.format(self.name))
            self.sig_image_changed.emit()

    def set_xscale(self, xscale, update=False) :
        """ Set the xscale of the plot. *xscale* is an array of the length 
        ``len(self.image_item.shape[0])``.
        """
        if self.transposed.get_value() :
            self._set_yscale(xscale, update)
        else :
            self._set_xscale(xscale, update)

    def set_yscale(self, yscale, update=False) :
        """ Set the yscale of the plot. *yscale* is an array of the length 
        ``len(self.image_item.image.shape[1])``.
        """
        if self.transposed.get_value() :
            self._set_xscale(yscale, update)
        else :
            self._set_yscale(yscale, update)

    def _set_xscale(self, xscale, update=False, force=False) :
        """ Set the scale of the horizontal axis of the plot. *force* can be 
        used to bypass the length checking.
        """
        # Sanity check
        if not force and self.image_item is not None and \
        len(xscale) != self.image_item.image.shape[0] :
            raise TypeError('Shape of xscale does not match data dimensions.')

        self.xscale = xscale
        # 'Autoscale' the image to the xscale
        self.xlim = (xscale[0], xscale[-1])

        if update :
            self._set_axes_scales(emit=True)

    def _set_yscale(self, yscale, update=False, force=False) :
        """ Set the scale of the vertical axis of the plot. *force* can be 
        used to bypass the length checking.
        """
        # Sanity check
        if not force and self.image_item is not None and \
        len(yscale) != self.image_item.image.shape[1] :
            raise TypeError('Shape of yscale does not match data dimensions.')

        self.yscale = yscale
        # 'Autoscale' the image to the xscale
        self.ylim = (yscale[0], yscale[-1])

        if update :
            self._set_axes_scales(emit=True)

    def transpose(self) :
        """ Transpose the image, i.e. swap the x- and y-axes. """
        self.transposed.set_value(not self.transposed.get_value())
        # Swap the scales
        new_xscale = self.yscale
        new_yscale = self.xscale
        self._set_xscale(new_xscale, force=True)
        self._set_yscale(new_yscale, force=True)
        # Update the image
        if not self.transposed.get_value() :
            # Take care of the back-transposition here
            self.set_image(self.image_item.image.T, lut=self.image_item.lut)
        else :
            self.set_image(self.image_item, lut=self.image_item.lut)

    def set_xlabel(self, label) :
        """ Shorthand for setting this plot's x axis label. """
        axis = self.getAxis('bottom')
        axis.setLabel(label)

    def set_ylabel(self, label) :
        """ Shorthand for setting this plot's y axis label. """
        axis = self.getAxis('left')
        axis.setLabel(label)

    def _set_axes_scales(self, emit=False) :
        """ Transform the image such that it matches the desired x and y 
        scales.
        """
        # Get image dimensions and requested origin (x0,y0) and top right 
        # corner (x1, y1)
        nx, ny = self.image_item.image.shape
        logger.debug(('<{}>_set_axes_scales(): self.image_item.image.shape={}' + 
                     ' x {}').format(self.name, nx, ny))
        [[x0, x1], [y0, y1]] = self.get_limits()
        # Calculate the scaling factors
        sx = (x1-x0)/nx
        sy = (y1-y0)/ny
        # Ensure nonzero
        sx = 1 if sx==0 else sx
        sy = 1 if sy==0 else sy
        # Define a transformation matrix that scales and translates the image 
        # such that it appears at the coordinates that match our x and y axes.
        transform = qt.QtGui.QTransform()
        transform.scale(sx, sy)
        # Carry out the translation in scaled coordinates
        transform.translate(x0/sx, y0/sy)
        # Finally, apply the transformation to the imageItem
        self.image_item.setTransform(transform)
        self._update_transform_factors()

        if emit :
            logger.info('<{}>Emitting sig_axes_changed.'.format(self.name))
            self.sig_axes_changed.emit()

    def get_limits(self) :
        """ Return ``[[x_min, x_max], [y_min, y_max]]``. """
        # Default to current viewrange but try to get more accurate values if 
        # possible
        if self.image_item is not None :
            x, y = self.image_item.image.shape
        else :
            x, y = 1, 1

        # Set the limits to image pixels if they are not defined
        if self.xlim is None :
            self.set_xscale(arange(0, x))
        x_min, x_max = self.xlim
        if self.ylim is None :
            self.set_yscale(arange(0, y))
        y_min, y_max = self.ylim

        logger.debug(('<{}>get_limits(): [[x_min, x_max], [y_min, y_max]] = '
                    + '[[{}, {}], [{}, {}]]').format(self.name, x_min, x_max, 
                                                     y_min, y_max))
        return [[x_min, x_max], [y_min, y_max]]

    def fix_viewrange(self) :
        """ Prevent zooming out by fixing the limits of the ViewBox. """
        logger.debug('<{}>fix_viewrange().'.format(self.name))
        [[x_min, x_max], [y_min, y_max]] = self.get_limits()
        self.setLimits(xMin=x_min, xMax=x_max, yMin=y_min, yMax=y_max,
                      maxXRange=x_max-x_min, maxYRange=y_max-y_min)

    def release_viewrange(self) :
        """ Undo the effects of :meth:`fix_viewrange 
        <data_slicer.imageplot.ImagePlot.fix_viewrange>`
        """
        logger.debug('<{}>release_viewrange().'.format(self.name))
        self.setLimits(xMin=-inf,
                       xMax=inf,
                       yMin=-inf,
                       yMax=inf,
                       maxXRange=inf,
                       maxYRange=inf)

    def _update_transform_factors(self) :
        """ Create a copy of the parameters that are necessary to reproduce 
        the current transform. This is necessary e.g. for the calculation of 
        the transform in :meth:`rotate 
        <data_slicer.imageplot.ImagePlot.rotate>`.
        """
        transform = self.image_item.transform()
        dx = transform.dx()
        dy = transform.dy()
        sx = transform.m11()
        sy = transform.m22()
        wx = self.image_item.width()
        wy = self.image_item.height()
        self.transform_factors = [dx, dy, sx, sy, wx, wy]

    def rotate(self, alpha=0) :
        """ Rotate the image_item by the given angle *alpha* (in degrees).  """
        # Get the details of the current transformation
        if self.transform_factors == [] :
            self._update_transform_factors()
        dx, dy, sx, sy, wx, wy = self.transform_factors

        # Build the transformation anew, adding a rotation
        # Remember that the order in which transformations are applied is 
        # reverted to how they are added in the code, i.e. last transform 
        # added in the code will come first (this is the reason we have to 
        # completely rebuild the transformation instead of just adding a 
        # rotation...)
        transform = self.image_item.transform()
        transform.reset()
        transform.translate(dx, dy)
        transform.translate(wx/2*sx, wy/2*sy)
        transform.rotate(alpha)
        transform.scale(sx, sy)
        transform.translate(-wx/2, -wy/2)

        self.release_viewrange()

        self.image_item.setTransform(transform)

    def mpl_export(self, *args, figsize=(5,5), title='', xlabel='', 
                   ylabel='', dpi=300) :
        """ Export the content of this plot to a png image using matplotlib. 
        The resulting image will have a white background and black ticklabes 
        and should therefore be more readable than pyqtgraph's native plot
        export options.

        **Parameters**

        =======  ===============================================================
        figsize  tuple of float; (height, width) of figure in inches
        title    str; figure title
        xlabel   str; x axis label
        ylabel   str; y axis label
        dpi      int; png resolution in pixels per inch
        args     positional arguments are absorbed and discarded (necessary 
                 to connect this method to signal handling)
        =======  ===============================================================
        """
        logger.debug('<ImagePlot.mpl_export()>')

        # Show the dialog with some options
        dialog = MPLExportDialog(self, parent=self)
#        ok_button = qt.QtGui.QPushButton('Done', dialog)
        if not dialog.exec_() : return
        # Replot to update the figure
        dialog.plot_preview()

        # Get a filename first
        fd = qt.QtGui.QFileDialog()
        filename = fd.getSaveFileName()[0]
        if not filename : return
        logger.debug('Outfilename: {}'.format(filename))

        # Update figure size before saving
        width, height = [float(box.text()) for box in [dialog.box_width, 
                                                       dialog.box_height]]
        dialog.figure.set_figwidth(width)
        dialog.figure.set_figheight(height)

        dialog.figure.savefig(filename, dpi=dpi)

    def lineplot(self, ax, dim=0, n=10, offset=0.2, lw=0.5, color='k', 
                 label_fmt='{:.2f}', n_ticks=5, **getlines_kwargs) :
        """
        Create a matplotlib plot with *n* lines extracted out of one of the 
        visible plots. The lines are normalized to their global maximum and 
        shifted from each other by *offset*.
        See :func:`get_lines <data_slicer.utilities.get_lines>` for more 
        options on the extraction of the lines.

        **Parameters**

        ===============  =======================================================
        plot             str; either "main" or "cut", specifies from which 
                         plot to extract the lines.
        dim              int; either 0 or 1, specifies in which direction to 
                         take the lines.
        ax               matplotlib.axes.Axes; the axes in which to plot.
        n                int; number of lines to extract.
        offset           float; spacing between neighboring lines.
        lw               float; linewidth of the plotted lines.
        color            any color argument understood by matplotlib; color 
                         of the plotted lines.
        label_fmt        str; a format string for the ticklabels.
        n_ticks          int; number of ticks to print.
        getlines_kwargs  other kwargs are passed to :func:`get_lines 
                         <data_slicer.utilities.get_lines>`
        ===============  =======================================================

        **Returns**

        ===========  ===========================================================
        lines2ds     list of Line2D objects; the drawn lines.
        xticks       list of float; locations of the 0 intensity value of 
                     each line.
        xtickvalues  list of float; if *momenta* were supplied, corresponding 
                     xtick values in units of *momenta*. Otherwise this is 
                     just a copy of *xticks*.
        xticklabels  list of str; *xtickvalues* formatted according to 
                     *label_fmt*.
        ===========  ===========================================================
        """
        data = self.image_data

        # Get the right axes
        axes = [self.xscale, self.yscale]
        for i,scale in enumerate(axes) :
            # Deal with *None*
            if scale is None :
                axes[i] = np.arange(data.shape[i])
        if dim==0 :
            data = data.T
        elif dim==1 :
            axes = axes[::-1]
        else :
            raise ValueError('*dim* should be one of (0, 1).')

        # Get the lines with an utility function
        lines, indices = get_lines(data, n, offset=offset, **getlines_kwargs) 

        # Plot the lines
        line2ds = []
        for line in lines :
            line2d = ax.plot(line, axes[0], lw=lw, color=color)[0]
            line2ds.append(line2d)

        # Create tick positions and labels
        xticks = [i*offset for i in range(n)]
        xtickvalues = axes[i][indices]
        xticklabels = [label_fmt.format(x) for x in xtickvalues]

        # Only render *n_ticks* ticks
        nth = int(n/n_ticks)
        ax.set_xticks(xticks[::nth])
        ax.set_xticklabels(xticklabels[::nth])

        return line2ds, xticks, xtickvalues, xticklabels
コード例 #13
0
class Crosshair() :
    """ Crosshair made up of two InfiniteLines. """
    def __init__(self, pos=(0,0)) :
        # Store the positions in TracedVariables
        self.hpos = TracedVariable(pos[1], name='hpos')
        self.vpos = TracedVariable(pos[0], name='vpos')

        # Initialize the InfiniteLines
        self.hline = pg.InfiniteLine(pos[1], movable=True, angle=0)
        self.vline = pg.InfiniteLine(pos[0], movable=True, angle=90)

        # Set the color
        self.set_color(BASE_LINECOLOR, HOVER_COLOR)

        # Register some callbacks
        self.hpos.sig_value_changed.connect(self.update_position_h)
        self.vpos.sig_value_changed.connect(self.update_position_v)

        self.hline.sigDragged.connect(self.on_dragged_h)
        self.vline.sigDragged.connect(self.on_dragged_v)

    def add_to(self, widget) :
        """ Add this crosshair to a Qt widget. """
        for line in [self.hline, self.vline] :
            line.setZValue(1)
            widget.addItem(line)

    def remove_from(self, widget) :
        """ Remove this crosshair from a pyqtgraph widget. """
        for line in [self.hline, self.vline] :
            widget.removeItem(line)

    def set_color(self, linecolor=BASE_LINECOLOR, hover_color=HOVER_COLOR) :
        """ Set the color and hover color of both InfiniteLines that make up 
        the crosshair. The arguments can be any pyqtgraph compatible color 
        specifiers.
        """
        for line in [self.hline, self.vline] :
            line.setPen(linecolor)
            line.setHoverPen(hover_color)

    def set_movable(self, movable=True) :
        """ Set whether or not this crosshair can be dragged by the mouse. """
        for line in [self.hline, self.vline] :
            line.setMovable = movable

    def move_to(self, pos) :
        """
        **Parameters**

        ===  ===================================================================
        pos  2-tuple; x and y coordinates of the desired location of the 
             crosshair in data coordinates.
        ===  ===================================================================
        """
        self.hpos.set_value(pos[1])
        self.vpos.set_value(pos[0])

    def update_position_h(self) :
        """ Callback for the :signal:`sig_value_changed 
        <data_slicer.utilities.TracedVariable.sig_value_changed>`. Whenever the 
        value of this TracedVariable is updated (possibly from outside this 
        Crosshair object), put the crosshair to the appropriate position.
        """
        self.hline.setValue(self.hpos.get_value())

    def update_position_v(self) :
        """ Confer update_position_h. """
        self.vline.setValue(self.vpos.get_value())

    def on_dragged_h(self) :
        """ Callback for dragging of InfiniteLines. Their visual position 
        should be reflected in the TracedVariables self.hpos and self.vpos.
        """
        self.hpos.set_value(self.hline.value())

    def on_dragged_v(self) :
        """ Callback for dragging of InfiniteLines. Their visual position 
        should be reflected in the TracedVariables self.hpos and self.vpos.
        """
        self.vpos.set_value(self.vline.value())

    def set_bounds(self, xmin, xmax, ymin, ymax) :
        """ Set the area in which the infinitelines can be dragged. """
        self.hline.setBounds([ymin, ymax])
        self.vline.setBounds([xmin, xmax])
コード例 #14
0
ファイル: test_logging.py プロジェクト: xy2259/data_slicer
import logging

from data_slicer import set_up_logging
from data_slicer.utilities import TracedVariable

# Set up logging
#logger = logging.getLogger('ds')
#logger.setLevel(logging.DEBUG)
#console_handler = logging.StreamHandler()
#console_handler.setLevel(logging.DEBUG)
#formatter = logging.Formatter('[%(levelname)s][%(name)s]%(message)s')
#console_handler.setFormatter(formatter)
#logger.addHandler(console_handler)
#logger.propagate = False

tv = TracedVariable(value=5)

tv.get_value()
tv.set_value(8)
tv.get_value()