diff --git a/hexrd/ui/constants.py b/hexrd/ui/constants.py index a662a2fa9..d2143b413 100644 --- a/hexrd/ui/constants.py +++ b/hexrd/ui/constants.py @@ -107,3 +107,7 @@ class LLNLTransform: 'IMAGE-PLATE-4', ], } + +KEY_ROTATE_ANGLE_FINE = 0.00175 +KEY_ROTATE_ANGLE_COARSE = 0.01 +KEY_TRANSLATE_DELTA = 0.5 diff --git a/hexrd/ui/hexrd_config.py b/hexrd/ui/hexrd_config.py index aaae1ba00..40e50fe3b 100644 --- a/hexrd/ui/hexrd_config.py +++ b/hexrd/ui/hexrd_config.py @@ -874,7 +874,8 @@ def raw_masks_dict(self): for det, mask in data: if det == name: final_mask = np.logical_and(final_mask, mask) - if self.threshold_mask_status: + if (self.threshold_mask_status and + self.threshold_masks.get(name) is not None): idx = self.current_imageseries_idx thresh_mask = self.threshold_masks[name][idx] final_mask = np.logical_and(final_mask, thresh_mask) diff --git a/hexrd/ui/image_canvas.py b/hexrd/ui/image_canvas.py index ec7f62dc6..406921107 100644 --- a/hexrd/ui/image_canvas.py +++ b/hexrd/ui/image_canvas.py @@ -1,7 +1,7 @@ import copy import math -from PySide2.QtCore import QThreadPool, QTimer, Signal +from PySide2.QtCore import QThreadPool, QTimer, Signal, Qt from PySide2.QtWidgets import QFileDialog, QMessageBox from matplotlib.backends.backend_qt5agg import FigureCanvas @@ -71,6 +71,8 @@ def __init__(self, parent=None, image_names=None): if image_names is not None: self.load_images(image_names) + self.setFocusPolicy(Qt.ClickFocus) + self.setup_connections() def setup_connections(self): diff --git a/hexrd/ui/interactive_template.py b/hexrd/ui/interactive_template.py index cbe8bbdd6..9b49d9665 100644 --- a/hexrd/ui/interactive_template.py +++ b/hexrd/ui/interactive_template.py @@ -1,24 +1,21 @@ import numpy as np -from PySide2.QtCore import Qt - from matplotlib import patches from matplotlib.path import Path from matplotlib.transforms import Affine2D from skimage.draw import polygon -from hexrd.ui.create_hedm_instrument import create_hedm_instrument -from hexrd.ui import resource_loader +from hexrd.ui.constants import ( + KEY_ROTATE_ANGLE_FINE, KEY_TRANSLATE_DELTA, ViewType +) from hexrd.ui.hexrd_config import HexrdConfig from hexrd.ui.utils import has_nan class InteractiveTemplate: - def __init__(self, parent=None): - self.parent = parent.image_tab_widget.image_canvases[0] - self.ax = self.parent.axes_images[0] - self.panels = create_hedm_instrument().detectors + def __init__(self, canvas, detector, axes=None, instrument=None): + self.current_canvas = canvas self.img = None self.shape = None self.press = None @@ -27,11 +24,54 @@ def __init__(self, parent=None): self.translation = [0, 0] self.complete = False self.event_key = None - self.parent.setFocusPolicy(Qt.ClickFocus) + self.detector = detector + self.instrument = instrument + self._static = True + self.axis_image = ( + axes.get_images()[0] if axes else canvas.axes_images[0]) + self._key_angle = KEY_ROTATE_ANGLE_FINE + + self.button_press_cid = None + self.button_release_cid = None + self.motion_cid = None + self.key_press_cid = None + self.button_drag_cid = None + + @property + def axis(self): + if not self.current_canvas.raw_axes: + return self.current_canvas.axis + + for axes in self.current_canvas.raw_axes.values(): + if axes.get_title() == self.detector: + return axes + + return list(self.current_canvas.raw_axes.values())[0] + + @property + def static_mode(self): + return self._static + + @static_mode.setter + def static_mode(self, mode): + if mode == self._static: + return + + self._static = mode + self.update_style(color='black') + if not mode: + self.connect_translate_rotate() + self.update_style(color='red') @property - def raw_axes(self): - return list(self.parent.raw_axes.values())[0] + def key_rotation_angle(self): + return self._key_angle + + @key_rotation_angle.setter + def key_rotation_angle(self, angle=None): + if angle is None: + angle = KEY_ROTATE_ANGLE + self._key_angle = angle def update_image(self, img): self.img = img @@ -41,32 +81,42 @@ def rotate_shape(self, angle): self.rotate_template(self.shape.xy, angle) self.redraw() - def create_shape(self, module, file_name, det, instr): + def create_polygon(self, verts, **polygon_kwargs): self.complete = False - with resource_loader.resource_path(module, file_name) as f: - data = np.loadtxt(f) - verts = self.panels['default'].cartToPixel(data) - verts[:, [0, 1]] = verts[:, [1, 0]] - self.shape = patches.Polygon(verts, fill=False, lw=1, color='cyan') + self.shape = patches.Polygon(verts, **polygon_kwargs) if has_nan(verts): # This template contains more than one polygon and the last point # should not be connected to the first. See Tardis IP for example. self.shape.set_closed(False) - self.shape_styles.append({'line': '-', 'width': 1, 'color': 'cyan'}) - self.update_position(instr, det) + self.shape_styles.append(polygon_kwargs) + self.update_position() self.connect_translate_rotate() - self.raw_axes.add_patch(self.shape) + self.axis.add_patch(self.shape) self.redraw() - def update_style(self, style, width, color): - self.shape_styles[-1] = {'line': style, 'width': width, 'color': color} - self.shape.set_linestyle(style) - self.shape.set_linewidth(width) - self.shape.set_edgecolor(color) + def update_style(self, style=None, width=None, color=None): + if not self.shape: + return + + if style: + self.shape.set_linestyle(style) + if width: + self.shape.set_linewidth(width) + if color: + self.shape.set_edgecolor(color) + self.shape_styles[-1] = { + 'line': self.shape.get_linestyle(), + 'width': self.shape.get_linewidth(), + 'color': self.shape.get_edgecolor() + } + self.shape.set_fill(False) self.redraw() - def update_position(self, instr, det): - pos = HexrdConfig().boundary_position(instr, det) + def update_position(self): + pos = None + if self.instrument is not None: + pos = HexrdConfig().boundary_position( + self.instrument, self.detector) if pos is None: self.center = self.get_midpoint() else: @@ -75,7 +125,7 @@ def update_position(self, instr, det): self.translate_template(dx, dy) self.total_rotation = pos['angle'] self.rotate_template(self.shape.xy, pos['angle']) - if instr == 'PXRDIP': + if self.instrument == 'PXRDIP': self.rotate_shape(angle=90) @property @@ -89,7 +139,7 @@ def masked_image(self): @property def bounds(self): - l, r, b, t = self.ax.get_extent() + l, r, b, t = self.axis_image.get_extent() x0, y0 = np.nanmin(self.shape.xy, axis=0) x1, y1 = np.nanmax(self.shape.xy, axis=0) return np.array([max(np.floor(y0), t), @@ -110,13 +160,13 @@ def rotation(self): return self.total_rotation def clear(self): - if self.shape in self.raw_axes.patches: + if self.shape in self.axis.patches: self.shape.remove() self.redraw() self.total_rotation = 0. def save_boundary(self, color): - if self.shape in self.raw_axes.patches: + if self.shape in self.axis.patches: self.shape.set_linestyle('--') self.redraw() @@ -134,26 +184,26 @@ def toggle_boundaries(self, show): # This template contains more than one polygon and the last point # should not be connected to the first. See Tardis IP for example. shape.set_closed(False) - self.raw_axes.add_patch(shape) + self.axis.add_patch(shape) if self.shape: - self.shape = self.raw_axes.patches[-1] + self.shape = self.axis.patches[-1] self.shape.remove() self.shape.set_linestyle(self.shape_styles[-1]['line']) - self.raw_axes.add_patch(self.shape) + self.axis.add_patch(self.shape) self.connect_translate_rotate() self.redraw() else: if self.shape: self.disconnect() - self.patches = [p for p in self.raw_axes.patches] + self.patches = [p for p in self.axis.patches] self.redraw() def disconnect(self): - self.parent.mpl_disconnect(self.button_press_cid) - self.parent.mpl_disconnect(self.button_release_cid) - self.parent.mpl_disconnect(self.motion_cid) - self.parent.mpl_disconnect(self.key_press_cid) - self.parent.mpl_disconnect(self.button_drag_cid) + self.current_canvas.mpl_disconnect(self.button_press_cid) + self.current_canvas.mpl_disconnect(self.button_release_cid) + self.current_canvas.mpl_disconnect(self.motion_cid) + self.current_canvas.mpl_disconnect(self.key_press_cid) + self.current_canvas.mpl_disconnect(self.button_drag_cid) def completed(self): self.disconnect() @@ -199,7 +249,7 @@ def get_paths(self): return all_paths def redraw(self): - self.parent.draw_idle() + self.current_canvas.draw_idle() def scale_template(self, sx=1, sy=1): xy = self.shape.xy @@ -214,6 +264,9 @@ def scale_template(self, sx=1, sy=1): self.redraw() def on_press(self, event): + if self.static_mode: + return + self.event_key = event.key if event.key is None: self.on_press_translate(event) @@ -227,23 +280,31 @@ def on_release(self, event): self.on_rotate_release(event) def on_key(self, event): + if self.static_mode: + return + if 'shift' in event.key: self.on_key_rotate(event) else: self.on_key_translate(event) def connect_translate_rotate(self): - self.button_press_cid = self.parent.mpl_connect( + if self.static_mode: + return + + self.disconnect() + + self.button_press_cid = self.current_canvas.mpl_connect( 'button_press_event', self.on_press) - self.button_release_cid = self.parent.mpl_connect( + self.button_release_cid = self.current_canvas.mpl_connect( 'button_release_event', self.on_release) - self.motion_cid = self.parent.mpl_connect( + self.motion_cid = self.current_canvas.mpl_connect( 'motion_notify_event', self.on_translate) - self.key_press_cid = self.parent.mpl_connect( + self.key_press_cid = self.current_canvas.mpl_connect( 'key_press_event', self.on_key) - self.button_drag_cid = self.parent.mpl_connect( + self.button_drag_cid = self.current_canvas.mpl_connect( 'motion_notify_event', self.on_rotate) - self.parent.setFocus() + self.current_canvas.setFocus() def translate_template(self, dx, dy): self.shape.set_xy(self.shape.xy + np.array([dx, dy])) @@ -253,7 +314,7 @@ def translate_template(self, dx, dy): def on_key_translate(self, event): dx0, dy0 = self.translation dx1, dy1 = 0, 0 - delta = 0.5 + delta = KEY_TRANSLATE_DELTA if event.key == 'right': dx1 = delta elif event.key == 'left': @@ -315,13 +376,33 @@ def on_press_rotate(self, event): # need to set the press value twice self.press = self.shape.xy, event.xdata, event.ydata self.center = self.get_midpoint() - self.shape.set_transform(self.ax.axes.transData) + self.shape.set_transform(self.axis_image.axes.transData) self.press = self.shape.xy, event.xdata, event.ydata def rotate_template(self, points, angle): + center = self.center + canvas = self.current_canvas + if canvas.mode == ViewType.polar: + # We need to correct for the extent ratio and the aspect ratio + # Make a copy to modify (we should *not* modify the original) + points = np.array(points) + extent = canvas.iviewer.pv.extent + + canvas_aspect = compute_aspect_ratio(canvas.axis) + extent_aspect = (extent[2] - extent[3]) / (extent[1] - extent[0]) + + aspect_ratio = extent_aspect * canvas_aspect + points[:, 0] *= aspect_ratio + center = (center[0] * aspect_ratio, center[1]) + x = [np.cos(angle), np.sin(angle)] y = [-np.sin(angle), np.cos(angle)] - verts = np.dot(points - self.center, np.array([x, y])) + self.center + verts = np.dot(points - center, np.array([x, y])) + center + + if canvas.mode == ViewType.polar: + # Reverse the aspect ratio correction + verts[:, 0] /= aspect_ratio + self.shape.set_xy(verts) def on_rotate(self, event): @@ -337,7 +418,7 @@ def on_rotate(self, event): self.redraw() def on_key_rotate(self, event): - angle = 0.00175 + angle = self.key_rotation_angle # !!! only catch arrow keys if event.key == 'shift+left' or event.key == 'shift+up': angle *= -1. @@ -353,7 +434,7 @@ def get_midpoint(self): return [(x1 + x0)/2, (y1 + y0)/2] def mouse_position(self, e): - xmin, xmax, ymin, ymax = self.ax.get_extent() + xmin, xmax, ymin, ymax = self.axis_image.get_extent() x, y = self.get_midpoint() xdata = e.xdata ydata = e.ydata @@ -388,3 +469,10 @@ def on_rotate_release(self, event): self.press = None self.rotate_template(xy, angle) self.redraw() + + +def compute_aspect_ratio(axis): + # Compute the aspect ratio of a matplotlib axis + ll, ur = axis.get_position() * axis.figure.get_size_inches() + width, height = ur - ll + return width / height diff --git a/hexrd/ui/llnl_import_tool_dialog.py b/hexrd/ui/llnl_import_tool_dialog.py index 3787dfe51..a04abe92b 100644 --- a/hexrd/ui/llnl_import_tool_dialog.py +++ b/hexrd/ui/llnl_import_tool_dialog.py @@ -1,4 +1,5 @@ import os +import numpy as np import yaml import tempfile import h5py @@ -18,6 +19,7 @@ from hexrd.ui.image_load_manager import ImageLoadManager from hexrd.ui.interactive_template import InteractiveTemplate from hexrd.ui import resource_loader +from hexrd.ui.create_hedm_instrument import create_hedm_instrument from hexrd.ui.ui_loader import UiLoader from hexrd.ui.constants import ( UI_TRANS_INDEX_ROTATE_90, YAML_EXTS, LLNLTransform, ViewType) @@ -56,6 +58,7 @@ def __init__(self, cmap=None, parent=None): self.defaults = {} self.import_in_progress = False self.loaded_images = [] + self.canvas = parent.image_tab_widget.active_canvas self.set_default_color() self.setup_connections() @@ -273,7 +276,10 @@ def load_images(self): # the QProgressDialog. ImageLoadManager().read_data(files, ui_parent=self.ui.parent()) self.cmap.block_updates(False) - self.it = InteractiveTemplate(self.parent()) + self.it = InteractiveTemplate( + self.canvas, self.detector, instrument=self.instrument) + # We should be able to immediately interact with the template + self.it.static_mode = False file_names = [os.path.split(f[0])[1] for f in files] self.ui.files_label.setText(', '.join(file_names)) @@ -319,6 +325,14 @@ def display_bounds(self): self.ui.bb_height.blockSignals(False) self.ui.bb_width.blockSignals(False) + def read_in_template_bounds(self, module, file_name): + with resource_loader.resource_path(module, file_name) as f: + data = np.loadtxt(f) + panels = create_hedm_instrument().detectors + verts = panels['default'].cartToPixel(data) + verts[:, [0, 1]] = verts[:, [1, 0]] + return verts + def add_template(self): if self.it is None or self.instrument is None or not self.detector: return @@ -330,11 +344,12 @@ def add_template(self): return self.it.clear() - self.it.create_shape( + verts = self.read_in_template_bounds( module=hexrd_resources, - file_name=f'{self.instrument}_{self.detector}_bnd.txt', - det=self.detector, - instr=self.instrument) + file_name=f'{self.instrument}_{self.detector}_bnd.txt' + ) + kwargs = {'fill': False, 'lw': 1, 'linestyle': '-'} + self.it.create_polygon(verts, **kwargs) self.it.update_image(HexrdConfig().image('default', 0)) self.update_template_style() @@ -380,14 +395,19 @@ def save_boundary_position(self): def swap_bounds_for_cropped(self): self.it.clear() line, width, color = self.it.shape_styles[-1].values() - self.it.create_shape( + verts = self.read_in_template_bounds( module=hexrd_resources, - file_name=f'TARDIS_IMAGE-PLATE-3_bnd_cropped.txt', - det=self.detector, - instr=self.instrument) + file_name=f'TARDIS_IMAGE-PLATE-3_bnd_cropped.txt' + ) + kwargs = { + 'fill': False, + 'lw': width, + 'color': color, + 'linestyle': '--' + } + self.it.create_polygon(verts, **kwargs) self.update_bbox_width(1330) self.update_bbox_height(238) - self.it.update_style('--', width, color) def crop_and_mask(self): self.save_boundary_position() diff --git a/hexrd/ui/main_window.py b/hexrd/ui/main_window.py index 5ae64979b..9ef69c1d9 100644 --- a/hexrd/ui/main_window.py +++ b/hexrd/ui/main_window.py @@ -303,6 +303,8 @@ def setup_connections(self): self.on_enable_canvas_toolbar) HexrdConfig().tab_images_changed.connect( self.update_drawn_mask_line_picker_canvas) + HexrdConfig().tab_images_changed.connect( + self.update_mask_region_canvas) ImageLoadManager().update_needed.connect(self.update_all) ImageLoadManager().new_images_loaded.connect(self.new_images_loaded) @@ -726,6 +728,7 @@ def on_action_edit_euler_angle_convention(self): def active_canvas_changed(self): self.update_drawn_mask_line_picker_canvas() + self.update_mask_region_canvas() def update_drawn_mask_line_picker_canvas(self): if hasattr(self, '_apply_drawn_mask_line_picker'): @@ -830,10 +833,17 @@ def action_edit_apply_powder_mask_to_polar(self): self.new_mask_added.emit(self.image_mode) HexrdConfig().polar_masks_changed.emit() + def update_mask_region_canvas(self): + if hasattr(self, '_masks_regions_dialog'): + self._masks_regions_dialog.canvas_changed( + self.ui.image_tab_widget.active_canvas + ) + def on_action_edit_apply_region_mask_triggered(self): - mrd = MaskRegionsDialog(self.ui) - mrd.new_mask_added.connect(self.new_mask_added.emit) - mrd.show() + self._masks_regions_dialog = MaskRegionsDialog(self.ui) + self._masks_regions_dialog.new_mask_added.connect( + self.new_mask_added.emit) + self._masks_regions_dialog.show() self.ui.image_tab_widget.toggle_off_toolbar() @@ -921,10 +931,11 @@ def on_show_raw_zoom_dialog(self): dialog.zoom_height = int(img.shape[0] / 5) def change_image_mode(self, mode): - # The line picker canvas change needs to be triggered *before* the image + # The masking canvas change needs to be triggered *before* the image # mode is changed. This makes sure that in-progress masks are completed # and associated with the correct image mode. self.update_drawn_mask_line_picker_canvas() + self.update_mask_region_canvas() self.image_mode = mode self.update_image_mode_enable_states() diff --git a/hexrd/ui/mask_regions_dialog.py b/hexrd/ui/mask_regions_dialog.py index f0e8880b6..04ee239af 100644 --- a/hexrd/ui/mask_regions_dialog.py +++ b/hexrd/ui/mask_regions_dialog.py @@ -2,9 +2,10 @@ from hexrd.ui.create_raw_mask import convert_polar_to_raw, create_raw_mask from hexrd.ui.create_polar_mask import create_polar_mask_from_raw +from hexrd.ui.interactive_template import InteractiveTemplate from hexrd.ui.utils import unique_name from hexrd.ui.hexrd_config import HexrdConfig -from hexrd.ui.constants import ViewType +from hexrd.ui.constants import KEY_ROTATE_ANGLE_COARSE, ViewType from hexrd.ui.ui_loader import UiLoader from hexrd.ui.utils import add_sample_points @@ -20,14 +21,13 @@ def __init__(self, parent=None): super().__init__(parent) self.parent = parent - self.images = [] self.canvas_ids = [] self.axes = None self.bg_cache = None self.press = [] - self.added_patches = [] - self.patches = {} - self.canvas = None + self.added_templates = [] + self.interactive_templates = {} + self.canvas = parent.image_tab_widget.active_canvas self.image_mode = None self.raw_mask_coords = [] self.drawing_axes = None @@ -45,25 +45,23 @@ def show(self): self.ui.show() def disconnect(self): - for ids, img in zip(self.canvas_ids, self.images): - [img.mpl_disconnect(id) for id in ids] + for id in self.canvas_ids: + self.canvas.mpl_disconnect(id) self.canvas_ids.clear() - self.images.clear() def setup_canvas_connections(self): - for canvas in self.parent.image_tab_widget.active_canvases: - press = canvas.mpl_connect( - 'button_press_event', self.button_pressed) - drag = canvas.mpl_connect( - 'motion_notify_event', self.drag_motion) - release = canvas.mpl_connect( - 'button_release_event', self.button_released) - enter = canvas.mpl_connect( - 'axes_enter_event', self.axes_entered) - exit = canvas.mpl_connect( - 'axes_leave_event', self.axes_exited) - self.canvas_ids.append([press, drag, release, enter, exit]) - self.images.append(canvas) + self.disconnect() + + self.canvas_ids.append(self.canvas.mpl_connect( + 'button_press_event', self.button_pressed)) + self.canvas_ids.append(self.canvas.mpl_connect( + 'motion_notify_event', self.drag_motion)) + self.canvas_ids.append(self.canvas.mpl_connect( + 'button_release_event', self.button_released)) + self.canvas_ids.append(self.canvas.mpl_connect( + 'axes_enter_event', self.axes_entered)) + self.canvas_ids.append(self.canvas.mpl_connect( + 'axes_leave_event', self.axes_exited)) def setup_ui_connections(self): self.ui.button_box.accepted.connect(self.apply_masks) @@ -71,81 +69,58 @@ def setup_ui_connections(self): self.ui.rejected.connect(self.cancel) self.ui.shape.currentIndexChanged.connect(self.select_shape) self.ui.undo.clicked.connect(self.undo_selection) - HexrdConfig().tab_images_changed.connect(self.tabbed_view_changed) def update_undo_enable_state(self): - enabled = bool(self.added_patches) + enabled = bool(len(self.added_templates)) self.ui.undo.setEnabled(enabled) def select_shape(self): self.selection = self.ui.shape.currentText() - self.patch = None + self.interactive_template = None - def create_patch(self): + def create_interactive_template(self): kwargs = { 'fill': False, 'animated': True, } - if self.selection == 'Rectangle': - self.patch = patches.Rectangle((0, 0), 0, 0, **kwargs) - elif self.selection == 'Ellipse': - self.patch = patches.Ellipse((0, 0), 0, 0, **kwargs) - self.axes.add_patch(self.patch) - self.patches.setdefault(self.det, []).append(self.patch) - self.added_patches.append(self.det) - - def update_patch(self, event): + self.interactive_template = InteractiveTemplate( + self.canvas, self.det, axes=self.axes) + self.interactive_template.create_polygon([[0, 0]], **kwargs) + self.interactive_template.update_style(color='red') + self.interactive_template.key_rotation_angle = KEY_ROTATE_ANGLE_COARSE + self.added_templates.append(self.det) + + def update_interactive_template(self, event): x0, y0 = self.press height = event.ydata - y0 width = event.xdata - x0 if self.selection == 'Rectangle': - self.patch.set_xy(self.press) - self.patch.set_height(height) - self.patch.set_width(width) + shape = patches.Rectangle(self.press, width, height) if self.selection == 'Ellipse': center = [(width / 2 + x0), (height / 2 + y0)] - self.patch.set_center(center) - self.patch.height = height - self.patch.width = width - - def tabbed_view_changed(self): - self.disconnect() - if self.ui.isVisible(): - self.setup_canvas_connections() - for canvas in self.parent.image_tab_widget.active_canvases: - for axes in canvas.raw_axes.values(): - for p in self.patches.get(axes.get_title(), []): - # Artists cannot be reused or simply copied, instead - # a new artist must be created - obj, *attrs = p.__str__().split('(') - patch = getattr(patches, obj)((0, 0), 0, 0, fill=False) - for attr in ['xy', 'center', 'width', 'height']: - try: - getattr(patch, 'set_' + attr)( - getattr(p, 'get_' + attr)()) - except Exception: - try: - setattr(patch, attr, getattr(p, attr)) - except Exception: - continue - axes.add_patch(patch) - self.patches[axes.get_title()] = axes.patches - - def discard_patch(self): - det = self.added_patches.pop() - self.raw_mask_coords.pop() - self.patches[det].pop().remove() + shape = patches.Ellipse(center, width, height) + verts = shape.get_verts() + verts = add_sample_points(verts, 300) + self.interactive_template.template.set_xy(verts) + self.interactive_template.center = ( + self.interactive_template.get_midpoint()) + + def discard_interactive_template(self): + det = self.added_templates.pop() + it = self.interactive_templates[det].pop() + it.disconnect() + it.template.remove() def undo_selection(self): - if not self.added_patches: + if not self.added_templates: return - self.discard_patch() + self.discard_interactive_template() self.canvas.draw_idle() self.update_undo_enable_state() + self.interactive_template = None def axes_entered(self, event): - self.canvas = event.canvas self.image_mode = self.canvas.mode if event.inaxes is self.canvas.azimuthal_integral_axis: @@ -202,6 +177,25 @@ def snap_rectangle_to_edges(self, event): # Trigger another drag motion event where we move the borders self.drag_motion(event) + def check_pick(self, event): + pick_found = False + for templates in self.interactive_templates.values(): + for it in templates: + it.static_mode = True + transformed_click = it.template.get_transform().transform( + (event.xdata, event.ydata)) + if (not pick_found and + it.template.contains_point(transformed_click) and + (self.image_mode == ViewType.polar or + event.inaxes.get_title() == it.detector)): + if self.interactive_template: + self.interactive_template.disconnect() + self.interactive_template = it + self.interactive_template.static_mode = False + self.interactive_template.on_press(event) + pick_found = True + return pick_found + def button_pressed(self, event): if self.image_mode not in (ViewType.raw, ViewType.polar): print('Masking must be done in raw or polar view') @@ -210,16 +204,26 @@ def button_pressed(self, event): if not self.axes: return - self.press = [event.xdata, event.ydata] - self.det = self.axes.get_title() - if not self.det: - self.det = self.image_mode - self.create_patch() + if event.button == 1: + # Determine if selecting an existing template or drawing a new one + pick_found = self.check_pick(event) + + if (pick_found or + self.interactive_template and + not self.interactive_template.static_mode): + return - # For animating the patch - self.bg_cache = self.canvas.copy_from_bbox(self.axes.bbox) + self.press = [event.xdata, event.ydata] + self.det = self.axes.get_title() + if not self.det: + self.det = self.image_mode + self.create_interactive_template() - self.drawing_axes = self.axes + # For animating the patch + self.canvas.draw() # Force canvas re-draw before caching + self.bg_cache = self.canvas.copy_from_bbox(self.axes.bbox) + + self.drawing_axes = self.axes def drag_motion(self, event): if ( @@ -229,25 +233,30 @@ def drag_motion(self, event): ): return - self.update_patch(event) + if not self.interactive_template.static_mode: + return + + self.update_interactive_template(event) # Update animation of patch self.canvas.restore_region(self.bg_cache) - self.axes.draw_artist(self.patch) + self.axes.draw_artist(self.interactive_template.template) self.canvas.blit(self.axes.bbox) def save_line_data(self): - data_coords = self.patch.get_patch_transform().transform( - self.patch.get_path().vertices[:-1]) + for det, its in self.interactive_templates.items(): + for it in its: + data_coords = it.template.get_patch_transform().transform( + it.template.get_path().vertices[:-1]) - # So that this gets converted between raw and polar correctly, - # make sure there are at least 300 points. - data_coords = add_sample_points(data_coords, 300) + # So that this gets converted between raw and polar correctly, + # make sure there are at least 300 points. + data_coords = add_sample_points(data_coords, 300) - if self.image_mode == ViewType.raw: - self.raw_mask_coords.append((self.det, data_coords)) - elif self.image_mode == ViewType.polar: - self.raw_mask_coords.append([data_coords]) + if self.image_mode == ViewType.raw: + self.raw_mask_coords.append((det, data_coords)) + elif self.image_mode == ViewType.polar: + self.raw_mask_coords.append([data_coords]) def create_masks(self): for data in self.raw_mask_coords: @@ -269,14 +278,16 @@ def create_masks(self): masks_changed_signal[self.image_mode].emit() def button_released(self, event): - if not self.press: + if not self.press or not self.interactive_template.static_mode: return # Save it - self.save_line_data() + self.interactive_template.update_style(color='black') + self.interactive_templates.setdefault(self.det, []).append( + self.interactive_template) # Turn off animation so the patch will stay - self.patch.set_animated(False) + self.interactive_template.template.set_animated(False) self.press.clear() self.det = None @@ -286,16 +297,37 @@ def button_released(self, event): self.update_undo_enable_state() def apply_masks(self): + if not self.interactive_templates: + return + + self.save_line_data() self.disconnect() self.create_masks() - while self.added_patches: - self.discard_patch() + while self.added_templates: + self.discard_interactive_template() self.new_mask_added.emit(self.image_mode) + self.disconnect() + self.reset_all() def cancel(self): - while self.added_patches: - self.discard_patch() + while self.added_templates: + self.discard_interactive_template() self.disconnect() if self.canvas is not None: self.canvas.draw_idle() + + def canvas_changed(self, canvas): + self.apply_masks() + self.canvas = canvas + if self.ui.isVisible(): + self.setup_canvas_connections() + + def reset_all(self): + self.press.clear() + self.added_templates.clear() + for key in self.interactive_templates.keys(): + interactive_templates = self.interactive_templates[key] + [it.disconnect() for it in interactive_templates] + self.interactive_templates.clear() + self.raw_mask_coords.clear() diff --git a/hexrd/ui/resources/ui/mask_regions_dialog.ui b/hexrd/ui/resources/ui/mask_regions_dialog.ui index 1d54a624e..5b5683726 100644 --- a/hexrd/ui/resources/ui/mask_regions_dialog.ui +++ b/hexrd/ui/resources/ui/mask_regions_dialog.ui @@ -6,40 +6,50 @@ 0 0 - 198 - 125 + 400 + 120 Mask Region - - - - - - - Shape: - - + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + Rectangle + - - - - - Rectangle - - - - - Ellipse - - - + + + Ellipse + - + + + + + + left-click and drag or arrow keys + + + + + + + Shape: + + - + false @@ -49,15 +59,36 @@ - - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + Rotate: + + + + + + + Translate: + + + + + + + shift + left-click and drag or shift + arrow keys + + + false + + shape + undo +