Skip to content

Commit

Permalink
Merge pull request #336 from hhslepicka/image_callback
Browse files Browse the repository at this point in the history
ENH: Add  method to Image View so users can easily hook-up data processing code of their own.
  • Loading branch information
hhslepicka authored Jun 5, 2018
2 parents 4f0e520 + f084d06 commit a0d3252
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 66 deletions.
3 changes: 3 additions & 0 deletions examples/camviewer/camviewer.ui
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@
<property name="whatsThis">
<string>Displays an image from a PV.</string>
</property>
<property name="maxRedrawRate" stdset="0">
<number>30</number>
</property>
</widget>
</item>
</layout>
Expand Down
66 changes: 32 additions & 34 deletions examples/image_processing/image_view.py
Original file line number Diff line number Diff line change
@@ -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
10 changes: 8 additions & 2 deletions examples/image_processing/image_view.ui
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,17 @@
<property name="whatsThis">
<string/>
</property>
<property name="readingOrder" stdset="0">
<enum>PyDMImageView::Clike</enum>
</property>
<property name="imageChannel" stdset="0">
<string/>
<string>ca://MTEST:TwoSpotImage</string>
</property>
<property name="widthChannel" stdset="0">
<string/>
<string>ca://MTEST:ImageWidth</string>
</property>
<property name="maxRedrawRate" stdset="0">
<number>4</number>
</property>
</widget>
</item>
Expand Down
3 changes: 3 additions & 0 deletions examples/image_view/image.ui
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
<property name="widthChannel" stdset="0">
<string>ca://MTEST:ImageWidth</string>
</property>
<property name="maxRedrawRate" stdset="0">
<number>30</number>
</property>
</widget>
</item>
</layout>
Expand Down
156 changes: 126 additions & 30 deletions pydm/widgets/image.py
Original file line number Diff line number Diff line change
@@ -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."""
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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

Expand All @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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))

0 comments on commit a0d3252

Please sign in to comment.