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
+