# This file is a part of pzp-hardware, a library of laboratory hardware support Pieces
# for the puzzlepiece GUI & automation framework. Check out https://pzp-hardware.readthedocs.io
# Licensed under the Apache License 2.0 - https://github.com/jdranczewski/pzp-hardware/blob/main/LICENSE
"""
:module_title:`Image preview mixin`
These Mixins allow you to add an image preview to your custom Piece easily.
They require an array param named ``image`` to exist on the Piece, and will
update the preview automatically when its value changes.
Add the mixin *before* ``pzp.Piece`` in your class definition, specifying any
settings as class variables::
import puzzlepiece as pzp
from pzp_hardware.generic.mixins import image_preview
import numpy as np
class Piece(image_preview.ImagePreview, pzp.Piece):
# including any of these settings is optional, these are the defaults:
live_toggle = False # include a toggle to enable live mode
autolevel_toggle = False # include a toggle to enable autoleveling
max_counts = 255 # the maximum image brightness (will be white with autolevel off)
use_numba = False # if you need increased performance, you can tell pyqtgraph to use numba
# horizontal_layout = True # you can use Piece settings to display the preview to the right
# # of the params
def define_params(self):
super().define_params()
@pzp.param.array(self, "image")
def image():
return np.random.random((1080, 1440)) * 255
"""
import puzzlepiece as pzp
import numpy as np
from qtpy import QtWidgets, QtCore
import pyqtgraph as pg
#MARK: Base
[docs]
class Base():
"""
Base for image previews, implements the live toggle and the autolevel toggle,
as well as the settings outlined below.
.. image:: ../images/pzp_hardware.generic.mixins.image_preview.Base.png
Will not display an image preview on its own, use
:class:`~pzp_hardware.generic.mixins.image_preview.ImagePreview` or
:class:`~pzp_hardware.generic.mixins.image_preview.LineoutImagePreview`,
which are based on this class.
"""
#: Display a toggle that lets the user refresh the image live
live_toggle = False
#: Display a toggle that makes the image brightness range adjust automatically
autolevel_toggle = False
#: Adjust the default range of the image view, this is the white point
max_counts = 255
#: Enable numba acceleration for some performance gains (but longer initial load times)
use_numba = False
def custom_layout(self):
layout = QtWidgets.QVBoxLayout()
if self.live_toggle or self.autolevel_toggle:
toggle_layout = QtWidgets.QGridLayout()
layout.addLayout(toggle_layout)
toggle_layout.setColumnStretch(1, 1)
if self.live_toggle:
# Add a PuzzleTimer for live view
delay = 0.05 if not self.puzzle.debug else 0.1 # Introduce artificial delay for debug mode
self.timer = pzp.threads.PuzzleTimer('Live', self.puzzle, self.params['image'].get_value, delay)
toggle_layout.addWidget(self.timer, 0, 0)
if self.autolevel_toggle:
def autolevel(value):
image = self["image"].value
if value and image is not None:
self.imgw.setImage(autolevels=True)
else:
self.imgw.setLevels([0, self.max_counts])
self.autolevel_toggle = pzp.param.ParamCheckbox("autolevel", 0, autolevel)
toggle_layout.addWidget(self.autolevel_toggle, 0, 2)
# numba makes image updates slightly faster
if self.use_numba:
pg.setConfigOption('useNumba', True)
return layout
def call_stop(self):
super().call_stop()
if hasattr(self, "timer"):
self.timer.stop()
#MARK: ImagePreview
[docs]
class ImagePreview(Base):
"""
Mixin for displaying an image preview in your Piece. See
:mod:`pzp_hardware.generic.mixins.image_preview` above for usage.
.. image:: ../images/pzp_hardware.generic.mixins.image_preview.ImagePreview.png
"""
def custom_layout(self):
layout = super().custom_layout()
# Make an ImageView
self.pw = pg.PlotWidget()
layout.addWidget(self.pw)
self.plot_item = plot_item = self.pw.getPlotItem()
plot_item.setAspectLocked(True)
plot_item.invertY(True)
self.imgw = pg.ImageItem(border='w', axisOrder='row-major', levels=[0, self.max_counts])
plot_item.addItem(self.imgw)
def update_image():
image_data = self.params['image'].value
if image_data is None:
return
self.imgw.setImage(
image_data,
autoLevels=self.autolevel_toggle and self.autolevel_toggle.value
)
if self.autolevel_toggle:
self.autolevel_toggle.set_value(0)
update_later = pzp.threads.CallLater(update_image)
self.params['image'].changed.connect(update_later)
return layout
#MARK: LineoutImagePreview
[docs]
class LineoutImagePreview(Base):
"""
Mixin for displaying an image preview in your Piece, with two movable lines
that let you plot the image's profile along them. An action is added to centre
the lines (keyboard shortcut: ``c``), and a hidden param is included for the
line crossing circle radius.
See :mod:`pzp_hardware.generic.mixins.image_preview` above for mixin usage.
.. image:: ../images/pzp_hardware.generic.mixins.image_preview.LineoutImagePreview.png
"""
def define_actions(self):
super().define_actions()
@pzp.action.define(self, 'Centre lines', QtCore.Qt.Key.Key_C)
def centre(self):
shape = self.params['image'].value.shape
self._inf_line_x.setValue(shape[0]//2)
self._inf_line_y.setValue(shape[1]//2)
def define_params(self):
super().define_params()
pzp.param.spinbox(self, 'circle_r', 200, visible=False)(None)
def custom_layout(self):
layout = super().custom_layout()
# Make the plots
self.gl = pg.GraphicsLayoutWidget()
layout.addWidget(self.gl)
self.gl.ci.layout.setRowStretchFactor(0, 5)
self.gl.ci.layout.setColumnStretchFactor(0, 5)
plot_main = self.gl.addPlot(0, 0)
plot_x = self.gl.addPlot(1, 0)
plot_x.setXLink(plot_main)
plot_y = self.gl.addPlot(0, 1)
plot_y.setYLink(plot_main)
plot_main.setAspectLocked(True)
plot_main.invertY(True)
plot_y.invertY(True)
self.imgw = pg.ImageItem(border='w', axisOrder='row-major', levels=[0, self.max_counts])
plot_main.addItem(self.imgw)
self._inf_line_x = pg.InfiniteLine(0, 0, movable=True)
self._inf_line_y = pg.InfiniteLine(0, 90, movable=True)
plot_main.addItem(self._inf_line_x)
plot_main.addItem(self._inf_line_y)
r = self.params['circle_r'].value
self._circle = QtWidgets.QGraphicsEllipseItem(-r, -r, r*2, r*2) # x, y, width, height
self._circle.setPen(pg.mkPen((255, 255, 0, 150)))
plot_main.addItem(self._circle)
def update_circle():
r = self.params['circle_r'].value
self._circle.setRect(self._inf_line_y.value()-r,
self._inf_line_x.value()-r,
r*2, r*2)
self.params['circle_r'].changed.connect(update_circle)
plot_line_x = plot_x.plot([0], [0])
plot_line_y = plot_y.plot([0], [0])
def update_image():
image_data = self.params['image'].value
if image_data is None:
return
self.imgw.setImage(
image_data,
autoLevels=self.autolevel_toggle and self.autolevel_toggle.value
)
self._inf_line_x.setBounds((0, image_data.shape[0]-1))
self._inf_line_y.setBounds((0, image_data.shape[1]-1))
plot_line_x.setData(image_data[int(self._inf_line_x.value())])
i = int(self._inf_line_y.value())
plot_line_y.setData(image_data[:, i], range(len(image_data[:, i])))
update_circle()
update_later = pzp.threads.CallLater(update_image)
self.params['image'].changed.connect(update_later)
self._inf_line_x.sigPositionChanged.connect(update_image)
self._inf_line_y.sigPositionChanged.connect(update_image)
if self.autolevel_toggle:
self.autolevel_toggle.set_value(0)
return layout