Source code for nexxT.examples.framework.ImageView

# SPDX-License-Identifier: Apache-2.0
# Copyright (C) 2020 ifm electronic gmbh
#
# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
#

"""
This viewer is a showcase to see how to write a viewer for nexxT. Note that there are multiple possibilities
to view images, and this one might actually not be the best option. You can use python toolkits such as pyqtgraph,
matpllotlibs or others supporting a QT5 backend.
"""

import logging
import numpy as np
from nexxT.Qt.QtGui import QPainter, QImage
from nexxT.Qt.QtWidgets import QWidget
from nexxT.interface import Filter, Services
from nexxT.examples.framework.ImageData import byteArrayToNumpy

logger = logging.getLogger(__name__)

[docs] class ImageView(Filter): """ This is the filter receiving the image data to be drawn. It has to be run in the GUI thread. """
[docs] def __init__(self, env): super().__init__(False, False, env) # create an input port for receiving the image data self.inPort = self.addStaticInputPort("video_in", 1, -1) # note that the widget shall not be created directly but in the onOpen(...) function. # reason is that the constructor and onInit(...) functions are called quite often and so they should # not perform expensive operations. self._widget = None # define the properties of this filter pc = self.propertyCollection() pc.defineProperty("caption", "view", "Caption for the MDI window. You can use 2D indices for aligning multiple views\n" "in a grid layout.") pc.defineProperty("scale", 1.0, "Scale factor for display", options=dict(min=0.01, max=16.0)) # we use the propertyChanged signal to synchronize the scale factor. pc.propertyChanged.connect(self.propChanged)
[docs] def propChanged(self, propColl, name): """ Slot called whenever a property of this filter has changed. :param pc: the PropertyCollection instance of this filter :param name: the name of the changed parameter :return: """ if name == "scale" and self._widget is not None: self._widget.setScale(propColl.getProperty("scale"))
[docs] def onOpen(self): """ Now we can create the widget. :return: """ pc = self.propertyCollection() # get the main window service, used for registering the "subplot" in a QMDISubWindow instance mw = Services.getService("MainWindow") # create the widget self._widget = DisplayWidget() # register the subplot in the main window mw.subplot(pc.getProperty("caption"), self, self._widget) # inform the display widget about the current scale self._widget.setScale(pc.getProperty("scale"))
[docs] def onClose(self): """ Inverse of onOpen :return: """ mw = Services.getService("MainWindow") # de-register the subplot mw.releaseSubplot(self._widget) # delete the widget reference self._widget = None
[docs] def interpretAndUpdate(self): """ The deferred update method, called from the MainWindow service at user-defined framerate. """ sample = self.inPort.getData() if sample.getDatatype() == "example/image": npa = byteArrayToNumpy(sample.getContent()) self._widget.setData(npa)
[docs] def onPortDataChanged(self, port): # pylint: disable=unused-argument """ Notification of new data. :param port: the port where the data arrived. :return: """ if self._widget.isVisible(): # don't consume processing time if not shown mw = Services.getService("MainWindow") mw.deferredUpdate(self, "interpretAndUpdate")
[docs] class DisplayWidget(QWidget): """ The widget actually displaying the image. It has a scale parameter for setting the scale factor of the drawn image. The widget is created from the Filter instance. Both are QObjects and both have to run in the main (=GUI) thread of the QApplication. """
[docs] def __init__(self, parent=None): super().__init__(parent=parent) self._img = QImage(16, 16, QImage.Format_RGB888) self._scale = 1.0 self._mv = None
[docs] def setScale(self, scale): """ Set the scale factor :param scale: a floating point (<1: size reduction) :return: """ self._scale = scale # check for size constraints self.checkSize() # update the displayed image self.update()
[docs] def setData(self, data): """ Called when new data arrives. This function converts the numpy array to a QImage which can then be drawn with a QT painter. :param data: the image as a numpy array :return: """ img = None if data.dtype is np.dtype(np.uint8): # uint8 images can either be 2 dimensional (intensity) or 3 dimensional with 3 channels (rgb) assert len(data.shape) == 2 or (len(data.shape) == 3 and data.shape[-1] == 3) # conversion will be performed later in this case elif data.dtype is np.dtype(np.uint16) and len(data.shape) == 2: # an uint16 intensity image self._mv = memoryview(data) # conversion is done immediately img = QImage(self._mv, data.shape[1], data.shape[0], data.shape[1], QImage.Format_Grayscale16) else: # all other cases: convert the image to a uint8 array scaling between min and max mind = np.nanmin(data) maxd = np.nanmax(data) data = np.clip((data.astype(np.float64)-mind)/(maxd-mind)*256, 0, 255).astype(np.uint8) if img is None: # if not yet done, convert the (uint8) data to a qimage self._mv = memoryview(data) img = QImage(self._mv, data.shape[1], data.shape[0], np.prod(data.shape[1:]), QImage.Format_RGB888 if len(data.shape) == 3 and data.shape[-1] == 3 else QImage.Format_Grayscale8) # save for later painting self._img = img # check for size constraints self.checkSize() # request QT for an update self.update()
[docs] def checkSize(self): """ make sure that the minimum size is consistent with the shown image. :return: """ size = self._img.size() * self._scale if size != self.minimumSize(): logger.info("Size changed: %s", size) self.setMinimumSize(size) self.parent().parent().adjustSize()
[docs] def paintEvent(self, paintEvent): # pylint: disable=unused-argument """ The paint event actually drawing the image :param paintEvent: a QPaintEvent instance :return: """ p = QPainter(self) p.scale(self._scale, self._scale) p.drawImage(0, 0, self._img)