跳转至

二、处理文件,相机和 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 指南,它们依赖于cvcv2.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.pngRandomColor.png

注意

在这里,我们使用 Python 的标准os.urandom()函数生成随机原始字节,然后将其转换为 Numpy 数组。 注意,也可以使用numpy.random.randint(0, 256, 120000).reshape(300, 400)之类的语句直接(更有效地)生成随机 Numpy 数组。 我们使用os.urandom()的唯一原因是帮助演示从原始字节的转换。

读/写视频文件

OpenCV 提供支持各种视频文件格式的VideoCaptureVideoWriter类。 支持的格式因系统而异,但应始终包含 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 键码,例如Esc27。 有关 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 的广泛功能,并给我们提出了创建高效有效实现的挑战。 用户会立即发现缺陷,例如帧速率低或跟踪不准确。 为了获得最佳结果,我们将尝试使用常规成像和深度成像的几种方法。

具体来说,我们的应用将执行实时面部合并。 给定两个摄像机输入流(或可选地,预录制的视频输入),应用会将一个流中的人脸叠加在另一个流中的人脸之上。 将应用过滤器和变形以使混合场景具有统一的外观。 用户应具有进入另一环境和另一角色的现场表演的经验。 这种类型的用户体验在迪士尼乐园等游乐园中很受欢迎。

我们将调用我们的应用CameoCameo是(在珠宝中)人物的小肖像,或者(在电影中)名人的非常简短的角色。

面向对象的设计

Python 应用可以以纯粹的程序样式编写。 这通常是通过小型应用完成的,例如前面讨论过的基本 I/O 脚本。 但是,从现在开始,我们将使用面向对象的样式,因为它可以促进模块化和可扩展性。

从我们对 OpenCV 的 I/O 功能的概述中,我们知道所有图像都是相似的,无论它们的来源还是目的地。 无论我们如何获取图像流或将其作为输出发送到哪里,我们都可以将相同的特定于应用的逻辑应用于该流中的每个帧。 在像Cameo这样的使用多个 I/O 流的应用中,I/O 代码和应用程​​序代码的分离变得特别方便。

我们将创建名为CaptureManagerWindowManager的类作为 I/O 流的高级接口。 我们的应用代码可以使用CaptureManager读取新帧,并且可以选择将每个帧分派到一个或多个输出,包括静止图像文件,视频文件和窗口(通过WindowManager类)。 WindowManager类允许我们的应用代码以面向对象的方式处理窗口和事件。

CaptureManagerWindowManager都是可扩展的。 我们可以实现不依赖 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 章,过滤图像)来扩展应用。 可选地,除了 OpenCV 支持的之外,我们还准备集成其他相机驱动程序或其他应用框架(附录 A,“集成 Pygame”)。


我们一直在努力

apachecn/AiLearning

【布客】中文翻译组