购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

2.4 Cameo:面向对象的设计

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

从对OpenCV的I/O功能的概述中,我们知道不管源图像或者目标图像是什么,所有图像都是相似的。不管获取的图像流是什么,或者将其作为输出发送到哪里,我们都可以对这个流的每一帧应用相同的特定于应用程序的逻辑。在使用多个I/O流的应用程序(例如Cameo)中,I/O代码和应用程序代码的分离变得特别方便。

我们将创建的类命名为CaptureManager和WindowManager,作为I/O流的高级接口。应用程序代码可以使用CaptureManager读取新帧,也可以将每一帧分派给一个或多个输出,包括静态图像文件、视频文件和窗口(通过WindowManager类)。WindowManager类允许应用程序代码以面向对象风格处理窗口和事件。

CaptureManager和WindowManager都是可扩展的。我们可以实现不依赖OpenCV的I/O。

2.4.1 基于managers.CaptureManager提取视频流

正如我们所看到的,OpenCV可以获取、显示和记录来自视频文件或来自摄像头的图像流,但是在每种情况下都会有一些特殊考虑的事项。CaptureManager类提取了一些差异并提供了一个更高级的接口,将图像从获取流分发到一个或多个输出——静态图像文件、视频文件,或者窗口。

CaptureManager对象是由VideoCapture对象初始化的,并拥有enterFrame和exitFrame方法,通常应该在应用程序主循环的每次迭代中调用这两个方法。在调用enterFrame和exitFrame之间,应用程序可以(任意次)设置一个channel属性并获得一个frame属性。channel属性初始为0,只有多摄像头相机使用其他值。frame属性是在调用enterFrame时,对应于当前通道状态的一幅图像。

CaptureManager类还拥有可以在任何时候调用的writeImage、startWriting Video和stopWritingVideo方法。实际的文件写入被推迟到exitFrame。同样,在执行exitFrame方法期间,可以在窗口中显示frame,这取决于应用程序代码将WindowManager类作为CaptureManager构造函数的参数提供,还是通过设置previewWindowManager属性提供。

如果应用程序代码操作frame,那么将在记录文件和窗口中体现这些操作。CaptureManager类有一个构造函数参数和一个名为shouldMirrorPreview的属性,如果想要在窗口中镜像(水平翻转)frame,但不记录在文件中,那么此属性应该为True。通常,在面对摄像头时,用户更喜欢镜像实时摄像头回传信号。

回想一下,VideoWriter对象需要一个帧率,但是OpenCV没有提供任何可靠的方法来为摄像头获取准确的帧率。CaptureManager类通过使用帧计数器和Python的标准time.time函数来解决此限制,如有必要还会估计帧率。这种方法并非万无一失。取决于帧率的波动和依赖于系统的time.time实现,估计的准确率在某些情况下可能仍然很糟糕。但是,如果部署到未知的硬件,这也比只假设用户摄像头有某个特定的帧率要好。

我们创建一个名为managers.py的文件,该文件将包含CaptureManager实现。这个实现非常很长,所以我们将分成几个部分介绍:

(1)首先,添加导入和构造函数,如下所示:

(2)接下来,为CaptureManager的属性添加下面的getter和setter方法:

请注意,大多数member变量是非公共的,由变量名称中下划线前缀所示,如self._enteredFrame。这些非公共变量与当前帧的状态和任何文件的写入操作相关。如前所述,应用程序代码只需要配置一些内容,这些内容是作为构造函数参数和可设置的公共属性(摄像头通道、窗口管理器以及镜像摄像头预览的选项)实现的。

本书假设读者对Python有一定的了解,但是如果你对这些@注释(例如@property)感到困惑,请参考有关decorator的Python文档,decorator是Python语言的内置特性,允许函数被另一个函数封装,通常用来在应用程序的几个地方应用用户定义的行为。具体来说,可以在https://docs.python.org/3/reference/compound_stmts.xhtml#grammar-token-decorator查看相关文档。

Python没有强制使用非公共的成员变量的概念,但是在开发人员想要将变量视为非公共的情况下,通常会看到单下划线前缀(_)或者双下划线前缀(__)。单下划线前缀只是一种约定,表示应该将变量视为受保护的(仅在类及其子类中访问)。双下划线前缀实际上会导致Python解释器重命名变量,这样MyClass.__myVariable就变成了MyClass._MyClass__myVariable。这被称为名称重整(非常恰当)。按照惯例,应该将这样的变量视为私有的(只能在类内访问,不能在子类中访问)。相同的前缀,具有相同的意义,可以应用于方法和变量。

(3)将enterFrame方法添加到managers.py:

请注意,enterFrame的实现只(同步地)抓取一帧,而来自通道的实际检索被推迟到frame变量的后续读取。

(4)接下来,把exitFrame方法添加到managers.py:

exitFrame的实现从当前通道获取图像,估计帧率,通过窗口管理器(如果有的话)显示图像,并完成将图像写入文件的所有挂起请求。

(5)其他几种方法也适用于文件的写入。将下列名为writeImage、startWriting Video和stopWritingVideo的公共方法的实现添加到managers.py:

上述方法只更新了文件写入操作的参数,实际的写入操作被推迟到exitFrame的下一次调用。

(6)在本节的前面,我们看到exitFrame调用了一个名为_writeVideoFrame的辅助方法。把下面的_writeVideoFrame实现添加到managers.py:

上述方法创建或添加视频文件的方式应该与之前的脚本相似(请参考2.2.4节)。但是,在帧率未知的情况下,我们在捕获会话开始时,跳过一些帧,这样就有时间构建帧率的估计。

我们对CaptureManager的实现就结束了。尽管CaptureManager的实现依赖于VideoCapture,我们可以完成不使用OpenCV作为输入的其他实现。例如,我们可以创建用套接字连接实例化的子类,将其字节流解析为图像流。另外,我们还可以使用第三方摄像头库创建子类,并提供与OpenCV不同的硬件支持。但是,对于Cameo,当前的实现就足够了。

2.4.2 基于managers.WindowManager提取窗口和键盘

正如我们所见,OpenCV提供了一些函数用于创建、撤销窗口,显示图像以及处理事件。这些函数不是窗口类的方法,因而要求将窗口名称作为参数传递。因为这个接口不是面向对象的,所以与OpenCV的一般风格不一致。而且,它不太可能与我们最终想要使用的(而不是OpenCV的)其他窗口或者事件处理接口兼容。

为了面向对象和适应性,我们将这个功能抽象成具有createWindow、destroy Window、show和processEvents方法的WindowManager类。作为一个属性,WindowManager有一个名为keypressCallback的函数,在响应按键时可以从processEvents调用(如果不是None的话)。keypressCallback对象必须是一个接受单个参数(尤其是ASCII键码)的函数。

我们将WindowManager的实现添加到managers.py。该实现首先定义下列类声明和__init__方法:

该实现接着使用下面的方法来管理窗口及其事件的生命周期:

当前的实现只支持键盘事件,对于Cameo足够了。但是,我们也可以修改Window Manager来支持鼠标事件。例如,类接口可以扩展为包含mouseCallback属性(和可选的构造函数参数),但是其他方面保持不变。使用OpenCV之外的事件框架,我们可以通过添加回调属性以同样的方式支持其他事件类型。

2.4.3 基于cameo.Cameo应用所有内容

我们的应用程序由带有两个方法(run和onKeypress)的Cameo类表示。在初始化时,Cameo对象创建了一个WindowManager对象(将onKeypress作为一个回调),以及一个使用摄像头(具体来说,是一个cv2.VideoCapture对象)和同一WindowManager对象的CaptureManager对象。在调用run时,应用程序执行一个主循环,并在这个主循环中处理帧和事件。

作为事件处理的结果,可能会调用onKeypress。空格键会产生一个屏幕截图,选项卡(Tab)键会使屏幕播放(视频录制)开始/停止,Esc键会使应用程序退出。

在与managers.py相同的目录中,创建一个名为cameo.py的文件,并在此实现Cameo类:

(1)首先,实现下面的import语句和__init__方法:

(2)接下来,添加以下run()方法的实现:

(3)下面是为完成Cameo类实现的onKeypress()方法:

(4)最后,添加一个__main__块来实例化并运行Cameo,如下所示:

在运行应用程序时,请注意实时摄像头回传信号是镜像的,而屏幕截图和屏幕播放则不是镜像的。这是预期的行为,因为在初始化CaptureManager类时,我们将True传给了shouldMirrorPreview。

图2-4是Cameo的一个屏幕截图,显示了一个窗口(标题为Cameo)和来自摄像头的当前帧。

图2-4 包含一个窗口和摄像头当前帧的Cameo截图

到目前为止,除了为预览而对帧进行镜像之外,我们没有对帧执行任何操作。我们将在第3章开始添加更有趣的效果。 tvqFnyUtF4FnK2yuQ1ppEisEWn/WNJCOu2TqXUOuistKdESkLUw0QgaBy/I1SKfH

点击中间区域
呼出菜单
上一章
目录
下一章
×