Python+Qt+OpenCV界面(03)
PyQt 作为 Python 和 Qt 的bridge, 可以运行于所有平台。
1. 绑定
使用Python的getter/setter这种property, 可以把控件属性绑定到python变量。
例如,ui界面有某edit,python代码有某变量var,怎么实现:ui界面edit内容改变则变量var即更新、变量var改变则ui界面edit内容即更新?
两个方式:
* 第一个, 界面改变更新变量可以通过signal/slot实现,例如:
myQLine.textChanged.connect(self.myVAR)
但是反过来麻烦,因为myVAR只是个plain object,python没有内建的C#那样的实现方式。
* 使用Python getter/setter这个property, 从而self.myVAR成为 Property, 例如:
class Foo(object):
@property
def xx(self):
“””This method runs whenever you try to access self.xx”””
print(“Getting self.xx”)
return self._xx
@xx.setter
def xx(self, value):
“””This method runs whenever you try to set self.xx”””
print(“Setting self.xx to %s”%(value,))
self._xx = value
# Here add code to update the control in this setter method. …
# so whenever anything modifies the value of xx, the QLineEdit will be updated.
总体上:
@yourName.setter
def yourName(self, value):
self.myQLineEdit.setText(value)
self._name = value
# Note that the name data is actually being held in an attribute _name
# because it has to differ from the name of the getter/setter.
2. 信号连接
信号与槽机制是Qt最重要特性,提供了任意两个QT对象之间的通信机制。 信号会在某个特定情况或动作下被触发,槽是用于接收并处理信号的函数。
2.1 传统的signals/slot连接方式
信号与槽机制常用的连接方式为:
connect(Object1,SIGNAL(signal),Object2,SLOT(slot))
connect函数中的Object1和Object2,两个对象,signal是Object1对象的信号,注意要用SIGNAL宏包起来。当一个特定事件发生的时候(如点击按钮)或者Object1调用emit函数的时候,signal信号被发射。slot(槽)就是一个可以被调用处理特定信号的函数(或方法),是普通的对象成员函数。
2.2 PyQt4 以后可以采用新的信号与槽方式
After PyQt4, a new customized signals can be defined as class attributes using the pyqtSignal() factory:
PyQt4.QtCore.pyqtSignal(types[, name])
valueChanged = pyqtSignal([int], [‘QString’])
稍微完整的像这样:
from PyQt4 import QtCore
class MyQObject(QtCore.QObject):
# 定义一个无参数的信号
signal1 = QtCore.pyqtSignal()
# 定义一个参数的信号,参数类型为整数,参数名称为qtSignal2
signal2 = QtCore.pyqtSignal(int, name=’qtSignal2′)
def connectSigSlot(self):
signal1.connect(self.myReceiver1)
signal2.connect(self.myReceiver2)
def myReceiver1(self):
print ‘myReceiver1 called’
def myReceiver2(self, arg):
print ‘myReceiver2 called with argument value %d’ % arg
def myEmitter(self, arg):
signal1.emit()
signal2.emit(10)
新的singal/slot的定义与使用方式是PyQT 4.5中的一大改革。可以让PyQT程序更清楚易读。PyQT 4.5以后的版本建议用这种新方式。
3. Pyqt5的
PyQt5动定义了很多QT内建信号, 但是为了灵活使用信号与槽机制,可以自定义signal.
这样通过 pyqtSignal()方法定义新的信号,新的信号作为类的属性。
3.1 信号定义
新的信号应该定义在QObject的子类中。新的信号必须作为定义类的一部分,不允许将信号作为类的属性在类定义之后通过动态的方式进行添加。通过这种方式新的信号才能自动的添加到QMetaObject类中。这就意味这新定义的信号将会出现在Qt-Designer,并且可以通过QMetaObject API实现内省。
例如:
# 定义一个“closed”信号,该信号没有参数
closed= pyqtSignal()
# 定义一个”range_changed”信号,该信号有两个int类型的参数
range_changed = pyqtSignal(int, int, name=’rangeChanged’)
helpSignal = pyqtSignal(str) # helpSignal 为str参数类型的信号
printSignal = pyqtSignal(list) # printSignal 为list参数类型的信号
# 声明一个多重载版本的信号,包括: 一个带int和str类型参数的信号 以及 带str参数的信号
previewSignal = pyqtSignal([int,str],[str])
3.2 信号和槽绑定
self.helpSignal.connect(self.showHelpMessage)
self.printSignal.connect(self.printPaper)
# 存在两个版本,从因此在绑定的时候需要显式的指定信号和槽的绑定关系。
self.previewSignal[str].connect(self.previewPaper)
self.previewSignal[int,str].connect(self.previewPaperWithArgs)
3.3 信号发射
自定义信号的发射,通过emit()方法类实现:
self.printButton.clicked.connect(self.emitPrintSignal)
self.previewButton.clicked.connect(self.emitPreviewSignal)
def emitPrintSignal(self):
pList = []
pList.append(self.numberSpinBox.value ())
pList.append(self.styleCombo.currentText())
self.printSignal.emit(pList)
def emitPreviewSignal(self):
if self.previewStatus.isChecked() == True:
self.previewSignal[int,str].emit(1080,” Full Screen”)
elif self.previewStatus.isChecked() == False:
self.previewSignal[str].emit(“Preview”)
3.4 槽函数实现
通过@PyQt4.QtCore.pyqtSlot装饰方法定义槽函数:
@PyQt4.QtCore.pyqtSlot()
def setValue_NoParameters(self):
”’无参数槽方法”’
pass
@PyQt4.QtCore.pyqtSlot(int)
def setValue_OneParameter(self,nIndex):
”’一个参数(整数)槽方法”’
pass
… …
Pyqt5:
def printPaper(self,list):
self.resultLabel.setText(“Print: “+”份数:”+ str(list[0]) +” 纸张:”+str(list[1]))
def previewPaperWithArgs(self,style,text):
self.resultLabel.setText(str(style)+text)
def previewPaper(self,text):
self.resultLabel.setText(text)
3.5 总结
自定义信号的一般流程如下:
定义信号 – 绑定信号和槽 – 定义槽函数 – 发射信号, 例如:
from PyQt5.QtCore import QObject, pyqtSignal
class NewSignal(QObject):
# 一个valueChanged的信号,该信号没有参数.
valueChanged = pyqtSignal()
def connect_and_emit_valueChanged(self):
# 绑定信号和槽函数
self.valueChanged.connect(self.handle_valueChanged)
# 发射信号.
self.trigger.emit()
def handle_valueChanged(self):
print(“trigger signal received”)
注意: signal和slot的调用逻辑,避免signal和slot出现死循环, 如在slot方法中继续发射该信号.
4. app
The pyqtSlot() decorator can be used to specify which of the signals should be connected to the slot.
For example if you were only interested in the integer variant of the signal,
then your slot definition would look like the following:
@pyqtSlot(int)
def on_spinbox_valueChanged(self, i):
# i will be an integer.
pass
If wanted to handle both variants of the signal, but with different Python methods,
then slot definitions might look like following:
@pyqtSlot(int, name=’on_spinbox_valueChanged’) # note int,
def spinbox_INT_value(self, i):
# i will be an integer.
pass
@pyqtSlot(str, name=’on_spinbox_valueChanged’) # note str,
def spinbox_QSTRING_value(self, s):
# s will be a Python string object (or a QString if they are enabled)
pass
The following shows an example using a button when you are not interested in the optional argument:
@pyqtSlot()
def on_button_clicked(self):
pass
5. 装饰符 decorator
pySlot这个装饰符号可以把一个method定义为slot, 例如:
@QtCore.pyqtSlot()
def mySlot1(self):
print ‘mySlot received a signal’)
@QtCore.pyqtSlot(int)
def mySlot2(self, arg):
print ‘mySlot2 received a signal with argument %d’ % arg)
整个slot的定义与旧的方法相较,顿时变得简单许多。
而且,如果,UI是通过pyuic4设计的,那么甚至可以通过slot的名称来指定要连接的控件和signal。
例如, UI中有一个名为myBtn的按钮,想要连接单击的clicked signal。那么只要使用装饰符定义如下slot:
@QtCore.pyqtSlot(bool)
def on_myBtn_clicked(self, checked):
print ‘myBtn clicked.’
PyQT会自动将这个slot与UI内的myBtn的clicked singal连接起来。非常省事。
REF: http://python.jobbole.com/81683/
6. pyqt bridged opencv
先要把 cv.iplimage 这个 OpenCV 图像 用Python 的 PyQt widget显示。
6.1 Image Class
从 QtGui.QImage 派生出我们CameraImage类:
$ vi myCvPyQt.py
import cv
from PyQt4 import QtGui
# image used to paint by QT frameworks,
# converted from opencv image format
class CameraImage(QtGui.QImage):
def __init__(self, opencvBgrImg):
depth, nChannels = opencvBgrImg.depth, opencvBgrImg.nChannels
if depth != cv.IPL_DEPTH_8U or nChannels != 3:
raise ValueError(“image must be 8-bit 3-channel”)
w, h = cv.GetSize(opencvBgrImg)
opencvRgbImg = cv.CreateImage((w, h), depth, nChannels)
# OpenCV images from files or camera is BGR format
# which not what PyQt wanted, so convert to RGB.
cv.CvtColor(opencvBgrImg, opencvRgbImg, cv.CV_BGR2RGB)
# NOTE: save a reference to tmp opencvRgbImg byte-content
# prevent the garbage collector delete it when __init__ returns.
self._imgData = opencvRgbImg.tostring()
# call super UI QtGui.QImage base class constructor to build img
# by byte-content, dimensions, format of image.
super(CameraImage, self).__init__(self._imgData, w, h, QtGui.QImage.Format_RGB888)
If all you want is to show an OpenCV image in a PyQt widget, that’s all you need.
6.2 CameraDevice Class
新建一个相机的类,方便后期操作,实现相机和控件的解偶。
也就是,惟一的一个相机,可以作为生产者,供许多widgets消费,而控件之间无干扰。
也就是,人一个widgets都已可完全的操作相机。
$ vi myCvPyQt.py
import cv
from PyQt4 import QtCore
class CameraDevice(QtCore.QObject):
_DEFAULT_FPS = 30
newFrame = QtCore.pyqtSignal(cv.iplimage) # define signal with args of Camera Device
def __init__(self, cameraId=0, mirrored=False, parent=None):
super(CameraDevice, self).__init__(parent)
self.mirrored = mirrored
self._cameraDevice = cv.CaptureFromCAM(cameraId) # get capturer
self._timer = QtCore.QTimer(self)
self._timer.timeout.connect(self._queryFrame)
self._timer.setInterval(1000/self.fps)
self.paused = False
@QtCore.pyqtSlot()
def _queryFrame(self):
frame = cv.QueryFrame(self._cameraDevice) # get frame
if self.mirrored:
mirroredFrame = cv.CreateImage(cv.GetSize(frame), frame.depth, frame.nChannels)
cv.Flip(frame, mirroredFrame, 1)
frame = mirroredFrame
self.newFrame.emit(frame) # trigger signal with args of Camera Device
@property
def paused(self):
return not self._timer.isActive()
@paused.setter
def paused(self, p):
if p:
self._timer.stop()
else:
self._timer.start()
@property
def frameSize(self):
w = cv.GetCaptureProperty(self._cameraDevice, cv.CV_CAP_PROP_FRAME_WIDTH)
h = cv.GetCaptureProperty(self._cameraDevice, cv.CV_CAP_PROP_FRAME_HEIGHT)
return int(w), int(h)
@property
def fps(self):
fps = int(cv.GetCaptureProperty(self._cameraDevice, cv.CV_CAP_PROP_FPS))
if not fps > 0:
fps = self._DEFAULT_FPS
return fps
Essentially, it uses a timer to query the camera for a new frame and emits a signal passing the captured frame as parameter.
The timer is important to avoid spending CPU time with unnecessary pooling.
6.3 CameraWidget Class
The main purpose of it is to draw the frames delivered by the camera device.
But, before drawing a frame, it must allow anyone interested to process it, changing it without interfering with any other camera widget.
$ vi myCvPyQt.py
import cv
from PyQt4 import QtCore
from PyQt4 import QtGui
class CameraWidget(QtGui.QWidget):
newFrame = QtCore.pyqtSignal(cv.iplimage) # a signal of Camera Widget
def __init__(self, cameraDevice, parent=None):
super(CameraWidget, self).__init__(parent)
self._frame = None
self._cameraDevice = cameraDevice # passing into camera device
self._cameraDevice.newFrame.connect(self._onDeviceNewFrame) # connect with signal of Camera Device
w, h = self._cameraDevice.frameSize
self.setMinimumSize(w, h)
self.setMaximumSize(w, h)
@QtCore.pyqtSlot(cv.iplimage)
def _onDeviceNewFrame(self, frame):
self._frame = cv.CloneImage(frame) # make local copy
self.newFrame.emit(self._frame) # trigger signal with args of Camera Widget, processed by…
self.update() # if device update then update
def changeEvent(self, e):
if e.type() == QtCore.QEvent.EnabledChange:
if self.isEnabled():
self._cameraDevice.newFrame.connect(self._onDeviceNewFrame)
else:
self._cameraDevice.newFrame.disconnect(self._onDeviceNewFrame)
def paintEvent(self, e):
if self._frame is None:
return
painter = QtGui.QPainter(self)
painter.drawImage(QtCore.QPoint(0, 0), CameraImage(self._frame))
# paint with frame whether already processed or not
every widget saves its own version of the frame by cv.CloneImage(frame). This way, they can do whatever they want safely.
However, to process the frame is not responsibility of the widget. Thus, it emits a signal with the saved frame as parameter by emit(self._frame) and anyone connected to it can do the hard work. It happens usually inside main block code.
6.4 Application
$ vi myCvPyQt.py
import sys
def _main_():
@QtCore.pyqtSlot(cv.iplimage)
def onWidgetNewFrame(frame):
cv.CvtColor(frame, frame, cv.CV_RGB2BGR)
msg = “… processing …”
font = cv.InitFont(cv.CV_FONT_HERSHEY_DUPLEX, 1.0, 1.0)
tsize, baseline = cv.GetTextSize(msg, font)
w, h = cv.GetSize(frame)
tpt = (w – tsize[0]) / 2, (h – tsize[1]) / 2
cv.PutText(frame, msg, tpt, font, cv.RGB(255, 0, 0))
app = QtGui.QApplication(sys.argv)
cameraDevice = CameraDevice(mirrored=True) # only one camera device
cameraWidget1 = CameraWidget(cameraDevice) # 1th widget
cameraWidget1.setWindowTitle(‘Orig Img’)
cameraWidget1.show()
cameraWidget2 = CameraWidget(cameraDevice) # 2st widget
cameraWidget2.newFrame.connect(onWidgetNewFrame) # connect signal with args of Camera Widget
cameraWidget2.setWindowTitle(‘Processed img’)
cameraWidget2.show()
sys.exit(app.exec_())
if __name__ == ‘__main__’:
main()
Two CameraWidget objects share the same CameraDevice, only the first widget processes the frames .
The result is two widgets showing different images resulting from the same frame.
Now you can import CameraWidget in a PyQt application to have fresh camera preview.
6.5 Total like this:
import cv
from PyQt4 import QtCore
from PyQt4 import QtGui
import sys
class CameraImage(QtGui.QImage):
… …
class CameraDevice(QtCore.QObject):
… …
class CameraWidget(QtGui.QWidget):
… …
def main():
… …
OK
BUT, better do the video capturing and conversion in another thread firstly, and then send a signal to the GUI instead of using timers.
The advantage of this is that every time a frame is captured the thread will shoot a signal to the main (which handles the UI) to update the component, which displays the OpenCV image (in opencv2 this is the Mat object).
https://gist.github.com/saghul/1055161
Work at home.