二、处理文件,相机和 GUI
本章介绍 OpenCV 的 I/O 功能。 我们还将讨论项目概念以及该项目面向对象设计的开始,我们将在后续章节中充实它们。
通过研究 I/O 功能和设计模式,我们以制作三明治的方式构建项目:从外部开始。面包切片和涂抹或端点和胶粘,先于馅料或算法。 我们之所以选择这种方法,是因为计算机视觉是外向的,它考虑了计算机外部的真实世界,并且我们希望通过通用接口将所有后续的算法工作应用于真实世界。
注意
基本 I/O 脚本
所有 CV 应用都需要获取图像作为输入。 大多数还需要产生图像作为输出。 交互式 CV 应用可能需要将摄像机作为输入源,将窗口作为输出目标。 但是,其他可能的来源和目的地包括图像文件,视频文件,和原始字节。 例如,如果我们将过程图形合并到我们的应用中,原始字节可能是通过网络连接接收/发送的,或者可能是由算法生成的。 让我们看看每种可能性。
读取/写入图像文件
OpenCV 提供,,imread()
和imwrite()
函数,这些函数支持静态图像的各种文件格式。 支持的格式因系统而异,但应始终包括 BMP 格式。 通常,PNG,JPEG 和 TIFF 也应该是受支持的格式。 可以从一种文件格式加载图像并保存为另一种文件格式。 例如,让我们将图像从 PNG 转换为 JPEG:
import cv2
image = cv2.imread('MyPic.png')
cv2.imwrite('MyPic.jpg', image)
注意
我们使用的大多数 OpenCV 功能都在cv2
模块中。 您可能会遇到其他 OpenCV 指南,它们依赖于cv
或cv2.cv
模块(它们是旧版)。 对于某些尚未在cv2
中重新定义的常量,我们确实使用了cv2.cv
。
默认情况下,即使文件使用灰度格式,imread()
也会以 BGR 颜色格式返回图像。 BGR(蓝绿红)表示与 RGB(红绿蓝)相同的色彩空间,但字节顺序是相反的。
(可选)我们可以将imread()
的模式指定为CV_LOAD_IMAGE_COLOR
(BGR),CV_LOAD_IMAGE_GRAYSCALE
(灰度)或CV_LOAD_IMAGE_UNCHANGED
(BGR 或灰度,取决于文件的色彩空间)。 例如,让我们将 PNG 加载为灰度图像(过程中丢失任何颜色信息),然后将其另存为灰度 PNG 图像:
import cv2
grayImage = cv2.imread('MyPic.png', cv2.CV_LOAD_IMAGE_GRAYSCALE)
cv2.imwrite('MyPicGray.png', grayImage)
无论哪种模式,imread()
都会丢弃任何 alpha 通道(透明度)。 imwrite()
函数要求图像为 BGR 或灰度格式,每个通道的位数可以由输出格式支持。 例如,bmp
每个通道需要 8 位,而 PNG 允许每个通道 8 位或 16 位。
在图像和原始字节之间转换
从概念上讲,字节是从 0 到 255 的整数。在当今的实时图形应用中,像素通常由每个通道一个字节表示,尽管其他表示形式也是可能的。
OpenCV 图像是numpy.array
类型的 2D 或 3D 数组。 8 位灰度图像是包含字节值的 2D 数组。 24 位 BGR 图像是 3D 数组,也包含字节值。 我们可以使用image[0, 0]
或image[0, 0, 0]
之类的表达式来访问这些值。 第一个索引是像素的y
坐标或行,0
是顶部。 第二个索引是像素的x
坐标或列,0
是最左侧。 第三个索引(如果适用)表示颜色通道。
例如,在左上角具有白色像素的 8 位灰度图像中,image[0, 0]
为255
。 对于左上角带有蓝色像素的 24 位 BGR 图像,image[0, 0]
为[255, 0, 0]
。
注意
作为使用类似image[0, 0]
或image[0, 0] = 128
的表达式的替代方法,我们可以使用类似image.item((0, 0))
或image.setitem((0, 0), 128)
的表达式。 后者的表达式对于单像素操作更有效。 但是,正如我们将在后续章节中看到的那样,我们通常希望对图像的大片段而不是单个像素执行操作。
假设每通道图像有 8 位,我们可以将其转换为一维的标准 Python bytearray
:
byteArray = bytearray(image)
相反,只要bytearray
包含适当顺序的字节,我们就可以进行铸造然后对其进行整形,以获得作为图像的numpy.array
类型:
grayImage = numpy.array(grayByteArray).reshape(height, width)
bgrImage = numpy.array(bgrByteArray).reshape(height, width, 3)
作为更完整的示例,让我们将包含随机字节的bytearray
转换为灰度图像和 BGR 图像:
import cv2
import numpy
import os
# Make an array of 120,000 random bytes.
randomByteArray = bytearray(os.urandom(120000))
flatNumpyArray = numpy.array(randomByteArray)
# Convert the array to make a 400x300 grayscale image.
grayImage = flatNumpyArray.reshape(300, 400)
cv2.imwrite('RandomGray.png', grayImage)
# Convert the array to make a 400x100 color image.
bgrImage = flatNumpyArray.reshape(100, 400, 3)
cv2.imwrite('RandomColor.png', bgrImage)
运行此脚本之后,我们应该在脚本目录中有一对随机生成的图像RandomGray.png
和RandomColor.png
。
注意
在这里,我们使用 Python 的标准os.urandom()
函数生成随机原始字节,然后将其转换为 Numpy 数组。 注意,也可以使用numpy.random.randint(0, 256, 120000).reshape(300, 400)
之类的语句直接(更有效地)生成随机 Numpy 数组。 我们使用os.urandom()
的唯一原因是帮助演示从原始字节的转换。
读/写视频文件
OpenCV 提供支持各种视频文件格式的VideoCapture
和VideoWriter
类。 支持的格式因系统而异,但应始终包含 AVI。 通过其read()
方法,可以轮询VideoCapture
类以获取新帧,直到到达其视频文件的末尾。 每帧都是 BGR 格式的图像。 相反,可以将图像传递到VideoWriter
类的write()
方法,该方法将图像附加到VideoWriter
中的文件。 让我们看一个示例,该示例从一个 AVI 文件读取帧并将其写入到另一个具有 YUV 编码的 AVI 文件中:
import cv2
videoCapture = cv2.VideoCapture('MyInputVid.avi')
fps = videoCapture.get(cv2.cv.CV_CAP_PROP_FPS)
size = (int(videoCapture.get(cv2.cv.CV_CAP_PROP_FRAME_WIDTH)),
int(videoCapture.get(cv2.cv.CV_CAP_PROP_FRAME_HEIGHT)))
videoWriter = cv2.VideoWriter(
'MyOutputVid.avi', cv2.cv.CV_FOURCC('I','4','2','0'), fps, size)
success, frame = videoCapture.read()
while success: # Loop until there are no more frames.
videoWriter.write(frame)
success, frame = videoCapture.read()
VideoWriter
类的构造器的参数值得特别注意。 必须指定视频的文件名。 具有该名称的任何先前存在的文件都将被覆盖。 还必须指定视频编解码器。 可用的编解码器可能因系统而异。 选项包括:
cv2.cv.CV_FOURCC('I','4','2','0')
:这是未压缩的 YUV,4:2:0 色度被二次采样。 这种编码具有广泛的兼容性,但会产生大文件。 文件扩展名应为avi
。cv2.cv.CV_FOURCC('P','I','M','1')
:这是 MPEG-1。 文件扩展名应为avi
。cv2.cv.CV_FOURCC('M','J','P','G')
:这是动态 JPEG。 文件扩展名应为avi
。cv2.cv.CV_FOURCC('T','H','E','O')
:这是 Ogg-Vorbis。 文件扩展名应为ogv
。cv2.cv.CV_FOURCC('F','L','V','1')
:这是 Flash 视频。 文件扩展名应为flv
。
还必须指定帧速率和帧大小。 由于我们正在从另一个视频复制,因此可以从VideoCapture
类的get()
方法读取这些属性。
捕捉相机帧
摄像机帧的流也由VideoCapture
类表示。 但是,对于摄像机,我们通过传递摄像机的设备索引而不是视频的文件名来构造VideoCapture
类。 让我们考虑一个示例,该示例从摄像机捕获 10 秒的视频并将其写入 AVI 文件:
import cv2
cameraCapture = cv2.VideoCapture(0)
fps = 30 # an assumption
size = (int(cameraCapture.get(cv2.cv.CV_CAP_PROP_FRAME_WIDTH)),
int(cameraCapture.get(cv2.cv.CV_CAP_PROP_FRAME_HEIGHT)))
videoWriter = cv2.VideoWriter(
'MyOutputVid.avi', cv2.cv.CV_FOURCC('I','4','2','0'), fps, size)
success, frame = cameraCapture.read()
numFramesRemaining = 10 * fps - 1
while success and numFramesRemaining > 0:
videoWriter.write(frame)
success, frame = cameraCapture.read()
numFramesRemaining -= 1
不幸的是,VideoCapture
类的get()
方法无法返回相机帧频的准确值; 它总是返回0
。 为了为相机创建合适的VideoWriter
类,我们必须对帧速率进行假设(就像我们之前在代码中所做的那样),或者使用计时器对其进行测量。 后一种方法更好,我们将在本章后面介绍。
摄像机的数量及其顺序当然取决于系统。 不幸的是,OpenCV 没有提供任何查询摄像机数量或其属性的方法。 如果使用无效索引来构造VideoCapture
类,则VideoCapture
类将不会产生任何帧。 其read()
方法将返回(false, None)
。
当我们需要同步一组摄像机或多头摄像机(如立体摄像机或 Kinect)时,read()
方法不合适。 然后,我们改用grab()
和retrieve()
方法。 对于一组相机:
success0 = cameraCapture0.grab()
success1 = cameraCapture1.grab()
if success0 and success1:
frame0 = cameraCapture0.retrieve()
frame1 = cameraCapture1.retrieve()
对于多头相机,我们必须指定头的索引作为retrieve()
的参数:
success = multiHeadCameraCapture.grab()
if success:
frame0 = multiHeadCameraCapture.retrieve(channel = 0)
frame1 = multiHeadCameraCapture.retrieve(channel = 1)
我们将在第 5 章,“检测前景/背景区域和深度”中更详细地研究多头相机。
在窗口中显示摄像机帧
OpenCV 允许使用namedWindow()
,imshow()
和destroyWindow()
函数创建,重绘和销毁命名窗口。 同样,任何窗口都可以通过waitKey()
函数捕获键盘输入,并通过setMouseCallback()
函数捕获鼠标输入。 让我们看一个显示实时摄像机输入帧的示例:
import cv2
clicked = False
def onMouse(event, x, y, flags, param):
global clicked
if event == cv2.cv.CV_EVENT_LBUTTONUP:
clicked = True
cameraCapture = cv2.VideoCapture(0)
cv2.namedWindow('MyWindow')
cv2.setMouseCallback('MyWindow', onMouse)
print 'Showing camera feed. Click window or press any key to stop.'
success, frame = cameraCapture.read()
while success and cv2.waitKey(1) == -1 and not clicked:
cv2.imshow('MyWindow', frame)
success, frame = cameraCapture.read()
cv2.destroyWindow('MyWindow')
waitKey()
的参数是等待键盘输入的毫秒数。 返回值可以是-1
(意味着没有按下任何键)或 ASCII 键码,例如Esc
的27
。 有关 ASCII 键码的列表,请参见这个页面。 另外,请注意,Python 提供了标准函数ord()
,该函数可以将字符转换为其 ASCII 键代码。 例如ord('a') returns 97
。
提示
在某些系统上,waitKey()
可能返回一个值,该值编码的不仅是 ASCII 键码。 (众所周知,当 OpenCV 使用 GTK 作为其后端 GUI 库时,在 Linux 上会发生一个错误。)在所有系统上,我们都可以通过读取返回值的最后一个字节来确保仅提取 ASCII 键码,如下所示:
keycode = cv2.waitKey(1)
if keycode != -1:
keycode &= 0xFF
OpenCV 的窗口功能和waitKey()
是相互依赖的。 仅当调用waitKey()
时才更新 OpenCV 窗口,并且仅当 OpenCV 窗口具有焦点时waitKey()
才捕获输入。
如代码示例所示,传递给setMouseCallback()
的鼠标回调应采用五个参数。 回调的param
参数设置为setMouseCallback()
的可选第三个参数。 默认为0
。 回调的event
参数是以下之一:
cv2.cv.CV_EVENT_MOUSEMOVE
:鼠标移动cv2.cv.CV_EVENT_LBUTTONDOWN
:向下按钮按下cv2.cv.CV_EVENT_RBUTTONDOWN
:向下按钮按下cv2.cv.CV_EVENT_MBUTTONDOWN
:中间按钮按下cv2.cv.CV_EVENT_LBUTTONUP
:向左按钮cv2.cv.CV_EVENT_RBUTTONUP
:向上按钮cv2.cv.CV_EVENT_MBUTTONUP
:中键向上cv2.cv.CV_EVENT_LBUTTONDBLCLK
:双击鼠标左键cv2.cv.CV_EVENT_RBUTTONDBLCLK
:右键单击双击cv2.cv.CV_EVENT_MBUTTONDBLCLK
:双击中键
鼠标回调的flags
参数可能是以下各项的按位组合:
cv2.cv.CV_EVENT_FLAG_LBUTTON
:按下左键cv2.cv.CV_EVENT_FLAG_RBUTTON
:按下右键cv2.cv.CV_EVENT_FLAG_MBUTTON
:按下中间键cv2.cv.CV_EVENT_FLAG_CTRLKEY
:按下Ctrl
键cv2.cv.CV_EVENT_FLAG_SHIFTKEY
:按下Shift
键cv2.cv.CV_EVENT_FLAG_ALTKEY
:按下Alt
键
不幸的是,OpenCV 不提供任何处理窗口事件的方法。 例如,当单击窗口的关闭按钮时,我们无法停止我们的应用。 由于 OpenCV 有限的事件处理和 GUI 功能,许多开发人员更喜欢将其与另一个应用框架集成。 在本章的后面,我们将设计一个抽象层,以帮助将 OpenCV 集成到任何应用框架中。
项目概念
OpenCV 通常是通过菜谱方法研究的,该菜谱方法涵盖许多算法,但与高级应用开发无关。 在某种程度上,这种方法是可以理解的,因为 OpenCV 的潜在应用是如此多样。 例如,我们可以在照片/视频编辑器,动作控制游戏,机器人的 AI 或心理学实验中使用它来记录参与者的眼球运动。 在这样的不同用例中,我们可以真正研究一组有用的抽象吗?
我相信我们可以并且越早开始创建抽象越好。 我们将围绕单个应用来组织对 OpenCV 的研究,但是,在每一步中,我们都将设计此应用的组件以使其可扩展和可重用。
我们将开发一个交互式应用,该应用可实时对摄像机输入进行面部跟踪和图像处理。 这类应用涵盖了 OpenCV 的广泛功能,并给我们提出了创建高效有效实现的挑战。 用户会立即发现缺陷,例如帧速率低或跟踪不准确。 为了获得最佳结果,我们将尝试使用常规成像和深度成像的几种方法。
具体来说,我们的应用将执行实时面部合并。 给定两个摄像机输入流(或可选地,预录制的视频输入),应用会将一个流中的人脸叠加在另一个流中的人脸之上。 将应用过滤器和变形以使混合场景具有统一的外观。 用户应具有进入另一环境和另一角色的现场表演的经验。 这种类型的用户体验在迪士尼乐园等游乐园中很受欢迎。
我们将调用我们的应用 Cameo。 Cameo 是(在珠宝中)人物的小肖像,(在电影中)是名人扮演的非常简短的角色。
面向对象的设计
Python 应用可以以纯粹的程序样式编写。 这通常是通过小型应用完成的,例如前面讨论过的基本 I/O 脚本。 但是,从现在开始,我们将使用面向对象的样式,因为它可以促进模块化和可扩展性。
从我们对 OpenCV 的 I/O 功能的概述中,我们知道所有图像都是相似的,无论它们的来源还是目的地。 无论我们如何获取图像流或将其作为输出发送到哪里,我们都可以将相同的特定于应用的逻辑应用于该流中的每个帧。 在像 Cameo 这样的使用多个 I/O 流的应用中,I/O 代码和应用程序代码的分离变得特别方便。
我们将创建名为CaptureManager
和WindowManager
的类作为 I/O 流的高级接口。 我们的应用代码可以使用CaptureManager
读取新帧,并且可以选择将每个帧分派到一个或多个输出,包括静止图像文件,视频文件和窗口(通过WindowManager
类)。 WindowManager
类允许我们的应用代码以面向对象的方式处理窗口和事件。
CaptureManager
和WindowManager
都是可扩展的。 我们可以实现不依赖 OpenCV 进行 I/O 的实现。 实际上,附录 A“与 Pygame 集成”使用了WindowManager
子类。
抽象化视频流 – manager.CaptureManager
如我们所见,OpenCV 可以捕获,显示和记录来自视频文件或摄像机的图像流,但是每种情况都有一些特殊的注意事项。 我们的CaptureManager
类抽象了一些差异,并提供了更高级别的接口,用于将图像从捕获流分发到一个或多个输出(静止图像文件,视频文件或窗口)。
CaptureManager
类由VideoCapture
类初始化,并具有enterFrame()
和exitFrame()
方法,通常应在应用主循环的每次迭代中调用它们。 在对enterFrame()
的调用与对exitFrame()
的调用之间,应用可以(任意次)设置channel
属性并获得frame
属性。 channel
属性最初是0
,只有多头相机使用其他值。 frame
属性是与调用enterFrame()
时当前通道状态相对应的图像。
CaptureManager
类也具有可以随时调用的writeImage()
,startWritingVideo()
和stopWritingVideo()
方法。 实际文件写入被推迟到exitFrame()
为止。 同样在exitFrame()
方法期间,frame
属性可能会显示在窗口中,具体取决于应用代码是否提供WindowManager
类作为CaptureManager
构造器的参数,还是通过设置属性[ previewWindowManager
。
如果应用代码操纵frame
,则操纵将反映在任何记录的文件和窗口中。 CaptureManager
类具有构造器参数和称为shouldMirrorPreview
的属性,如果我们希望frame
在窗口中而不是在已记录文件中进行镜像(水平翻转),则应为True
。 通常,面对摄像机时,用户希望对实时摄像机源进行镜像。
回想一下VideoWriter
类需要帧速率,但是 OpenCV 没有提供任何方法来获取摄像机的准确帧速率。 CaptureManager
类通过使用帧计数器和 Python 的标准time.time()
函数估计帧率来解决此限制。 这种方法不是万无一失的。 根据帧频波动和time.time()
的系统相关实现,在某些情况下,估计的准确率可能仍然很差。 但是,如果我们要部署到未知的硬件,则比仅假设用户的摄像机具有特定的帧速率要好。
让我们创建一个名为managers.py
的文件,其中将包含CaptureManager
的实现。 事实证明,实现时间很长。 因此,我们将分几部分进行研究。 首先,让我们添加导入,构造器和属性,如下所示:
import cv2
import numpy
import time
class CaptureManager(object):
def __init__(self, capture, previewWindowManager = None,
shouldMirrorPreview = False):
self.previewWindowManager = previewWindowManager
self.shouldMirrorPreview = shouldMirrorPreview
self._capture = capture
self._channel = 0
self._enteredFrame = False
self._frame = None
self._imageFilename = None
self._videoFilename = None
self._videoEncoding = None
self._videoWriter = None
self._startTime = None
self._framesElapsed = long(0)
self._fpsEstimate = None
@property
def channel(self):
return self._channel
@channel.setter
def channel(self, value):
if self._channel != value:
self._channel = value
self._frame = None
@property
def frame(self):
if self._enteredFrame and self._frame is None:
_, self._frame = self._capture.retrieve(channel = self.channel)
return self._frame
@property
def isWritingImage (self):
return self._imageFilename is not None
@property
def isWritingVideo(self):
return self._videoFilename is not None
请注意,大多数成员变量都是非公开的,如变量名中的下划线前缀所表示,例如self._enteredFrame
。 这些非公共变量与当前帧的状态以及任何文件写入操作有关。 如前所述,应用代码只需要配置一些东西,这些东西就可以作为构造器参数和可设置的公共属性来实现:相机通道,窗口管理器和镜像相机预览的选项。
注意
按照惯例,在 Python 中,以单个下划线为前缀的变量应被视为受保护的(只能在该类及其子类中访问),而以双下划线为前缀的变量应视为私有(仅在该类内访问)。
继续执行,让我们将enterFrame()
和exitFrame()
方法添加到managers.py
中:
def enterFrame(self):
"""Capture the next frame, if any."""
# But first, check that any previous frame was exited.
assert not self._enteredFrame, \
'previous enterFrame() had no matching exitFrame()'
if self._capture is not None:
self._enteredFrame = self._capture.grab()
def exitFrame (self):
"""Draw to the window. Write to files. Release the frame."""
# Check whether any grabbed frame is retrievable.
# The getter may retrieve and cache the frame.
if self.frame is None:
self._enteredFrame = False
return
# Update the FPS estimate and related variables.
if self._framesElapsed == 0:
self._startTime = time.time()
else:
timeElapsed = time.time() - self._startTime
self._fpsEstimate = self._framesElapsed / timeElapsed
self._framesElapsed += 1
# Draw to the window, if any.
if self.previewWindowManager is not None:
if self.shouldMirrorPreview:
mirroredFrame = numpy.fliplr(self._frame).copy()
self.previewWindowManager.show(mirroredFrame)
else:
self.previewWindowManager.show(self._frame)
# Write to the image file, if any.
if self.isWritingImage:
cv2.imwrite(self._imageFilename, self._frame)
self._imageFilename = None
# Write to the video file, if any.
self._writeVideoFrame()
# Release the frame.
self._frame = None
self._enteredFrame = False
请注意,enterFrame()
的实现仅抓取(同步)帧,而从通道的实际检索被推迟到frame
变量的后续读取。 exitFrame()
的实现从当前通道获取图像,估计帧速率,通过窗口管理器(如果有)显示图像,并完成对将图像写入文件的所有未决请求。
其他几种方法也与文件写入有关。 为了完成我们的类实现,让我们将其余的文件写入方法添加到managers.py
中:
def writeImage(self, filename):
"""Write the next exited frame to an image file."""
self._imageFilename = filename
def startWritingVideo(
self, filename,
encoding = cv2.cv.CV_FOURCC('I','4','2','0')):
"""Start writing exited frames to a video file."""
self._videoFilename = filename
self._videoEncoding = encoding
def stopWritingVideo (self):
"""Stop writing exited frames to a video file."""
self._videoFilename = None
self._videoEncoding = None
self._videoWriter = None
def _writeVideoFrame(self):
if not self.isWritingVideo:
return
if self._videoWriter is None:
fps = self._capture.get(cv2.cv.CV_CAP_PROP_FPS)
if fps == 0.0:
# The capture's FPS is unknown so use an estimate.
if self._framesElapsed < 20:
# Wait until more frames elapse so that the
# estimate is more stable.
return
else:
fps = self._fpsEstimate
size = (int(self._capture.get(
cv2.cv.CV_CAP_PROP_FRAME_WIDTH)),
int(self._capture.get(
cv2.cv.CV_CAP_PROP_FRAME_HEIGHT)))
self._videoWriter = cv2.VideoWriter(
self._videoFilename, self._videoEncoding,
fps, size)
self._videoWriter.write(self._frame)
公用方法writeImage()
,startWritingVideo()
和stopWritingVideo()
仅记录用于文件写入操作的参数,而实际的写入操作则推迟到exitFrame()
的下一次调用。 非公开方法_writeVideoFrame()
以我们先前的脚本应该熟悉的方式创建或附加到视频文件。 (请参阅“读/写视频文件”部分。)但是,在帧速率未知的情况下,我们会在捕获会话开始时跳过一些帧,以便有时间构建帧速率的估计。
尽管我们当前的CaptureManager
实现依赖于VideoCapture
,但我们可以进行其他不使用 OpenCV 进行输入的实现。 例如,我们可以创建一个用套接字连接实例化的子类,该子类的字节流可以解析为图像流。 另外,我们可以创建一个子类,该子类使用第三方相机库,其硬件支持与 OpenCV 提供的硬件支持不同。 但是,对于 Cameo,我们当前的实现方式就足够了。
抽象化窗口和键盘 – manager.WindowManager
如我们所见,OpenCV 提供的功能可导致创建窗口,破坏窗口,显示图像以及进程事件。 这些函数不是窗口类的方法,而是需要窗口的名称作为参数传递。 由于此接口不是面向对象的,因此与 OpenCV 的常规样式不一致。 而且,它不太可能与我们最终想要代替 OpenCV 使用的其他窗口或事件处理接口兼容。
出于面向对象和适应性的考虑,我们使用createWindow()
,destroyWindow()
,show()
和processEvents()
方法将此功能抽象为WindowManager
类。 作为属性,WindowManager
类具有一个称为keypressCallback
的功能对象,该对象将响应任何按键从processEvents()
调用(如果不是None
)。 keypressCallback
对象必须采用单个参数,即 ASCII 键代码。
让我们将以下WindowManager
的实现添加到managers.py
中:
class WindowManager(object):
def __init__(self, windowName, keypressCallback = None):
self.keypressCallback = keypressCallback
self._windowName = windowName
self._isWindowCreated = False
@property
def isWindowCreated(self):
return self._isWindowCreated
def createWindow (self):
cv2.namedWindow(self._windowName)
self._isWindowCreated = True
def show(self, frame):
cv2.imshow(self._windowName, frame)
def destroyWindow (self):
cv2.destroyWindow(self._windowName)
self._isWindowCreated = False
def processEvents (self):
keycode = cv2.waitKey(1)
if self.keypressCallback is not None and keycode != -1:
# Discard any non-ASCII info encoded by GTK.
keycode &= 0xFF
self.keypressCallback(keycode)
我们当前的实现仅支持键盘事件,这对于 Cameo 足够了。 但是,我们也可以修改WindowManager
以支持鼠标事件。 例如,可以将类的接口扩展为包括mouseCallback
属性(和可选的构造器参数),但否则可以保持不变。 使用 OpenCV 以外的其他事件框架,我们可以通过添加回调属性以相同的方式支持其他事件类型。
附录 A“与 Pygame 集成”,显示了WindowManager
子类,该子类是通过 Pygame 的窗口处理和事件框架而不是 OpenCV 来实现的。 通过正确处理退出事件(例如,当用户单击窗口的关闭按钮时),此实现在WindowManager
基类上得到了改进。 潜在地,许多其他事件类型也可以通过 Pygame 处理。
应用所有内容 – cameo.Cameo
我们的应用以Cameo
类表示,具有两种方法:run()
和onKeypress()
。 初始化时, Cameo
类创建一个带有onKeypress()
作为回调的WindowManager
类,并使用摄像机和WindowManager
类创建一个CaptureManager
类。 调用run()
时,应用执行一个主循环,在该循环中处理帧和事件。 作为事件处理的结果,可以调用onKeypress()
。 空格键将截取屏幕快照,TAB
导致屏幕录像(视频录制)开始/停止,Esc
导致应用退出。
在与managers.py
相同的目录中,我们创建一个名为cameo.py
的文件,其中包含Cameo
的以下实现:
import cv2
from managers import WindowManager, CaptureManager
class Cameo(object):
def __init__(self):
self._windowManager = WindowManager('Cameo',
self.onKeypress)
self._captureManager = CaptureManager(
cv2.VideoCapture(0), self._windowManager, True)
def run(self):
"""Run the main loop."""
self._windowManager.createWindow()
while self._windowManager.isWindowCreated:
self._captureManager.enterFrame()
frame = self._captureManager.frame
# TODO: Filter the frame (Chapter 3).
self._captureManager.exitFrame()
self._windowManager.processEvents()
def onKeypress (self, keycode):
"""Handle a keypress.
space -> Take a screenshot.
tab -> Start/stop recording a screencast.
escape -> Quit.
"""
if keycode == 32: # space
self._captureManager.writeImage('screenshot.png')
elif keycode == 9: # tab
if not self._captureManager.isWritingVideo:
self._captureManager.startWritingVideo(
'screencast.avi')
else:
self._captureManager.stopWritingVideo()
elif keycode == 27: # escape
self._windowManager.destroyWindow()
if __name__=="__main__":
Cameo().run()
运行该应用时,请注意,实时摄像机的提要是镜像的,而屏幕截图和截屏不是。 这是预期的行为,因为我们在初始化CaptureManager
类时将shouldMirrorPreview
传递给True
。
到目前为止,除了镜像它们以进行预览之外,我们不会以其他任何方式操作它们。 我们将在第 3 章,“过滤图像”中开始添加更多有趣的效果。
总结
到现在为止,我们应该有一个应用,它可以显示摄像机的供稿,监听键盘输入并(按命令)记录屏幕截图或截屏视频。 我们准备通过在每个帧的开始和结尾之间插入一些图像过滤代码(第 3 章,“过滤图像”)来扩展应用。 (可选)我们还准备集成 Open GV 支持的其他相机驱动程序或其他应用框架(附录 A,“与 Pygame 集成”)。