diff --git a/examples/camviewer/camviewer.ui b/examples/camviewer/camviewer.ui
index 03b3dd646..5f686903b 100644
--- a/examples/camviewer/camviewer.ui
+++ b/examples/camviewer/camviewer.ui
@@ -44,6 +44,9 @@
Displays an image from a PV.
+
+ 30
+
diff --git a/examples/image_processing/image_view.py b/examples/image_processing/image_view.py
index bde5119fa..0b4e1a9cb 100644
--- a/examples/image_processing/image_view.py
+++ b/examples/image_processing/image_view.py
@@ -1,49 +1,47 @@
-from pydm.PyQt.QtCore import QObject, pyqtSlot, pyqtSignal
-from pydm.widgets.channel import PyDMChannel
from pydm import Display
-import numpy as np
+import threading
from os import path
from skimage.feature import blob_doh
from marker import ImageMarker
from pyqtgraph import mkPen
+
class ImageViewer(Display):
+
def __init__(self, parent=None, args=None):
super(ImageViewer, self).__init__(parent=parent, args=args)
- self.image_channel = "ca://MTEST:TwoSpotImage"
- self.image_width_channel = "ca://MTEST:ImageWidth"
- self.ui.imageView.widthChannel = self.image_width_channel
- self.ui.imageView.image_channel = ""
- self.markers = []
-
+ self.markers_lock = threading.Lock()
+ self.ui.imageView.process_image = self.process_image
+ self.ui.imageView.newImageSignal.connect(self.draw_markers)
+ self.markers = list()
+ self.blobs = list()
+
def ui_filename(self):
return 'image_view.ui'
-
+
def ui_filepath(self):
return path.join(path.dirname(path.realpath(__file__)), self.ui_filename())
-
- @pyqtSlot(np.ndarray)
- def new_image_received(self, new_waveform):
- #Reshape the 1D waveform into 2D
- img = new_waveform.reshape((int(512),-1), order='C')
- #Find blobs in the image with scikit-image
- blobs= blob_doh(img, max_sigma=512, min_sigma=64, threshold=.02)
- #Remove any existing blob markers
- for m in self.markers:
- self.ui.imageView.getView().removeItem(m)
- self.markers = []
- #For each blob, add a blob marker to the image
- for blob in blobs:
- x, y, size = blob
- m = ImageMarker((y,x), size=size, pen=mkPen((100,100,255), width=3))
- self.ui.imageView.getView().addItem(m)
- self.markers.append(m)
- #Show number of blobs in text label
- self.ui.numBlobsLabel.setText(str(len(blobs)))
- #Send the original image data to the image widget
- self.ui.imageView.image_value_changed(new_waveform)
-
- def channels(self):
- return [PyDMChannel(address=self.image_channel, value_slot=self.new_image_received)]
+
+ def draw_markers(self, *args, **kwargs):
+ with self.markers_lock:
+ for m in self.markers:
+ if m in self.ui.imageView.getView().addedItems:
+ self.ui.imageView.getView().removeItem(m)
+
+ for blob in self.blobs:
+ x, y, size = blob
+ m = ImageMarker((y, x), size=size, pen=mkPen((100, 100, 255), width=3))
+ self.markers.append(m)
+ self.ui.imageView.getView().addItem(m)
+
+ self.ui.numBlobsLabel.setText(str(len(self.blobs)))
+
+ def process_image(self, new_image):
+ # Find blobs in the image with scikit-image
+ self.blobs = blob_doh(new_image, max_sigma=512, min_sigma=64, threshold=.02)
+
+ # Send the original image data to the image widget
+ return new_image
+
intelclass = ImageViewer
diff --git a/examples/image_processing/image_view.ui b/examples/image_processing/image_view.ui
index a999b4e32..a1a1d7fcc 100644
--- a/examples/image_processing/image_view.ui
+++ b/examples/image_processing/image_view.ui
@@ -22,11 +22,17 @@
+
+ PyDMImageView::Clike
+
-
+ ca://MTEST:TwoSpotImage
-
+ ca://MTEST:ImageWidth
+
+
+ 4
diff --git a/examples/image_view/image.ui b/examples/image_view/image.ui
index 8bdc510f7..92303857e 100644
--- a/examples/image_view/image.ui
+++ b/examples/image_view/image.ui
@@ -31,6 +31,9 @@
ca://MTEST:ImageWidth
+
+ 30
+
diff --git a/pydm/widgets/image.py b/pydm/widgets/image.py
index a7af081b7..0513ac42f 100644
--- a/pydm/widgets/image.py
+++ b/pydm/widgets/image.py
@@ -1,15 +1,19 @@
from ..PyQt.QtGui import QActionGroup
-from ..PyQt.QtCore import pyqtSlot, pyqtProperty, QTimer, Q_ENUMS
+from ..PyQt.QtCore import pyqtSignal, pyqtSlot, pyqtProperty, QTimer, Q_ENUMS, QThread
from pyqtgraph import ImageView
from pyqtgraph import ColorMap
from pyqtgraph.graphicsItems.ViewBox.ViewBoxMenu import ViewBoxMenu
import numpy as np
+import threading
+import logging
from .channel import PyDMChannel
from .colormaps import cmaps, cmap_names, PyDMColorMap
from .base import PyDMWidget
import pyqtgraph
pyqtgraph.setConfigOption('imageAxisOrder', 'row-major')
+logger = logging.getLogger(__name__)
+
class ReadingOrder(object):
"""Class to build ReadingOrder ENUM property."""
@@ -18,6 +22,56 @@ class ReadingOrder(object):
Clike = 1
+class ImageUpdateThread(QThread):
+ updateSignal = pyqtSignal(list)
+
+ def __init__(self, image_view):
+ QThread.__init__(self)
+ self.image_view = image_view
+
+ def run(self):
+ img = self.image_view.image_waveform
+ needs_redraw = self.image_view.needs_redraw
+ image_dimensions = len(img.shape)
+ width = self.image_view.imageWidth
+ reading_order = self.image_view.readingOrder
+ normalize_data = self.image_view._normalize_data
+ cm_min = self.image_view.cm_min
+ cm_max = self.image_view.cm_max
+
+ if not needs_redraw:
+ logging.debug("ImageUpdateThread - needs redraw is False. Aborting.")
+ return
+ if image_dimensions == 1:
+ if width < 1:
+ # We don't have a width for this image yet, so we can't draw it
+ logging.debug(
+ "ImageUpdateThread - no width available. Aborting.")
+ return
+ try:
+ if reading_order == ReadingOrder.Clike:
+ img = img.reshape((-1, width), order='C')
+ else:
+ img = img.reshape((width, -1), order='F')
+ except ValueError:
+ logger.error("Invalid width for image during reshape: %d", width)
+
+ if len(img) <= 0:
+ return
+ logging.debug("ImageUpdateThread - Will Process Image")
+ img = self.image_view.process_image(img)
+ if normalize_data:
+ mini = img.min()
+ maxi = img.max()
+ else:
+ mini = cm_min
+ maxi = cm_max
+ logging.debug("ImageUpdateThread - Emit Update Signal")
+ self.updateSignal.emit([mini, maxi, img])
+ logging.debug("ImageUpdateThread - Set Needs Redraw -> False")
+ self.image_view.needs_redraw = False
+
+
class PyDMImageView(ImageView, PyDMWidget, PyDMColorMap, ReadingOrder):
"""
A PyQtGraph ImageView with support for Channels and more from PyDM.
@@ -29,6 +83,9 @@ class PyDMImageView(ImageView, PyDMWidget, PyDMColorMap, ReadingOrder):
relative to the :attr:`colorMapMin` and :attr:`colorMapMax` property or to
the minimum and maximum values of the image.
+ Use the :attr:`newImageSignal` to hook up to a signal that is emitted when a new
+ image is rendered in the widget.
+
Parameters
----------
parent : QWidget
@@ -51,12 +108,14 @@ def __init__(self, parent=None, image_channel=None, width_channel=None):
"""Initialize widget."""
ImageView.__init__(self, parent)
PyDMWidget.__init__(self)
+ self.thread = None
self.axes = dict({'t': None, "x": 0, "y": 1, "c": None})
self._imagechannel = image_channel
self._widthchannel = width_channel
self.image_waveform = np.zeros(0)
self._image_width = 0
self._normalize_data = False
+ self._auto_downsample = True
# Hide some itens of the widget.
self.ui.histogram.hide()
@@ -91,6 +150,7 @@ def __init__(self, parent=None, image_channel=None, width_channel=None):
self.redraw_timer.timeout.connect(self.redrawImage)
self._redraw_rate = 30
self.maxRedrawRate = self._redraw_rate
+ self.newImageSignal = self.getImageItem().sigImageChanged
def widget_ctx_menu(self):
"""
@@ -270,6 +330,7 @@ def image_value_changed(self, new_image):
"""
if new_image is None or new_image.size == 0:
return
+ logging.debug("ImageView Received New Image - Needs Redraw -> True")
self.image_waveform = new_image
self.needs_redraw = True
@@ -287,42 +348,78 @@ def image_width_changed(self, new_width):
return
self._image_width = int(new_width)
+ def process_image(self, image):
+ """
+ Boilerplate method to be used by applications in order to
+ add calculations and also modify the image before it is
+ displayed at the widget.
+
+ .. warning::
+ This code runs in a separated QThread so it **MUST** not try to write
+ to QWidgets.
+
+ Parameters
+ ----------
+ image : np.ndarray
+ The Image Data as a 2D numpy array
+
+ Returns
+ -------
+ np.ndarray
+ The Image Data as a 2D numpy array after processing.
+ """
+ return image
+
def redrawImage(self):
"""
Set the image data into the ImageItem, if needed.
If necessary, reshape the image to 2D first.
"""
- if not self.needs_redraw:
+ if self.thread is not None and not self.thread.isFinished():
+ logger.warning(
+ "Image processing has taken longer than the refresh rate.")
return
- image_dimensions = len(self.image_waveform.shape)
- if image_dimensions == 1:
- if self.imageWidth < 1:
- # We don't have a width for this image yet, so we can't draw it
- return
- if self.readingOrder == ReadingOrder.Clike:
- img = self.image_waveform.reshape((-1, self.imageWidth),
- order='C')
- else:
- img = self.image_waveform.reshape((self.imageWidth, -1),
- order='F')
- else:
- img = self.image_waveform
-
- if len(img) <= 0:
- return
- if self._normalize_data:
- mini = self.image_waveform.min()
- maxi = self.image_waveform.max()
- else:
- mini = self.cm_min
- maxi = self.cm_max
+ self.thread = ImageUpdateThread(self)
+ self.thread.updateSignal.connect(self.__updateDisplay)
+ logging.debug("ImageView RedrawImage Thread Launched")
+ self.thread.start()
+
+ @pyqtSlot(list)
+ def __updateDisplay(self, data):
+ logging.debug("ImageView Update Display with new image")
+ mini, maxi = data[0], data[1]
+ img = data[2]
self.getImageItem().setLevels([mini, maxi])
self.getImageItem().setImage(
img,
autoLevels=False,
- autoDownsample=True)
- self.needs_redraw = False
+ autoDownsample=self.autoDownsample)
+
+ @pyqtProperty(bool)
+ def autoDownsample(self):
+ """
+ Return if we should or not apply the
+ autoDownsample option to PyQtGraph.
+
+ Return
+ ------
+ bool
+ """
+ return self._auto_downsample
+
+ @autoDownsample.setter
+ def autoDownsample(self, new_value):
+ """
+ Whether we should or not apply the
+ autoDownsample option to PyQtGraph.
+
+ Parameters
+ ----------
+ new_value: bool
+ """
+ if new_value != self._auto_downsample:
+ self._auto_downsample = new_value
@pyqtProperty(int)
def imageWidth(self):
@@ -371,9 +468,8 @@ def normalizeData(self, new_norm):
----------
new_norm: bool
"""
- if self._normalize_data == new_norm:
- return
- self._normalize_data = new_norm
+ if self._normalize_data != new_norm:
+ self._normalize_data = new_norm
@pyqtProperty(ReadingOrder)
def readingOrder(self):
@@ -504,4 +600,4 @@ def maxRedrawRate(self, redraw_rate):
redraw_rate : int
"""
self._redraw_rate = redraw_rate
- self.redraw_timer.setInterval(int((1.0/self._redraw_rate)*1000))
+ self.redraw_timer.setInterval(int((1.0 / self._redraw_rate) * 1000))