pyqtgraph/imageview/ImageView.py

655 lines
24 KiB
Python
Raw Normal View History

2010-03-22 05:48:52 +00:00
# -*- coding: utf-8 -*-
"""
ImageView.py - Widget for basic image dispay and analysis
Copyright 2010 Luke Campagnola
Distributed under MIT/X11 license. See license.txt for more infomation.
Widget used for displaying 2D or 3D data. Features:
- float or int (including 16-bit int) image display via ImageItem
- zoom/pan via GraphicsView
- black/white level controls
- time slider for 3D data sets
- ROI plotting
- Image normalization through a variety of methods
"""
from ImageViewTemplate import *
from pyqtgraph.graphicsItems.ImageItem import *
from pyqtgraph.graphicsItems.ROI import *
from pyqtgraph.graphicsItems.LinearRegionItem import *
from pyqtgraph.graphicsItems.InfiniteLine import *
from pyqtgraph.graphicsItems.ViewBox import *
#from widgets import ROI
from pyqtgraph.Qt import QtCore, QtGui
import sys
2010-11-22 03:50:04 +00:00
#from numpy import ndarray
2012-03-02 03:17:55 +00:00
import pyqtgraph.ptime as ptime
2010-11-22 03:50:04 +00:00
import numpy as np
2012-03-02 03:17:55 +00:00
import pyqtgraph.debug as debug
from pyqtgraph.SignalProxy import SignalProxy
2010-03-22 05:48:52 +00:00
class PlotROI(ROI):
def __init__(self, size):
ROI.__init__(self, pos=[0,0], size=size, scaleSnap=True, translateSnap=True)
self.addScaleHandle([1, 1], [0, 0])
self.addRotateHandle([0, 0], [0.5, 0.5])
2010-03-22 05:48:52 +00:00
class ImageView(QtGui.QWidget):
2012-04-18 04:02:15 +00:00
"""
Widget used for display and analysis of image data.
Implements many features:
2012-04-18 04:02:15 +00:00
* Displays 2D and 3D image data. For 3D data, a z-axis
slider is displayed allowing the user to select which frame is displayed.
* Displays histogram of image data with movable region defining the dark/light levels
* Editable gradient provides a color lookup table
* Frame slider may also be moved using left/right arrow keys as well as pgup, pgdn, home, and end.
* Basic analysis features including:
* ROI and embedded plot for measuring image values across frames
* Image normalization / background subtraction
Basic Usage::
imv = pg.ImageView()
imv.show()
imv.setImage(data)
"""
sigTimeChanged = QtCore.Signal(object, object)
sigProcessingChanged = QtCore.Signal(object)
2010-03-22 05:48:52 +00:00
def __init__(self, parent=None, name="ImageView", *args):
QtGui.QWidget.__init__(self, parent, *args)
self.levelMax = 4096
self.levelMin = 0
self.name = name
self.image = None
self.axes = {}
2010-03-22 05:48:52 +00:00
self.imageDisp = None
self.ui = Ui_Form()
self.ui.setupUi(self)
self.scene = self.ui.graphicsView.scene()
self.ignoreTimeLine = False
#if 'linux' in sys.platform.lower(): ## Stupid GL bug in linux.
# self.ui.graphicsView.setViewport(QtGui.QWidget())
#self.ui.graphicsView.enableMouse(True)
#self.ui.graphicsView.autoPixelRange = False
#self.ui.graphicsView.setAspectLocked(True)
#self.ui.graphicsView.invertY()
#self.ui.graphicsView.enableMouse()
self.view = ViewBox()
self.ui.graphicsView.setCentralItem(self.view)
self.view.setAspectLocked(True)
self.view.invertY()
#self.ticks = [t[0] for t in self.ui.gradientWidget.listTicks()]
#self.ticks[0].colorChangeAllowed = False
#self.ticks[1].colorChangeAllowed = False
#self.ui.gradientWidget.allowAdd = False
#self.ui.gradientWidget.setTickColor(self.ticks[1], QtGui.QColor(255,255,255))
#self.ui.gradientWidget.setOrientation('right')
2010-03-22 05:48:52 +00:00
self.imageItem = ImageItem()
self.view.addItem(self.imageItem)
2010-03-22 05:48:52 +00:00
self.currentIndex = 0
self.ui.histogram.setImageItem(self.imageItem)
2010-03-22 05:48:52 +00:00
self.ui.normGroup.hide()
self.roi = PlotROI(10)
self.roi.setZValue(20)
self.view.addItem(self.roi)
2010-03-22 05:48:52 +00:00
self.roi.hide()
self.normRoi = PlotROI(10)
self.normRoi.setPen(QtGui.QPen(QtGui.QColor(255,255,0)))
self.normRoi.setZValue(20)
self.view.addItem(self.normRoi)
self.normRoi.hide()
#self.ui.roiPlot.hide()
2010-03-22 05:48:52 +00:00
self.roiCurve = self.ui.roiPlot.plot()
self.timeLine = InfiniteLine(0, movable=True)
self.timeLine.setPen(QtGui.QPen(QtGui.QColor(255, 255, 0, 200)))
self.timeLine.setZValue(1)
self.ui.roiPlot.addItem(self.timeLine)
self.ui.splitter.setSizes([self.height()-35, 35])
self.ui.roiPlot.hideAxis('left')
self.keysPressed = {}
self.playTimer = QtCore.QTimer()
self.playRate = 0
self.lastPlayTime = 0
#self.normLines = []
#for i in [0,1]:
#l = InfiniteLine(self.ui.roiPlot, 0)
#l.setPen(QtGui.QPen(QtGui.QColor(0, 100, 200, 200)))
#self.ui.roiPlot.addItem(l)
#self.normLines.append(l)
#l.hide()
self.normRgn = LinearRegionItem()
self.normRgn.setZValue(0)
self.ui.roiPlot.addItem(self.normRgn)
self.normRgn.hide()
2010-03-22 05:48:52 +00:00
## wrap functions from view box
for fn in ['addItem', 'removeItem']:
setattr(self, fn, getattr(self.view, fn))
## wrap functions from histogram
for fn in ['setHistogramRange', 'autoHistogramRange', 'getLookupTable', 'getLevels']:
setattr(self, fn, getattr(self.ui.histogram, fn))
2010-03-22 05:48:52 +00:00
self.timeLine.sigPositionChanged.connect(self.timeLineChanged)
#self.ui.gradientWidget.sigGradientChanged.connect(self.updateImage)
self.ui.roiBtn.clicked.connect(self.roiClicked)
self.roi.sigRegionChanged.connect(self.roiChanged)
self.ui.normBtn.toggled.connect(self.normToggled)
self.ui.normDivideRadio.clicked.connect(self.normRadioChanged)
self.ui.normSubtractRadio.clicked.connect(self.normRadioChanged)
self.ui.normOffRadio.clicked.connect(self.normRadioChanged)
self.ui.normROICheck.clicked.connect(self.updateNorm)
self.ui.normFrameCheck.clicked.connect(self.updateNorm)
self.ui.normTimeRangeCheck.clicked.connect(self.updateNorm)
self.playTimer.timeout.connect(self.timeout)
self.normProxy = SignalProxy(self.normRgn.sigRegionChanged, slot=self.updateNorm)
self.normRoi.sigRegionChangeFinished.connect(self.updateNorm)
2010-03-22 05:48:52 +00:00
self.ui.roiPlot.registerPlot(self.name + '_ROI')
self.noRepeatKeys = [QtCore.Qt.Key_Right, QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Down, QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown]
self.roiClicked() ## initialize roi plot to correct shape / visibility
2012-04-18 04:02:15 +00:00
def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, xvals=None, pos=None, scale=None):
"""
Set the image to be displayed in the widget.
============== =======================================================================
**Arguments:**
*img* (numpy array) the image to be displayed.
*xvals* (numpy array) 1D array of z-axis values corresponding to the third axis
in a 3D image. For video, this array should contain the time of each frame.
*autoRange* (bool) whether to scale/pan the view to fit the image.
*autoLevels* (bool) whether to update the white/black levels to fit the image.
*levels* (min, max); the white and black level values to use.
*axes* Dictionary indicating the interpretation for each axis.
This is only needed to override the default guess. Format is::
{'t':0, 'x':1, 'y':2, 'c':3};
============== =======================================================================
"""
prof = debug.Profiler('ImageView.setImage', disabled=True)
if not isinstance(img, np.ndarray):
raise Exception("Image must be specified as ndarray.")
self.image = img
if xvals is not None:
self.tVals = xvals
elif hasattr(img, 'xvals'):
try:
self.tVals = img.xvals(0)
except:
self.tVals = np.arange(img.shape[0])
else:
self.tVals = np.arange(img.shape[0])
#self.ui.timeSlider.setValue(0)
#self.ui.normStartSlider.setValue(0)
#self.ui.timeSlider.setMaximum(img.shape[0]-1)
prof.mark('1')
if axes is None:
if img.ndim == 2:
self.axes = {'t': None, 'x': 0, 'y': 1, 'c': None}
elif img.ndim == 3:
if img.shape[2] <= 4:
self.axes = {'t': None, 'x': 0, 'y': 1, 'c': 2}
else:
self.axes = {'t': 0, 'x': 1, 'y': 2, 'c': None}
elif img.ndim == 4:
self.axes = {'t': 0, 'x': 1, 'y': 2, 'c': 3}
else:
raise Exception("Can not interpret image with dimensions %s" % (str(img.shape)))
elif isinstance(axes, dict):
self.axes = axes.copy()
elif isinstance(axes, list) or isinstance(axes, tuple):
self.axes = {}
for i in range(len(axes)):
self.axes[axes[i]] = i
else:
raise Exception("Can not interpret axis specification %s. Must be like {'t': 2, 'x': 0, 'y': 1} or ('t', 'x', 'y', 'c')" % (str(axes)))
for x in ['t', 'x', 'y', 'c']:
self.axes[x] = self.axes.get(x, None)
prof.mark('2')
self.imageDisp = None
prof.mark('3')
self.currentIndex = 0
self.updateImage()
if levels is None and autoLevels:
self.autoLevels()
if levels is not None: ## this does nothing since getProcessedImage sets these values again.
self.levelMax = levels[1]
self.levelMin = levels[0]
if self.ui.roiBtn.isChecked():
self.roiChanged()
prof.mark('4')
if self.axes['t'] is not None:
#self.ui.roiPlot.show()
self.ui.roiPlot.setXRange(self.tVals.min(), self.tVals.max())
self.timeLine.setValue(0)
#self.ui.roiPlot.setMouseEnabled(False, False)
if len(self.tVals) > 1:
start = self.tVals.min()
stop = self.tVals.max() + abs(self.tVals[-1] - self.tVals[0]) * 0.02
elif len(self.tVals) == 1:
start = self.tVals[0] - 0.5
stop = self.tVals[0] + 0.5
else:
start = 0
stop = 1
for s in [self.timeLine, self.normRgn]:
s.setBounds([start, stop])
#else:
#self.ui.roiPlot.hide()
prof.mark('5')
self.imageItem.resetTransform()
if scale is not None:
self.imageItem.scale(*scale)
if pos is not None:
self.imageItem.setPos(*pos)
prof.mark('6')
if autoRange:
self.autoRange()
self.roiClicked()
prof.mark('7')
prof.finish()
def play(self, rate):
"""Begin automatically stepping frames forward at the given rate (in fps).
This can also be accessed by pressing the spacebar."""
#print "play:", rate
self.playRate = rate
if rate == 0:
self.playTimer.stop()
return
self.lastPlayTime = ptime.time()
if not self.playTimer.isActive():
self.playTimer.start(16)
def autoLevels(self):
"""Set the min/max levels automatically to match the image data."""
#image = self.getProcessedImage()
self.setLevels(self.levelMin, self.levelMax)
#self.ui.histogram.imageChanged(autoLevel=True)
def setLevels(self, min, max):
"""Set the min/max (bright and dark) levels."""
self.ui.histogram.setLevels(min, max)
def autoRange(self):
"""Auto scale and pan the view around the image."""
image = self.getProcessedImage()
#self.ui.graphicsView.setRange(QtCore.QRectF(0, 0, image.shape[self.axes['x']], image.shape[self.axes['y']]), padding=0., lockAspect=True)
self.view.setRange(self.imageItem.boundingRect(), padding=0.)
def getProcessedImage(self):
"""Returns the image data after it has been processed by any normalization options in use."""
if self.imageDisp is None:
image = self.normalize(self.image)
self.imageDisp = image
self.levelMin, self.levelMax = map(float, ImageView.quickMinMax(self.imageDisp))
self.ui.histogram.setHistogramRange(self.levelMin, self.levelMax)
return self.imageDisp
def close(self):
2012-04-18 04:02:15 +00:00
"""Closes the widget nicely, making sure to clear the graphics scene and release memory."""
self.ui.roiPlot.close()
self.ui.graphicsView.close()
#self.ui.gradientWidget.sigGradientChanged.disconnect(self.updateImage)
self.scene.clear()
del self.image
del self.imageDisp
self.setParent(None)
def keyPressEvent(self, ev):
#print ev.key()
if ev.key() == QtCore.Qt.Key_Space:
if self.playRate == 0:
fps = (self.getProcessedImage().shape[0]-1) / (self.tVals[-1] - self.tVals[0])
self.play(fps)
#print fps
2010-03-22 05:48:52 +00:00
else:
self.play(0)
ev.accept()
elif ev.key() == QtCore.Qt.Key_Home:
self.setCurrentIndex(0)
self.play(0)
ev.accept()
elif ev.key() == QtCore.Qt.Key_End:
self.setCurrentIndex(self.getProcessedImage().shape[0]-1)
self.play(0)
ev.accept()
elif ev.key() in self.noRepeatKeys:
ev.accept()
if ev.isAutoRepeat():
return
self.keysPressed[ev.key()] = 1
self.evalKeyState()
else:
QtGui.QWidget.keyPressEvent(self, ev)
def keyReleaseEvent(self, ev):
if ev.key() in [QtCore.Qt.Key_Space, QtCore.Qt.Key_Home, QtCore.Qt.Key_End]:
ev.accept()
elif ev.key() in self.noRepeatKeys:
ev.accept()
if ev.isAutoRepeat():
return
try:
del self.keysPressed[ev.key()]
except:
self.keysPressed = {}
self.evalKeyState()
else:
QtGui.QWidget.keyReleaseEvent(self, ev)
def evalKeyState(self):
if len(self.keysPressed) == 1:
key = self.keysPressed.keys()[0]
if key == QtCore.Qt.Key_Right:
self.play(20)
self.jumpFrames(1)
self.lastPlayTime = ptime.time() + 0.2 ## 2ms wait before start
## This happens *after* jumpFrames, since it might take longer than 2ms
elif key == QtCore.Qt.Key_Left:
self.play(-20)
self.jumpFrames(-1)
self.lastPlayTime = ptime.time() + 0.2
elif key == QtCore.Qt.Key_Up:
self.play(-100)
elif key == QtCore.Qt.Key_Down:
self.play(100)
elif key == QtCore.Qt.Key_PageUp:
self.play(-1000)
elif key == QtCore.Qt.Key_PageDown:
self.play(1000)
else:
self.play(0)
2010-03-22 05:48:52 +00:00
def timeout(self):
now = ptime.time()
dt = now - self.lastPlayTime
if dt < 0:
return
n = int(self.playRate * dt)
#print n, dt
if n != 0:
#print n, dt, self.lastPlayTime
self.lastPlayTime += (float(n)/self.playRate)
if self.currentIndex+n > self.image.shape[0]:
self.play(0)
self.jumpFrames(n)
def setCurrentIndex(self, ind):
2012-04-18 04:02:15 +00:00
"""Set the currently displayed frame index."""
self.currentIndex = np.clip(ind, 0, self.getProcessedImage().shape[0]-1)
self.updateImage()
self.ignoreTimeLine = True
self.timeLine.setValue(self.tVals[self.currentIndex])
self.ignoreTimeLine = False
def jumpFrames(self, n):
2012-04-18 04:02:15 +00:00
"""Move video frame ahead n frames (may be negative)"""
if self.axes['t'] is not None:
self.setCurrentIndex(self.currentIndex + n)
def normRadioChanged(self):
self.imageDisp = None
self.updateImage()
self.roiChanged()
self.sigProcessingChanged.emit(self)
def updateNorm(self):
#for l, sl in zip(self.normLines, [self.ui.normStartSlider, self.ui.normStopSlider]):
#if self.ui.normTimeRangeCheck.isChecked():
#l.show()
#else:
#l.hide()
#i, t = self.timeIndex(sl)
#l.setPos(t)
if self.ui.normTimeRangeCheck.isChecked():
#print "show!"
self.normRgn.show()
else:
self.normRgn.hide()
if self.ui.normROICheck.isChecked():
#print "show!"
self.normRoi.show()
else:
self.normRoi.hide()
2010-03-22 05:48:52 +00:00
if not self.ui.normOffRadio.isChecked():
self.imageDisp = None
self.updateImage()
self.roiChanged()
self.sigProcessingChanged.emit(self)
2010-03-22 05:48:52 +00:00
def normToggled(self, b):
self.ui.normGroup.setVisible(b)
self.normRoi.setVisible(b and self.ui.normROICheck.isChecked())
self.normRgn.setVisible(b and self.ui.normTimeRangeCheck.isChecked())
2010-03-22 05:48:52 +00:00
def hasTimeAxis(self):
return 't' in self.axes and self.axes['t'] is not None
2010-03-22 05:48:52 +00:00
def roiClicked(self):
showRoiPlot = False
2010-03-22 05:48:52 +00:00
if self.ui.roiBtn.isChecked():
showRoiPlot = True
2010-03-22 05:48:52 +00:00
self.roi.show()
#self.ui.roiPlot.show()
self.ui.roiPlot.setMouseEnabled(True, True)
self.ui.splitter.setSizes([self.height()*0.6, self.height()*0.4])
self.roiCurve.show()
2010-03-22 05:48:52 +00:00
self.roiChanged()
self.ui.roiPlot.showAxis('left')
2010-03-22 05:48:52 +00:00
else:
self.roi.hide()
self.ui.roiPlot.setMouseEnabled(False, False)
self.roiCurve.hide()
self.ui.roiPlot.hideAxis('left')
if self.hasTimeAxis():
showRoiPlot = True
mn = self.tVals.min()
mx = self.tVals.max()
self.ui.roiPlot.setXRange(mn, mx, padding=0.01)
self.timeLine.show()
self.timeLine.setBounds([mn, mx])
self.ui.roiPlot.show()
if not self.ui.roiBtn.isChecked():
self.ui.splitter.setSizes([self.height()-35, 35])
else:
self.timeLine.hide()
#self.ui.roiPlot.hide()
self.ui.roiPlot.setVisible(showRoiPlot)
2010-03-22 05:48:52 +00:00
def roiChanged(self):
if self.image is None:
return
image = self.getProcessedImage()
if image.ndim == 2:
axes = (0, 1)
elif image.ndim == 3:
axes = (1, 2)
else:
return
2010-11-22 03:50:04 +00:00
data = self.roi.getArrayRegion(image.view(np.ndarray), self.imageItem, axes)
2010-03-22 05:48:52 +00:00
if data is not None:
while data.ndim > 1:
data = data.mean(axis=1)
if image.ndim == 3:
self.roiCurve.setData(y=data, x=self.tVals)
else:
self.roiCurve.setData(y=data, x=range(len(data)))
2010-03-22 05:48:52 +00:00
#self.ui.roiPlot.replot()
@staticmethod
def quickMinMax(data):
while data.size > 1e6:
ax = np.argmax(data.shape)
sl = [slice(None)] * data.ndim
sl[ax] = slice(None, None, 2)
data = data[sl]
return data.min(), data.max()
2010-03-22 05:48:52 +00:00
def normalize(self, image):
if self.ui.normOffRadio.isChecked():
return image
div = self.ui.normDivideRadio.isChecked()
2010-11-22 03:50:04 +00:00
norm = image.view(np.ndarray).copy()
2010-03-22 05:48:52 +00:00
#if div:
#norm = ones(image.shape)
#else:
#norm = zeros(image.shape)
if div:
norm = norm.astype(np.float32)
2010-03-22 05:48:52 +00:00
if self.ui.normTimeRangeCheck.isChecked() and image.ndim == 3:
(sind, start) = self.timeIndex(self.normRgn.lines[0])
(eind, end) = self.timeIndex(self.normRgn.lines[1])
2010-03-22 05:48:52 +00:00
#print start, end, sind, eind
n = image[sind:eind+1].mean(axis=0)
n.shape = (1,) + n.shape
if div:
norm /= n
else:
norm -= n
if self.ui.normFrameCheck.isChecked() and image.ndim == 3:
n = image.mean(axis=1).mean(axis=1)
n.shape = n.shape + (1, 1)
if div:
norm /= n
else:
norm -= n
if self.ui.normROICheck.isChecked() and image.ndim == 3:
n = self.normRoi.getArrayRegion(norm, self.imageItem, (1, 2)).mean(axis=1).mean(axis=1)
n = n[:,np.newaxis,np.newaxis]
#print start, end, sind, eind
if div:
norm /= n
else:
norm -= n
2010-03-22 05:48:52 +00:00
return norm
def timeLineChanged(self):
#(ind, time) = self.timeIndex(self.ui.timeSlider)
if self.ignoreTimeLine:
return
self.play(0)
(ind, time) = self.timeIndex(self.timeLine)
2010-03-22 05:48:52 +00:00
if ind != self.currentIndex:
self.currentIndex = ind
self.updateImage()
#self.timeLine.setPos(time)
#self.emit(QtCore.SIGNAL('timeChanged'), ind, time)
self.sigTimeChanged.emit(ind, time)
2010-03-22 05:48:52 +00:00
def updateImage(self):
## Redraw image on screen
if self.image is None:
return
image = self.getProcessedImage()
#print "update:", image.ndim, image.max(), image.min(), self.blackLevel(), self.whiteLevel()
if self.axes['t'] is None:
#self.ui.timeSlider.hide()
self.imageItem.updateImage(image)
#self.ui.roiPlot.hide()
#self.ui.roiBtn.hide()
2010-03-22 05:48:52 +00:00
else:
#self.ui.roiBtn.show()
self.ui.roiPlot.show()
#self.ui.timeSlider.show()
self.imageItem.updateImage(image[self.currentIndex])
2010-03-22 05:48:52 +00:00
2010-03-22 05:48:52 +00:00
def timeIndex(self, slider):
2012-04-18 04:02:15 +00:00
## Return the time and frame index indicated by a slider
2010-03-22 05:48:52 +00:00
if self.image is None:
return (0,0)
#v = slider.value()
#vmax = slider.maximum()
#f = float(v) / vmax
t = slider.value()
#t = 0.0
2010-03-22 05:48:52 +00:00
#xv = self.image.xvals('Time')
xv = self.tVals
if xv is None:
ind = int(t)
#ind = int(f * self.image.shape[0])
2010-03-22 05:48:52 +00:00
else:
if len(xv) < 2:
return (0,0)
totTime = xv[-1] + (xv[-1]-xv[-2])
#t = f * totTime
2010-11-22 03:50:04 +00:00
inds = np.argwhere(xv < t)
2010-03-22 05:48:52 +00:00
if len(inds) < 1:
return (0,t)
ind = inds[-1,0]
#print ind
return ind, t
#def whiteLevel(self):
#return self.levelMin + (self.levelMax-self.levelMin) * self.ui.gradientWidget.tickValue(self.ticks[1])
##return self.levelMin + (self.levelMax-self.levelMin) * self.ui.whiteSlider.value() / self.ui.whiteSlider.maximum()
2010-03-22 05:48:52 +00:00
#def blackLevel(self):
#return self.levelMin + (self.levelMax-self.levelMin) * self.ui.gradientWidget.tickValue(self.ticks[0])
##return self.levelMin + ((self.levelMax-self.levelMin) / self.ui.blackSlider.maximum()) * self.ui.blackSlider.value()