class MarkerControl(StraditizerControlBase, QWidget): """Widget to control the appearance of the marks This widget controls the appearance of the :class:`straditize.cross_mark.CrossMarks` instances in the :attr:`~straditize.straditizer.Straditizer.marks` attribute of the :attr:`~straditize.widgets.StraditizerControlBase.straditizer`""" _toolbar = None @property def marks(self): """The :class:`~straditize.cross_mark.CrossMarks` of the straditizer """ return chain(self.straditizer.marks, self.straditizer.magni_marks) @docstrings.dedent def __init__(self, straditizer_widgets, item, *args, **kwargs): """ Parameters ---------- %(StraditizerControlBase.init_straditizercontrol.parameters)s """ super(MarkerControl, self).__init__(*args, **kwargs) self.init_straditizercontrol(straditizer_widgets, item) vbox = QVBoxLayout() # auto hide button box_hide = QGridLayout() self.cb_auto_hide = QCheckBox('Auto-hide') self.cb_show_connected = QCheckBox('Show additionals') self.cb_drag_hline = QCheckBox('Drag in y-direction') self.cb_drag_vline = QCheckBox('Drag in x-direction') self.cb_selectable_hline = QCheckBox('Horizontal lines selectable') self.cb_selectable_vline = QCheckBox('Vertical lines selectable') self.cb_show_hlines = QCheckBox('Show horizontal lines') self.cb_show_vlines = QCheckBox('Show vertical lines') box_hide.addWidget(self.cb_auto_hide, 0, 0) box_hide.addWidget(self.cb_show_connected, 0, 1) box_hide.addWidget(self.cb_show_vlines, 1, 0) box_hide.addWidget(self.cb_show_hlines, 1, 1) box_hide.addWidget(self.cb_drag_hline, 2, 0) box_hide.addWidget(self.cb_drag_vline, 2, 1) box_hide.addWidget(self.cb_selectable_hline, 3, 0) box_hide.addWidget(self.cb_selectable_vline, 3, 1) vbox.addLayout(box_hide) style_box = QGridLayout() style_box.addWidget(QLabel('Unselected:'), 0, 1) selected_label = QLabel('Selected:') style_box.addWidget(selected_label, 0, 2) max_width = selected_label.sizeHint().width() # line color self.lbl_color_select = ColorLabel() self.lbl_color_unselect = ColorLabel() self.lbl_color_select.setMaximumWidth(max_width) self.lbl_color_unselect.setMaximumWidth(max_width) style_box.addWidget(QLabel('Line color:'), 1, 0) style_box.addWidget(self.lbl_color_unselect, 1, 1) style_box.addWidget(self.lbl_color_select, 1, 2) # line width self.txt_line_width = QLineEdit() self.txt_line_width_select = QLineEdit() validator = QDoubleValidator() validator.setBottom(0) self.txt_line_width.setValidator(validator) self.txt_line_width_select.setValidator(validator) style_box.addWidget(QLabel('Line width:'), 2, 0) style_box.addWidget(self.txt_line_width, 2, 1) style_box.addWidget(self.txt_line_width_select, 2, 2) vbox.addLayout(style_box) # line style hbox_line_style = QHBoxLayout() hbox_line_style.addWidget(QLabel('Line style')) self.combo_line_style = QComboBox() hbox_line_style.addWidget(self.combo_line_style) vbox.addLayout(hbox_line_style) self.fill_linestyles() self.combo_line_style.setSizeAdjustPolicy(QComboBox.AdjustToContents) # marker style hbox_marker_style = QHBoxLayout() hbox_marker_style.addWidget(QLabel('Marker size')) self.txt_marker_size = QLineEdit() self.txt_marker_size.setMinimumWidth(40) self.txt_marker_size.setText(str(mpl.rcParams['lines.markersize'])) validator = QDoubleValidator() validator.setBottom(0) self.txt_marker_size.setValidator(validator) hbox_marker_style.addWidget(self.txt_marker_size) hbox_marker_style.addWidget(QLabel('Marker style')) self.combo_marker_style = QComboBox() hbox_marker_style.addWidget(self.combo_marker_style) vbox.addLayout(hbox_marker_style) self.setLayout(vbox) self.widgets2disable = [ self.lbl_color_select, self.lbl_color_unselect, self.cb_auto_hide, self.txt_line_width, self.txt_line_width_select, self.combo_line_style, self.cb_show_connected, self.txt_marker_size, self.combo_marker_style, self.cb_drag_vline, self.cb_drag_hline, self.cb_selectable_vline, self.cb_selectable_hline, self.cb_show_hlines, self.cb_show_vlines ] self.fill_markerstyles() self.tb_actions = [] # --------------------------------------------------------------------- # ---------------------------- connections ---------------------------- # --------------------------------------------------------------------- self.lbl_color_select.color_changed.connect(self.change_select_colors) self.lbl_color_unselect.color_changed.connect( self.change_unselect_colors) self.txt_line_width.textChanged.connect(self.change_line_widths) self.txt_line_width_select.textChanged.connect( self.change_selection_line_widths) self.cb_auto_hide.stateChanged.connect(self.change_auto_hide) self.combo_marker_style.currentIndexChanged.connect( self.change_marker_style) self.combo_line_style.currentIndexChanged.connect( self.change_line_style) self.txt_marker_size.textChanged.connect(self.change_marker_size) self.cb_show_connected.stateChanged.connect( self.change_show_connected_artists) self.cb_drag_hline.stateChanged.connect(self.change_hline_draggable) self.cb_drag_vline.stateChanged.connect(self.change_vline_draggable) self.cb_selectable_hline.stateChanged.connect( self.change_hline_selectable) self.cb_selectable_vline.stateChanged.connect( self.change_vline_selectable) self.cb_show_vlines.stateChanged.connect(self.change_show_vlines) self.cb_show_hlines.stateChanged.connect(self.change_show_hlines) def draw_figs(self): """Draw the figures of the :attr:`marks`""" for canvas in {m.fig.canvas for m in self.marks}: canvas.draw_idle() def fill_linestyles(self): """Fill the :attr:`combo_line_style` combobox""" self.line_styles = [('-', 'solid'), ('--', 'dashed'), ('-.', 'dashdot'), (':', 'dotted'), ('None', None, '', ' ')] self.combo_line_style.addItems([t[0] for t in self.line_styles]) def fill_markerstyles(self): """Fill the :attr:`combo_marker_style` combobox""" self.marker_styles = [('point', (".", )), ('pixel', (",", )), ('circle', ("o", )), ('triangle down', ("v", "1")), ('triangle up', ("^", "2")), ('triangle left', ("<", "3")), ('triangle right', (">", "4")), ('octagon', ("8", )), ('square', ("s", )), ('pentagon', ("p", )), ('plus (filled)', ("P", )), ('star', ("*", )), ('hexagon1', ("h", )), ('hexagon2', ("H", )), ('plus', ("+", )), ('x', ("x", )), ('x (filled)', ("X", )), ('diamond', ("D", )), ('thin diamond', ("d", )), ('vline', ("|", )), ('hline', ("_", )), ('no marker', ('None', None, '', ' '))] self.combo_marker_style.addItems([ '%s (%s)' % (key.capitalize(), ','.join(map(str, filter(None, t)))) for key, t in self.marker_styles ]) def set_marker_item(self, marker): """Switch the :attr:`combo_marker_style` to the given `marker` Parameters ---------- marker: str A matplotlib marker string""" for i, (key, t) in enumerate(self.marker_styles): if marker in t: block = self.combo_marker_style.blockSignals(True) self.combo_marker_style.setCurrentIndex(i) self.combo_marker_style.blockSignals(block) break def set_line_style_item(self, ls): """Switch the :attr:`combo_line_style` to the given linestyle Parameters ---------- ls: str The matplotlib linestyle string """ for i, t in enumerate(self.line_styles): if ls in t: block = self.combo_line_style.blockSignals(True) self.combo_line_style.setCurrentIndex(i) self.combo_line_style.blockSignals(block) break def should_be_enabled(self, w): """Check if a widget `w` should be enabled or disabled Parameters ---------- w: PyQt5.QtWidgets.QWidget The widget to (potentially) enable Returns ------- bool True if the :attr:`straditizer` of this instance has marks. Otherwise False""" return self.straditizer is not None and bool(self.straditizer.marks) def change_select_colors(self, color): """Change the selection color of the marks Change the selection color of the marks to the given color. Parameters ---------- color: PyQt5.QtGui.QColor or a matplotlib color The color to use """ if isinstance(color, QtGui.QColor): color = np.array(color.getRgb()) / 255. for mark in self.marks: key = 'color' if 'color' in mark._select_props else 'c' mark._select_props[key] = color mark._unselect_props[key] = mark.hline.get_c() self.draw_figs() def change_unselect_colors(self, color): """Change the :attr:`straditize.cross_mark.CrossMark.cunselect` color Change the unselection color of the marks to the given color. Parameters ---------- color: PyQt5.QtGui.QColor or a matplotlib color The color to use """ if isinstance(color, QtGui.QColor): color = np.array(color.getRgb()) / 255. for mark in self.marks: key = 'color' if 'color' in mark._unselect_props else 'c' mark._unselect_props[key] = color for l in chain(mark.hlines, mark.vlines, mark.line_connections): l.set_color(color) self.draw_figs() def change_line_widths(self, lw): """Change the linewidth of the marks Parameters ---------- lw: float The line width to use """ lw = float(lw or 0) for mark in self.marks: key = 'lw' if 'lw' in mark._unselect_props else 'linewidth' mark._unselect_props[key] = lw if not mark.auto_hide: for l in chain(mark.hlines, mark.vlines, mark.line_connections): l.set_lw(lw) self.draw_figs() def change_selection_line_widths(self, lw): """Change the linewidth for selected marks Parameters ---------- lw: float The linewidth for selected marks""" lw = float(lw or 0) for mark in self.marks: key = 'lw' if 'lw' in mark._select_props else 'linewidth' mark._select_props[key] = lw def change_auto_hide(self, auto_hide): """Toggle the :attr:`~straditize.cross_mark.CrossMark.auto_hide` This method disables or enables the :attr:`~straditize.cross_mark.CrossMark.auto_hide` of the marks Parameters ---------- auto_hide: bool or PyQt5.QtGui.Qt.Checked or PyQt5.QtGui.Qt.Unchecked The value to use for the auto_hide. :data:`PyQt5.QtGui.Qt.Checked` is equivalent to ``True`` """ if auto_hide is Qt.Checked: auto_hide = True elif auto_hide is Qt.Unchecked: auto_hide = False for mark in self.marks: mark.auto_hide = auto_hide if auto_hide: for l in chain(mark.hlines, mark.vlines, mark.line_connections): l.set_lw(0) else: lw = mark._unselect_props.get( 'lw', mark._unselect_props.get('linewidth')) for l in chain(mark.hlines, mark.vlines, mark.line_connections): l.set_lw(lw) self.draw_figs() def change_show_connected_artists(self, show): """Change the visibility of connected artists Parameters ---------- show: bool The visibility for the :meth:`straditize.cross_mark.CrossMarks.set_connected_artists_visible` method""" if show is Qt.Checked: show = True elif show is Qt.Unchecked: show = False for mark in self.marks: mark.set_connected_artists_visible(show) self.draw_figs() def change_line_style(self, i): """Change the line style of the marks Parameters ---------- i: int The index of the line style in the :attr:`line_styles` attribute to use """ ls = self.line_styles[i][0] for mark in self.marks: for l in chain(mark.hlines, mark.vlines, mark.line_connections): l.set_ls(ls) self.draw_figs() def change_marker_style(self, i): """Change the marker style of the marks Parameters ---------- i: int The index of the marker style in the :attr:`marker_styles` attribute to use """ marker = self.marker_styles[i][1][0] for mark in self.marks: for l in chain(mark.hlines, mark.vlines, mark.line_connections): l.set_marker(marker) self.draw_figs() def change_marker_size(self, markersize): """Change the size of the markers Parameters ---------- markersize: float The size of the marker to use""" markersize = float(markersize or 0) for mark in self.marks: for l in chain(mark.hlines, mark.vlines, mark.line_connections): l.set_markersize(markersize) self.draw_figs() def fill_from_mark(self, mark): """Set the widgets of this :class:`MarkerControl` from a mark This method sets the color labels, combo boxes, check boxes and text edits to match the properties of the given `mark` Parameters ---------- mark: straditize.cross_mark.CrossMark The mark to use the attributes from """ line = mark.hline try: cselect = mark._select_props['c'] except KeyError: try: cselect = mark._select_props['color'] except KeyError: cselect = mark.hline.get_c() lw_key = 'lw' if 'lw' in mark._unselect_props else 'linewidth' self.set_line_style_item(line.get_linestyle()) self.set_marker_item(line.get_marker()) self.txt_line_width.setText(str(mark._unselect_props.get(lw_key, 0))) self.txt_line_width_select.setText( str(mark._select_props.get(lw_key, 0))) self.txt_marker_size.setText(str(line.get_markersize())) self.lbl_color_select._set_color(cselect) self.lbl_color_unselect._set_color(mark.hline.get_c()) self.cb_auto_hide.setChecked(mark.auto_hide) self.cb_show_connected.setChecked(mark.show_connected_artists) try: mark.x except NotImplementedError: self.cb_drag_vline.setEnabled(False) self.cb_show_vlines.setEnabled(False) self.cb_selectable_vline.setEnabled(False) else: self.cb_drag_vline.setEnabled(True) self.cb_show_vlines.setEnabled(True) self.cb_selectable_vline.setEnabled(True) try: mark.y except NotImplementedError: self.cb_drag_hline.setEnabled(False) self.cb_show_hlines.setEnabled(False) self.cb_selectable_hline.setEnabled(False) else: self.cb_drag_hline.setEnabled(True) self.cb_show_hlines.setEnabled(True) self.cb_selectable_hline.setEnabled(True) self.cb_drag_hline.setChecked('h' in mark.draggable) self.cb_drag_vline.setChecked('v' in mark.draggable) self.cb_show_hlines.setChecked(not mark.hide_horizontal) self.cb_show_vlines.setChecked(not mark.hide_vertical) self.cb_selectable_hline.setChecked('h' in mark.selectable) self.cb_selectable_vline.setChecked('v' in mark.selectable) @property def line_props(self): """The properties of the lines as a :class:`dict`""" return { 'ls': self.combo_line_style.currentText(), 'marker': self.marker_styles[self.combo_marker_style.currentIndex()][1][0], 'lw': float(self.txt_line_width.text().strip() or 0), 'markersize': float(self.txt_marker_size.text().strip() or 0), 'c': self.lbl_color_unselect.color.getRgbF(), } @property def select_props(self): """The properties of selected marks as a :class:`dict`""" return { 'c': self.lbl_color_select.color.getRgbF(), 'lw': float(self.txt_line_width_select.text().strip() or 0) } def update_mark(self, mark): """Update the properties of a mark to match the settings Parameters ---------- mark: straditize.cross_mark.CrossMarks The mark to update """ if len(self.straditizer.marks) < 2: return # line properties props = self.line_props mark._unselect_props.update(props) mark._select_props.update(self.select_props) # auto_hide auto_hide = self.cb_auto_hide.isChecked() mark.auto_hide = auto_hide if auto_hide: props['lw'] = 0 # show_connected show_connected = self.cb_show_connected.isChecked() mark.set_connected_artists_visible(show_connected) # drag hline drag_hline = self.cb_drag_hline.isChecked() if drag_hline and 'h' not in mark.draggable: mark.draggable.append('h') elif not drag_hline and 'h' in mark.draggable: mark.draggable.remove('h') # drag vline drag_vline = self.cb_drag_vline.isChecked() if drag_vline and 'v' not in mark.draggable: mark.draggable.append('v') elif not drag_vline and 'v' in mark.draggable: mark.draggable.remove('v') # select hline select_hline = self.cb_selectable_hline.isChecked() if select_hline and 'h' not in mark.selectable: mark.selectable.append('h') elif not select_hline and 'h' in mark.selectable: mark.selectable.remove('h') # select hline select_vline = self.cb_selectable_vline.isChecked() if select_vline and 'v' not in mark.selectable: mark.selectable.append('v') elif not select_vline and 'v' in mark.selectable: mark.selectable.remove('v') show_horizontal = self.cb_show_hlines.isChecked() mark.hide_horizontal = ~show_horizontal show_vertical = self.cb_show_vlines.isChecked() mark.hide_vertical = ~show_vertical for l in chain(mark.hlines, mark.vlines): l.update(props) for l in mark.hlines: l.set_visible(show_horizontal) for l in mark.vlines: l.set_visible(show_vertical) def change_hline_draggable(self, state): """Enable or disable the dragging of horizontal lines Parameters ---------- state: Qt.Checked or Qt.Unchecked If Qt.Checked, the horizontal lines can be dragged and dropped""" if state == Qt.Checked: for mark in self.marks: mark.draggable = np.unique(np.r_[['h'], mark.draggable]) else: for mark in self.marks: mark.draggable = np.array(list(set(mark.draggable) - {'h'})) def change_hline_selectable(self, state): """Enable or disable the selection of horizontal lines Parameters ---------- state: Qt.Checked or Qt.Unchecked If Qt.Checked, the horizontal lines can be selected""" if state == Qt.Checked: for mark in self.marks: mark.selectable = np.unique(np.r_[['h'], mark.selectable]) else: for mark in self.marks: mark.selectable = np.array(list(set(mark.selectable) - {'h'})) def change_vline_draggable(self, state): """Enable or disable the dragging of vertical lines Parameters ---------- state: Qt.Checked or Qt.Unchecked If Qt.Checked, the vertical lines can be dragged and dropped""" if state == Qt.Checked: for mark in self.marks: mark.draggable = np.unique(np.r_[['v'], mark.draggable]) else: for mark in self.marks: mark.draggable = np.array(list(set(mark.draggable) - {'v'})) def change_vline_selectable(self, state): """Enable or disable the selection of vertical lines Parameters ---------- state: Qt.Checked or Qt.Unchecked If Qt.Checked, the vertical lines can be selected""" if state == Qt.Checked: for mark in self.marks: mark.selectable = np.unique(np.r_[['v'], mark.selectable]) else: for mark in self.marks: mark.selectable = np.array(list(set(mark.selectable) - {'v'})) def change_show_hlines(self, state): """Enable of disable the visibility of horizontal lines Parameters ---------- state: Qt.Checked or Qt.Unchecked If Qt.Checked, all horizontal lines are hidden""" if state == Qt.Checked: for mark in self.marks: mark.hide_horizontal = False mark.set_visible(True) else: for mark in self.marks: mark.hide_horizontal = True mark.set_visible(True) self.draw_figs() def change_show_vlines(self, state): """Enable of disable the visibility of vertical lines Parameters ---------- state: Qt.Checked or Qt.Unchecked If Qt.Checked, all vertical lines are hidden""" if state == Qt.Checked: for mark in self.marks: mark.hide_vertical = False mark.set_visible(True) else: for mark in self.marks: mark.hide_vertical = True mark.set_visible(True) self.draw_figs() def enable_or_disable_widgets(self, b): """Renabled to use the :meth:`refresh` method """ self.refresh() def fill_after_adding(self, mark): if not self._filled: self.refresh() def go_to_right_mark(self): """Move the plot to the next right cross mark""" ax = self.straditizer.marks[0].ax if ax.xaxis_inverted(): return self.go_to_smaller_x_mark(min(ax.get_xlim())) else: return self.go_to_greater_x_mark(max(ax.get_xlim())) def go_to_left_mark(self): """Move the plot to the previous left cross mark""" ax = self.straditizer.marks[0].ax if ax.xaxis_inverted(): return self.go_to_greater_x_mark(max(ax.get_xlim())) else: return self.go_to_smaller_x_mark(min(ax.get_xlim())) def go_to_greater_x_mark(self, x): """Move the plot to the next mark with a x-position greater than `x` Parameters ---------- x: float The reference x-position that shall be smaller than the new centered mark""" def is_visible(mark): return (np.searchsorted(np.sort(xlim), mark.xa) == 1).any() and (np.searchsorted( np.sort(ylim), mark.ya) == 1).any() ax = next(self.marks).ax xlim = np.asarray(ax.get_xlim()) ylim = np.asarray(ax.get_ylim()) marks = self.straditizer.marks if len(marks[0].xa) > 1: # get the mark in the center yc = ylim.mean() try: mark = min(filter(is_visible, marks), key=lambda m: np.abs(m.ya - yc).min()) except ValueError: # empty sequence mark = min(marks, key=lambda m: np.abs(m.ya - yc).min()) # if all edges are visible already, we return if (np.searchsorted(xlim, mark.xa) == 1).all(): return mask = mark.xa > x if not mask.any(): # already on the right side return i = mark.xa[mask].argmin() x = mark.xa[mask][i] dx = np.diff(xlim) / 2. ax.set_xlim(x - dx, x + dx) ax.set_ylim(*ax.get_ylim()) else: distances = ((mark.xa > x).any() and (mark.xa[mark.xa > x] - x).min() for mark in marks) try: dist, mark = min((t for t in zip(distances, marks) if t[0]), key=lambda t: t[0]) except ValueError: # empty sequence return mask = mark.xa > x i = mark.xa[mask].argmin() x = mark.xa[mask][i] j = np.abs(mark.ya - ylim.mean()).argmin() y = mark.ya[j] dx = np.diff(xlim) / 2. ax.set_xlim(x - dx, x + dx) dy = np.diff(ylim) / 2. ax.set_ylim(y - dy, y + dy) self.straditizer.draw_figure() def go_to_smaller_x_mark(self, x): """Move the plot to the next mark with a x-position smaller than `x` Parameters ---------- x: float The reference x-position that shall be greater than the new centered mark""" def is_visible(mark): return (np.searchsorted(np.sort(xlim), mark.xa) == 1).any() and (np.searchsorted( np.sort(ylim), mark.ya) == 1).any() ax = next(self.marks).ax xlim = np.asarray(ax.get_xlim()) ylim = np.asarray(ax.get_ylim()) marks = self.straditizer.marks if len(marks[0].xa) > 1: # get the mark in the center yc = ylim.mean() try: mark = min(filter(is_visible, marks), key=lambda m: np.abs(m.ya - yc).min()) except ValueError: # empty sequence mark = min(marks, key=lambda m: np.abs(m.ya - yc).min()) # if all edges are visible already, we return if (np.searchsorted(xlim, mark.xa) == 1).all(): return mask = mark.xa < x if not mask.any(): # already on the right side return i = mark.xa[mask].argmin() x = mark.xa[mask][i] dx = np.diff(xlim) / 2. ax.set_xlim(x - dx, x + dx) ax.set_ylim(*ax.get_ylim()) else: distances = ((mark.xa < x).any() and (mark.xa[mark.xa < x] - x).min() for mark in marks) try: dist, mark = min((t for t in zip(distances, marks) if t[0]), key=lambda t: t[0]) except ValueError: # empty sequence return mask = mark.xa < x i = mark.xa[mask].argmin() x = mark.xa[mask][i] j = np.abs(mark.ya - ylim.mean()).argmin() y = mark.ya[j] dx = np.diff(xlim) / 2. ax.set_xlim(x - dx, x + dx) dy = np.diff(ylim) / 2. ax.set_ylim(y - dy, y + dy) self.straditizer.draw_figure() def go_to_upper_mark(self): """Go to the next mark above the current y-limits""" ax = self.straditizer.marks[0].ax if ax.xaxis_inverted(): return self.go_to_greater_y_mark(max(ax.get_ylim())) else: return self.go_to_smaller_y_mark(min(ax.get_ylim())) def go_to_lower_mark(self): """Go to the next mark below the current y-limits""" ax = self.straditizer.marks[0].ax if ax.xaxis_inverted(): return self.go_to_smaller_y_mark(min(ax.get_ylim())) else: return self.go_to_greater_y_mark(max(ax.get_ylim())) def go_to_greater_y_mark(self, y): """Move the plot to the next mark with a y-position greater than `y` Parameters ---------- y: float The reference y-position that shall be smaller than the new centered mark""" def is_visible(mark): return (np.searchsorted(np.sort(xlim), mark.xa) == 1).any() and (np.searchsorted( np.sort(ylim), mark.ya) == 1).any() ax = next(self.marks).ax xlim = np.asarray(ax.get_xlim()) ylim = np.asarray(ax.get_ylim()) marks = self.straditizer.marks if len(marks[0].ya) > 1: # get the mark in the center xc = xlim.mean() try: mark = min(filter(is_visible, marks), key=lambda m: np.abs(m.xa - xc).min()) except ValueError: # empty sequence mark = min(marks, key=lambda m: np.abs(m.xa - xc).min()) # if all edges are visible already, we return if (np.searchsorted(ylim, mark.ya) == 1).all(): return mask = mark.ya > y if not mask.any(): # already on the right side return i = mark.ya[mask].argmin() y = mark.ya[mask][i] dy = np.diff(ylim) / 2. ax.set_ylim(y - dy, y + dy) else: distances = ((mark.ya > y).any() and (mark.xa[mark.ya > y] - y).min() for mark in marks) try: dist, mark = min((t for t in zip(distances, marks) if t[0]), key=lambda t: t[0]) except ValueError: # empty sequence return mask = mark.ya > y i = mark.ya[mask].argmin() y = mark.ya[mask][i] j = np.abs(mark.xa - xlim.mean()).argmin() x = mark.xa[j] dx = np.diff(xlim) / 2. ax.set_xlim(x - dx, x + dx) dy = np.diff(ylim) / 2. ax.set_ylim(y - dy, y + dy) self.straditizer.draw_figure() def go_to_smaller_y_mark(self, y): """Move the plot to the next mark with a y-position smaller than `x` Parameters ---------- y: float The reference y-position that shall be smaller than the new centered mark""" def is_visible(mark): return (np.searchsorted(np.sort(xlim), mark.xa) == 1).any() and (np.searchsorted( np.sort(ylim), mark.ya) == 1).any() ax = next(self.marks).ax xlim = np.asarray(ax.get_xlim()) ylim = np.asarray(ax.get_ylim()) marks = self.straditizer.marks if len(marks[0].ya) > 1: # get the mark in the center xc = xlim.mean() try: mark = min(filter(is_visible, marks), key=lambda m: np.abs(m.xa - xc).min()) except ValueError: # empty sequence mark = min(marks, key=lambda m: np.abs(m.xa - xc).min()) # if all edges are visible already, we return if (np.searchsorted(ylim, mark.ya) == 1).all(): return mask = mark.ya < y if not mask.any(): # already on the right side return i = mark.ya[mask].argmin() y = mark.ya[mask][i] dy = np.diff(ylim) / 2. ax.set_ylim(y - dy, y + dy) else: distances = ((mark.ya < y).any() and (mark.ya[mark.ya < y] - y).min() for mark in marks) try: dist, mark = min((t for t in zip(distances, marks) if t[0]), key=lambda t: t[0]) except ValueError: # empty sequence return mask = mark.ya < y i = mark.ya[mask].argmin() y = mark.ya[mask][i] j = np.abs(mark.xa - xlim.mean()).argmin() x = mark.xa[j] dx = np.diff(xlim) / 2. ax.set_xlim(x - dx, x + dx) dy = np.diff(ylim) / 2. ax.set_ylim(y - dy, y + dy) self.straditizer.draw_figure() def add_toolbar_widgets(self, mark): """Add the navigation actions to the toolbar""" tb = self.straditizer.marks[0].ax.figure.canvas.toolbar if not isinstance(tb, QToolBar): return if self.tb_actions: self.remove_actions() self.tb_actions.append(tb.addSeparator()) try: mark.x except NotImplementedError: add_right = False else: a = tb.addAction(QIcon(get_icon('left_mark.png')), 'left mark', self.go_to_left_mark) a.setToolTip('Move to the next cross mark on the left') self.tb_actions.append(a) add_right = True try: mark.y except NotImplementedError: pass else: a = tb.addAction(QIcon(get_icon('upper_mark.png')), 'upper mark', self.go_to_upper_mark) a.setToolTip('Move to the next cross mark above') self.tb_actions.append(a) a = tb.addAction(QIcon(get_icon('lower_mark.png')), 'lower mark', self.go_to_lower_mark) a.setToolTip('Move to the next cross mark below') self.tb_actions.append(a) if add_right: a = tb.addAction(QIcon(get_icon('right_mark.png')), 'right mark', self.go_to_right_mark) a.setToolTip('Move to the next cross mark on the right') self.tb_actions.append(a) self._toolbar = tb def remove_actions(self): """Remove the navigation actions from the toolbar""" if self._toolbar is None: return tb = self._toolbar for a in self.tb_actions: tb.removeAction(a) self.tb_actions.clear() def refresh(self): """Reimplemented to also set the properties of this widget """ super(MarkerControl, self).refresh() if self.straditizer is not None and self.straditizer.marks: self._filled = True mark = self.straditizer.marks[0] self.fill_from_mark(mark) self.add_toolbar_widgets(mark) else: self.remove_actions() self._filled = False if self.straditizer is not None: self.straditizer.mark_added.connect(self.fill_after_adding) self.straditizer.mark_added.connect(self.update_mark)
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 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() 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 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) 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() 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() 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() 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() 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() 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() 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._pattern_selection.close() self.uncheck_pattern_selection() del self._pattern_selection 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)) 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()) 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() 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() 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() 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() 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() 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() 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'))) 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'))) 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 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) 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() 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 def end_selection(self): """Finish the selection and disconnect everything""" if getattr(self, '_pattern_selection', None) is not None: self._pattern_selection.close() 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) 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) 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) 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() 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) 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() 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