Files
SwissMX/CustomROI.py
Thierry Zamofing db7e5e537f have swissmx.py running with some commented modules in ESC network
git dt 98297263 swissmx.py
 git dt 7445a5aa CustomROI.py app_config.py app_utils.py epics_widgets/MotorTweak.py epics_widgets/SmaractMotorTweak.py
2022-07-15 09:41:44 +02:00

628 lines
20 KiB
Python

import logging
from PyQt5 import QtGui, QtCore
from PyQt5.QtGui import QColor, QTransform
from PyQt5.QtWidgets import QMenu, QAction, QSpinBox, QMenu
#from GenericDialog import GenericDialog #ZAC: orig. code
from pyqtgraph import ROI, Point, RectROI, mkPen
from pyqtgraph import functions as fn
from app_config import settings
from math import *
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
class Retangulo(ROI):
"""
Rectangular ROI subclass with a single scale handle at the top-right corner.
============== =============================================================
**Arguments**
pos (length-2 sequence) The position of the ROI origin.
See ROI().
size (length-2 sequence) The size of the ROI. See ROI().
centered (bool) If True, scale handles affect the ROI relative to its
center, rather than its origin.
sideScalers (bool) If True, extra scale handles are added at the top and
right edges.
\**args All extra keyword arguments are passed to ROI()
============== =============================================================
"""
def __init__(self, pos, size, centered=False, sideScalers=False, **args):
ROI.__init__(self, pos, size, **args)
if centered:
center = [0.5, 0.5]
else:
center = [0, 0]
self.addScaleRotateHandle([0, 0.5], [1, 0.5])
self.addScaleHandle([1, 1], center)
if sideScalers:
self.addScaleHandle([1, 0.5], [center[0], 0.5])
self.addScaleHandle([0.5, 1], [0.5, center[1]])
class Grid(ROI):
"""a grid"""
def __init__(
self,
x_step,
y_step,
x_offset,
y_offset,
grid_index=0,
parent=None,
gonio_xy=(0, 0),
**kargs
):
self._grid_index = grid_index
self._size = None
self._pos = None
self._shape = None
ROI.__init__(self, (500, 500), (300, 300), **kargs)
self.parent = parent
self.addScaleHandle([1, 1], [0, 0])
self.addScaleHandle([0, 0], [1, 1])
self.addScaleRotateHandle([1, 0], [0, 0])
# self.addScaleRotateHandle([1, 0.5], [0.5, 0.5])
self._steps = x_step / 1000., y_step / 1000.
self._offsets = x_offset / 1000, y_offset / 1000
self._beam_position = parent.get_beam_mark_on_camera_xy()
self.set_gonio_xy(*gonio_xy)
self.setToolTip("grid")
parent.pixelsPerMillimeter.connect(self.update_ppm)
parent.beamCameraCoordinatesChanged.connect(self.update_beam_position)
self.enable_stage_tracking()
self._ppm = parent.getPpm()
self._mm_size = 300 / self._ppm, 300 / self._ppm
self._original_ppm = self._ppm # marker only works at original zoom
# because zoom axis is not aligned
# with viewing axis
self._fx, self._fy = self.parent.getFastX(), self.parent.getFastY()
self.invertible = True
self.sigRegionChanged.connect(self.invalidate)
self.sigRegionChangeFinished.connect(self.calculate_gonio_xy)
self.scaleSnap = False
self.aspectLocked = False
self.translatable = True
self.rotateAllowed = False
self.removable = True
self.follow_stage()
# self.calculate_gonio_xy()
def enable_stage_tracking(self):
self.parent.fast_x_position.connect(self.follow_x)
self.parent.fast_y_position.connect(self.follow_y)
def disable_stage_tracking(self):
self.parent.fast_x_position.disconnect(self.follow_x)
self.parent.fast_y_position.disconnect(self.follow_y)
def follow_x(self, v):
self.follow_stage(fx=v)
def follow_y(self, v):
self.follow_stage(fy=v)
def removeClicked(self):
self.parent.pixelsPerMillimeter.disconnect(self.update_ppm)
self.parent.beamCameraCoordinatesChanged.disconnect(self.update_beam_position)
self.parent.fast_x_position.disconnect(self.follow_x)
self.parent.fast_y_position.disconnect(self.follow_y)
super(Grid, self).removeClicked()
def calculate_gonio_xy(self):
self.update_grid_pos()
state = self.getState()
angle = state["angle"]
w, h = state["size"]
w, h = 1000 * w / self._ppm, 1000 * h / self._ppm
self._mm_size = w / 1000, h / 1000
self.calculate_targets()
def update_ppm(self, ppm):
self._ppm = ppm
self.update_grid_size(ppm)
self.update_grid_pos()
self.stateChanged()
def update_grid_size(self, ppm, finish=False):
mmw, mmh = self._mm_size
w, h = mmw * ppm, mmh * ppm
self.setSize((w, h), update=False)
def follow_stage(self, fx=None, fy=None):
self._fx, self._fy = self.parent.getFastX(), self.parent.getFastY()
self.update_grid_pos()
self.stateChanged()
def set_gonio_xy(self, x, y):
self._gx, self._gy = x, y
def update_grid_pos(self):
idx = self._grid_index
ppm = self._ppm
bx, by = self.parent.get_beam_mark_on_camera_xy()
gx, gy = self._gx, self._gy # marker position in gonio coords
fx, fy = self._fx, self._fy
dx, dy = (
ppm * (fx - gx),
ppm * (fy - gy),
) # distance to beam in pixels for new ppm
x, y = bx - dx, by + dy
self.setPos(x, y, update=False)
mmw, mmh = self._mm_size
def calculate_targets(self):
state = self.getState()
angle = radians(state["angle"])
cosa = cos(angle)
sina = sin(angle)
w, h = state["size"]
xs, ys = self._steps
w, h = w / self._ppm, h / self._ppm # mm
gx, gy = self._gx, self._gy
nx = int(abs(w) / xs)
ny = int(abs(h) / ys)
points = []
dx = xs
dy = ys
for yi in range(ny):
ly = yi * dy
y = gy + ly
for xi in range(nx):
lx = xi * dx
x = gx + lx
l = sqrt((lx) ** 2 + (ly) ** 2)
try:
alpha = acos((lx) / l)
except:
alpha = 0
x2 = gx + l * cos(alpha + angle)
y2 = gy - l * sin(alpha + angle)
# print(
# "l={:8.3f} alpha={:.3f} xy={:8.3f}{:8.3f} xy2={:8.3f}{:8.3f}".format(
# l, degrees(alpha), x, y, x2, y2
# )
# )
points.append((x2, y2))
self._grid_dimensions = (nx, ny)
self._points = points
self.parent.gridUpdated.emit(self._grid_index)
def get_sorted_grid_targets(self):
pass
def get_grid_targets(self):
return self._points
def update_beam_position(self, pos):
self._beam_position = pos
def invalidate(self):
state = self.getState()
x, y = state["pos"]
w, h = state["size"]
self._mm_size = w / self._ppm, h / self._ppm
fx, fy = self.camera2gonio(x, y)
self.set_gonio_xy(fx, fy)
self._shape = None
self.prepareGeometryChange()
def boundingRect(self):
return self.shape().boundingRect()
def shape(self):
w, h = self.getState()["size"]
if self._shape is None:
radius = self.getState()["size"][1]
p = QtGui.QPainterPath()
p.addRect(0, 0, w, h)
stroker = QtGui.QPainterPathStroker()
stroker.setWidth(10)
outline = stroker.createStroke(p)
self._shape = outline
return self._shape
def paint(self, p, *args):
state = self.getState()
w, h = state["size"]
p.setRenderHint(QtGui.QPainter.Antialiasing)
p.setPen(fn.mkPen(width=5, color=[200, 200, 100]))
p.drawRect(0, 0, w, h)
p.setPen(fn.mkPen(width=3, color=[200, 100, 100]))
ppm = self._ppm
xs, ys = self._steps
w1, h1 = w / ppm, h / ppm # mm
nx = int(abs(w1) / xs)
ny = int(abs(h1) / ys)
sw = (xs) * ppm
sh = (ys) * ppm
# print((nx,ny), (w1, h1), (sw, sh))
r = 10
a, b = state["pos"]
if nx * ny > 1000:
return
for y in range(ny):
for x in range(nx):
p.drawEllipse((x * sw) - r, (y * sh) - r, 2 * r, 2 * r)
def camera2gonio(self, x, y):
bx, by = self.parent.get_beam_mark_on_camera_xy()
ppm = self._ppm
dx, dy = (x - bx) / ppm, (y - by) / ppm
fx, fy = self._fx + dx, self._fy - dy
return fx, fy
def gonio2camera(self, x, y):
ppm = self._ppm
bx, by = self.parent.get_beam_mark_on_camera_xy()
gx, gy = self._gx, self._gy # marker position in gonio coords
fx, fy = self._fx, self._fy
dx, dy = (
ppm * (fx - gx),
ppm * (fy - gy),
) # distance to beam in pixels for new ppm
x, y = bx - dx, by + dy
return x, y
class BeamMark(ROI):
"""A crosshair ROI whose position is at the center of the crosshairs. By default, it is scalable, rotatable and translatable."""
def __init__(self, pos=None, size=None, parent=None, **kargs):
if size is None:
size = [20, 20]
if pos is None:
pos = [0, 0]
self._size = size
self._pos = pos
self._shape = None
ROI.__init__(self, pos, size, **kargs)
parent.pixelsPerMillimeter.connect(self.my_update_ppm)
parent.beamCameraCoordinatesChanged.connect(lambda x, y: self.setPos((x, y)))
self._scale_handler = None
self.sigRegionChanged.connect(self.invalidate)
self.aspectLocked = False
self.translatable = False
self.rotateAllowed = False
self.invertible = False
self.removable = False
self.scaleSnap = True
self.snapSize = 1
self.sigRegionChangeFinished.connect(self.show_size)
self.flip_ud()
def show_size(self):
try:
ppm = self._ppm
except:
return
w, h = self.getState()["size"]
# convert to micrometer
nw, nh = (w * 1000) / ppm, (h * 1000) / ppm
logger.debug("persisting new beam marker size: {}".format((nw, nh)))
settings.setValue("beam/size", (nw, nh))
def toggle_handle(self):
if self._scale_handler is None:
logger.debug("adding beam marker resize handle")
self._scale_handler = self.addScaleHandle([0.5, 0.5], [0, 0])
else:
logger.debug("removing beam marker resize handle")
self.removeHandle(self._scale_handler)
self._scale_handler = None
def get_camera_beam_position(self):
return self.getState()["pos"]
def set_beam_size_marker_dialog(self):
w, h = settings.value("beam/size")
d = GenericDialog(
title="Beamsize",
message="Enter the size of the beam in microns",
inputs={
"width": ("Width (\u00B5)", int(w), QSpinBox()),
"height": ("Height (\u00B5)", int(h), QSpinBox()),
},
)
if d.exec():
results = d.results
logger.info("Updating beamsize to {}".format(results))
w, h = results["width"], results["height"]
logger.debug("types {}".format(type(w)))
settings.setValue("beam/size", (w, h))
self.set_beam_size((w, h))
def flip_ud(self):
"""flip up-down"""
t = self.transform()
m11 = t.m11()
m12 = t.m12()
m13 = t.m13()
m21 = t.m21()
m22 = t.m22()
m23 = t.m23()
m31 = t.m31()
m32 = t.m32()
m33 = t.m33()
t.setMatrix(m11, m12, m13, m21, -m22, m23, m31, m32, m33)
self.setTransform(t)
def set_beam_size(self, size=None):
if size is not None:
self._size = size
ppm = self._ppm
w, h = self._size
nw, nh = (w / 1000) * ppm, (h / 1000) * ppm
self.setSize((nw, nh))
self.setToolTip("Beamsize (W x H): {:.1f}\u00B5 x {:.1f}\u00B5".format(w, h))
def my_update_ppm(self, ppm):
self._ppm = ppm
self.set_beam_size()
def invalidate(self):
self._shape = None
self.prepareGeometryChange()
def boundingRect(self):
return self.shape().boundingRect()
def shape(self):
w, h = self.size()
if self._shape is None:
p = QtGui.QPainterPath()
p.moveTo(Point(0, -h))
p.lineTo(Point(0, h))
p.moveTo(Point(-w, 0))
p.lineTo(Point(w, 0))
#
p.moveTo(Point(-w, -h))
p.lineTo(Point(w, -h))
p.lineTo(Point(w, h))
p.lineTo(Point(-w, -h))
stroker = QtGui.QPainterPathStroker()
outline = stroker.createStroke(p)
self._shape = outline
return self._shape
def paint(self, p, *args):
w, h = self.getState()["size"]
v1, v2 = -(h * 0.8) / 2, (h * 0.8) / 2
h1, h2 = -(w * 0.8) / 2, (w * 0.8) / 2
p.setRenderHint(QtGui.QPainter.Antialiasing)
p.setPen(fn.mkPen(width=3, color=[200, 100, 100]))
p.drawLine(Point(0, v1), Point(0, v2))
p.drawLine(Point(h1, 0), Point(h2, 0))
p.setPen(fn.mkPen(width=3, color=[200, 200, 100]))
p.drawRect(-w / 2, -h / 2, w, h)
# p.drawText(-w, -h, '{:.0f}x{:.0f}'.format(*self._size))
class CrystalCircle(ROI):
"""A crosshair ROI whose position is at the center of the crosshairs. By default, it is scalable, rotatable and translatable."""
def __init__(
self,
pos=None,
size=None,
parent=None,
gonio_xy=(0, 0),
model_row_index=1,
is_fiducial=False,
ppm=False,
**kargs
):
if size is None:
size = [20, 20]
if pos is None:
pos = [0, 0]
self._size = size
self._shape = None
self.parent = parent
self._model_row_index = model_row_index
self._is_fiducial = is_fiducial
ROI.__init__(self, pos, size, **kargs)
self._beam_position = parent.get_beam_mark_on_camera_xy()
self.set_gonio_xy(*gonio_xy)
self.setToolTip(f"{'fiducial' if is_fiducial else 'crystal'} {model_row_index}")
# parent.pixelsPerMillimeter.connect(self.update_pixels_per_millimeter)
# parent.beamCameraCoordinatesChanged.connect(self.update_beam_position)
# parent.fast_x_position.connect(self.follow_x)
# parent.fast_y_position.connect(self.follow_y)
self._ppm = ppm
self._original_ppm = ppm # marker only works at original zoom
# because zoom axis is not aligned
# with viewing axis
# self.calculate_gonio_xy()
self.sigRegionChanged.connect(self.invalidate)
# self.sigRegionChangeFinished.connect(self.calculate_gonio_xy)
# self.sigHoverEvent.connect(self.my_hovering)
self.aspectLocked = False
self.translatable = True
self.rotateAllowed = False
self.invertible = False
self.deletable = False
def enable_stage_tracking(self):
self.parent.fast_x_position.connect(self.follow_x)
self.parent.fast_y_position.connect(self.follow_y)
def disable_stage_tracking(self):
self.parent.fast_x_position.disconnect(self.follow_x)
self.parent.fast_y_position.disconnect(self.follow_y)
def follow_x(self, v):
self.follow_stage(fx=v)
def follow_y(self, v):
self.follow_stage(fy=v)
def my_hovering(self, event):
print("hovering: {}".format(self.mouseHovering))
def get_model_row(self):
return self._model_row_index
def update_ppm(self, ppm):
self._ppm = ppm
self.recalc_camera_position()
def follow_stage(self, fx=None, fy=None):
self._fx, self._fy = self.parent.getFastX(), self.parent.getFastY()
self.recalc_camera_position()
def set_gonio_xy(self, x, y):
self._gx, self._gy = x, y
def disconnect_signals(self):
self.parent.pixelsPerMillimeter.disconnect(self.update_ppm)
self.parent.beamCameraCoordinatesChanged.disconnect(self.update_beam_position)
self.parent.fast_x_position.disconnect(self.follow_x)
self.parent.fast_y_position.disconnect(self.follow_y)
def reconnect_signals(self):
self.parent.pixelsPerMillimeter.connect(self.update_ppm)
self.parent.beamCameraCoordinatesChanged.connect(self.update_beam_position)
self.parent.fast_x_position.connect(self.follow_x)
self.parent.fast_y_position.connect(self.follow_y)
def removeClicked(self):
self.disconnect_signals()
super(CrystalCircle, self).removeClicked()
def recalc_camera_position(self):
idx = self._model_row_index
ppm = self._ppm
bx, by = self.parent.get_beam_mark_on_camera_xy()
gx, gy = self._gx, self._gy # marker position in gonio coords
fx, fy = self._fx, self._fy
# distance to beam in pixels for new ppm
dx, dy = (ppm * (fx - gx), ppm * (fy - gy))
x, y = bx - dx, by + dy
# logger.debug(f"recalc {idx}: cam = {(x,y)}, gonio = {(gx, gy)}")
self.setPos(x, y)
def update_beam_position(self, pos):
self._beam_position = pos
def show_size(self):
try:
ppm = self._ppm
except:
return
w, h = self.getState()["size"]
# convert to micrometer
nw, nh = (w * 1000) / ppm, (h * 1000) / ppm
def invalidate(self):
self._shape = None
self.prepareGeometryChange()
def boundingRect(self):
return self.shape().boundingRect()
def shape(self):
if self._shape is None:
radius = self.getState()["size"][1]
p = QtGui.QPainterPath()
p.moveTo(Point(0, -radius))
p.lineTo(Point(0, radius))
p.moveTo(Point(-radius, 0))
p.lineTo(Point(radius, 0))
stroker = QtGui.QPainterPathStroker()
stroker.setWidth(10)
outline = stroker.createStroke(p)
self._shape = outline
return self._shape
def paint(self, p, *args):
if abs(self._ppm - self._original_ppm) < 1.:
if self._is_fiducial:
pen = mkPen(color="r", width=2)
else:
pen = mkPen(color="y", width=2)
else:
pen = mkPen(color="#ffffaa88", width=2)
x, y = self.getState()["size"]
p.setRenderHint(QtGui.QPainter.Antialiasing)
p.setPen(pen)
p.drawLine(Point(0, -x), Point(0, x))
p.drawLine(Point(-x, 0), Point(x, 0))
p.drawEllipse(-x, -y, 2 * x, 2 * y)
class Crosshair(ROI):
"""A crosshair ROI whose position is at the center of the crosshairs. By default, it is scalable, rotatable and translatable."""
def __init__(self, pos=None, size=None, **kargs):
if size is None:
size = [10, 10]
if pos is None:
pos = [0, 0]
self._shape = None
ROI.__init__(self, pos, size, **kargs)
self.sigRegionChanged.connect(self.invalidate)
self.aspectLocked = True
def invalidate(self):
self._shape = None
self.prepareGeometryChange()
def boundingRect(self):
return self.shape().boundingRect()
def shape(self):
if self._shape is None:
radius = self.getState()["size"][1]
p = QtGui.QPainterPath()
p.moveTo(Point(0, -radius))
p.lineTo(Point(0, radius))
p.moveTo(Point(-radius, 0))
p.lineTo(Point(radius, 0))
# p = self.mapToDevice(p)
stroker = QtGui.QPainterPathStroker()
stroker.setWidth(10)
outline = stroker.createStroke(p)
# self._shape = self.mapFromDevice(outline)
self._shape = outline
return self._shape
def paint(self, p, *args):
radius = self.getState()["size"][1]
p.setRenderHint(QtGui.QPainter.Antialiasing)
p.setPen(self.currentPen)
p.drawLine(Point(0, -radius), Point(0, radius))
p.drawLine(Point(-radius, 0), Point(radius, 0))