Source code for data_slicer.pit

"""
PIT is a widget that combines slices from different sides with intensity 
distribution curves and other convenient features.
"""

import argparse
import importlib
import logging
import pathlib
import pickle
import pkg_resources
import sys
from copy import copy
from types import FunctionType

import matplotlib.pyplot as plt
import numpy as np
import pyqtgraph as pg
import pyqtgraph.console
from pyqtgraph.Qt import QtCore, QtGui, QtWidgets
from qtconsole.rich_jupyter_widget import RichJupyterWidget
from qtconsole.inprocess import QtInProcessKernelManager

import data_slicer.dataloading as dl
from data_slicer.cmaps import convert_ds_to_matplotlib, load_cmap
from data_slicer.cutline import Cutline
from data_slicer.imageplot import *
from data_slicer.model import Model
from data_slicer.utilities import CACHED_CMAPS_FILENAME, CONFIG_DIR, \
                                  make_slice, plot_cuts, TracedVariable

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

# +-----------------------------------------+ #
# | Appearance definitions and preparations | # ================================
# +-----------------------------------------+ #

app_style="""
QMainWindow{
    background-color: black;
    }
"""

console_style = """
color: rgb(0, 0, 0);
background-color: rgb(255, 255, 255);
border: 1px solid rgb(50, 50, 50);
"""

DEFAULT_CMAP = 'viridis'

[docs]class EmbedIPython(RichJupyterWidget): """ Some voodoo to get an ipython console in a Qt application. """ def __init__(self, **kwarg): super(RichJupyterWidget, self).__init__() self.kernel_manager = QtInProcessKernelManager() self.kernel_manager.start_kernel() self.kernel = self.kernel_manager.kernel self.kernel.gui = 'qt4' self.kernel.shell.push(kwarg) self.kernel_client = self.kernel_manager.client() self.kernel_client.start_channels()
# Prepare sample data data_path = pkg_resources.resource_filename('data_slicer', 'data/') SAMPLE_DATA_FILE = data_path + 'pit.p' # Add the plugin directory to the python path plugin_path = pathlib.Path.home() / CONFIG_DIR / 'plugins/' sys.path.append(str(plugin_path)) # Number of dimensions to handle NDIM = 3 # What axes look like if they have not been initialized EMPTY_AXES = np.array(3*[None]) # +-----------------------+ # # | Main class definition | # ================================================== # +-----------------------+ #
[docs]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. """ def __init__(self, main_window) : self.main_window = main_window # Initialize instance variables # np.array that contains the 3D data self.data = None self.axes = np.array([[0, 1], [0, 1], [0, 1]]) # Indices of *data* that are displayed in the main plot self.displayed_axes = (0,1) # Index along the z axis at which to produce a slice self.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 self._roll_state = 0
[docs] def get_config_dir(self) : """ Return the path to the configuration directory on this system. """ return pathlib.Path.home() / CONFIG_DIR
[docs] def get_data(self) : """ Convenience `getter` method. Allows writing ``self.get_data()`` instead of ``self.data.get_value()``. """ return self.data.get_value()
[docs] def set_data(self, data=None, axes=None) : """ Convenience `setter` method. Allows writing ``self.set_data(d)`` instead of ``self.data.set_value(d)``. Additionally allows setting new *axes*. If *axes* is ``None``, the axes are reset to pixels. """ if data is not None : self.data.set_value(data) if axes is not None : self.axes = axes self.main_window.set_axes() else : self.axes = EMPTY_AXES # Call on_z_dim_change here because it was not executed with the # proper axes on the data change before self.on_z_dim_change()
[docs] 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') if axes is None : self.axes = np.array(3*[None]) else : 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()
[docs] def load(self, filename) : """ Alias to :func:`open <data_slicer.pit.PITDataHandler.open>`. """ self.open(filename)
[docs] def open(self, filename) : """ Open a file that's readable by :mod:`dataloading <data_slicer.dataloading>`. """ D = dl.load_data(filename) self.prepare_data(D.data, D.axes)
[docs] def get_main_data(self) : """ Return the 2d array that is currently displayed in the main plot. """ return self.main_window.main_plot.image_data
[docs] def get_cut_data(self) : """ Return the 2d array that is currently displayed in the cut plot. """ return self.main_window.cut_plot.image_data
[docs] def get_hprofile(self) : """ Return an array containing the y values displayed in the horizontal profile plot (mw.y_plot). .. seealso:: :func:`data_slicer.imageplot.CursorPlot.get_data` """ return self.main_window.y_plot.get_data()[1]
[docs] def get_vprofile(self) : """ Return an array containing the x values displayed in the vertical profile plot (mw.x_plot). .. seealso:: :func:`data_slicer.imageplot.CursorPlot.get_data` """ return self.main_window.x_plot.get_data()[0]
[docs] def get_iprofile(self) : """ Return an array containing the y values displayed in the integrated intensity profile plot (mw.integrated_plot). .. seealso:: :func:`data_slicer.imageplot.CursorPlot.get_data` """ return self.main_window.integrated_plot.get_data()[1]
[docs] 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)
[docs] 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)
[docs] 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])
[docs] 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()
[docs] 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] if zscale is not None : zmin = zscale[0] zmax = zscale[-1] else : zmin = 0 zmax = self.data.get_value().shape[2] ip.set_secondary_axis(zmin, zmax)
[docs] def calculate_integrated_intensity(self) : self.integrated = self.get_data().sum(0).sum(0)
[docs] 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, silent=True) except IndexError : logger.debug(('update_image_data(): z index {} out of range for ' 'data of length {}.').format( z, self.image_data.shape[0]))
[docs] 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), axes=self.axes) # Setting the data triggers a call to self.redraw_plots() self.on_z_dim_change() # Reset cut_plot's axes cp = self.main_window.cut_plot # cp.xlim = None # cp.ylim = None self.main_window.set_axes() if update : self._roll_state = (self._roll_state + i) % NDIM
[docs] 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*. =========== =========================================================== .. seealso:: :func:`get_lines <data_slicer.utilities.get_lines>` """ # 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)
[docs] def plot_all_slices(self, dim=2, integrate=0, zs=None, labels='default', max_ppf=16, max_nfigs=2, **kwargs) : """ 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 plots per figure. 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. kwargs dict; keyword arguments passed on to :func:`pcolormesh <matplotlib.axes._subplots.AxesSubplot.pcolormesh>`. Additionally, the kwarg `gamma` for power-law color mapping is accepted. ========= ============================================================ .. seealso:: :func:`~data_slicer.utilities.plot_cuts` """ 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)
[docs] def overlay_model(self, model) : """ Display a model over the data. *model* should be function of two variables, namely the currently displayed x- and y-axes. **Parameters** ===== ================================================================= model callable or :class:`Model <data_slicer.model.Model>`; ===== ================================================================= .. seealso:: :class:`Model <data_slicer.model.Model>` """ if isinstance(model, FunctionType) : model = Model(model) elif not isinstance(model, Model) : raise ValueError('*model* has to be a function or a ' 'data_slicer.Model instance') # Remove the old model self.remove_model() # Calculate model data in the required range and get an isocurve self.model = model # Bypass the minimum axes size limitation self.model.MIN_AXIS_LENGTH = 0 model_axes = [self.axes[i] for i in self.displayed_axes] # Invert order for transposed view if self.main_window.main_plot.transposed.get_value() : self.model.set_axes(model_axes[::-1]) else : self.model.set_axes(model_axes) self._update_isocurve() self._update_model_cut() # Connect signal handling self.z.sig_value_changed.connect(self._update_isocurve) self.main_window.cutline.sig_region_changed.connect(self._update_model_cut)
[docs] def remove_model(self) : """ Remove the current model's visible and invisible parts. """ # Remove the visible items from the plots try : self.main_window.main_plot.removeItem(self.iso) self.iso = None self.model = None except AttributeError : logger.debug('remove_model(): no model to remove found.') return # Remove signal handling try : self.z.sig_value_changed.disconnect(self._update_isocurve) except TypeError as e : logger.debug(e) try : self.main_window.cutline.sig_region_changed.disconnect( self._update_model_cut) except TypeError as e : logger.debug(e) # Redraw clean plots self.main_window.redraw_plots()
def _update_isocurve(self) : try : self.iso = self.model.get_isocurve(self.z.get_value(), axisOrder='row-major') except AttributeError : logger.debug('_update_isocurve(): no model found.') return # Make sure the isocurveItem is above the plot and add it to the main # plot self.iso.setZValue(10) self.iso.setParentItem(self.main_window.main_plot.image_item) def _update_model_cut(self) : try : model_cut = self.main_window.cutline.get_array_region( self.model.data.T, self.main_window.main_plot.image_item, self.displayed_axes) except AttributeError : logger.debug('_update_model_cut(): model or data not found.') return self.model_cut = self.main_window.cut_plot.plot(model_cut, pen=self.iso.pen)
[docs]class MainWindow(QtWidgets.QMainWindow) : """ The main window of PIT. Defines the basic GUI layouts and acts as the controller, keeping track of the data and handling the communication between the different GUI elements. """ title = 'Python Image Tool' # width, height in pixels size = (1200, 800) def __init__(self, data=None, background='default') : super().__init__() # Initialize instance variables # Plot transparency alpha self.alpha = 1 # Plot powerlaw normalization exponent gamma self.gamma = 1 # Relative colormap maximum self.vmax = 1 # Need to store original transformation information for `rotate()` self._transform_factors = [] self.data_handler = PITDataHandler(self) # Aesthetics self.setStyleSheet(app_style) self.cmaps = dict() self.set_cmap(DEFAULT_CMAP) # Autoload plugins (needs to happen before _init_UI() because the # plugins are needed to populate the console's namespace) self._autoloaded_plugins = self._autoload_plugins() self._init_UI() # Connect signal handling self.cutline.sig_initialized.connect(self.on_cutline_initialized) # Prepare sample data for initialization if data is None : self.load_startup_data() else : self.data_handler.prepare_data(data) def _init_UI(self) : """ Initialize the elements of the user interface. """ # Set the window title self.setWindowTitle(self.title) self.resize(*self.size) # Create a "central widget" and its layout self.central_widget = QtWidgets.QWidget() self.layout = QtWidgets.QGridLayout() self.central_widget.setLayout(self.layout) self.setCentralWidget(self.central_widget) # Create the 3D (main) and cut ImagePlots self.main_plot = ImagePlot(name='main_plot') self.main_plot.show_cursor() self.cut_plot = CrosshairImagePlot(name='cut_plot') # Create the intensity distribution plots self.x_plot = CursorPlot(name='x_plot', orientation='horizontal') self.y_plot = CursorPlot(name='y_plot') self.x_plot.register_traced_variable(self.cut_plot.pos[1]) self.y_plot.register_traced_variable(self.cut_plot.pos[0]) self.x_plot.pos.sig_value_changed.connect(self.update_y_plot) self.y_plot.pos.sig_value_changed.connect(self.update_x_plot) # self.cut_plot.sig_image_changed.connect(self.update_xy_plots) # Set up the python console namespace = dict(pit=self.data_handler, mw=self) # Add the autoloaded plugins for name, plugin in self._autoloaded_plugins : namespace.update({name: plugin}) self.console = EmbedIPython(**namespace) self.console.kernel.shell.run_cell('%pylab qt') self.console.setStyleSheet(console_style) for name, plugin in self._autoloaded_plugins : self.print_to_console('Autoloaded plugin {}'.format(name)) # self.console.syntax_style = 'monokai' # Connect singal handling for printing to console self.main_plot.sig_clicked.connect(self.print_to_console) self.cut_plot.sig_clicked.connect(self.print_to_console) # Create the integrated intensity plot ip = CursorPlot(name='z selector') ip.register_traced_variable(self.data_handler.z) ip.change_width_enabled = True ip.slider_width.sig_value_changed.connect( \ lambda : self.update_main_plot(emit=False)) self.integrated_plot = ip # Disable context menus for plot in [self.x_plot, self.y_plot, self.integrated_plot] : plot.plotItem.vb.setMenuEnabled(False) # Add ROI to the main ImageView self.cutline = Cutline(self.main_plot) self.cutline.initialize() # Scalebars. scalebar1 is for the `gamma` value scalebar1 = Scalebar() self.gamma_values = np.concatenate((np.linspace(0.1, 1, 50), np.linspace(1.1, 10, 50))) scalebar1.pos.set_value(0.5) scalebar1.pos.sig_value_changed.connect(self.on_gamma_slider_move) # Label the scalebar gamma_label = pg.TextItem('γ', anchor=(0.5, 0.5)) gamma_label.setPos(0.5, 0.5) scalebar1.addItem(gamma_label) self.scalebar1 = scalebar1 # scalebar2 is for vmax (relative colorscale maximum) scalebar2 = Scalebar() scalebar2.pos.set_value(self.vmax) scalebar2.pos.sig_value_changed.connect(self.on_vmax_slider_move) # Label the scalebar vmax_label = pg.TextItem('Colorscale', anchor=(0.5, 0.5)) vmax_label.setPos(0.5, 0.5) scalebar2.addItem(vmax_label) self.scalebar2 = scalebar2 # Align all the gui elements self._align() self.show() def _align(self) : """ Align all the GUI elements in the QLayout:: 0 1 2 3 4 +---+---+---+---+---+ | | | e | 0 + main | cut | d + | | | c | 1 +-------+-------+---+ | | mdc | | 2 + z +-------+---+ | | console | 3 +---+---+---+---+---+ (Units of subdivision [sd]) """ # subdivision sd = 3 # Get a short handle l = self.layout # addWIdget(row, column, rowSpan, columnSpan) # Main (3D) ImageView in top left l.addWidget(self.main_plot, 0, 0, 2*sd, 2*sd) # Cut to the right of Main l.addWidget(self.cut_plot, 0, 2*sd, 2*sd, 2*sd) # EDC and MDC plots l.addWidget(self.x_plot, 0, 4*sd, 2*sd, 2) l.addWidget(self.y_plot, 2*sd, 2*sd, 1*sd, 2*sd) # Integrated z-intensity plot l.addWidget(self.integrated_plot, 2*sd, 0, 2*sd, 2*sd) # Console l.addWidget(self.console, 3*sd, 2*sd, 1*sd, 3*sd) # Scalebars l.addWidget(self.scalebar1, 2*sd, 4*sd, 1, 1*sd) l.addWidget(self.scalebar2, 2*sd+1, 4*sd, 1, 1*sd) nrows = 4*sd ncols = 5*sd # Need to manually set all row- and columnspans as well as min-sizes for i in range(nrows) : l.setRowMinimumHeight(i, 50) l.setRowStretch(i, 1) for i in range(ncols) : l.setColumnMinimumWidth(i, 50) l.setColumnStretch(i, 1) def _autoload_plugins(self) : """ Load all the plugins specified in the config file ``CONF_DIR/plugins/autoload.txt``. """ logger.debug('Autoloading plugins...') # Parse the autoload.txt file if it exists try : with open(plugin_path / 'autoload.txt', 'r') as f : lines = f.readlines() except FileNotFoundError : logger.debug('autoload.txt not found at {}.'.format(plugin_path)) return [] # Load all the plugins! plugins = [] for line in lines : # Skip commented lines if line.startswith('#') : continue plugin_name = line.strip() plugin = self.load_plugin(plugin_name) # Try to use a shortname if plugin.shortname is not None : plugin_name = plugin.shortname plugins.append((plugin_name, plugin)) return plugins
[docs] def load_plugin(self, plugin_name) : """ Load a user supplied plugin and connect the plugin's `main` class with PIT. The plugin should be a python module which is placed in the pythonpath or in the `plugins` directory. **Parameters** =========== =========================================================== plugin_name str; name of the plugin module as it appears in the `plugins` directory. =========== =========================================================== """ # For debug purposes, determine the full path to the module location. path_to_plugin = str(plugin_path / plugin_name) logger.debug('Importing {}.'.format(path_to_plugin)) # Load the module and connect it to PIT module = importlib.import_module(plugin_name) plugin = module.main(self, self.data_handler) print('Importing plugin {} ({}).'.format(plugin_name, plugin.name)) # self.print_to_console('Importing plugin {} ({}).'.format(plugin_name, # plugin.name)) return plugin
[docs] def print_to_console(self, message) : """ Print a *message* to the embedded ipython console. """ self.console.kernel.stdout.write(str(message) + '\n')
[docs] def load_startup_data(self) : with open(SAMPLE_DATA_FILE, 'rb') as f : data = pickle.load(f) self.data_handler.prepare_data(data)
[docs] def brain(self) : """ Load the example dataset from an MRI brain scan. This example data is taken from the OpenNeuro database. Openneuro Accession Number: ds000108 Authored by: Wager, T.D., Davidson, M.L., Hughes, B.L., Lindquist, M.A., Ochsner, K.N. (2008). Prefrontal-subcortical pathways mediating successful emotion regulation. Neuron, 59(6):1037-50. doi: ``10.1016/j.neuron.2008.09.006`` """ BRAIN_DATA_FILE = data_path + 'brain.p' with open(BRAIN_DATA_FILE, 'rb') as f : data = pickle.load(f) self.data_handler.prepare_data(data) # Print the docstring to emphasize the source of the data self.print_to_console(self.brain.__doc__)
[docs] def update_main_plot(self, **image_kwargs) : """ Change *self.main_plot*`s currently displayed :class:`image_item <data_slicer.imageplot.ImagePlot.image_item>` to the slice of *self.data_handler.data* corresponding to the current value of *self.z*. """ logger.debug('update_main_plot()') self.data_handler.update_image_data() logger.debug('self.image_data.shape={}'.format(self.image_data.shape)) if image_kwargs != {} : self.image_kwargs = image_kwargs # Add image to main_plot self.set_image(self.image_data, **image_kwargs)
[docs] def set_axes(self) : """ Set the x- and y-scales of the plots. The :class:`ImagePlot <data_slicer.imageplot.ImagePlot>` object takes care of keeping the scales as they are, once they are set. """ xaxis = self.data_handler.axes[0] yaxis = self.data_handler.axes[1] if xaxis is not None : len_x = len(xaxis) else : len_x = 'None' if yaxis is not None : len_y = len(yaxis) else : len_y = 'None' logger.debug(('set_axes(): len(xaxis), len(yaxis)={}, ' + '{}').format(len_x, len_y)) self.main_plot.set_xscale(xaxis) self.main_plot.set_yscale(yaxis, update=True) self.main_plot.fix_viewrange() self.cutline.initialize()
[docs] def update_x_plot(self) : logger.debug('update_x_plot()') # Get shorthands for plot xp = self.x_plot try : old = xp.listDataItems()[0] xp.removeItem(old) except IndexError : pass # Get the correct position indicator pos = self.y_plot.pos if pos.allowed_values is not None : i_x = int( min(pos.get_value(), pos.allowed_values.max()-1)) logger.debug(('xp.pos.get_value()={}; i_x: ' '{}').format(xp.pos.get_value(), i_x)) else : i_x = 0 if not self.main_plot.transposed.get_value() : xprofile = self.data_handler.cut_data[i_x] else : xprofile = self.data_handler.cut_data[:,i_x] y = np.arange(len(xprofile)) + 0.5 xp.plot(xprofile, y)
[docs] def update_y_plot(self) : logger.debug('update_y_plot()') # Get shorthands for plot yp = self.y_plot try : old = yp.listDataItems()[0] yp.removeItem(old) except IndexError : pass # Get the correct position indicator pos = self.x_plot.pos if pos.allowed_values is not None : i_y = int( min(pos.get_value(), pos.allowed_values.max()-1)) logger.debug(('yp.pos.get_value()={}; i_y: ' '{}').format(yp.pos.get_value(), i_y)) else : i_y = 0 if not self.main_plot.transposed.get_value() : yprofile = self.data_handler.cut_data[:,i_y] else : yprofile = self.data_handler.cut_data[i_y] x = np.arange(len(yprofile)) + 0.5 yp.plot(x, yprofile)
[docs] def update_xy_plots(self) : """ Update the x and y profile plots. """ logger.debug('update_xy_plots()') self.update_x_plot() self.update_y_plot()
[docs] def set_cmap(self, cmap_name) : """ Set the colormap to *cmap* where *cmap* is one of the names registered in :mod:`<data_slicer.cmaps>` which includes all matplotlib and custom cmaps. """ # See if the cmap has been used and cached before. If not, create it # now. if cmap_name in self.cmaps : cmap = self.cmaps[cmap_name] else : cmap = load_cmap(cmap_name) self.cmaps.update({cmap_name: cmap}) self.cmap = cmap self.cmap_name = cmap_name # Since the cmap changed it forgot our settings for alpha and gamma self.cmap.set_alpha(self.alpha) self.cmap.set_gamma(self.gamma) self.cmap_changed()
def cmap_changed(self) : """ Recalculate the lookup table and redraw the plots such that the changes are immediately reflected. """ self.lut = self.cmap.getLookupTable() self.redraw_plots()
[docs] def transpose(self) : """ Transpose the main_plot, i.e. swap out its x- and y-axes. This wraps the main_plot's :meth:`transpose <data_slicer.imageplot.ImagePlot.transpose>` method. """ self.main_plot.transpose()
[docs] def rotate(self, alpha=0) : """ Rotate the main image by the given angle *alpha* (in degrees). """ self.main_plot.rotate(alpha)
[docs] def keyPressEvent(self, event) : """ Define all responses to keyboard presses. Currently defined: === ================================================================ key action === ================================================================ r Flip orientation of cutline. Also useful to bring it back to visibility. === ================================================================ """ key = event.key() logger.debug('keyPressEvent(): key={}'.format(key)) # if key == QtCore.Qt.Key_Right : # self.data_handler.z.set_value(self.data_handler.z.get_value() + 1) # elif key == QtCore.Qt.Key_Left : # self.data_handler.z.set_value(self.data_handler.z.get_value() - 1) # Flip Cutline on *R* key if key == QtCore.Qt.Key_R : self.cutline.flip_orientation() else : event.ignore() return # If any if-statement matched, we accepted the event event.accept()
[docs] def redraw_plots(self, image=None) : """ Redraw plotted data to reflect changes in data or its colors. """ logger.debug('redraw_plots()') try : # Redraw main plot self.set_image(image, displayed_axes=self.data_handler.displayed_axes) # Redraw cut plot self.update_cut() except AttributeError as e : # In some cases (namely initialization) the mainwindow is not # defined yet logger.debug('AttributeError: {}'.format(e))
[docs] def set_image(self, image=None, *args, **kwargs) : """ Wraps the underlying ImagePlot3d's set_image method. See :func:`~data_slicer.imageplot.ImagePlot3d.set_image`. *image* can be *None* i.e. in order to just update the plot with a new colormap. """ # Reset the transformation self._transform_factors = [] if image is None : image = self.image_data self.main_plot.set_image(image, *args, lut=self.lut, **kwargs)
[docs] def update_cut(self) : """ Take a cut of *self.data_handler.data* along *self.cutline*. This is used to update only the cut plot without affecting the main plot. """ logger.debug('update_cut()') data = self.data_handler.get_data() axes = self.data_handler.displayed_axes # Transpose, if necessary if self.main_plot.transposed.get_value() : axes = axes[::-1] try : cut = self.cutline.get_array_region(data, self.main_plot.image_item, axes=axes) except Exception as e : logger.error(e) return self.data_handler.cut_data = cut self.cut_plot.set_image(cut, lut=self.lut)
[docs] def on_cutline_initialized(self) : """ Need to reconnect the signal to the cut_plot. And directly update the cut_plot. """ self.cutline.sig_region_changed.connect(self.update_cut) self.update_cut()
[docs] def on_gamma_slider_move(self) : """ When the user moves the gamma slider, update gamma. """ ind = min(int(100*self.scalebar1.pos.get_value()), len(self.gamma_values)-1) gamma = self.gamma_values[ind] self.set_gamma(gamma)
[docs] def on_vmax_slider_move(self) : """ When the user moves the vmax slider, update vmax. """ vmax = int(np.round(100*self.scalebar2.pos.get_value()))/100 self.set_vmax(vmax)
[docs] def set_alpha(self, alpha) : """ Set the alpha value of the currently used cmap. *alpha* can be a single float or an array of length ``len(self.cmap.color)``. """ self.alpha = alpha self.cmap.set_alpha(alpha) self.cmap_changed()
[docs] def set_gamma(self, gamma=1) : """ Set the exponent for the power-law norm that maps the colors to values. I.e. the values where the colours are defined are mapped like ``y=x**gamma``. """ self.gamma = gamma self.cmap.set_gamma(gamma) self.cmap_changed() # Additionally, we need to update the slider position. We need to # hack a bit to avoid infinite signal loops: avoid emitting of the # signal and update the slider position by hand with a call to # scalebar1.set_position(). self.scalebar1.pos._value = indexof(gamma, self.gamma_values)/100 self.scalebar1.set_position()
[docs] def set_vmax(self, vmax=1) : """ Set the relative maximum of the colormap. I.e. the colors are mapped to the range `min(data)` - `vmax*max(data)`. """ self.vmax = vmax self.cmap.set_vmax(vmax) self.cmap_changed() # Additionally, we need to update the slider position. We need to # hack a bit to avoid infinite signal loops: avoid emitting of the # signal and update the slider position by hand with a call to # scalebar1.set_position(). self.scalebar2.pos._value = vmax self.scalebar2.set_position()
[docs] def cmap_changed(self) : """ Recalculate the lookup table and redraw the plots such that the changes are immediately reflected. """ self.lut = self.cmap.getLookupTable() self.redraw_plots()
# Set up argument parsing parser = argparse.ArgumentParser() parser.add_argument('filename', help='Name of file to open.', default=None, nargs='?') # Hook for setuptools entry point
[docs]def start_main_window() : args = parser.parse_args() app = QtWidgets.QApplication([]) mw = MainWindow() if args.filename is not None : mw.data_handler.load(args.filename) app.exec_()
if __name__=="__main__" : from data_slicer import set_up_logging start_main_window()