Source code for straditize.widgets.selection_toolbar

"""Module for the selection toolbar

This module defines the selection toolbar that is added to the
:class:`psyplot_gui.main.MainWindow` for selecting features in the
stratigraphic diagram and the data reader image.

**Disclaimer**

Copyright (C) 2018-2019  Philipp S. Sommer

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>."""
from itertools import chain
import six
import numpy as np
from straditize.widgets import get_icon, StraditizerControlBase, InfoButton
from psyplot_gui.compat.qtcompat import (
    QIcon, QtCore, QComboBox, QToolBar, with_qt5, QMenu, Qt, QLabel,
    QCheckBox)
from matplotlib.backend_tools import cursors
import matplotlib.widgets as mwid
import matplotlib.path as mplp

if with_qt5:
    from PyQt5.QtWidgets import QActionGroup, QSlider
else:
    from PyQt4.QtGui import QActionGroup, QSlider


[docs]class PointOrRectangleSelector(mwid.RectangleSelector): """RectangleSelector that allows to select points This class reimplements the :class:`matplotlib.widgets.RectangleSelector` to select points"""
[docs] def press(self, *args, **kwargs): ret = super(PointOrRectangleSelector, self).press(*args, **kwargs) if self.eventpress is not None: x = self.eventpress.xdata y = self.eventpress.ydata self.extents = x, x, y, y return ret
[docs]class SelectionToolbar(QToolBar, StraditizerControlBase): """A toolbar for selecting features in the straditizer and data image The current data object is set in the :attr:`combo` and can be accessed through the :attr:`data_obj` attribute. It's either the straditizer or the data_reader that is accessed""" _idPress = None _idRelease = None #: A signal that is emitted when something is selected selected = QtCore.pyqtSignal() set_cursor_id = None reset_cursor_id = None #: The QCombobox that defines the data object to be used combo = None @property def ax(self): """The :class:`matplotlib.axes.Axes` of the :attr:`data_obj`""" return self.data_obj.ax @property def data(self): """The np.ndarray of the :attr:`data_obj` image""" text = self.combo.currentText() if text == 'Reader': return self.straditizer.data_reader.binary elif text == 'Reader - Greyscale': return self.straditizer.data_reader.to_grey_pil( self.straditizer.data_reader.image) else: from straditize.binary import DataReader return DataReader.to_grey_pil(self.straditizer.image) @property def data_obj(self): """The data object as set in the :attr:`combo`. Either a :class:`~straditize.straditizer.Straditizer` or a :class:`straditize.binary.DataReader` instance. """ text = self.combo.currentText() if text in ['Reader', 'Reader - Greyscale']: return self.straditizer.data_reader else: return self.straditizer @data_obj.setter def data_obj(self, value): """The data object as set in the :attr:`combo`. Either a :class:`~straditize.straditizer.Straditizer` or a :class:`straditize.binary.DataReader` instance. """ if self.straditizer is None: return if isinstance(value, six.string_types): possible_values = { self.combo.itemText(i) for i in range(self.combo.count())} if value not in possible_values: raise ValueError( 'Do not understand %r! Please use one of %r' % ( value, possible_values)) else: self.combo.setCurrentText(value) else: if value is self.straditizer: self.combo.setCurrentText('Straditizer') elif value and value is self.straditizer.data_reader: self.combo.setCurrentText('Reader') else: raise ValueError('Do not understand %r! Please either use ' 'the Straditizer or DataReader instance!' % ( value, )) @property def fig(self): """The :class:`~matplotlib.figure.Figure` of the :attr:`data_obj`""" try: return self.ax.figure except AttributeError: return None @property def canvas(self): """The canvas of the :attr:`data_obj`""" try: return self.fig.canvas except AttributeError: return None @property def toolbar(self): """The toolbar of the :attr:`canvas`""" return self.canvas.toolbar @property def select_action(self): """The rectangle selection tool""" return self._actions['select'] @property def wand_action(self): """The wand selection tool""" return self._actions['wand_select'] @property def new_select_action(self): """The action to make new selection with one of the selection tools""" return self._type_actions['new_select'] @property def add_select_action(self): """The action to add to the current selection with the selection tools """ return self._type_actions['add_select'] @property def remove_select_action(self): """ An action to remove from the current selection with the selection tools """ return self._type_actions['remove_select'] @property def select_all_action(self): """An action to select all features in the :attr:`data`""" return self._actions['select_all'] @property def expand_select_action(self): """An action to expand the current selection to the full feature""" return self._actions['expand_select'] @property def invert_select_action(self): """An action to invert the current selection""" return self._actions['invert_select'] @property def clear_select_action(self): """An action to clear the current selection""" return self._actions['clear_select'] @property def select_right_action(self): """An action to select everything in the data column to the right""" return self._actions['select_right'] @property def select_pattern_action(self): """An action to start a pattern selection""" return self._actions['select_pattern'] @property def widgets2disable(self): if not self._actions: return [] elif self._selecting: return [self.combo] else: return list(chain([self.combo], self._actions.values(), self._appearance_actions.values())) @property def labels(self): """The labeled data that is displayed""" if self.data_obj._selection_arr is not None: return self.data_obj._selection_arr text = self.combo.currentText() if text == 'Reader': return self.straditizer.data_reader.labels.copy() elif text == 'Reader - Greyscale': return self.straditizer.data_reader.color_labels() else: return self.straditizer.get_labels() @property def rect_callbacks(self): """The functions to call after the rectangle selection. If not set manually, it is the :meth:`select_rect` method. Note that this is cleared at every call of the :meth:`end_selection`. Callables in this list must accept two arguments ``(slx, sly)``: the first one is the x-slice, and the second one the y-slice. They both correspond to the :attr:`data` attribute.""" return self._rect_callbacks or [self.select_rect] @rect_callbacks.setter def rect_callbacks(self, value): """The functions to call after the rectangle selection. If not set manually, it is the :meth:`select_rect` method. Note that this is cleared at every call of the :meth:`end_selection`. Callables in this list must accept two arguments ``(slx, sly)``: the first one is the x-slice, and the second one the y-slice. They both correspond to the :attr:`data` attribute.""" self._rect_callbacks = value @property def poly_callbacks(self): """The functions to call after the polygon selection If not set manually, it is the :meth:`select_poly` method. Note that this is cleared at every call of the :meth:`end_selection`. Callables in this list must accept one argument, a ``np.ndarray`` of shape ``(N, 2)``. This array defines the ``N`` x- and y-coordinates of the points of the polygon""" return self._poly_callbacks or [self.select_poly] @poly_callbacks.setter def poly_callbacks(self, value): """The functions to call after the polygon selection. If not set manually, it is the :meth:`poly_callbacks` method. Note that this is cleared at every call of the :meth:`end_selection`. Callables in this list must accept one argument, a ``np.ndarray`` of shape ``(N, 2)``. This array defines the ``N`` x- and y-coordinates of the points of the polygon""" self._poly_callbacks = value #: A :class:`PointOrRectangleSelector` to select features in the image selector = None _pattern_selection = None def __init__(self, straditizer_widgets, *args, **kwargs): super(SelectionToolbar, self).__init__(*args, **kwargs) self._actions = {} self._wand_actions = {} self._pattern_actions = {} self._select_actions = {} self._appearance_actions = {} # Boolean that is True if we are in a selection process self._selecting = False self.init_straditizercontrol(straditizer_widgets) self._ids_select = [] self._rect_callbacks = [] self._poly_callbacks = [] self._selection_mode = None self._lastCursor = None self.create_actions() self._changed_selection = False self._connected = [] self._action_clicked = None self.wand_type = 'labels' self.select_type = 'rect' self.pattern_type = 'binary' self.auto_expand = False
[docs] def create_actions(self): """Define the actions for the toolbar and set everything up""" # Reader toolbar self.combo = QComboBox() self.combo.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.addWidget(self.combo) select_group = QActionGroup(self) # select action self._actions['select'] = a = self.addAction( QIcon(get_icon('select.png')), 'select', self.toggle_selection) a.setToolTip('Select pixels within a rectangle') a.setCheckable(True) select_group.addAction(a) # select menu select_menu = QMenu(self) self._select_actions['rect_select'] = menu_a = select_menu.addAction( QIcon(get_icon('select.png')), 'rectangle', self.set_rect_select_mode) menu_a.setToolTip('Select a rectangle') a.setToolTip(menu_a.toolTip()) self._select_actions['poly_select'] = menu_a = select_menu.addAction( QIcon(get_icon('poly_select.png')), 'polygon', self.set_poly_select_mode) menu_a.setToolTip('Select a rectangle') a.setToolTip(menu_a.toolTip()) a.setMenu(select_menu) # wand_select action self._actions['wand_select'] = a = self.addAction( QIcon(get_icon('wand_select.png')), 'select', self.toggle_selection) a.setCheckable(True) select_group.addAction(a) # wand menu tool_menu = QMenu(self) self._wand_actions['wand_select'] = menu_a = tool_menu.addAction( QIcon(get_icon('wand_select.png')), 'wand', self.set_label_wand_mode) menu_a.setToolTip('Select labels within a rectangle') a.setToolTip(menu_a.toolTip()) self._wand_actions['color_select'] = menu_a = tool_menu.addAction( QIcon(get_icon('color_select.png')), 'color wand', self.set_color_wand_mode) menu_a.setToolTip('Select colors') self._wand_actions['row_select'] = menu_a = tool_menu.addAction( QIcon(get_icon('row_select.png')), 'row selection', self.set_row_wand_mode) menu_a.setToolTip('Select pixel rows') self._wand_actions['col_select'] = menu_a = tool_menu.addAction( QIcon(get_icon('col_select.png')), 'column selection', self.set_col_wand_mode) menu_a.setToolTip('Select pixel columns') a.setMenu(tool_menu) # color_wand widgets self.distance_slider = slider = QSlider(Qt.Horizontal) slider.setMinimum(0) slider.setMaximum(255) slider.setValue(30) slider.setSingleStep(1) self.lbl_slider = QLabel('30') slider.valueChanged.connect(lambda i: self.lbl_slider.setText(str(i))) slider.setMaximumWidth(self.combo.sizeHint().width()) self.cb_whole_fig = QCheckBox('Whole plot') self.cb_whole_fig.setToolTip('Select the colors on the entire plot') self.cb_use_alpha = QCheckBox('Use alpha') self.cb_use_alpha.setToolTip('Use the alpha channel, i.e. the ' 'transparency of the RGBA image.') self.color_wand_actions = [ self.addWidget(slider), self.addWidget(self.lbl_slider), self.addWidget(self.cb_whole_fig), self.addWidget(self.cb_use_alpha)] self.set_label_wand_mode() self.addSeparator() type_group = QActionGroup(self) self._type_actions = {} # new selection action self._type_actions['new_select'] = a = self.addAction( QIcon(get_icon('new_selection.png')), 'Create a new selection') a.setToolTip('Select pixels within a rectangle and ignore the current ' 'selection') a.setCheckable(True) type_group.addAction(a) # add to selection action self._type_actions['add_select'] = a = self.addAction( QIcon(get_icon('add_select.png')), 'Add to selection') a.setToolTip('Select pixels within a rectangle and add them to the ' 'current selection') a.setCheckable(True) type_group.addAction(a) # remove action self._type_actions['remove_select'] = a = self.addAction( QIcon(get_icon('remove_select.png')), 'Remove from selection') a.setToolTip('Select pixels within a rectangle and remove them from ' 'the current selection') a.setCheckable(True) type_group.addAction(a) # info button self.addSeparator() self.info_button = InfoButton(self, 'selection_toolbar.rst') self.addWidget(self.info_button) # selection appearence options self.addSeparator() self.sl_alpha = slider = QSlider(Qt.Horizontal) self._appearance_actions['alpha'] = self.addWidget(slider) slider.setMinimum(0) slider.setMaximum(100) slider.setValue(100) slider.setSingleStep(1) self.lbl_alpha_slider = QLabel('100 %') slider.valueChanged.connect( lambda i: self.lbl_alpha_slider.setText(str(i) + ' %')) slider.valueChanged.connect(self.update_alpha) slider.setMaximumWidth(self.combo.sizeHint().width()) # Select all and invert selection buttons self.addSeparator() self._actions['select_all'] = a = self.addAction( QIcon(get_icon('select_all.png')), 'all', self.select_all) a.setToolTip('Select all labels') self._actions['expand_select'] = a = self.addAction( QIcon(get_icon('expand_select.png')), 'expand', self.expand_selection) a.setToolTip('Expand the selected areas to select the entire feature') self._actions['invert_select'] = a = self.addAction( QIcon(get_icon('invert_select.png')), 'invert', self.invert_selection) a.setToolTip('Invert selection') self._actions['clear_select'] = a = self.addAction( QIcon(get_icon('clear_select.png')), 'clear', self.clear_selection) a.setToolTip('Clear selection') self._actions['select_right'] = a = self.addAction( QIcon(get_icon('select_right.png')), 'right', self.select_everything_to_the_right) a.setToolTip('Select everything to the right of each column') self._actions['select_pattern'] = a = self.addAction( QIcon(get_icon('pattern.png')), 'pattern', self.start_pattern_selection) a.setCheckable(True) a.setToolTip( 'Select a binary pattern/hatch within the current selection') # wand menu pattern_menu = QMenu(self) self._pattern_actions['binary'] = menu_a = pattern_menu.addAction( QIcon(get_icon('pattern.png')), 'Binary', self.set_binary_pattern_mode) menu_a.setToolTip( 'Select a binary pattern/hatch within the current selection') a.setToolTip(menu_a.toolTip()) self._pattern_actions['grey'] = menu_a = pattern_menu.addAction( QIcon(get_icon('pattern_grey.png')), 'Greyscale', self.set_grey_pattern_mode) menu_a.setToolTip( 'Select a pattern/hatch within the current selection based on ' 'grey scale colors') a.setMenu(pattern_menu) self.new_select_action.setChecked(True) for a in self._type_actions.values(): a.toggled.connect(self.add_or_remove_pattern) self.refresh()
[docs] def should_be_enabled(self, w): if self.straditizer is None: return False elif (self._actions and w in [self.remove_select_action, self.invert_select_action, self.clear_select_action, self.expand_select_action, self.select_right_action] and not self._selecting): return False elif w in self._appearance_actions.values() and not self._selecting: return False elif (self.combo and not self.combo.currentText().startswith('Reader') and w is self.select_right_action): return False return True
[docs] def disable_actions(self): if self._changed_selection: return for a in self._actions.values(): if a.isChecked(): a.setChecked(False) self.toggle_selection() else: a.setChecked(False)
[docs] def select_all(self): """Select all features in the image See Also -------- straditize.label_selection.LabelSelection.select_all_labels""" obj = self.data_obj if obj._selection_arr is None: rgba = obj.image_array() if hasattr(obj, 'image_array') else None self.start_selection(self.labels, rgba=rgba) obj.select_all_labels() self.canvas.draw()
[docs] def invert_selection(self): """Invert the current selection""" obj = self.data_obj if obj._selection_arr is None: rgba = obj.image_array() if hasattr(obj, 'image_array') else None self.start_selection(self.labels, rgba=rgba) if (obj._selection_arr != obj._orig_selection_arr).any(): selection = obj.selected_part # clear the current selection obj._selection_arr[:] = np.where( obj._selection_arr.astype(bool) & (~selection), obj._orig_selection_arr.max() + 1, obj._orig_selection_arr) obj._select_img.set_array(obj._selection_arr) obj.unselect_all_labels() else: obj.select_all_other_labels() self.canvas.draw()
[docs] def clear_selection(self): """Clear the current selection""" obj = self.data_obj if obj._selection_arr is None: return obj._selection_arr[:] = obj._orig_selection_arr.copy() obj._select_img.set_array(obj._selection_arr) obj.unselect_all_labels() self.canvas.draw()
[docs] def expand_selection(self): """Expand the selected areas to select the full labels""" obj = self.data_obj if obj._selection_arr is None: return arr = obj._orig_selection_arr.copy() selected_labels = np.unique(arr[obj.selected_part]) obj._selection_arr = arr obj._select_img.set_array(arr) obj.unselect_all_labels() obj.select_labels(selected_labels) self.canvas.draw()
[docs] def update_alpha(self, i): """Set the transparency of the selection image Parameters ---------- i: int The transparency between 0 and 100""" self.data_obj._select_img.set_alpha(i / 100.) self.data_obj._update_magni_img() self.canvas.draw()
[docs] def select_everything_to_the_right(self): """Selects everything to the right of the current selection""" reader = self.data_obj if reader._selection_arr is None: return bounds = reader.column_bounds selection = reader.selected_part new_select = np.zeros_like(selection) for start, end in bounds: can_be_selected = reader._selection_arr[:, start:end].astype(bool) end = start + can_be_selected.shape[1] last_in_row = selection[:, start:end].argmax(axis=-1).reshape( (-1, 1)) dist2start = np.tile(np.arange(end - start)[np.newaxis], (len(selection), 1)) can_be_selected[dist2start <= last_in_row] = False can_be_selected[~np.tile(last_in_row.astype(bool), (1, end - start))] = False new_select[:, start:end] = can_be_selected max_label = reader._orig_selection_arr.max() reader._selection_arr[new_select] = max_label + 1 reader._select_img.set_array(reader._selection_arr) reader._update_magni_img() self.canvas.draw()
[docs] def start_pattern_selection(self): """Open the pattern selection dialog This method will enable the pattern selection by starting a :class:`straditize.widgets.pattern_selection.PatternSelectionWidget`""" from straditize.widgets.pattern_selection import PatternSelectionWidget if self.select_pattern_action.isChecked(): from straditize.binary import DataReader from psyplot_gui.main import mainwindow obj = self.data_obj if obj._selection_arr is None: if hasattr(obj, 'image_array'): rgba = obj.image_array() else: rgba = None self.start_selection(self.labels, rgba=rgba) self.select_all() if not obj.selected_part.any(): self.select_pattern_action.setChecked(False) raise ValueError( "No data in the image is selected. Please select the " "coarse region in which the pattern should be searched.") if self.pattern_type == 'binary': arr = DataReader.to_binary_pil(obj.image) else: arr = DataReader.to_grey_pil(obj.image) self._pattern_selection = w = PatternSelectionWidget( arr, obj) w.to_dock(mainwindow, 'Pattern selection') w.btn_close.clicked.connect(self.uncheck_pattern_selection) w.btn_cancel.clicked.connect(self.uncheck_pattern_selection) self.disable_actions() pattern_action = self.select_pattern_action for a in self._actions.values(): a.setEnabled(False) pattern_action.setEnabled(True) pattern_action.setChecked(True) w.show_plugin() w.maybe_tabify() w.raise_() elif self._pattern_selection is not None: self._pattern_selection.cancel() self.uncheck_pattern_selection() del self._pattern_selection
[docs] def uncheck_pattern_selection(self): """Disable the pattern selection""" self.select_pattern_action.setChecked(False) del self._pattern_selection for a in self._actions.values(): a.setEnabled(self.should_be_enabled(a))
[docs] def add_or_remove_pattern(self): """Enable the removing or adding of the pattern selection""" if getattr(self, '_pattern_selection', None) is None: return current = self._pattern_selection.remove_selection new = self.remove_select_action.isChecked() if new is not current: self._pattern_selection.remove_selection = new if self._pattern_selection.btn_select.isChecked(): self._pattern_selection.modify_selection( self._pattern_selection.sl_thresh.value())
[docs] def set_rect_select_mode(self): """Set the current wand tool to the color wand""" self.select_type = 'rect' self.select_action.setIcon(QIcon(get_icon('select.png'))) self._action_clicked = None self.toggle_selection()
[docs] def set_poly_select_mode(self): """Set the current wand tool to the color wand""" self.select_type = 'poly' self.select_action.setIcon(QIcon(get_icon('poly_select.png'))) self._action_clicked = None self.toggle_selection()
[docs] def set_label_wand_mode(self): """Set the current wand tool to the color wand""" self.wand_type = 'labels' self.wand_action.setIcon(QIcon(get_icon('wand_select.png'))) for a in self.color_wand_actions: a.setVisible(False) self._action_clicked = None self.toggle_selection()
[docs] def set_color_wand_mode(self): """Set the current wand tool to the color wand""" self.wand_type = 'color' self.wand_action.setIcon(QIcon(get_icon('color_select.png'))) for a in self.color_wand_actions: a.setVisible(True) self._action_clicked = None self.toggle_selection()
[docs] def set_row_wand_mode(self): """Set the current wand tool to the color wand""" self.wand_type = 'rows' self.wand_action.setIcon(QIcon(get_icon('row_select.png'))) for a in self.color_wand_actions: a.setVisible(False) self._action_clicked = None self.toggle_selection()
[docs] def set_col_wand_mode(self): """Set the current wand tool to the color wand""" self.wand_type = 'cols' self.wand_action.setIcon(QIcon(get_icon('col_select.png'))) for a in self.color_wand_actions: a.setVisible(False) self._action_clicked = None self.toggle_selection()
[docs] def set_binary_pattern_mode(self): """Set the current pattern mode to the binary pattern""" self.pattern_type = 'binary' self.select_pattern_action.setIcon(QIcon(get_icon('pattern.png')))
[docs] def set_grey_pattern_mode(self): """Set the current pattern mode to the binary pattern""" self.pattern_type = 'grey' self.select_pattern_action.setIcon(QIcon(get_icon('pattern_grey.png')))
[docs] def disconnect(self): if self.set_cursor_id is not None: if self.canvas is None: self.canvas.mpl_disconnect(self.set_cursor_id) self.canvas.mpl_disconnect(self.reset_cursor_id) self.set_cursor_id = None self.reset_cursor_id = None if self.selector is not None: self.selector.disconnect_events() self.selector = None
[docs] def toggle_selection(self): """Activate selection mode""" if self.canvas is None: return self.disconnect() key = next((key for key, a in self._actions.items() if a.isChecked()), None) if key is None or key == self._action_clicked: self._action_clicked = None if key is not None: self._actions[key].setChecked(False) else: if self.wand_action.isChecked() and self.wand_type == 'color': self.selector = PointOrRectangleSelector( self.ax, self.on_rect_select, rectprops=dict(fc='none'), lineprops=dict(c='none'), useblit=True) elif self.select_action.isChecked() and self.select_type == 'poly': self.selector = mwid.LassoSelector( self.ax, self.on_poly_select) else: self.selector = PointOrRectangleSelector( self.ax, self.on_rect_select, useblit=True) self.set_cursor_id = self.canvas.mpl_connect( 'axes_enter_event', self._on_axes_enter) self.reset_cursor_id = self.canvas.mpl_connect( 'axes_leave_event', self._on_axes_leave) self._action_clicked = next(key for key, a in self._actions.items() if a.isChecked()) self.toolbar.set_message(self.toolbar.mode)
[docs] def enable_or_disable_widgets(self, b): super(SelectionToolbar, self).enable_or_disable_widgets(b) if not b: for w in [self.clear_select_action, self.invert_select_action, self.expand_select_action]: w.setEnabled(self.should_be_enabled(w)) if self._actions and not self.select_action.isEnabled(): for a in self._actions.values(): if a.isChecked(): a.setChecked(False) self.toggle_selection()
[docs] def refresh(self): super(SelectionToolbar, self).refresh() combo = self.combo if self.straditizer is None: combo.clear() else: if not combo.count(): combo.addItem('Straditizer') if self.straditizer.data_reader is not None: if not any(combo.itemText(i) == 'Reader' for i in range(combo.count())): combo.addItem('Reader') combo.addItem('Reader - Greyscale') else: for i in range(combo.count()): if combo.itemText(i).startswith('Reader'): combo.removeItem(i)
def _on_axes_enter(self, event): ax = self.ax if ax is None: return if (event.inaxes is ax and self.toolbar._active == '' and self.selector is not None): if self._lastCursor != cursors.SELECT_REGION: self.toolbar.set_cursor(cursors.SELECT_REGION) self._lastCursor = cursors.SELECT_REGION def _on_axes_leave(self, event): ax = self.ax if ax is None: return if (event.inaxes is ax and self.toolbar._active == '' and self.selector is not None): if self._lastCursor != cursors.POINTER: self.toolbar.set_cursor(cursors.POINTER) self._lastCursor = cursors.POINTER
[docs] def end_selection(self): """Finish the selection and disconnect everything""" if getattr(self, '_pattern_selection', None) is not None: self._pattern_selection.remove_plugin() del self._pattern_selection self._selecting = False self._action_clicked = None self.toggle_selection() self.auto_expand = False self._labels = None self._rect_callbacks.clear() self._poly_callbacks.clear() self._wand_actions['color_select'].setEnabled(True)
[docs] def get_xy_slice(self, lastx, lasty, x, y): """Transform x- and y-coordinates to :class:`slice` objects Parameters ---------- lastx: int The initial x-coordinate lasty: int The initial y-coordinate x: int The final x-coordinate y: int The final y-coordinate Returns ------- slice The ``slice(lastx, x)`` slice The ``slice(lasty, y)``""" all_x = np.floor(np.sort([lastx, x])).astype(int) all_y = np.floor(np.sort([lasty, y])).astype(int) extent = getattr(self.data_obj, 'extent', None) if extent is not None: all_x -= np.ceil(extent[0]).astype(int) all_y -= np.ceil(min(extent[2:])).astype(int) if self.wand_action.isChecked() and self.wand_type == 'color': all_x[0] = all_x[1] all_y[0] = all_y[1] all_x[all_x < 0] = 0 all_y[all_y < 0] = 0 all_x[1] += 1 all_y[1] += 1 return slice(*all_x), slice(*all_y)
[docs] def on_rect_select(self, e0, e1): """Call the :attr:`rect_callbacks` after a rectangle selection Parameters ---------- e0: matplotlib.backend_bases.Event The initial event e1: matplotlib.backend_bases.Event The final event""" slx, sly = self.get_xy_slice(e0.xdata, e0.ydata, e1.xdata, e1.ydata) for func in self.rect_callbacks: func(slx, sly)
[docs] def select_rect(self, slx, sly): """Select the data defined by a rectangle Parameters ---------- slx: slice The x-slice of the rectangle sly: slice The y-slice of the rectangle See Also -------- rect_callbacks""" obj = self.data_obj if obj._selection_arr is None: arr = self.labels rgba = obj.image_array() if hasattr(obj, 'image_array') else None self.start_selection(arr, rgba=rgba) expand = False if self.select_action.isChecked(): arr = self._select_rectangle(slx, sly) expand = True elif self.wand_type == 'labels': arr = self._select_labels(slx, sly) elif self.wand_type == 'rows': arr = self._select_rows(slx, sly) elif self.wand_type == 'cols': arr = self._select_cols(slx, sly) else: arr = self._select_colors(slx, sly) expand = True if arr is not None: obj._selection_arr = arr obj._select_img.set_array(arr) obj._update_magni_img() if expand and self.auto_expand: self.expand_selection() else: self.canvas.draw()
[docs] def on_poly_select(self, points): """Call the :attr:`poly_callbacks` after a polygon selection Parameters ---------- e0: matplotlib.backend_bases.Event The initial event e1: matplotlib.backend_bases.Event The final event""" for func in self.poly_callbacks: func(points)
[docs] def select_poly(self, points): """Select the data defined by a polygon Parameters ---------- points: np.ndarray of shape (N, 2) The x- and y-coordinates of the vertices of the polygon See Also -------- poly_callbacks""" obj = self.data_obj if obj._selection_arr is None: rgba = obj.image_array() if hasattr(obj, 'image_array') else None self.start_selection(self.labels, rgba=rgba) arr = self.labels mpath = mplp.Path(points) x = np.arange(obj._selection_arr.shape[1], dtype=int) y = np.arange(obj._selection_arr.shape[0], dtype=int) extent = getattr(obj, 'extent', None) if extent is not None: x += np.ceil(extent[0]).astype(int) y += np.ceil(min(extent[2:])).astype(int) pointsx, pointsy = np.array(points).T x0, x1 = x.searchsorted([pointsx.min(), pointsx.max()]) y0, y1 = y.searchsorted([pointsy.min(), pointsy.max()]) X, Y = np.meshgrid(x[x0:x1], y[y0:y1]) points = np.array((X.flatten(), Y.flatten())).T mask = np.zeros_like(obj._selection_arr, dtype=bool) mask[y0:y1, x0:x1] = ( mpath.contains_points(points).reshape(X.shape) & obj._selection_arr[y0:y1, x0:x1].astype(bool)) if self.remove_select_action.isChecked(): arr[mask] = -1 else: if self.new_select_action.isChecked(): arr = obj._orig_selection_arr.copy() obj._select_img.set_cmap(obj._select_cmap) obj._select_img.set_norm(obj._select_norm) arr[mask] = arr.max() + 1 obj._selection_arr = arr obj._select_img.set_array(arr) obj._update_magni_img() if self.auto_expand: self.expand_selection() else: self.canvas.draw()
def _select_rectangle(self, slx, sly): """Select a rectangle within the array""" obj = self.data_obj arr = self.labels data_mask = obj._selection_arr.astype(bool) if self.remove_select_action.isChecked(): arr[sly, slx][data_mask[sly, slx]] = -1 else: if self.new_select_action.isChecked(): arr = obj._orig_selection_arr.copy() obj._select_img.set_cmap(obj._select_cmap) obj._select_img.set_norm(obj._select_norm) arr[sly, slx][data_mask[sly, slx]] = arr.max() + 1 return arr def _select_labels(self, slx, sly): """Select the unique labels in the array""" obj = self.data_obj arr = self.labels data_mask = obj._selection_arr.astype(bool) current_selected = obj.selected_labels new_selected = np.unique( arr[sly, slx][data_mask[sly, slx]]) valid_labels = np.unique( obj._orig_selection_arr[sly, slx][data_mask[sly, slx]]) valid_labels = valid_labels[valid_labels > 0] if not len(valid_labels): return if new_selected[0] == -1 or new_selected[-1] > obj._select_nlabels: mask = np.isin(obj._orig_selection_arr, valid_labels) current_selected = np.unique( np.r_[current_selected, obj._orig_selection_arr[sly, slx][ arr[sly, slx] > obj._select_nlabels]]) arr[mask] = obj._orig_selection_arr[mask] curr = set(current_selected) valid = set(valid_labels) if self.remove_select_action.isChecked(): new = curr - valid elif self.add_select_action.isChecked(): new = curr | valid else: new = valid arr = obj._orig_selection_arr.copy() obj.select_labels(np.array(sorted(new))) return arr def _select_rows(self, slx, sly): """Select the pixel rows defined by `sly` Parameters ---------- slx: slice The x-slice (is ignored) sly: slice The y-slice defining the rows to select""" obj = self.data_obj arr = self.labels rows = np.arange(arr.shape[0])[sly] if self.remove_select_action.isChecked(): arr[rows, :] = np.where(arr[rows, :], -1, 0) else: if self.new_select_action.isChecked(): arr = obj._orig_selection_arr.copy() obj._select_img.set_cmap(obj._select_cmap) obj._select_img.set_norm(obj._select_norm) arr[rows, :] = np.where(arr[rows, :], arr.max() + 1, 0) return arr def _select_cols(self, slx, sly): """Select the pixel columns defined by `slx` Parameters ---------- slx: slice The x-slice defining the columns to select sly: slice The y-slice (is ignored)""" obj = self.data_obj arr = self.labels cols = np.arange(arr.shape[1])[slx] if self.remove_select_action.isChecked(): arr[:, cols] = np.where(arr[:, cols], -1, 0) else: if self.new_select_action.isChecked(): arr = obj._orig_selection_arr.copy() obj._select_img.set_cmap(obj._select_cmap) obj._select_img.set_norm(obj._select_norm) arr[:, cols] = np.where(arr[:, cols], arr.max() + 1, 0) return arr def _select_colors(self, slx, sly): """Select the array based on the colors""" if self.cb_use_alpha.isChecked(): rgba = self._rgba n = 4 else: rgba = self._rgba[..., :-1] n = 3 rgba = rgba.astype(int) # get the unique colors colors = list( map(np.array, set(map(tuple, rgba[sly, slx].reshape((-1, n)))))) obj = self.data_obj arr = self.labels mask = np.zeros_like(arr, dtype=bool) max_dist = self.distance_slider.value() data_mask = obj._selection_arr.astype(bool) for c in colors: mask[np.all(np.abs(rgba - c.reshape((1, 1, -1))) <= max_dist, axis=-1)] = True if not self.cb_whole_fig.isChecked(): import skimage.morphology as skim all_labels = skim.label(mask, 8, return_num=False) selected_labels = np.unique(all_labels[sly, slx]) mask[~np.isin(all_labels, selected_labels)] = False if self.remove_select_action.isChecked(): arr[mask & data_mask] = -1 else: if self.new_select_action.isChecked(): arr = obj._orig_selection_arr.copy() obj._select_img.set_cmap(obj._select_cmap) obj._select_img.set_norm(obj._select_norm) arr[mask & data_mask] = arr.max() + 1 return arr def _remove_selected_labels(self): self.data_obj.remove_selected_labels(disable=True) def _disable_selection(self): return self.data_obj.disable_label_selection()
[docs] def start_selection(self, arr=None, rgba=None, rect_callbacks=None, poly_callbacks=None, apply_funcs=(), cancel_funcs=(), remove_on_apply=True): """Start the selection in the current :attr:`data_obj` Parameters ---------- arr: np.ndarray The labeled selection array that is used. If specified, the :meth:`~straditize.label_selection.enable_label_selection` method is called of the :attr:`data_obj` with the given `arr`. If this parameter is ``None``, then we expect that this method has already been called rgba: np.ndarray The RGBA image that shall be used for the color selection (see the :meth:`set_color_wand_mode`) rect_callbacks: list A list of callbacks that shall be called after a rectangle selection has been made by the user (see :attr:`rect_callbacks`) poly_callbacks: list A list of callbacks that shall be called after a polygon selection has been made by the user (see :attr:`poly_callbacks`) apply_funcs: list A list of callables that shall be connected to the :attr:`~straditize.widgets.StraditizerWidgets.apply_button` cancel_funcs: list A list of callables that shall be connected to the :attr:`~straditize.widgets.StraditizerWidgets.cancel_button` remove_on_apply: bool If True and the :attr:`~straditize.widgets.StraditizerWidgets.apply_button` is clicked, the selected labels will be removed.""" obj = self.data_obj if arr is not None: obj.enable_label_selection( arr, arr.max(), set_picker=False, zorder=obj.plot_im.zorder + 0.1, extent=obj.plot_im.get_extent()) self._selecting = True self._rgba = rgba if rgba is None: self.set_label_wand_mode() self._wand_actions['color_select'].setEnabled(False) else: self._wand_actions['color_select'].setEnabled(True) self.connect2apply( (self._remove_selected_labels if remove_on_apply else self._disable_selection), obj.remove_small_selection_ellipses, obj.draw_figure, self.end_selection, *apply_funcs) self.connect2cancel(self._disable_selection, obj.remove_small_selection_ellipses, obj.draw_figure, self.end_selection, *cancel_funcs) if self.should_be_enabled(self._appearance_actions['alpha']): self.update_alpha(self.sl_alpha.value()) for w in chain(self._actions.values(), self._appearance_actions.values()): w.setEnabled(self.should_be_enabled(w)) if remove_on_apply: self.straditizer_widgets.apply_button.setText('Remove') if rect_callbacks is not None: self._rect_callbacks = rect_callbacks[:] if poly_callbacks is not None: self._poly_callbacks = poly_callbacks[:] del obj