本章介绍 OpenCV 的 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
子类。
如我们所见,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,我们当前的实现方式就足够了。
如我们所见,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
类表示,具有两种方法: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 集成”)。