class MicroViewWidget(mvBase):
    __clsName = "MicroViewWidget"

    def tr(self, string):
        return QCoreApplication.translate(self.__clsName, string)

    def __init__(self, parent=None):
        super().__init__(parent)

        # Go before setupUi for QMetaObject.connectSlotsByName to work
        self._scene = MicroViewScene(self)
        self._scene.setObjectName("scene")

        self._ui = mvClass()
        self._ui.setupUi(self)

        self._ui.view.setScene(self._scene)
        # Apparently this is necessary with Qt5, as otherwise updating fails
        # on image change; there are white rectangles on the updated area
        # until the mouse is moved in or out of the view
        self._ui.view.setViewportUpdateMode(
            QGraphicsView.BoundingRectViewportUpdate)
        self._ui.view.setRenderHints(QPainter.Antialiasing)
        self._scene.imageItem.signals.mouseMoved.connect(
            self._updateCurrentPixelInfo)

        self._imageData = np.array([])
        self._intensityMin = None
        self._intensityMax = None
        self._sliderFactor = 1
        self._ui.autoButton.pressed.connect(self.autoIntensity)

        self._playing = False
        self._playTimer = QTimer()
        if not (qtpy.PYQT4 or qtpy.PYSIDE):
            self._playTimer.setTimerType(Qt.PreciseTimer)
        self._playTimer.setSingleShot(False)

        # set up preview button
        self._locEnabledStr = "Localizations are shown"
        self._locDisabledStr = "Localizations are not shown"
        self._ui.locButton.setToolTip(self.tr(self._locEnabledStr))
        self._ui.locButton.toggled.connect(self.showLocalizationsChanged)

        # connect signals and slots
        self._ui.framenoBox.valueChanged.connect(self.selectFrame)
        self._ui.playButton.pressed.connect(
            lambda: self.setPlaying(not self._playing))
        self._playTimer.timeout.connect(self.nextFrame)
        self._ui.zoomInButton.pressed.connect(self.zoomIn)
        self._ui.zoomOriginalButton.pressed.connect(self.zoomOriginal)
        self._ui.zoomOutButton.pressed.connect(self.zoomOut)
        self._ui.zoomFitButton.pressed.connect(self.zoomFit)
        self._scene.roiChanged.connect(self.roiChanged)

        # set button icons
        self._ui.locButton.setIcon(QIcon.fromTheme("view-preview"))
        self._ui.zoomOutButton.setIcon(QIcon.fromTheme("zoom-out"))
        self._ui.zoomOriginalButton.setIcon(QIcon.fromTheme("zoom-original"))
        self._ui.zoomFitButton.setIcon(QIcon.fromTheme("zoom-fit-best"))
        self._ui.zoomInButton.setIcon(QIcon.fromTheme("zoom-in"))
        self._ui.roiButton.setIcon(QIcon.fromTheme("draw-polygon"))

        self._playIcon = QIcon.fromTheme("media-playback-start")
        self._pauseIcon = QIcon.fromTheme("media-playback-pause")
        self._ui.playButton.setIcon(self._playIcon)

        # these are to be setEnable(False)'ed if there is no image sequence
        self._noImsDisable = [
            self._ui.zoomOutButton, self._ui.zoomOriginalButton,
            self._ui.zoomFitButton, self._ui.zoomInButton, self._ui.roiButton,
            self._ui.view, self._ui.pixelInfo, self._ui.frameSelector,
            self._ui.contrastGroup
        ]

        # initialize image data
        self._locDataGood = None
        self._locDataBad = None
        self._locMarkers = None
        self.setImageSequence(None)

    def setRoi(self, roi):
        self._scene.roi = roi

    roiChanged = Signal(QPolygonF)

    @Property(QPolygonF,
              fset=setRoi,
              doc="Polygon describing the region of interest (ROI)")
    def roi(self):
        return self._scene.roi

    def setImageSequence(self, ims):
        self._locDataGood = None
        self._locDataBad = None

        if ims is None:
            self._ims = None
            self._imageData = None
            for w in self._noImsDisable:
                w.setEnabled(False)
            self.drawImage()
            self.drawLocalizations()
            return

        for w in self._noImsDisable:
            w.setEnabled(True)
        self._ui.framenoBox.setMaximum(len(ims))
        self._ui.framenoSlider.setMaximum(len(ims))
        self._ims = ims
        try:
            self._imageData = self._ims[self._ui.framenoBox.value() - 1]
        except Exception:
            self.frameReadError.emit(self._ui.framenoBox.value() - 1)
            return

        if np.issubdtype(self._imageData.dtype, np.floating):
            # ugly hack; get min and max corresponding to integer types based
            # on the range of values in the first image
            min = self._imageData.min()
            if min < 0:
                types = (np.int8, np.int16, np.int32, np.int64)
            else:
                types = (np.uint8, np.uint16, np.uint32, np.uint64)
            max = self._imageData.max()
            if min >= 0. and max <= 1.:
                min = 0
                max = 1
            else:
                for t in types:
                    ii = np.iinfo(t)
                    if min >= ii.min and max <= ii.max:
                        min = ii.min
                        max = ii.max
                        break
        else:
            min = np.iinfo(ims.pixel_type).min
            max = np.iinfo(ims.pixel_type).max

        if min == 0. and max == 1.:
            self._ui.minSlider.setRange(0, 1000)
            self._ui.maxSlider.setRange(0, 1000)
            self._ui.minSpinBox.setDecimals(3)
            self._ui.minSpinBox.setRange(0, 1)
            self._ui.maxSpinBox.setDecimals(3)
            self._ui.maxSpinBox.setRange(0, 1)
            self._sliderFactor = 1000
        else:
            self._ui.minSlider.setRange(min, max)
            self._ui.maxSlider.setRange(min, max)
            self._ui.minSpinBox.setDecimals(0)
            self._ui.minSpinBox.setRange(min, max)
            self._ui.maxSpinBox.setDecimals(0)
            self._ui.maxSpinBox.setRange(min, max)
            self._sliderFactor = 1

        if (self._intensityMin is None) or (self._intensityMax is None):
            self.autoIntensity()
        else:
            self.drawImage()

        self._scene.setSceneRect(self._scene.itemsBoundingRect())

        self.currentFrameChanged.emit()

    @Slot(int)
    def on_minSlider_valueChanged(self, val):
        self._ui.minSpinBox.setValue(float(val) / self._sliderFactor)

    @Slot(int)
    def on_maxSlider_valueChanged(self, val):
        self._ui.maxSpinBox.setValue(float(val) / self._sliderFactor)

    @Slot(float)
    def on_minSpinBox_valueChanged(self, val):
        self._ui.minSlider.setValue(round(val * self._sliderFactor))
        self.setMinIntensity(val)

    @Slot(float)
    def on_maxSpinBox_valueChanged(self, val):
        self._ui.maxSlider.setValue(round(val * self._sliderFactor))
        self.setMaxIntensity(val)

    @Slot(pd.DataFrame)
    def setLocalizationData(self, good, bad):
        self._locDataGood = good
        self._locDataBad = bad
        self.drawLocalizations()

    def setPlaying(self, play):
        if self._ims is None:
            return

        if play == self._playing:
            return
        if play:
            self._playTimer.setInterval(1000 / self._ui.fpsBox.value())
            self._playTimer.start()
        else:
            self._playTimer.stop()
        self._ui.fpsBox.setEnabled(not play)
        self._ui.framenoBox.setEnabled(not play)
        self._ui.framenoSlider.setEnabled(not play)
        self._ui.framenoLabel.setEnabled(not play)
        self._ui.playButton.setIcon(
            self._pauseIcon if play else self._playIcon)
        self._playing = play

    def drawImage(self):
        if self._imageData is None:
            self._scene.setImage(QPixmap())
            return

        img_buf = self._imageData.astype(np.float)
        if (self._intensityMin is None) or (self._intensityMax is None):
            self._intensityMin = np.min(img_buf)
            self._intensityMax = np.max(img_buf)
        img_buf -= float(self._intensityMin)
        img_buf *= 255. / float(self._intensityMax - self._intensityMin)
        np.clip(img_buf, 0., 255., img_buf)

        # convert grayscale to RGB 32bit
        # far faster than calling img_buf.astype(np.uint8).repeat(4)
        qi = np.empty((img_buf.shape[0], img_buf.shape[1], 4), dtype=np.uint8)
        qi[:, :, 0] = qi[:, :, 1] = qi[:, :, 2] = qi[:, :, 3] = img_buf

        # prevent QImage from being garbage collected
        self._qImg = QImage(qi, self._imageData.shape[1],
                            self._imageData.shape[0], QImage.Format_RGB32)
        self._scene.setImage(self._qImg)

    def drawLocalizations(self):
        if isinstance(self._locMarkers, QGraphicsItem):
            self._scene.removeItem(self._locMarkers)
            self._locMarkers = None
        if not self.showLocalizations:
            return

        try:
            sel = self._locDataGood["frame"] == self._ui.framenoBox.value() - 1
            dGood = self._locDataGood[sel]
        except Exception:
            return

        try:
            sel = self._locDataBad["frame"] == self._ui.framenoBox.value() - 1
            dBad = self._locDataBad[sel]
        except Exception:
            pass

        markerList = []
        for n, d in dBad.iterrows():
            markerList.append(LocalizationMarker(d, Qt.red))
        for n, d in dGood.iterrows():
            markerList.append(LocalizationMarker(d, Qt.green))

        self._locMarkers = self._scene.createItemGroup(markerList)

    @Slot()
    def autoIntensity(self):
        if self._imageData is None:
            return

        self._intensityMin = np.min(self._imageData)
        self._intensityMax = np.max(self._imageData)
        if self._intensityMin == self._intensityMax:
            if self._intensityMax == 0:
                self._intensityMax = 1
            else:
                self._intensityMin = self._intensityMax - 1
        self._ui.minSlider.setValue(self._intensityMin)
        self._ui.maxSlider.setValue(self._intensityMax)
        self.drawImage()

    @Slot(int)
    def setMinIntensity(self, v):
        self._intensityMin = min(v, self._intensityMax - 1)
        self._ui.minSlider.setValue(self._intensityMin)
        self.drawImage()

    @Slot(int)
    def setMaxIntensity(self, v):
        self._intensityMax = max(v, self._intensityMin + 1)
        self._ui.maxSlider.setValue(self._intensityMax)
        self.drawImage()

    currentFrameChanged = Signal()

    @Slot(int)
    def selectFrame(self, frameno):
        if self._ims is None:
            return

        try:
            self._imageData = self._ims[frameno - 1]
        except Exception:
            self.frameReadError.emit(frameno - 1)
        self.currentFrameChanged.emit()
        self.drawImage()
        self.drawLocalizations()

    frameReadError = Signal(int)

    @Slot()
    def nextFrame(self):
        if self._ims is None:
            return

        next = self._ui.framenoBox.value() + 1
        if next > self._ui.framenoBox.maximum():
            next = 1
        self._ui.framenoBox.setValue(next)

    @Slot()
    def zoomIn(self):
        self._ui.view.scale(1.5, 1.5)

    @Slot()
    def zoomOut(self):
        self._ui.view.scale(2. / 3., 2. / 3.)

    @Slot()
    def zoomOriginal(self):
        self._ui.view.setTransform(QTransform())

    @Slot()
    def zoomFit(self):
        self._ui.view.fitInView(self._scene.imageItem, Qt.KeepAspectRatio)

    def getCurrentFrame(self):
        return self._imageData

    @Property(int, doc="Number of the currently displayed frame")
    def currentFrameNumber(self):
        return self._ui.framenoBox.value() - 1

    def _updateCurrentPixelInfo(self, x, y):
        if x >= self._imageData.shape[1] or y >= self._imageData.shape[0]:
            # Sometimes, when hitting the border of the image, the coordinates
            # are out of range
            return
        self._ui.posLabel.setText("({x}, {y})".format(x=x, y=y))
        self._ui.intLabel.setText(locale.str(self._imageData[y, x]))

    @Slot(bool)
    def on_roiButton_toggled(self, checked):
        self._scene.roiMode = checked

    @Slot(bool)
    def on_scene_roiModeChanged(self, enabled):
        self._ui.roiButton.setChecked(enabled)

    def setShowLocalizations(self, show):
        self._ui.locButton.setChecked(show)

    showLocalizationsChanged = Signal(bool)

    @Property(bool, fset=setShowLocalizations, notify=showLocalizationsChanged)
    def showLocalizations(self):
        return self._ui.locButton.isChecked()

    def on_locButton_toggled(self, checked):
        tooltip = (self.tr(self._locEnabledStr)
                   if checked else self.tr(self._locDisabledStr))
        self._ui.locButton.setToolTip(tooltip)

        self.drawLocalizations()
Exemple #2
0
class PyDMEmbeddedDisplay(QFrame, PyDMPrimitiveWidget):
    """
    A QFrame capable of rendering a PyDM Display

    Parameters
    ----------
    parent : QWidget
        The parent widget for the Label

    """
    def __init__(self, parent=None):
        QFrame.__init__(self, parent)
        PyDMPrimitiveWidget.__init__(self)
        self.app = QApplication.instance()
        self._filename = None
        self._macros = None
        self._embedded_widget = None
        self._disconnect_when_hidden = True
        self._is_connected = False
        self._only_load_when_shown = True
        self._needs_load = True
        self._load_error_timer = None
        self._load_error = None
        self.layout = QVBoxLayout(self)
        self.err_label = QLabel(self)
        self.err_label.setAlignment(Qt.AlignHCenter)
        self.layout.addWidget(self.err_label)
        self.layout.setContentsMargins(0, 0, 0, 0)
        self.err_label.hide()
        if not is_pydm_app():
            self.setFrameShape(QFrame.Box)
        else:
            self.setFrameShape(QFrame.NoFrame)

    def minimumSizeHint(self):
        """
        This property holds the recommended minimum size for the widget.

        Returns
        -------
        QSize
        """
        # This is totally arbitrary, I just want *some* visible nonzero size
        return QSize(100, 100)

    @Property(str)
    def macros(self):
        """
        JSON-formatted string containing macro variables to pass to the embedded file.

        Returns
        -------
        str
        """
        if self._macros is None:
            return ""
        return self._macros

    @macros.setter
    def macros(self, new_macros):
        """
        JSON-formatted string containing macro variables to pass to the embedded file.

        .. warning::
        If the macros property is not defined before the filename property,
        The widget will not have any macros defined when it loads the embedded file.
        This behavior will be fixed soon.

        Parameters
        ----------
        new_macros : str
        """
        new_macros = str(new_macros)
        if new_macros != self._macros:
            self._macros = new_macros
            self._needs_load = True
            self.load_if_needed()

    @Property(str)
    def filename(self):
        """
        Filename of the display to embed.

        Returns
        -------
        str
        """
        if self._filename is None:
            return ""
        return self._filename

    @filename.setter
    def filename(self, filename):
        """
        Filename of the display to embed.

        Parameters
        ----------
        filename : str
        """
        filename = str(filename)
        if filename != self._filename:
            self._filename = filename
            self._needs_load = True
            if is_qt_designer():
                if self._load_error_timer:
                    # Kill the timer here. If new filename still causes the problem, it will be restarted
                    self._load_error_timer.stop()
                    self._load_error_timer = None
                self.clear_error_text()
            self.load_if_needed()

    def parsed_macros(self):
        """
        Dictionary containing the key value pair for each macro specified.

        Returns
        --------
        dict
        """
        parent_display = self.find_parent_display()
        parent_macros = {}
        if parent_display:
            parent_macros = copy.copy(parent_display.macros())
        widget_macros = macro.parse_macro_string(self.macros)
        parent_macros.update(widget_macros)
        return parent_macros

    def load_if_needed(self):
        if self._needs_load and (not self._only_load_when_shown
                                 or self.isVisible() or is_qt_designer()):
            self.embedded_widget = self.open_file()

    def open_file(self, force=False):
        """
        Opens the widget specified in the widget's filename property.

        Returns
        -------
        display : QWidget
        """
        if (not force) and (not self._needs_load):
            return

        if not self.filename:
            return

        try:
            parent_display = self.find_parent_display()
            base_path = ""
            if parent_display:
                base_path = os.path.dirname(parent_display.loaded_file())

            fname = find_file(self.filename, base_path=base_path)
            w = load_file(fname, macros=self.parsed_macros(), target=None)
            self._needs_load = False
            self.clear_error_text()
            return w
        except Exception as e:
            self._load_error = e
            if self._load_error_timer:
                self._load_error_timer.stop()
            self._load_error_timer = QTimer(self)
            self._load_error_timer.setSingleShot(True)
            self._load_error_timer.setTimerType(Qt.VeryCoarseTimer)
            self._load_error_timer.timeout.connect(
                self._display_designer_load_error)
            self._load_error_timer.start(1000)
        return None

    def clear_error_text(self):
        if self._load_error_timer:
            self._load_error_timer.stop()
        self.err_label.clear()
        self.err_label.hide()

    def display_error_text(self, e):
        self.err_label.setText(
            "Could not open {filename}.\nError: {err}".format(
                filename=self._filename, err=e))
        self.err_label.show()

    @property
    def embedded_widget(self):
        """
        The embedded widget being displayed.

        Returns
        -------
        QWidget
        """
        return self._embedded_widget

    @embedded_widget.setter
    def embedded_widget(self, new_widget):
        """
        Defines the embedded widget to display inside the QFrame

        Parameters
        ----------
        new_widget : QWidget
        """
        should_reconnect = False
        if new_widget is self._embedded_widget:
            return
        if self._embedded_widget is not None:
            self.layout.removeWidget(self._embedded_widget)
            self._embedded_widget.deleteLater()
            self._embedded_widget = None
        if new_widget is not None:
            self._embedded_widget = new_widget
            self._embedded_widget.setParent(self)
            self.layout.addWidget(self._embedded_widget)
            self.err_label.hide()
            self._embedded_widget.show()
            self._is_connected = True

    def connect(self):
        """
        Establish the connection between the embedded widget and
        the channels associated with it.
        """
        if self._is_connected or self.embedded_widget is None:
            return
        establish_widget_connections(self.embedded_widget)
        self._is_connected = True

    def disconnect(self):
        """
        Disconnects the embedded widget from the channels
        associated with it.
        """
        if not self._is_connected or self.embedded_widget is None:
            return
        close_widget_connections(self.embedded_widget)
        self._is_connected = False

    @Property(bool)
    def loadWhenShown(self):
        """
        If True, only load and display the file once the
        PyDMEmbeddedDisplayWidget is visible on screen.  This is very useful
        if you have many different PyDMEmbeddedWidgets in different tabs of a
        QTabBar or PyDMTabBar: only the tab that the user is looking at will
        be loaded, which can greatly speed up the launch time of a display.
        
        If this property is changed from 'True' to 'False', and the file has
        not been loaded yet, it will be loaded immediately.
        
        Returns
        -------
        bool
        """
        return self._only_load_when_shown

    @loadWhenShown.setter
    def loadWhenShown(self, val):
        self._only_load_when_shown = val
        self.load_if_needed()

    @Property(bool)
    def disconnectWhenHidden(self):
        """
        Disconnect from PVs when this widget is not visible.

        Returns
        -------
        bool
        """
        return self._disconnect_when_hidden

    @disconnectWhenHidden.setter
    def disconnectWhenHidden(self, disconnect_when_hidden):
        """
        Disconnect from PVs when this widget is not visible.

        Parameters
        ----------
        disconnect_when_hidden : bool
        """
        self._disconnect_when_hidden = disconnect_when_hidden

    def showEvent(self, e):
        """
        Show events are sent to widgets that become visible on the screen.

        Parameters
        ----------
        event : QShowEvent
        """
        if self._only_load_when_shown:
            w = self.open_file()
            if w:
                self.embedded_widget = w
        if self.disconnectWhenHidden:
            self.connect()

    def hideEvent(self, e):
        """
        Hide events are sent to widgets that become invisible on the screen.

        Parameters
        ----------
        event : QHideEvent
        """
        if self.disconnectWhenHidden:
            self.disconnect()

    def _display_designer_load_error(self):
        self._load_error_timer = None
        logger.exception("Exception while opening embedded display file.",
                         exc_info=self._load_error)
        if self._load_error:
            self.display_error_text(self._load_error)
Exemple #3
0
class FilterWidget(filterBase):
    __clsName = "LocFilter"
    filterChangeDelay = 200

    varNameRex = re.compile(r"\{(\w*)\}")

    def tr(self, string):
        return QCoreApplication.translate(self.__clsName, string)

    def __init__(self, parent=None):
        super().__init__(parent)
        self._ui = filterClass()
        self._ui.setupUi(self)

        self._delayTimer = QTimer(self)
        self._delayTimer.setInterval(self.filterChangeDelay)
        self._delayTimer.setSingleShot(True)
        if not (qtpy.PYQT4 or qtpy.PYSIDE):
            self._delayTimer.setTimerType(Qt.PreciseTimer)
        self._delayTimer.timeout.connect(self.filterChanged)

        self._ui.filterEdit.textChanged.connect(self._delayTimer.start)

        self._menu = QMenu()
        self._menu.triggered.connect(self._addVariable)

    filterChanged = Signal()

    @Slot(list)
    def setVariables(self, var):
        self._menu.clear()
        for v in var:
            self._menu.addAction(v)

    def setFilterString(self, filt):
        self._ui.filterEdit.setPlainText(filt)

    @Property(str, fset=setFilterString, doc="String describing the filter")
    def filterString(self):
        s = self._ui.filterEdit.toPlainText()
        return self.varNameRex.subn("\\1", s)[0]

    def getFilter(self):
        filterStr = self.filterString
        filterStrList = filterStr.split("\n")

        def filterFunc(data):
            filter = np.ones(len(data), dtype=bool)
            for f in filterStrList:
                with suppress(Exception):
                    filter &= data.eval(f, local_dict={}, global_dict={})
            return filter

        return filterFunc

    @Slot(QAction)
    def _addVariable(self, act):
        self._ui.filterEdit.textCursor().insertText(act.text())

    @Slot(str)
    def on_showVarLabel_linkActivated(self, link):
        if not self._menu.isEmpty():
            self._menu.exec(QCursor.pos())