Python OpenCV 计算机视觉:1~5

2023-04-27 15:29:40 浏览数 (2)

一、设置 OpenCV

本章是设置 Python 2.7,OpenCV 和相关库的快速指南。 设置完成后,我们还将查看 OpenCV 的 Python 示例脚本和文档。

涵盖了以下相关库:

  • NumPy:这是 OpenCV 的 Python 绑定的依赖项。 它提供数值计算功能,包括有效的数组。
  • SciPy:这是与 NumPy 密切相关的科学计算库。 OpenCV 不需要它,但是它对于处理 OpenCV 映像中的数据很有用。
  • OpenNI:这是 OpenCV 的可选依赖项。 它增加了对某些深度相机的支持,例如 Asus XtionPRO。
  • SensorKinect:这是一个 OpenNI 插件,是 OpenCV 的可选依赖项。 它增加了对 Microsoft Kinect 深度相机的支持。

出于本书的目的,可以将 OpenNI 和 SensorKinect 视为可选的。 它们在第 5 章“分隔前景/背景区域深度”的整个过程中使用,但在其他各章或附录中未使用。

在撰写本文时,OpenCV 2.4.3 是最新版本。 在某些操作系统上,设置早期版本(2.3.1)更容易。 这些版本之间的差异不应影响我们将在本书中构建的项目。

OpenCV Wiki 中提供了一些其他信息,尤其是有关 OpenCV 的构建选项及其依赖项的信息。 但是,在撰写本文时,Wiki 尚未与 OpenCV 2.4.3 保持同步。

选择和使用正确的设置工具

我们可以自由选择各种设置工具,具体取决于我们的操作系统以及我们要执行的配置数量。 让我们来概述一下 Windows,Mac,Ubuntu 和其他类似 Unix 的系统的工具。

选择 Windows XP,Windows Vista,Windows 7 或 Windows 8

Windows 未预装 Python。 但是,安装向导可用于预编译的 Python,NumPy,SciPy 和 OpenCV。 或者,我们可以从源代码构建。 OpenCV 的构建系统使用 CMake 进行配置,使用 Visual Studio 或 MinGW 进行编译。

如果我们想支持包括 Kinect 在内的深度相机,我们应该首先安装 OpenNI 和 SensorKinect,它们可以作为预编译的二进制文件与安装向导一起使用。 然后,我们必须从源代码构建 OpenCV。


注意

OpenCV 的预编译版本不支持深度相机。


在 Windows 上,OpenCV 对 32 位 Python 的支持比对 64 位 Python 的支持更好。 即使我们是从源代码构建的,我也建议使用 32 位 Python。 幸运的是,32 位 Python 在 Windows 的 32 位或 64 位版本上都能正常工作。


注意

以下某些步骤涉及编辑系统的Path变量。 可以在控制面板环境变量窗口中完成此任务。

在 Windows Vista/Windows 7/Windows 8 上,打开开始菜单,然后启动控制面板。 现在,转到系统和安全性 | 系统 | 高级系统设置。 单击环境变量按钮。

在 Windows XP 上,打开开始菜单,然后转到控制面板 | 系统。 选择高级标签。 单击环境变量按钮。

现在,在系统变量下,选择路径下一步编辑按钮。 按照指示进行更改。 要应用更改,请单击所有 OK 按钮(直到我们回到控制面板的主窗口中)。 然后,注销并重新登录(或者重新启动)。


使用二进制安装程序(不支持深度摄像头)

以下是将设置为 32 位 Python 2.7,NumPy 和 OpenCV 的步骤:

  1. 从这个页面下载并安装 32 位 Python 2.7.3。
  2. 从这个页面下载并安装 NumPy 1.6.2。
  3. 从这个页面
  4. 从这个页面下载 OpenCV 2.4.3 的自解压 ZIP。 运行自解压 ZIP,并在出现提示时输入任何目标文件夹,我们将其称为<unzip_destination>。 创建一个子文件夹<unzip_destination>opencv
  5. <unzip_destination>opencvbuildpython2.7cv2.pyd复制到C:Python2.7Libsite-packages(假设我们将 Python 2.7 安装到默认位置)。 现在,新安装的 Python 可以找到 OpenCV。
  6. 如果我们希望默认情况下使用新的 Python 安装运行 Python 脚本,则需要最后一步。 编辑系统的Path变量并附加;C:Python2.7(假设我们将 Python 2.7 安装到默认位置)。 删除所有以前的 Python 路径,例如;C:Python2.6。 注销并重新登录(或者重新启动)。

使用 CMake 和编译器

Windows 不附带任何编译器或 CMake。 我们需要安装它们。 如果要支持包括 Kinect 在内的深度相机,还需要安装 OpenNI 和 SensorKinect。

假设我们已经从二进制文件(如前所述)或从源代码安装了 32 位 Python 2.7,NumPy 和 SciPy。 现在,我们可以继续安装编译器和 CMake,可以选择安装 OpenNI 和 SensorKinect,然后从源代码构建 OpenCV:

从这个页面下载并安装 CMake 2.8.9。 运行安装程序时,选择将 CMake 添加到所有用户的系统路径将 CMake 添加到当前用户的系统路径

下载并安装 Microsoft Visual Studio 2010,Microsoft Visual C Express 2010 或 MinGW。 请注意,OpenCV 2.4.3 不能使用较新的版本(Microsoft Visual Studio 2012 和 Microsoft Visual Studio Express 2012)进行编译。

对于 Microsoft Visual Studio 2010,请使用购买的所有安装媒体。 在安装过程中,包括所有可选的 C 组件。 安装完成后重新启动。

对于 Microsoft Visual C Express 2010,请从这个页面获取安装程序。 安装完成后重新启动。

对于 MinGW,请从这个页面中获取安装程序。 运行安装程序时,请确保目标路径不包含空格,并且包括可选的 C 编译器。 编辑系统的Path变量,然后附加;C:MinGWbin(假定 MinGW 已安装到默认位置)。重新引导系统。

(可选)从这个页面下载并安装 OpenNI 1.5.4.0(32 位)。 另外,对于 64 位 Python,请使用这个页面(64 位)。

(可选)从这个页面下载并安装 SensorKinect 0.93(32 位)。 另外,对于 64 位 Python ,请使用这个页面(64 位)。

从这个页面下载 OpenCV 2.4.3 的自解压 ZIP。 运行自解压 ZIP,并在出现提示时输入任何目标文件夹,我们将其称为<unzip_destination>。 创建一个子文件夹<unzip_destination>opencv

打开命令提示符,然后在另一个文件夹中进行构建:

代码语言:javascript复制
> mkdir<build_folder>

将目录更改为构建文件夹:

代码语言:javascript复制
> cd <build_folder>

现在,我们准备配置我们的构建。 要了解所有选项,我们可以阅读<unzip_destination>opencvCMakeLists.txt中的代码。 但是,出于本书的目的,我们只需要使用将为我们提供带有 Python 绑定的发行版本的选项,以及(可选)通过 OpenNI 和 SensorKinect 支持深度相机的选项即可。

对于 Visual Studio 2010 或 Visual C Express 2010,请运行:

代码语言:javascript复制
> cmake -D:CMAKE_BUILD_TYPE=RELEASE -D:WITH_OPENNI=ON -G "Visual Studio 10" <unzip_destination>opencv

或者,对于 MinGW,运行:

代码语言:javascript复制
> cmake -D:CMAKE_BUILD_TYPE=RELEASE -D:WITH_OPENNI=ON -G "MinGWMakefiles" <unzip_destination>opencv

如果未安装 OpenNI,请省略-D:WITH_OPENNI=ON。 (在这种情况下,将不支持深度相机。)如果将 OpenNI 和 SensorKinect 安装在非默认位置,请修改命令以包括-D:OPENNI_LIB_DIR=<openni_install_destination>Lib -D:OPENNI_INCLUDE_DIR=<openni_install_destination>Include -D:OPENNI_PRIME_SENSOR_MODULE_BIN_DIR=<sensorkinect_install_destination>SensorBin

CMake 可能报告它未能找到某些依赖项。 OpenCV 的许多依赖项是可选的。 因此,不要太在意。 如果构建无法完成或以后遇到问题,请尝试安装缺少的依赖项(通常以预构建的二进制文件形式提供),然后从此步骤重新构建 OpenCV。

配置好构建系统后,我们就可以进行编译了。

对于 Visual Studio 或 Visual C Express,打开<build_folder>/OpenCV.sln。 选择Release配置并构建。 如果出现构建错误,请仔细检查是否已选择Release配置。

或者,对于 MinGW,运行:

代码语言:javascript复制
> mingw32-make.

<build_folder>libReleasecv2.pyd(来自 Visual Studio 版本)或<build_folder>libcv2.pyd(来自 MinGW 版本)复制到C:Python2.7Libsite-packages(假定将 Python 2.7 安装到默认位置)。 现在,Python 安装可以找到 OpenCV 的一部分。

最后, 我们需要确保 Python 和其他进程可以找到其余的 OpenCV。 编辑系统的Path变量,并附加;<build_folder>/bin/Release(对于 Visual Studio 版本)或;<build_folder>/bin(对于 MinGW 版本)。 重新启动系统。

选择 Mac OS X Snow Leopard,Mac OS X Lion 或 Mac OS X Mountain Lion

Mac 的某些版本预装有 Python 2.7。 但是,Apple 预先定制了预装的 Python,以满足系统的内部需求。 通常,我们不应该在 Apple 的 Python 之上安装任何库。 如果这样做,我们的库可能会在系统更新期间中断,或者更糟的是,可能会与系统所需的预安装库冲突。 相反,我们应该安装标准 Python 2.7,然后在其顶部安装我们的库。

对于 Mac,有几种获取标准 Python 2.7,NumPy,SciPy 和 OpenCV 的可能方法。 所有方法最终都需要使用 Xcode Developer Tools 从源代码编译 OpenCV。 但是,根据方法的不同,第三方工具会以各种方式为我们自动完成此任务。 我们将研究使用 MacPorts 或 Homebrew 的方法。 这些工具可以完成 CMake 可以做的所有事情,此外,它们还可以帮助我们解决依赖关系并将开发库与系统库分离。


提示

我推荐 MacPorts,特别是如果您想通过 OpenNI 和 SensorKinect 编译具有深度摄像头支持的 OpenCV。 相关补丁和构建脚本(包括我维护的补丁)已为 MacPorts 准备就绪。 相比之下,Homebrew 当前不提供使用深度相机支持编译 OpenCV 的现成解决方案。


在继续之前,请确保正确设置 Xcode 开发人员工具:

  1. 从 Mac App Store 或这个页面下载并安装 Xcode。 在安装过程中,如果可以选择安装命令行工具,请选择它。
  2. 打开 Xcode 并接受许可协议。
  3. 如果安装程序没有为我们提供安装命令行工具的选项,则最后一步是必需的。 转到 Xcode | 首选项 | 下载下一步命令行工具旁边的安装按钮。 等待安装完成并退出 Xcode。

现在,我们拥有任何方法所需的编译器。

将 MacPorts 与现成的包一起使用

我们可以使用 MacPorts 包管理器来帮助我们设置 Python 2.7,NumPy 和 OpenCV。 MacPorts 提供了终端命令,这些命令可自动执行各种开源软件OSS)的下载,编译和安装过程。 MacPorts 还根据需要安装依赖项。 对于每个软件,依赖关系和构建秘籍都在称为 Portfile 的配置文件中定义。 MacPorts 存储库是 Portfiles 的集合。

从已经设置了 Xcode 及其命令行工具的系统开始,以下步骤将为我们提供通过 MacPorts 进行的 OpenCV 安装:

从这个页面下载并安装 MacPorts。

如果我们想要支持 Kinect 深度相机,我们需要告诉 MacPorts 在哪里下载我编写的一些自定义 Portfile。 为此,请编辑/opt/local/etc/macports/sources.conf(假设 MacPorts 已安装到默认位置)。 在rsync://rsync.macports.org/release/ports/ [default]行上方,添加以下行:

代码语言:javascript复制
http://nummist.com/opencv/ports.tar.gz

保存文件。 现在,MacPorts 知道先在我的在线存储库中搜索端口文件,然后在默认的在线存储库中搜索。

打开终端并运行以下命令以更新 MacPorts:

代码语言:javascript复制
$ sudo port selfupdate

出现提示时,输入您的密码。

现在(如果我们使用的是我的存储库),运行以下命令以安装带有 Python 2.7 绑定的 OpenCV 并支持包括 Kinect 在内的深度摄像机:

代码语言:javascript复制
$ sudo port install opencv  python27  openni_sensorkinect

或者(有或没有我的存储库),运行以下命令以安装具有 Python 2.7 绑定的 OpenCV 并支持除 Kinect 之外的深度相机:

代码语言:javascript复制
$ sudo port install opencv  python27  openni

依赖关系(包括 Python 2.7,NumPy,OpenNI 和(在第一个示例中)SensorKinect)也会自动安装。

通过在命令中添加 python27,我们指定我们希望具有 Python 2.7 绑定的opencv变体(构建配置)。 同样, openni_sensorkinect通过 OpenNI 和 SensorKinect 指定了对深度摄像头的最大支持。 如果您不想使用深度相机,则可以省略 openni_sensorkinect;如果您不想使用与 OpenNI 兼容的深度相机,而不能使用 Kinect,则可以将其替换为 openni。 要在安装之前查看可用变体的完整列表,我们可以输入:

代码语言:javascript复制
$ port variants opencv

根据我们的定制需求,我们可以向install命令添加其他变体。 为了获得更大的灵活性,我们可以编写我们自己的变体(如下一节所述)。

另外,运行以下命令来安装 SciPy:

代码语言:javascript复制
$ sudo port install py27-scipy

Python 安装的可执行文件名为python2.7。 如果我们想将默认的python可执行文件链接到python2.7,我们也要运行:

代码语言:javascript复制
$ sudo port install python_select
$ sudo port select python python27

将 MacPorts 与您自己的自定义包一起使用

通过一些额外的步骤,我们可以更改 MacPorts 编译 OpenCV 或任何其他软件的方式。 如前所述,MacPorts 的构建秘籍在称为 Portfiles 的配置文件中定义。 通过创建或编辑 Portfile,我们可以访问高度可配置的构建工具(例如 CMake),同时还可以受益于 MacPorts 的功能(例如依赖关系解析)。

假设我们已经安装了 MacPorts。 现在,我们可以配置 MacPorts 以使用我们编写的自定义 Portfile:

在某个地方创建一个文件夹来保存我们的自定义 Portfile。 我们将此文件夹称为<local_repository>

编辑文件/opt/local/etc/macports/sources.conf(假设 MacPorts 已安装到默认位置)。 在rsync://rsync.macports.org/release/ports/ [default]行上方,添加以下行:

代码语言:javascript复制
file://<local_repository>

例如,如果<local_repository>/Users/Joe/Portfiles,则添加:

代码语言:javascript复制
file:///Users/Joe/Portfiles

注意三斜杠。

保存文件。 现在,MacPorts 知道先在<local_repository>中搜索端口文件,然后在其默认的在线存储库中搜索。

打开终端并更新 MacPorts,以确保我们具有默认存储库中的最新 Portfile:

代码语言:javascript复制
$ sudo port selfupdate

让我们以默认存储库的opencv Portfile 为例进行复制。 我们还应该复制目录结构,该结构确定 MacPorts 如何对包进行分类。

代码语言:javascript复制
$ mkdir <local_repository>/graphics/
$ cp /opt/local/var/macports/sources/rsync.macports.org/release/ports/graphics/opencv <local_repository>/graphics

另外,对于包含 Kinect 支持的示例,我们可以从这个页面下载我的在线存储库,解压缩并将其整个graphics文件夹复制到<local_repository>

代码语言:javascript复制
$ cp <unzip_destination>/graphics <local_repository>

编辑<local_repository>/graphics/opencv/Portfile。 请注意,此文件指定 CMake 配置标志,依赖关系和变体。 有关端口文件编辑的详细信息,请转到这个页面。

要查看与 OpenCV 相关的 CMake 配置标志,我们需要查看其源代码。 从http://sourceforge.net/projects/opencvlibrary/files/opencv-unix/2.4.3/OpenCV-2.4.3.tar.bz2/download下载源代码存档,将其解压缩到任何位置,然后阅读<unzip_destination>/OpenCV-2.4.3/CMakeLists.txt

对 Portfile 进行任何编辑后,保存它。

现在,我们需要在本地存储库中生成一个索引文件,以便 MacPorts 可以找到新的 Portfile:

代码语言:javascript复制
$ cd <local_repository>
$ portindex

从现在开始,我们可以像对待其他 MacPorts 包一样对待自定义opencv。 例如,我们可以按以下方式安装它:

代码语言:javascript复制
$ sudo port install opencv  python27  openni_sensorkinect

注意,本地存储库的 Portfile 优先于默认存储库的 Portfile,这是因为/opt/local/etc/macports/sources.conf中列出的顺序。

将 Homebrew 与现成的包一起使用(不支持深度相机)

Homebrew 是另一个可以帮助我们的包管理器。 通常,MacPorts 和 Homebrew 不应安装在同一台计算机上。

从已经安装了 Xcode 及其命令行工具的系统开始,以下步骤将通过 Homebrew 为我们提供 OpenCV 安装:

打开终端并运行以下命令来安装 Homebrew:

代码语言:javascript复制
$ ruby -e "$(curl -fsSkLraw.github.com/mxcl/homebrew/go)"

与 MacPorts 不同,Homebrew 不会自动将其可执行文件放入PATH中。 为此,创建或编辑文件~/.profile,并在顶部添加以下行:

代码语言:javascript复制
export PATH=/usr/local/bin:/usr/local/sbin:$PATH

保存文件并运行以下命令以刷新PATH

代码语言:javascript复制
$ source ~/.profile

请注意,由 Homebrew 安装的可执行文件现在优先于由系统安装的可执行文件。

对于 Homebrew 的自我诊断报告,请运行:

代码语言:javascript复制
$ brew doctor

遵循其提供的任何故障排除建议。

现在,更新 Homebrew:

代码语言:javascript复制
$ brew update

运行以下命令以安装 Python 2.7:

代码语言:javascript复制
$ brew install python

现在,我们可以安装 NumPy。 Homebrew 对 Python 库包的选择是有限的,因此我们使用 Homebrew 的 Python 随附的名为 PIP 的单独的包管理工具:

代码语言:javascript复制
$ pip install numpy

SciPy 包含一些 Fortran 代码,因此我们需要适当的编译器。 我们可以使用 Homebrew 来安装 gfortran 编译器:

代码语言:javascript复制
$ brew install gfortran

现在,我们可以安装 SciPy:

代码语言:javascript复制
$ pip install scipy

要在 64 位系统上安装 OpenCV(自 2006 年底以来,所有 Mac 新硬件),请运行:

代码语言:javascript复制
$ brew install opencv

或者,要在 32 位系统上安装 OpenCV,请运行:

代码语言:javascript复制
$ brew install opencv --build32

提示

下载示例代码

您可以通过http://www.packtpub.com下载从帐户购买的所有 Packt 图书的示例代码文件。 如果您在其他地方购买了此书,则可以访问http://www.packtpub.com/support并注册以将文件直接通过电子邮件发送给您。


将 Homebrew 与您自己的自定义包一起使用

Homebrew 使编辑现有包定义变得容易:

代码语言:javascript复制
$ brew edit opencv

包定义实际上是 Ruby 编程语言中的脚本。 可在 Homebrew Wiki 中找到有关编辑它们的提示。 脚本可以指定 Make 或 CMake 配置标志等。

要查看与 OpenCV 相关的 CMake 配置标志,我们需要查看其源代码。 从这个页面下载源代码存档,将其解压缩到任何位置,然后阅读<unzip_destination>/OpenCV-2.4.3/CMakeLists.txt

对 Ruby 脚本进行任何编辑后,保存它。

定制包装可以视为正常包装。 例如,可以按以下方式安装:

代码语言:javascript复制
$ brew install opencv

选择 Ubuntu 12.04 LTS 或 Ubuntu 12.10

Ubuntu 预先安装了 Python 2.7。 标准的 Ubuntu 存储库包含不支持深度摄像头的 OpenCV 2.3.1 包。 另外,可以使用 CMake 和 GCC 从源代码构建 OpenCV 2.4.3。 从源代码构建时,OpenCV 可以通过 OpenNI 和 SensorKinect 支持深度相机,这些相机可以作为预编译的二进制文件与安装脚本一起使用。

使用 Ubuntu 存储库(不支持深度相机)

我们可以使用 Apt 包管理器安装 OpenCV 2.3.1 及其依赖项:

打开终端并运行此命令以更新 Apt:

代码语言:javascript复制
$ sudo apt-get update

现在,运行以下命令以使用 Python 绑定安装 NumPy,SciPy 和 OpenCV:

代码语言:javascript复制
$ sudo apt-get install python-numpy
$ sudo apt-get install python-scipy
$ sudo apt-get install libopencv-*
$ sudo apt-get install python-opencv

出现有关包安装的提示时,输入Y

同样,我们可以使用 Ubuntu 软件中心,它是 Apt 的图形前端。

通过可自定义的现成脚本使用 CMake

Ubuntu 预先安装了 GCC 编译器。 但是,我们需要安装 CMake 构建系统。 我们还需要安装或重新安装其他各种库,其中一些库需要进行特殊配置以与 OpenCV 兼容。 因为依赖关系很复杂,所以我编写了一个脚本来下载,配置和构建 OpenCV 及其相关库,以便最终的 OpenCV 安装支持包括 Kinect 在内的深度摄像机:

从这个页面下载我的安装脚本,并将其放在任何位置,例如<script_folder>

(可选)编辑脚本以自定义 OpenCV 的构建配置。 若要查看与 OpenCV 相关的 CMake 配置标志,我们需要查看其源代码。 从这个页面下载源代码存档,将其解压缩到任何位置,然后读取<unzip_destination>/OpenCV-2.4.3/CMakeLists.txt

对脚本进行任何编辑后,将其保存。

打开终端并运行此命令以更新 Apt:

代码语言:javascript复制
$ sudo apt-get update

将目录更改为<script_folder>:

代码语言:javascript复制
$ cd <script_folder>

设置脚本的权限,使其可执行:

代码语言:javascript复制
$ chmod  x install_opencv_ubuntu.sh

执行脚本:

代码语言:javascript复制
$ ./install_opencv_ubuntu.sh

出现提示时,输入密码。 出现有关包安装的提示时,输入Y

安装脚本创建一个文件夹<script_folder>/opencv,其中包含该脚本临时使用的下载和构建文件。 安装脚本终止后,可以安全地删除<script_folder>/opencv。 不过,首先,您可能想在<script_folder>/opencv/samples/python<script_folder>/opencv/samples/python2中查看 OpenCV 的 Python 示例。

选择其他类似 Unix 的系统

用于 Ubuntu 的方法(如前所述)可能适用于从 Ubuntu 12.04 LTS 或 Ubuntu 12.10 派生的任何 Linux 发行版,例如:

  • Kubuntu 12.04 LTS 或 Kubuntu 12.10
  • Xubuntu 12.04 LTS 或 Xubuntu 12.10
  • Linux Mint 13 或 Linux Mint 14

在 Debian Linux 及其衍生版本上,Apt 包管理器的工作方式与在 Ubuntu 上相同,尽管可用包可能有所不同。

在 Gentoo Linux 及其衍生版本上,Portage 包管理器类似于 MacPorts(如前所述),尽管可用包可能有所不同。

在其他类似 Unix 的系统上,包管理器和可用包可能会有所不同。 请查阅包管理员的文档,并搜索名称中带有opencv的任何包。 请记住,OpenCV 及其 Python 绑定可能会分为多个包。

另外,查找由系统提供商,存储库维护者或社区发布的所有安装说明。 由于 OpenCV 使用摄像机驱动程序和媒体编解码器,因此在多媒体支持较差的系统上,使其所有功能正常工作可能会很棘手。 在某些情况下,可能需要重新配置或重新安装系统包才能兼容。

如果包可用于 OpenCV,请检查其版本号。 为了本书的目的,建议使用 OpenCV 2.3.1 或更高版本。 还要检查包是否提供 Python 绑定,以及它们是否通过 OpenNI 和 SensorKinect 提供深度相机支持。 最后,检查开发人员社区中是否有人报告使用包的成功或失败。

相反,如果我们想从源代码中进行 OpenCV 的自定义构建,则参考 Ubuntu 的安装脚本(先前已讨论过)并将其调整为适用于另一个系统上的包管理器和包可能会有帮助。

运行示例

运行一些示例脚本是测试 OpenCV 正确设置的好方法。 这些示例包含在 OpenCV 的源代码存档中。

在 Windows 上,我们应该已经下载并解压缩了 OpenCV 的自解压 ZIP。 在<unzip_destination>/opencv/samples中找到样本。

在包括 Mac 在内的类似 Unix 的系统上,请从这个页面下载源代码归档文件并将其解压缩到任何位置(如果我们尚未这样做的话)。 在<unzip_destination>/OpenCV-2.4.3/samples中找到样本。

一些示例脚本需要命令行参数。 但是,以下脚本(以及其他脚本)应该在没有任何参数的情况下运行:

  • python/camera.py:这将显示网络摄像头供稿(假设已插入网络摄像头)。
  • python/drawing.py:绘制一系列形状,如屏幕保护程序。
  • python2/hist.py:显示照片。 按ABCDE查看照片的变化,以及颜色或灰度值的相应直方图。
  • python2/opt_flow.py(Ubuntu 包中缺少):这将显示一个网络摄像头源,其中叠加了可视化的光流(运动方向)。 例如,在网络摄像头上慢慢挥手以查看效果。 按12进行可视化显示。

要退出脚本,请按Esc(不是窗口的关闭按钮)。

如果遇到消息ImportError: No module named cv2.cv,那么我们正在从不了解 OpenCV 的 Python 安装中运行脚本。 有两种可能的解释:

OpenCV 安装中的某些步骤可能失败或错过。 返回并查看步骤。

如果在计算机上安装了多个 Python,则可能是使用了错误的 Python 启动脚本。 例如,在 Mac 上,可能是为 MacPorts Python 安装了 OpenCV,但我们正在使用系统的 Python 运行脚本。 返回并查看有关编辑系统路径的安装步骤。 另外,尝试使用以下命令从命令行手动启动脚本:

代码语言:javascript复制
$ python python/camera.py

您还可以使用以下命令:

代码语言:javascript复制
$ python2.7 python/camera.py

作为选择其他 Python 安装的另一种可能的方法,请尝试编辑示例脚本以删除#!行。 这些行可能会将脚本与错误的 Python 安装(对于我们的特定设置)明确关联。

查找文档,帮助和更新

OpenCV 的文档在线发布在这个页面上。 该文档包含有关 OpenCV 的新 C API,其新的 Python API(基于 C API),其旧的 C API 和其旧的 Python API(基于 C API)的组合 API 参考。 查找类或函数时,请确保阅读有关新 Python API(cv2模块)而不是旧 Python API(cv模块)的部分。


注意

标题为 OpenCV 2.1 Python 参考的文档可能会显示在 Google 搜索OpenCV Python API的文档中。 避免使用此文档,因为它已过时并且仅涵盖旧的(类似于 C 的)Python API。


该文档也可以作为几个可下载的 PDF 文件获得:

  • API 参考
  • 教程 (这些教程使用 C 代码。有关教程代码的 Python 端口,参见 Abid RahmanK。 的资料库位于这个页面。)
  • 用户指南(不完整)

如果您在飞机上或无法访问互联网的其他地方编写代码,则肯定要保留文档的脱机副本。

如果文档似乎无法回答您的问题,请尝试与 OpenCV 社区联系。 在一些网站上,您会找到有用的人:

  • OpenCV 官方论坛
  • DavidMillánEscrivá 的博客(本书的审阅者之一)
  • Abid Rahman K. 博客(本书的审阅者之一)
  • 我这本书的网站

最后,如果您是高级用户,想尝试使用来自最新(不稳定)的 OpenCV 源代码的新功能,错误修复和示例脚本,请查看项目的存储库。

总结

到现在为止,我们应该已经安装了 OpenCV,该安装可以完成本书中描述的项目所需的一切。 根据我们采用的方法,我们可能还会有一组工具和脚本,可用于重新配置和重建 OpenCV,以满足我们未来的需求。

我们知道在哪里可以找到 OpenCV 的 Python 示例。 这些示例涵盖的功能范围不同于本书的项目,但它们可作为额外的学习辅助工具。

二、处理文件,相机和 GUI

本章介绍 OpenCV 的 I/O 功能。 我们还将讨论项目概念以及该项目面向对象设计的开始,我们将在后续章节中充实它们。

通过研究 I/O 功能和设计模式,我们以制作三明治的方式构建项目:从外部开始。面包切片和涂抹或端点和胶粘,先于馅料或算法。 我们之所以选择这种方法,是因为计算机视觉是外向的,它考虑了计算机外部的真实世界,并且我们希望通过通用接口将所有后续的算法工作应用于真实世界。


注意

可以从我的网站下载本章的所有完成代码。


基本 I/O 脚本

所有 CV 应用都需要获取图像作为输入。 大多数还需要产生图像作为输出。 交互式 CV 应用可能需要将摄像机作为输入源,将窗口作为输出目标。 但是,其他可能的来源和目的地包括图像文件,视频文件,和原始字节。 例如,如果我们将过程图形合并到我们的应用中,原始字节可能是通过网络连接接收/发送的,或者可能是由算法生成的。 让我们看看每种可能性。

读取/写入图像文件

OpenCV 提供,,imread()imwrite()函数,这些函数支持静态图像的各种文件格式。 支持的格式因系统而异,但应始终包括 BMP 格式。 通常,PNG,JPEG 和 TIFF 也应该是受支持的格式。 可以从一种文件格式加载图像并保存为另一种文件格式。 例如,让我们将图像从 PNG 转换为 JPEG:

代码语言:javascript复制
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 图像:

代码语言:javascript复制
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

代码语言:javascript复制
byteArray = bytearray(image)

相反,只要bytearray包含适当顺序的字节,我们就可以进行铸造然后对其进行整形,以获得作为图像的numpy.array类型:

代码语言:javascript复制
grayImage = numpy.array(grayByteArray).reshape(height, width)
bgrImage = numpy.array(bgrByteArray).reshape(height, width, 3)

作为更完整的示例,让我们将包含随机字节的bytearray转换为灰度图像和 BGR 图像:

代码语言:javascript复制
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 文件中:

代码语言:javascript复制
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 文件:

代码语言:javascript复制
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()方法。 对于一组相机:

代码语言:javascript复制
success0 = cameraCapture0.grab()
success1 = cameraCapture1.grab()
if success0 and success1:
    frame0 = cameraCapture0.retrieve()
    frame1 = cameraCapture1.retrieve()

对于多头相机,我们必须指定头的索引作为retrieve()的参数:

代码语言:javascript复制
success = multiHeadCameraCapture.grab()
if success:
    frame0 = multiHeadCameraCapture.retrieve(channel = 0)
    frame1 = multiHeadCameraCapture.retrieve(channel = 1)

我们将在第 5 章,“检测前景/背景区域和深度”中更详细地研究多头相机。

在窗口中显示摄像机帧

OpenCV 允许使用namedWindow()imshow()destroyWindow()函数创建,重绘和销毁命名窗口。 同样,任何窗口都可以通过waitKey()函数捕获键盘输入,并通过setMouseCallback()函数捕获鼠标输入。 让我们看一个显示实时摄像机输入帧的示例:

代码语言:javascript复制
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 键码,如下所示:

代码语言:javascript复制
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 代码和应用程​​序代码的分离变得特别方便。

我们将创建名为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的实现。 事实证明,实现时间很长。 因此,我们将分几部分进行研究。 首先,让我们添加导入,构造器和属性,如下所示:

代码语言:javascript复制
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中:

代码语言:javascript复制
    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中:

代码语言:javascript复制
    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中:

代码语言:javascript复制
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的以下实现:

代码语言:javascript复制
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 集成”)。

三、过滤图像

本章介绍了一些更改图像的技术。 我们的目标是实现艺术效果,类似于在图像编辑应用(例如 Photoshop 或 Gimp)中可以找到的过滤器。

在实现过滤器时,您可以尝试将其应用于任何 BGR 图像,然后保存或显示结果。 要充分欣赏每种效果,请在各种照明条件和拍摄对象下进行尝试。 在本章的最后,我们将把过滤器集成到 Cameo 应用中。


注意

可以从我的网站下载本章的所有完成代码。


创建模块

CaptureManagerWindowManager类一样,我们的过滤器应可在 Cameo 外部重用。 因此,我们应该将过滤器分成各自的 Python 模块或文件。

让我们在与cameo.py相同的目录中创建一个名为filters.py的文件。 我们需要在filters.py中使用以下import语句:

代码语言:javascript复制
import cv2
import numpy
import utils

我们还要在同一目录中创建一个名为utils.py的文件。 它应包含以下导入语句:

代码语言:javascript复制
import cv2
import numpy
import scipy.interpolate

我们将在filters.py中添加过滤器函数和类,而utils.py中将使用更多通用数学函数。

通道混合 – 在 Technicolor 中查看

通道混合是重新映射颜色的简单技术。 目标像素处的颜色是(仅)相应源像素处的颜色的函数。 更具体地说,在目标像素处的每个通道的值是在源像素处的任何或所有通道的值的函数。 对于 BGR 图片,使用伪代码:

代码语言:javascript复制
dst.b = funcB(src.b, src.g, src.r)
dst.g = funcG(src.b, src.g, src.r)
dst.r = funcR(src.b, src.g, src.r)

我们可以定义这些函数,但是我们可以。 潜在地,我们可以映射场景的颜色与摄影机或眼睛通常所映射的颜色有很大不同。

通道混合的一种用途是模拟 RGB 或 BGR 内部的其他一些较小的色彩空间。 通过为任意两个通道分配相等的值,我们可以折叠部分颜色空间,并产生一种印象,即我们的调色板仅基于两种颜色的光(相加混合)或两种墨水(相减地混合)。 这种类型的效果可以提供怀旧的价值,因为早期的彩色胶片和早期的数字图形的调色板比当今的数字图形更为有限。

作为示例,让我们发明一些概念色彩空间,这些色彩空间让人想起 1920 年代的 Technicolor 电影和 1980 年代的 CGA 图形。 所有这些概念性色彩空间都可以代表灰色,但不能代表 RGB 的整个色彩范围:

  • RC(红色,青色):请注意,红色和青色可以混合成灰色。 该色彩空间类似于 Technicolor 流程 2 和 CGA 调色板 3。
  • RGV(红色,绿色,值):请注意,红色和绿色不能混合成灰色。 因此,我们还需要指定值或白度。 此色彩空间类似于 Technicolor 流程 1。
  • CMV(青色,洋红色,值):请注意,青色和洋红色不能混合以产生灰色。 因此,我们还需要指定值或白度。 此颜色空间类似于 CGA 调色板 1。

以下是《The Toll of the Sea》(1922)(以 Technicolor 流程 2 拍摄的电影)的屏幕截图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P10kfOE4-1681874125803)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv-cv-py/img/00003.jpg)]

以下图像来自《Commander Keen:再见银河》(1991),该游戏支持 CGA 调色板 1。(有关彩色图像,请参见本书的电子版。):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r28i74lz-1681874125804)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv-cv-py/img/00004.jpg)]

模拟 RC 色彩空间

RC 色彩空间易于在 BGR 中进行模拟。 蓝色和绿色可以混合成青色。 通过平均 B 和 G 通道并将结果存储在 B 和 G 中,我们有效地将这两个通道合为一个 C。为了支持这种效果,让我们向filters.py添加以下函数:

代码语言:javascript复制
def recolorRC(src, dst):
    """Simulate conversion from BGR to RC (red, cyan).

    The source and destination images must both be in BGR format.

    Blues and greens are replaced with cyans.

    Pseudocode:
    dst.b = dst.g = 0.5 * (src.b   src.g)
    dst.r = src.r

    """
    b, g, r = cv2.split(src)
    cv2.addWeighted(b, 0.5, g, 0.5, 0, b)
    cv2.merge((b, b, r), dst)

此函数发生了三件事:

  1. 使用split(),我们将源图像的通道提取为一维数组。 以这种格式放置数据后,我们可以编写清晰,简单的通道混合代码。
  2. 使用addWeighted(),我们用 B 和 G 的平均值替换 B 通道的值。addWeighted()的参数(按顺序)是第一个源数组,一个权重应用于第一个源数组,第二个源数组, 应用于第二个源数组的权重,添加到结果中的常量以及一个目标数组。
  3. 使用merge(),我们用修改后的通道替换目标图像中的值。 请注意,我们两次使用b作为参数,因为我们希望目标的 B 和 G 通道相等。

分割,修改和合并通道的类似步骤也可以应用到我们的其他色彩空间模拟中。

模拟 RGV 色彩空间

在 BGR 中,RGV 色彩空间难以模拟。 我们的直觉可能会说我们应该将所有 B 通道值都设置为0,因为 RGV 不能代表蓝色。 但是,此更改将是错误的,因为它将丢弃亮度的蓝色分量,从而将灰色和浅蓝色变成黄色。 相反,我们希望灰色保持灰色,而浅蓝色变为灰色。 为了获得此结果,我们应将 B 值减小到 B,G 和 R 的每个像素最小值。让我们在filters.py中实现以下效果:

代码语言:javascript复制
def recolorRGV(src, dst):
    """Simulate conversion from BGR to RGV (red, green, value).

    The source and destination images must both be in BGR format.

    Blues are desaturated.

    Pseudocode:
    dst.b = min(src.b, src.g, src.r)
    dst.g = src.g
    dst.r = src.r

    """
    b, g, r = cv2.split(src)
    cv2.min(b, g, b)
    cv2.min(b, r, b)
    cv2.merge((b, g, r), dst)

min()函数计算前两个参数的每个元素的最小值,并将它们写入第三个参数。

模拟 CMV 颜色空间

模拟 CMV 颜色空间与模拟 RGV 非常相似,除了光谱的去饱和部分是黄色而不是蓝色。 要使黄色去饱和,应将 B 值增加到 B,G 和 R 的每个像素最大值。以下是可以添加到filters.py的实现:

代码语言:javascript复制
def recolorCMV(src, dst):
    """Simulate conversion from BGR to CMV (cyan, magenta, value).

    The source and destination images must both be in BGR format.

    Yellows are desaturated.

    Pseudocode:
    dst.b = max(src.b, src.g, src.r)
    dst.g = src.g
    dst.r = src.r

    """
    b, g, r = cv2.split(src)
    cv2.max(b, g, b)
    cv2.max(b, r, b)
    cv2.merge((b, g, r), dst)

max()函数计算前两个参数的每个元素的最大值,并将它们写入第三个参数。

通过设计,前面的三个效果往往会产生严重的颜色失真,尤其是当源图像首先是彩色的时。 如果我们想制作微妙的效果,则将通道使用任意函数混合可能不是最好的方法。

曲线 – 弯曲色彩空间

曲线是另一种用于重新映射颜色的技术。 通道混合和曲线类似,只要目标像素处的颜色是相应源像素处的颜色(仅)即可。 但是,具体而言,通道混合和曲线是不同的方法。 使用曲线时,目标像素处的通道值是(仅)源像素处的相同通道值的函数。 此外,我们不直接定义函数; 相反,对于每个函数,我们定义一组控制点,从中插入函数。 对于 BGR 图片,使用伪代码:

代码语言:javascript复制
dst.b = funcB(src.b) where funcB interpolates pointsB
dst.g = funcG(src.g) where funcG interpolates pointsG
dst.r = funcR(src.r) where funcR interpolates pointsR

插值的类型可能在实现之间有所不同,尽管插值类型应避免在控制点处出现不连续的斜率,而产生曲线。 只要控制点数量足够,我们将使用三次样条插值

绘制曲线

迈向基于曲线的过滤器的第一步是将控制点转换为函数。 大部分工作都是通过名为interp1d()的 SciPy 函数完成的,该函数接受两个数组(xy坐标)并返回一个对点进行插值的函数。 作为interp1d()的可选参数,我们可以指定一种插值,原则上可以是linearnearestzeroslinear(球面线性),quadratic或[ cubic,尽管并非所有选项都在当前版本的 SciPy 中实现。 另一个可选参数bounds_error可以设置为False,以允许外插和内插。

让我们编辑utils.py并添加一个将interp1d()包裹起来的函数,它的接口稍微简单一些:

代码语言:javascript复制
def createCurveFunc(points):
    """Return a function derived from control points."""
    if points is None:
        return None
    numPoints = len(points)
    if numPoints < 2:
        return None
    xs, ys = zip(*points)
    if numPoints < 4:
        kind = 'linear'
        # 'quadratic' is not implemented.
    else:
        kind = 'cubic'
    return scipy.interpolate.interp1d(xs, ys, kind,
                                      bounds_error = False)

我们的函数采用两个(x, y)对数组,而不是两个单独的坐标数组,这可能是指定控制点更容易理解的方式。 必须对数组进行排序,以使x从一个索引增加到下一个索引。 通常,为获得自然效果,y值也应增加,并且第一个和最后一个控制点应为(0, 0)(255, 255),以便保留黑色和白色。 请注意,我们会将x视为通道的输入值,并将y视为相应的输出值。 例如,(128, 160)可使通道的中间色调变亮。

请注意,cubic插值至少需要四个控制点。 如果只有两个或三个控制点,我们将退回到linear插值,但是,为获得自然外观效果,应避免这种情况。

缓存和应用曲线

现在我们可以获得插值任意控制点的曲线的函数 。 但是,此函数可能很昂贵。 我们不希望每个通道每个像素运行一次(例如,如果应用于640 x 480视频的三个通道,则每帧运行 921,600 次)。 幸运的是,我们通常只处理 256 个可能的输入值(每个通道 8 位),并且可以廉价地预先计算并存储许多输出值。 然后,我们的每通道每像素成本只是对缓存的输出值的查找。

让我们编辑utils.py并添加函数以为给定函数创建一个查找数组,并将该查找数组应用于另一个数组(例如,图像):

代码语言:javascript复制
def createLookupArray(func, length = 256):
    """Return a lookup for whole-number inputs to a function.

    The lookup values are clamped to [0, length - 1].

    """
    if func is None:
        return None
    lookupArray = numpy.empty(length)
    i = 0
    while i < length:
        func_i = func(i)
        lookupArray[i] = min(max(0, func_i), length - 1)
        i  = 1
    return lookupArray

def applyLookupArray(lookupArray, src, dst):
    """Map a source to a destination using a lookup."""
    if lookupArray is None:
        return
    dst[:] = lookupArray[src]

请注意,createLookupArray()中的方法仅限于整数输入值,因为该输入值用作数组的索引。 applyLookupArray()函数通过使用源数组的值作为查找数组的索引来工作。 Python 的切片符号([:])用于将查找到的值复制到目标数组中。

让我们考虑另一个优化。 如果我们总是想连续应用两个或更多曲线怎么办? 执行多次查找效率低下,并且可能导致精度损失。 我们可以通过在创建查找数组之前将两个曲线函数组合为一个函数来避免此问题。 让我们再次编辑utils.py并添加以下函数,该函数返回两个给定函数的组合:

代码语言:javascript复制
def createCompositeFunc(func0, func1):
    """Return a composite of two functions."""
    if func0 is None:
        return func1
    if func1 is None:
        return func0
    return lambda x: func0(func1(x))

createCompositeFunc()中的方法仅限于每个都带有单个参数的输入函数。 参数必须是兼容类型。 请注意,使用 Python 的lambda关键字创建匿名函数。

这是最终的优化问题。 如果我们想对图像的所有通道应用相同的曲线怎么办? 在这种情况下,拆分和合并通道是浪费的,因为我们不需要区分通道。 我们只需要applyLookupArray()使用的一维索引。 让我们编辑utils.py以添加一个函数,该函数将一维接口返回到预先存在的给定数组,该数组可能是多维的:

代码语言:javascript复制
def createFlatView(array):
    """Return a 1D view of an array of any dimensionality."""
    flatView = array.view()
    flatView.shape = array.size
    return flatView

返回类型为numpy.view,其接口与numpy.array几乎相同,但是numpy.view仅拥有对数据的引用,而不是副本。

createFlatView()中的方法适用于具有任意数量通道的图像。 因此,当我们希望所有通道都相同时,它可以抽象出灰度图像和彩色图像之间的差异。

设计面向对象的曲线过滤器

由于我们为每个曲线缓存了一个查找数组,因此基于曲线的过滤器具有与之关联的数据。 因此,它们需要是类,而不仅仅是函数。 让我们制作一对曲线过滤器类,以及可以应用任何函数而不仅仅是曲线函数的相应高级类:

  • VFuncFilter:这是一个用函数实例化的类,以后可以使用apply()将其应用于图像。 该函数适用​​于灰度图像的 V(值)通道或彩色图像的所有通道。
  • VcurveFilter:这是VFuncFilter的子类。 而不是使用函数实例化,而是使用一组控制点实例化,这些控制点在内部用于创建曲线函数。
  • BGRFuncFilter:这是一个用最多四个函数实例化的类,以后可以使用apply()将其应用于 BGR 图像。 这些函数之一适用于所有通道,其他三个函数分别适用于单个通道。 首先应用整体函数,然后再应用每通道函数。
  • BGRCurveFilter:这是BGRFuncFilter的子类。 而不是用四个函数实例化,而是用四组控制点实例化,这些控制点在内部用于创建曲线函数。

此外,所有这些类都接受数字类型的构造器参数,例如numpy.uint8,每个通道 8 位。 此类型用于确定查找数组中应包含多少个条目。

让我们首先看一下VFuncFilterVcurveFilter的实现,它们都可以添加到filters.py中:

代码语言:javascript复制
class VFuncFilter(object):
    """A filter that applies a function to V (or all of BGR)."""

    def __init__(self, vFunc = None, dtype = numpy.uint8):
        length = numpy.iinfo(dtype).max   1
        self._vLookupArray = utils.createLookupArray(vFunc, length)

    def apply(self, src, dst):
        """Apply the filter with a BGR or gray source/destination."""
        srcFlatView = utils.flatView(src)
        dstFlatView = utils.flatView(dst)
        utils.applyLookupArray(self._vLookupArray, srcFlatView,
                               dstFlatView)

class VCurveFilter(VFuncFilter):
    """A filter that applies a curve to V (or all of BGR)."""

    def __init__(self, vPoints, dtype = numpy.uint8):
        VFuncFilter.__init__(self, utils.createCurveFunc(vPoints),
                             dtype)

在这里,我们正在内部使用一些以前的函数:createCurveFunc()createLookupArray()flatView()applyLookupArray()。 我们还使用numpy.iinfo()根据给定的数字类型确定相关的查找值范围。

现在,让我们看一下BGRFuncFilterBGRCurveFilter的实现,它们也都可以添加到filters.py中:

代码语言:javascript复制
class BGRFuncFilter(object):
    """A filter that applies different functions to each of BGR."""

    def __init__(self, vFunc = None, bFunc = None, gFunc = None,
                 rFunc = None, dtype = numpy.uint8):
        length = numpy.iinfo(dtype).max   1
        self._bLookupArray = utils.createLookupArray(
            utils.createCompositeFunc(bFunc, vFunc), length)
        self._gLookupArray = utils.createLookupArray(
            utils.createCompositeFunc(gFunc, vFunc), length)
        self._rLookupArray = utils.createLookupArray(
            utils.createCompositeFunc(rFunc, vFunc), length)

    def apply(self, src, dst):
        """Apply the filter with a BGR source/destination."""
        b, g, r = cv2.split(src)
        utils.applyLookupArray(self._bLookupArray, b, b)
        utils.applyLookupArray(self._gLookupArray, g, g)
        utils.applyLookupArray(self._rLookupArray, r, r)
        cv2.merge([b, g, r], dst)

class BGRCurveFilter(BGRFuncFilter):
    """A filter that applies different curves to each of BGR."""

    def __init__(self, vPoints = None, bPoints = None,
                 gPoints = None, rPoints = None, dtype = numpy.uint8):
        BGRFuncFilter.__init__(self,
                               utils.createCurveFunc(vPoints),
                               utils.createCurveFunc(bPoints),
                               utils.createCurveFunc(gPoints),
                               utils.createCurveFunc(rPoints), dtype)

同样,我们正在内部使用一些以前的函数:createCurveFunc()createCompositeFunc()createLookupArray()applyLookupArray()。 我们还使用iinfo()split()merge()

这四个类可以按原样使用,在实例化时将自定义函数或控制点作为参数传递。 或者,我们可以创建其他子类,这些子类对某些函数或控制点进行硬编码。 这样的子类可以实例化而无需任何参数。

模拟摄影胶片

曲线的常用用法是模拟数字前摄影中常用的调色板。 每种类型的胶卷都有自己独特的颜色(或灰色)表示法,但我们可以概括一些与数字传感器的区别。 胶片容易遭受细节损失和阴影饱和度的困扰,而数字趋向于遭受高光部分的这些缺陷。 而且,薄膜在光谱的不同部分上趋于具有不均匀的饱和度。 因此,每部影片都有弹出或跳出的某些颜色。

因此,当我们想到漂亮的电影照片时,我们可能会想到明亮的场景并呈现出某些主导色彩。 在另一个极端,我们可能还记得曝光不足的胶片的暗淡外观,而实验室技术人员的努力无法改善这种模糊的外观。

我们将使用曲线创建四个不同的胶片状过滤器。 他们的灵感来自三种胶片和一种处理技术:

  • 柯达 Portra,专为肖像和婚礼而设计的胶卷系列
  • 通用电影系列富士 Provia
  • 富士 Velvia,针对风景优化的电影系列
  • 交叉处理,一种非标准的胶片处理技术,有时用于在时装和乐队摄影中产生低劣的外观

每个电影模拟效果都是BGRCurveFilter的非常简单的子类。 我们只是重写构造器为每个通道指定一组控制点。 控制点的选择基于摄影师 Petteri Sulonen 的建议。 在这个页面查看他关于胶片状曲线的文章。

Portra,Provia 和 Velvia 效果应产生外观正常的图像。 除了前后比较之外,效果应该不明显。

模拟柯达 Portra

Portra 具有较宽的高光范围,倾向于暖色(琥珀色),而阴影较冷(更蓝)。 作为人像电影,它倾向于使人们的肤色更白皙。 而且,它会夸大某些常见的衣服颜色,例如乳白色(例如婚纱)和深蓝色(例如西装或牛仔裤)。 让我们将 Portra 过滤器的此实现添加到filters.py

代码语言:javascript复制
class BGRPortraCurveFilter(BGRCurveFilter):
    """A filter that applies Portra-like curves to BGR."""

    def __init__(self, dtype = numpy.uint8):
        BGRCurveFilter.__init__(
            self,
            vPoints = [(0,0),(23,20),(157,173),(255,255)],
            bPoints = [(0,0),(41,46),(231,228),(255,255)],
            gPoints = [(0,0),(52,47),(189,196),(255,255)],
            rPoints = [(0,0),(69,69),(213,218),(255,255)],
            dtype = dtype)

模拟富士 Provia

普罗维亚(Provia)具有强烈的对比度,并且在大多数色调中略微凉爽(蓝色)。 天空,水,和阴影比太阳增强更多。 让我们将 Provia 过滤器的实现添加到filters.py

代码语言:javascript复制
class BGRProviaCurveFilter(BGRCurveFilter):
    """A filter that applies Provia-like curves to BGR."""

    def __init__(self, dtype = numpy.uint8):
        BGRCurveFilter.__init__(
            self,
            bPoints = [(0,0),(35,25),(205,227),(255,255)],
            gPoints = [(0,0),(27,21),(196,207),(255,255)],
            rPoints = [(0,0),(59,54),(202,210),(255,255)],
            dtype = dtype)

模拟富士 Velvia

Velvia 具有深阴影和鲜艳的色彩。 它通常可以在白天产生蔚蓝的天空,在日落时产生深红色的云。 效果很难模拟,但是这是我们可以添加到filters.py的尝试:

代码语言:javascript复制
class BGRVelviaCurveFilter(BGRCurveFilter):
    """A filter that applies Velvia-like curves to BGR."""

    def __init__(self, dtype = numpy.uint8):
        BGRCurveFilter.__init__(
            self,
            vPoints = [(0,0),(128,118),(221,215),(255,255)],
            bPoints = [(0,0),(25,21),(122,153),(165,206),(255,255)],
            gPoints = [(0,0),(25,21),(95,102),(181,208),(255,255)],
            rPoints = [(0,0),(41,28),(183,209),(255,255)],
            dtype = dtype)

模拟交叉处理

交叉处理会在阴影中产生强烈的蓝色或绿蓝色调,在高光部分产生强烈的黄色或绿黄色。 黑色和白色不一定要保留。 另外,对比度非常高。 交叉处理的照片看起来很不舒服。 人们看起来黄疸,而无生命的物体看起来很脏。 让我们编辑filters.py以添加以下交叉处理过滤器的实现:

代码语言:javascript复制
class BGRCrossProcessCurveFilter(BGRCurveFilter):
    """A filter that applies cross-process-like curves to BGR."""

    def __init__(self, dtype = numpy.uint8):
        BGRCurveFilter.__init__(
            self,
            bPoints = [(0,20),(255,235)],
            gPoints = [(0,0),(56,39),(208,226),(255,255)],
            rPoints = [(0,0),(56,22),(211,255),(255,255)],
            dtype = dtype)

突出显示边缘

边缘在人类和计算机视觉中都扮演着重要角色。 我们作为人类,仅通过查看背光轮廓或粗略草图就可以轻松识别许多对象类型及其姿势。 确实,当艺术强调边缘和姿势时,它似乎常常传达出原型的想法,例如罗丹的《思想家》或乔·舒斯特的《超人》。 软件也可以推断出边缘,姿势和原型。 我们将在后面的章节中讨论这类推理。

目前,我们对简单使用边缘来达到艺术效果感兴趣。 我们将使用粗体黑线跟踪图像的边缘。 效果应让人联想到用毡笔绘制的漫画书或其他插图。

OpenCV 提供了许多边缘过滤器,包括Laplacian()Sobel()Scharr()。 这些过滤器应该将非边缘区域变成黑色,而将边缘区域变成白色或饱和色。 但是,它们易于将噪声误识别为边缘。 可以通过在尝试查找边缘之前对图像进行模糊处理来缓解此缺陷。 OpenCV 还提供了许多模糊过滤器,包括blur()(简单平均值),medianBlur()GaussianBlur()。 边缘查找和模糊过滤器的参数有所不同,但始终包含ksize,这是一个奇数,代表过滤器核的宽度和高度(以像素为单位)。


注意

是一组权重,这些权重应用于源图像中的区域以在目标图像中生成单个像素。 例如,7ksize表示在生成每个目的地像素时考虑了49 (7 x 7)源像素。 我们可以将核视为一块磨砂玻璃,它在源图像上方移动,并让源光散射扩散。


为了模糊,让我们使用medianBlur(),它可以有效消除数字视频噪声,尤其是在彩色图像中。 对于边缘查找,让我们使用Laplacian(),它会产生粗体的边缘线,尤其是在灰度图像中。 应用medianBlur()之后,但应用Laplacian()之前,我们应该从 BGR 转换为灰度。

获得Laplacian()的结果后,我们可以将其反转为白色背景上的黑色边缘。 然后,我们可以对其进行归一化(使其值的范围为 0 到 1),然后将其与源图像的相乘以使边缘变暗。 让我们在filters.py中实现这种方法:

代码语言:javascript复制
def strokeEdges(src, dst, blurKsize = 7, edgeKsize = 5):
    if blurKsize >= 3:
        blurredSrc = cv2.medianBlur(src, blurKsize)
        graySrc = cv2.cvtColor(blurredSrc, cv2.COLOR_BGR2GRAY)
    else:
        graySrc = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY)
    cv2.Laplacian(graySrc, cv2.cv.CV_8U, graySrc, ksize = edgeKsize)
    normalizedInverseAlpha = (1.0 / 255) * (255 - graySrc)
    channels = cv2.split(src)
    for channel in channels:
        channel[:] = channel * normalizedInverseAlpha
    cv2.merge(channels, dst)

请注意,我们允许将核大小指定为strokeEdges()的参数。 blurKsize自变量用作medianBlur()ksize,而edgeKsize用作Laplacian()ksize。 使用网络摄像头,我发现7blurKsize值和5edgeKsize值看起来最好。 不幸的是,medianBlur()与像7这样的大型ksize一样昂贵。 如果在运行strokeEdges()时遇到性能问题,请尝试减小blurKsize的值。 要关闭模糊,请将其设置为小于3的值。

自定义核 – 令人费解

如我们所见,OpenCV 的许多预定义过滤器使用核。 请记住,核是一组权重,这些权重确定如何根据输入像素的邻域计算每个输出像素。 核的另一个术语是卷积矩阵。 它混合或卷积区域中的像素。 类似地,基于核的过滤器可以称为卷积过滤器。

OpenCV 提供了非常通用的函数filter2D()和,该函数应用了我们指定的任何核或卷积矩阵。 要了解如何使用此函数,让我们首先学习卷积矩阵的格式。 它是一个二维数组,具有奇数行和列。 中心元素对应于关注像素,其他元素对应于该像素的邻居。 每个元素都包含一个整数或浮点值,该值是应用于输入像素值的权重。 考虑以下示例:

代码语言:javascript复制
kernel = numpy.array([[-1, -1, -1],
                      [-1,  9, -1],
                      [-1, -1, -1]])

在此,关注像素的权重为9,其相邻像素的权重为-1。 对于感兴趣的像素,输出颜色将是其输入颜色的九倍,减去所有八个相邻像素的输入颜色。 如果感兴趣的像素与其相邻像素已经有点不同,,这种差异就会加剧。 效果是,随着邻居之间的对比度增加,图像看起来更加清晰

继续我们的示例,我们可以将此卷积矩阵应用于源图像和目标图像,如下所示:

代码语言:javascript复制
cv2.filter2D(src, -1, kernel, dst)

第二个参数指定目标图像的每通道深度(例如,每通道 8 位的cv2.CV_8U)。 负值(此处使用)表示目标图像的深度与源图像的深度相同。


注意

对于彩色图像,请注意filter2D()将核均等地应用于每个通道。 要在不同的通道上使用不同的核,我们还必须使用split()merge()函数,就像我们在较早的通道混合函数中所做的那样。 (请参阅“模拟 RC 色彩空间”部分。)


基于这个简单的示例,让我们向filters.py添加两个类。 一类VConvolutionFilter通常代表卷积过滤器。 子类SharpenFilter将专门代表我们的锐化过滤器。 让我们编辑filters.py来实现这两个新类,如下所示:

代码语言:javascript复制
class VConvolutionFilter(object):
    """A filter that applies a convolution to V (or all of BGR)."""

    def __init__(self, kernel):
        self._kernel = kernel

    def apply(self, src, dst):
        """Apply the filter with a BGR or gray source/destination."""
        cv2.filter2D(src, -1, self._kernel, dst)

class SharpenFilter(VConvolutionFilter):
    """A sharpen filter with a 1-pixel radius."""

    def __init__(self):
        kernel = numpy.array([[-1, -1, -1],
                              [-1,  9, -1],
                              [-1, -1, -1]])
        VConvolutionFilter.__init__(self, kernel)

该模式与VCurveFilter类及其子类非常相似。 (请参阅“设计面向对象的曲线过滤器”部分。)

注意,权重总和为1。 每当我们要保持图像的整体亮度不变时,都应该是这种情况。 如果我们稍微修改锐化核,使其权重总和为0,则我们有一个边缘检测核,该边缘会将边缘变为白色,将非边缘变为黑色。 例如,让我们在filters.py中添加以下边缘检测过滤器:

代码语言:javascript复制
class FindEdgesFilter(VConvolutionFilter):
    """An edge-finding filter with a 1-pixel radius."""

    def __init__(self):
        kernel = numpy.array([[-1, -1, -1],
                              [-1,  8, -1],
                              [-1, -1, -1]])
        VConvolutionFilter.__init__(self, kernel)

接下来,让我们做一个模糊过滤器。 通常,对于模糊效果,权重之和应为1,并且在整个邻域中应为正。 例如,我们可以对邻域取一个简单的平均值,如下所示:

代码语言:javascript复制
class BlurFilter(VConvolutionFilter):
    """A blur filter with a 2-pixel radius."""

    def __init__(self):
        kernel = numpy.array([[0.04, 0.04, 0.04, 0.04, 0.04],
                              [0.04, 0.04, 0.04, 0.04, 0.04],
                              [0.04, 0.04, 0.04, 0.04, 0.04],
                              [0.04, 0.04, 0.04, 0.04, 0.04],
                              [0.04, 0.04, 0.04, 0.04, 0.04]])
        VConvolutionFilter.__init__(self, kernel)

我们的锐化,边缘检测和模糊过滤器使用高度对称的核。 但是有时,对称性较低的核会产生有趣的效果。 让我们考虑一个在一侧模糊(权重为正)而在另一侧锐化(权重为负)的核。 它将产生凸纹或浮雕效果。 这是我们可以添加到filters.py的实现:

代码语言:javascript复制
class EmbossFilter(VConvolutionFilter):
    """An emboss filter with a 1-pixel radius."""

    def __init__(self):
        kernel = numpy.array([[-2, -1, 0],
                              [-1,  1, 1],
                              [ 0,  1, 2]])
        VConvolutionFilter.__init__(self, kernel)

这套自定义卷积过滤器非常基础。 实际上,它比 OpenCV 的现成的过滤器集更基础。 但是,通过一些试验,您应该能够编写自己的核,从而产生独特的外观。

修改应用

现在,我们已经为几个过滤器提供了高级函数和类,将这些过滤器中的任何一个应用到 Cameo 中捕获的帧上都是微不足道的。 让我们编辑cameo.py,并在以下摘录中添加以粗体显示的行:

代码语言:javascript复制
import cv2
import filters
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)
        self._curveFilter = filters.BGRPortraCurveFilter()

    def run(self):
        """Run the main loop."""
        self._windowManager.createWindow()
        while self._windowManager.isWindowCreated:
            self._captureManager.enterFrame()
            frame = self._captureManager.frame

            # TODO: Track faces (Chapter 3).

            filters.strokeEdges(frame, frame)
            self._curveFilter.apply(frame, frame)

            self._captureManager.exitFrame()
            self._windowManager.processEvents()

    # ... The rest is the same as in Chapter 2.

在这里,我选择应用两种效果:抚摸边缘和模拟 Portra 胶片的颜色。 随时修改代码以应用您喜欢的任何过滤器。

这是来自 Cameo 的屏幕截图,带有描边和类似 Portra 的颜色:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FFuLh8Ji-1681874125805)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv-cv-py/img/00005.jpg)]

总结

在这一点上,我们应该有一个显示过滤后的相机提要的应用。 我们还应该有更多的过滤器实现,可以很容易地与我们当前使用的实现互换。 现在,我们准备分析每个框架,以便在下一章中找到要操纵的面孔。

四、使用 Haar 级联跟踪人脸

本章介绍一些 OpenCV 的跟踪功能,以及定义特定类型的可跟踪对象的数据文件。 具体来说,我们看一下 Haar 级联分类器,它们可以分析相邻图像区域之间的对比度,以确定给定图像或子图像是否与已知类型匹配。 我们考虑如何在层次结构中组合多个 Haar 级联分类器,以使一个分类器标识父区域(出于我们的目的,是面部),而其他分类器标识子区域(眼睛,鼻子和嘴巴)。

我们还绕过了谦虚但重要的矩形主题。 通过绘制,复制和调整矩形图像区域的大小,我们可以对正在跟踪的图像区域执行简单的操作。

在本章的最后,我们将把面部跟踪和矩形操作集成到 Cameo 中。 最后,我们将进行一些面对面的互动!


注意

可以从我的网站下载本章的所有完成代码。


概念化 Haar 级联

当我们谈论对对象进行分类并跟踪它们的位置时,我们究竟要精确指出什么? 什么构成对象的可识别部分?

即使来自网络摄像头的摄影图像也可能包含许多细节,以使我们(人类)观看愉悦。 然而,关于照明,视角,观看距离,相机抖动和数字噪声的变化,图像细节趋于不稳定。 此外,就分类而言,即使是物理细节上的实际差异也可能使我们不感兴趣。 我在学校里被教过,在显微镜下没有两个雪花看起来像。 幸运的是,作为一个加拿大孩子,我已经学会了如何在没有显微镜的情况下识别雪花,因为相似之处在体积上更加明显。

因此,一些抽象图像细节的方法可用于产生稳定的分类和跟踪结果。 抽象被称为特征,据说是从图像数据中提取的。 特征应该比像素少得多,尽管任何像素都可能影响多个特征。 可以基于图像相应特征之间的距离来评估两个图像之间的相似度。 例如,可以根据空间坐标或颜色坐标来定义距离。 类似 Haar 的特征是通常应用于实时面部跟踪的一种特征。 它们是 2001 年由 Paul Viola 和 Michael Jones 首次用于此目的的。每个类似 Haar 的特征都描述了相邻图像区域之间的对比度模式。 例如,边缘,顶点和细线均会产生鲜明的特征。 对于任何给定的图像,特征可以根据区域的大小而变化,这可以称为窗口大小。 尽管窗口大小不同,但只有两个比例不同的两个图像应该能够产生相似的特征。 因此,生成多个窗口大小的特征很有用。 这样的特征集合称为级联。 我们可以说 Haar 级联是尺度不变的,换句话说,它对尺度变化具有鲁棒性。 OpenCV 为规模不变的 Haar 级联提供了分类器和跟踪器,它期望采用特定的文件格式。 如在 OpenCV 中实现的 Haar 级联,对于旋转的变化并不稳健。 例如,上下颠倒的脸部不被视为与直立的脸部相似,并且侧面观察的脸部不被视为与从正面观看的脸部相似。 考虑到图像的多种转换以及多种窗口大小,更复杂,更耗费资源的实现可以提高 Haar 级联的旋转鲁棒性。 但是,我们将局限于 OpenCV 中的实现。

获取 Haar 级联数据

作为 OpenCV 设置的一部分,可能有一个名为haarcascades的目录。 它包含使用 OpenCV 随附的工具针对特定主题进行训练的级联。 目录的完整路径取决于您的系统和设置 OpenCV 的方法,如下所示:

  • 从源归档文件构建<unzip_destination>/data/haarcascades
  • 带有自解压 ZIP 的 Windows<unzip_destination>/data/haarcascades
  • 带有 MacPorts 的 Mac/opt/local/share/OpenCV/haarcascades
  • 带有 Homebrew 的 Mac:不包括haarcascades文件;请参见haarcascades文件。 要获取它,请下载源存档
  • 带有 APT 或软件中心的 Ubuntu:不包括haarcascades文件; 要获取它,请下载源归档文件

提示

如果找不到haarcascades,请从这个页面 下载源归档文件(或从这个页面下载 Windows 自解压 ZIP) ,将其解压缩,然后寻找<unzip_destination>/data/haarcascades


找到haarcascades后,在与cameo.py相同的文件夹中创建一个名为cascades的目录,并将以下文件从haarcascades复制到cascades

代码语言:javascript复制
haarcascade_frontalface_alt.xml
haarcascade_eye.xml
haarcascade_mcs_nose.xml
haarcascade_mcs_mouth.xml

顾名思义,这些级联用于跟踪脸部,眼睛,鼻子和嘴巴。 他们需要正面,正面查看主题。 稍后在构建高级跟踪器时将使用它们。 如果您对如何生成这些数据集感到好奇,请参考附录 B,“为自定义目标生成 Haar 级联”。 有了足够的耐心和功能强大的计算机,您可以制作自己的瀑布,并针对各种类型的物体进行训练。

创建模块

我们应该继续保持专用代码与可重用代码之间的良好分离。 让我们为跟踪类及其助手创建新的模块。

应该在与cameo.py相同的目录中(并且等效地在cascades的父目录中)创建一个名为trackers.py的文件。 让我们将以下import语句放在trackers.py的开头:

代码语言:javascript复制
import cv2
import rects
import utils

除了trackers.pycameo.py之外,让我们制作另一个名为rects.py的文件,其中包含以下导入语句:

代码语言:javascript复制
import cv2

我们的脸部追踪器和一张脸部定义将出现在trackers.py中,而各种帮助者将出现在rects.py和我们先前存在的utils.py文件中。

将人脸定义为矩形层次结构

在开始实现高级跟踪器之前,我们应该定义我们想要获得的跟踪结果的类型。 对于许多应用而言,估计对象在真实 3D 空间中的摆放方式非常重要。 但是,我们的应用是关于图像处理的。 因此,我们更关心 2D 图像空间。 人脸的正立正面图应占据图像中的大致矩形区域。 在这样的区域内,眼睛,鼻子和嘴巴应该占据粗糙的矩形子区域。 让我们打开trackers.py并添加一个包含相关数据的类:

代码语言:javascript复制
class Face(object):
    """Data on facial features: face, eyes, nose, mouth."""

    def __init__(self):
        self.faceRect = None
        self.leftEyeRect = None
        self.rightEyeRect = None
        self.noseRect = None
        self.mouthRect = None

注意

每当我们的代码包含作为属性或函数参数的矩形时,我们都将假定其为(x, y, w, h)格式,单位为像素,左上角为(x, y),右下角为[ (x w, y h)。 OpenCV 有时使用兼容的表示形式,但并非总是如此。 因此,在向 OpenCV 发送矩形或从 OpenCV 接收矩形时,我们必须小心。 例如,有时 OpenCV 需要左上角和右下角作为坐标对。


跟踪,剪切和粘贴矩形

当我上小学时,我的手艺很差。 我经常不得不将未完成的手工艺品项目带回家,母亲自愿在那里为我完成这些项目,以便我可以花更多的时间在计算机上。 如果不考虑那些日子,我将永远不会剪切并粘贴一张纸或一个字节数组。

与工艺一样,如果我们首先绘制轮廓,则更容易发现图形程序中的错误。 出于调试目的,Cameo 将包括一个选项,可在Face表示的任何矩形周围绘制线条。 OpenCV 提供了用于绘图的rectangle()函数。 但是,其参数表示的矩形不同于Face。 为了方便起见,让我们在rects.py中添加以下rectangle()包装器:

代码语言:javascript复制
def outlineRect(image, rect, color):
    if rect is None:
        return
    x, y, w, h = rect
    cv2.rectangle(image, (x, y), (x w, y h), color)

在此,color通常应为 BGR 三元组(值的范围为 0 到 255)或灰度值(范围为 0 到 255),具体取决于图像的格式。

接下来,Cameo 必须支持将一个矩形的内容复制到另一个矩形。 我们可以使用 Python 的切片符号在图像内读取或写入矩形。 记住图像的第一个索引是y坐标或行,我们可以将矩形指定为image[y:y h, x:x w]。 对于复制,如果矩形的源和目标的大小不同,则会出现复杂问题。 当然,我们希望两个面孔的大小不同,因此我们必须解决这种情况。 OpenCV 提供resize()函数,使我们可以指定目标大小和插值方法。 结合切片和调整大小,我们可以将以下复制函数实现添加到rects.py中:

代码语言:javascript复制
def copyRect(src, dst, srcRect, dstRect,
             interpolation = cv2.INTER_LINEAR):
    """Copy part of the source to part of the destination."""

    x0, y0, w0, h0 = srcRect
    x1, y1, w1, h1 = dstRect

    # Resize the contents of the source sub-rectangle.
    # Put the result in the destination sub-rectangle.
    dst[y1:y1 h1, x1:x1 w1] = 
        cv2.resize(src[y0:y0 h0, x0:x0 w0], (w1, h1),
                   interpolation = interpolation)

OpenCV 支持以下插值选项:

  • cv2.INTER_NEAREST:这是最近邻插值,价格便宜,但会产生块状结果
  • cv2.INTER_LINEAR:这是双线性插值(默认值),在实时应用的成本和质量之间提供了很好的折衷
  • cv2.INTER_AREA:这是像素面积关系,在缩小时可能会在成本和质量之间提供更好的折衷,但在放大时会产生块状结果
  • cv2.INTER_CUBIC:这是在4 x 4像素邻域上的双三次插值,是一种高成本,高质量的方法
  • cv2.INTER_LANCZOS4:这是在8 x 8像素邻域上进行 Lanczos 插值的方法,是成本最高,质量最高的方法

如果我们要支持交换两个或更多矩形内容,则复制变得更加复杂。 考虑以下方法,这是错误的:

代码语言:javascript复制
copyRect(image, image, rect0, rect1) # overwrite rect1
copyRect(image, image, rect1, rect0) # copy from rect1
# Oops! rect1 was already overwritten by the time we copied from it!

相反,我们需要在覆盖任何内容之前将其中一个矩形复制到一个临时数组。 让我们编辑rects.py以添加以下函数,该函数交换单个源图像中两个或更多矩形的内容:

代码语言:javascript复制
def swapRects(src, dst, rects,
              interpolation = cv2.INTER_LINEAR):
    """Copy the source with two or more sub-rectangles swapped."""

    if dst is not src:
        dst[:] = src

    numRects = len(rects)
    if numRects < 2:
        return

    # Copy the contents of the last rectangle into temporary storage.
    x, y, w, h = rects[numRects - 1]
    temp = src[y:y h, x:x w].copy()

    # Copy the contents of each rectangle into the next.
    i = numRects - 2
    while i >= 0:
        copyRect(src, dst, rects[i], rects[i 1], interpolation)
        i -= 1

    # Copy the temporarily stored content into the first rectangle.
    copyRect(temp, dst, (0, 0, w, h), rects[0], interpolation)

交换是圆形的,因此它可以支持任意数量的矩形。 每个矩形的内容都以下一个矩形为准,但最后一个矩形的内容以第一个矩形为准。

对于 Cameo,这种方法应该可以很好地为我们服务,但是仍然不是万无一失。 直觉可能告诉我们,以下代码应保持image不变:

代码语言:javascript复制
swapRects(image, image, rect0, rect1)
swapRects(image, image, rect1, rect0)

但是,如果rect0rect1重叠,则我们的直觉可能不正确。 如果看到奇怪的结果,请研究交换重叠矩形的可能性。

添加更多工具函数

上一章,我们为一些辅助函数创建了一个名为utils的模块。 几个额外的帮助程序函数将使我们更容易编写跟踪器。

首先,了解图像是灰度还是彩色可能很有用。 我们可以根据图像的维数来判断。 彩色图像是 3D 数组,而灰度图像的尺寸较小。 让我们在utils.py中添加以下函数以测试图像是否为灰度级:

代码语言:javascript复制
def isGray(image):
    """Return True if the image has one channel per pixel."""
    return image.ndim < 3

其次,了解图像的尺寸并将这些尺寸除以给定的因子可能会很有用。 图像(或其他数组)的高度和宽度分别是其shape属性中的前两个条目。 让我们在utils.py中添加以下函数,以获取图像的尺寸除以一个值:

代码语言:javascript复制
def widthHeightDividedBy(image, divisor):
    """Return an image's dimensions, divided by a value."""
    h, w = image.shape[:2]
    return (w/divisor, h/divisor)

现在,让我们回到本章的主要主题跟踪上。

追踪人脸

使用 OpenCV 的 Haar 级联分类器的挑战不仅在于获得跟踪结果,而且还在于获得跟踪结果。 它以高帧频获得一系列有意义的跟踪结果。 我们可以强制执行的一种常识是,某些被跟踪的对象应该具有层次关系,其中一个相对于另一个。 例如,鼻子应该在脸中间。 通过尝试跟踪整个面部和面部的一部分,我们可以使应用代码能够执行更详细的操作并检查给定的跟踪结果有多好。 有鼻子的脸比没有脸的脸更好。 同时,我们可以支持某些优化,例如仅在某些位置查找具有特定大小的脸孔和鼻子。

我们将在名为FaceTracker的类中实现优化的分层跟踪器,该类提供了简单的接口。 FaceTracker可以使用与跟踪精度和性能之间的权衡有关的某些可选配置参数进行初始化。 在任何给定时间,FaceTracker的最新跟踪结果都存储在称为faces的属性中,该属性是Face实例的列表。 最初,此列表为空。 它通过update()方法刷新,该方法接受图像供跟踪器分析。 最后,出于调试目的,可以通过drawDebugRects()方法绘制faces的矩形,该方法接受图像作为绘制表面。 每帧,实时面部跟踪应用都会调用update(),读取faces,甚至可能调用drawDebugRects()

在内部,FaceTracker使用称为CascadeClassifier的 OpenCV 类。 CascadeClassifier使用级联数据文件初始化,例如我们之前找到并复制的文件。 出于我们的目的,CascadeClassifier的重要方法是detectMultiScale(),它执行的跟踪可能对规模变化具有鲁棒性。 detectMultiScale()的可能参数为:

  • image:这是要分析的图像。 每个通道必须具有 8 位。
  • scaleFactor:此缩放因子在两个连续的遍中将窗口大小分开。 较高的值可提高性能,但会降低比例变化的鲁棒性。
  • minNeighbors:此值比比赛中所需的最小区域数小 1。 (一个匹配项可能会合并多个相邻区域。)
  • flags:有几个标志,但并非所有组合都有效。 有效的独立标志和有效组合包括:
    • cv2.cv.CV_HAAR_SCALE_IMAGE:缩放每个窗口图像区域以匹配特征数据。 (默认方法是相反的:缩放特征数据以匹配窗口。)缩放图像可以对现代硬件进行某些优化。 该标志不得与其他标志结合使用。
    • cv2.cv.CV_HAAR_DO_CANNY_PRUNING:急切拒绝包含太多或太少边缘以致无法匹配对象类型的区域。 此标志不应与cv2.cv.CV_HAAR_FIND_BIGGEST_OBJECT结合使用。
    • cv2.cv.CV_HAAR_FIND_BIGGEST_OBJECT:最多接受一场比赛(最大一场)。
    • cv2.cv.CV_HAAR_FIND_BIGGEST_OBJECT | cv2.cv.HAAR_DO_ROUGH SEARCH:最多最多接受一场比赛(最大一场比赛),并跳过一些细化(缩小)这场比赛区域的步骤。 minNeighbors参数应大于0
  • minSize:代表要寻找的最小物体尺寸的一对像素尺寸。 较高的值可提高性能。
  • maxSize:一对像素尺寸,代表要寻找的最大物体尺寸。 较低的值可以提高性能。

detectMultiScale()的返回值是一个匹配项列表,每个匹配项均以[x, y, w, h]格式表示为矩形。

同样,FaceTracker的初始化器接受scaleFactorminNeighborsflags作为参数。 给定的值将传递给FaceTracker内部进行的所有detectMultiScale()调用。 同样在初始化期间,FaceTracker使用脸部,眼睛,鼻子和嘴巴数据创建CascadeClassifiers。 让我们将初始化器的以下实现和faces属性添加到trackers.py中:

代码语言:javascript复制
class FaceTracker(object):
    """A tracker for facial features: face, eyes, nose, mouth."""

    def __init__(self, scaleFactor = 1.2, minNeighbors = 2,
                 flags = cv2.cv.CV_HAAR_SCALE_IMAGE):

        self.scaleFactor = scaleFactor
        self.minNeighbors = minNeighbors
        self.flags = flags

        self._faces = []

        self._faceClassifier = cv2.CascadeClassifier(
            'cascades/haarcascade_frontalface_alt.xml')
        self._eyeClassifier = cv2.CascadeClassifier(
            'cascades/haarcascade_eye.xml')
        self._noseClassifier = cv2.CascadeClassifier(
            'cascades/haarcascade_mcs_nose.xml')
        self._mouthClassifier = cv2.CascadeClassifier(
            'cascades/haarcascade_mcs_mouth.xml')

    @property
    def faces(self):
        """The tracked facial features."""
        return self._faces

FaceTrackerupdate()方法首先创建给定图像的均等灰度变体。 如 OpenCV 的equalizeHist()函数中所实现的均衡,可以标准化图像的亮度并增加其对比度。 均衡作为预处理步骤,使我们的跟踪器对光照变化更加鲁棒,而转换为灰度可提高性能。 接下来,我们将预处理后的图像输入到我们的面部分类器中。 对于每个匹配的矩形,我们在某些子区域中搜索左眼和右眼,鼻子和嘴巴。 最终,匹配的矩形和子矩形存储在faces中的Face实例中。 对于每种类型的跟踪,我们指定与图像大小成比例的最小对象大小。 我们对FaceTracker的实现应继续使用update()的以下代码:

代码语言:javascript复制
    def update(self, image):
        """Update the tracked facial features."""

        self._faces = []

        if utils.isGray(image):
            image = cv2.equalizeHist(image)
        else:
            image = cv2.cvtColor(image, cv2.cv.CV_BGR2GRAY)
            cv2.equalizeHist(image, image)

        minSize = utils.widthHeightDividedBy(image, 8)

        faceRects = self._faceClassifier.detectMultiScale(
            image, self.scaleFactor, self.minNeighbors, self.flags,
            minSize)

        if faceRects is not None:
            for faceRect in faceRects:

                face = Face()
                face.faceRect = faceRect

                x, y, w, h = faceRect

                # Seek an eye in the upper-left part of the face.
                searchRect = (x w/7, y, w*2/7, h/2)
                face.leftEyeRect = self._detectOneObject(
                    self._eyeClassifier, image, searchRect, 64)

                # Seek an eye in the upper-right part of the face.
                searchRect = (x w*4/7, y, w*2/7, h/2)
                face.rightEyeRect = self._detectOneObject(
                    self._eyeClassifier, image, searchRect, 64)

                # Seek a nose in the middle part of the face.
                searchRect = (x w/4, y h/4, w/2, h/2)
                face.noseRect = self._detectOneObject(
                    self._noseClassifier, image, searchRect, 32)

                # Seek a mouth in the lower-middle part of the face.
                searchRect = (x w/6, y h*2/3, w*2/3, h/3)
                face.mouthRect = self._detectOneObject(
                    self._mouthClassifier, image, searchRect, 16)

                self._faces.append(face)

注意,update()依赖于本章前面的utils.isGray()utils.widthHeightDividedBy()。 此外,它还依赖于私有助手方法_detectOneObject(),该方法被多次调用,以处理跟踪面部多个子部分的重复工作。 作为参数,_detectOneObject()需要分类器,图像,矩形和最小对象大小。 矩形是给定分类器应搜索的图像子区域。 例如,鼻子分类器应搜索面部中间。 限制搜索范围可以提高性能,并有助于消除误报。 在内部,_detectOneObject()通过在图像的切片上运行分类器并返回第一个匹配项(如果没有匹配项,则返回None)来工作。 无论我们是否使用cv2.cv.CV_HAAR_FIND_BIGGEST_OBJECT标志,此方法都有效。 我们对FaceTracker的实现应继续使用_detectOneObject()的以下代码:

代码语言:javascript复制
    def _detectOneObject(self, classifier, image, rect,
                          imageSizeToMinSizeRatio):

        x, y, w, h = rect

        minSize = utils.widthHeightDividedBy(
            image, imageSizeToMinSizeRatio)

        subImage = image[y:y h, x:x w]

        subRects = classifier.detectMultiScale(
            subImage, self.scaleFactor, self.minNeighbors,
            self.flags, minSize)

        if len(subRects) == 0:
            return None

        subX, subY, subW, subH = subRects[0]
        return (x subX, y subY, subW, subH)

最后,FaceTracker应该提供基本的绘图功能,以便可以显示其跟踪结果以进行调试。 以下方法实现使用我们的rects.outlineRect()函数简单地定义颜色,遍历Face实例并绘制每个Face的矩形到给定图像:

代码语言:javascript复制
def drawDebugRects(self, image):
        """Draw rectangles around the tracked facial features."""

        if utils.isGray(image):
            faceColor = 255
            leftEyeColor = 255
            rightEyeColor = 255
            noseColor = 255
            mouthColor = 255
        else:
            faceColor = (255, 255, 255) # white
            leftEyeColor = (0, 0, 255) # red
            rightEyeColor = (0, 255, 255) # yellow
            noseColor = (0, 255, 0) # green
            mouthColor = (255, 0, 0) # blue

        for face in self.faces:
            rects.outlineRect(image, face.faceRect, faceColor)
            rects.outlineRect(image, face.leftEyeRect, leftEyeColor)
            rects.outlineRect(image, face.rightEyeRect,
                              rightEyeColor)
            rects.outlineRect(image, face.noseRect, noseColor)
            rects.outlineRect(image, face.mouthRect, mouthColor)

现在,我们有了一个高级跟踪器,该跟踪器隐藏了 Haar 级联分类器的详细信息,同时允许应用代码提供新图像,获取有关跟踪结果的数据并要求进行调试绘图。

修改应用

让我们看一下将面部跟踪和交换集成到 Cameo 中的两种方法。 第一种方法使用单个相机提要,并交换在此相机提要中找到的脸部矩形。 第二种方法使用两个摄像头提要,并将面矩形从一个摄像头提要复制到另一个摄像头。

现在,我们将限制自己只处理整个面孔,而不要处理诸如眼睛之类的子元素。 但是,例如,您可以修改代码以仅交换眼睛。 如果尝试这样做,请小心检查脸部的相关子矩形是否不是None

在一个相机源中交换人脸

对于单相机版本,修改非常简单。 在初始化Cameo时,我们创建一个FaceTracker和一个布尔变量,指示是否应为FaceTracker绘制调试矩形。 响应X键,在onKeypress()中切换布尔值。 作为run()主循环的一部分,我们用当前帧更新FaceTracker。 然后,获取得到的FaceFace对象(在faces属性中),并使用rects.swapRects()交换它们的faceRects。 另外,根据布尔值,我们可能会绘制调试矩形,以反映任何交换之前面部元素的原始位置。

代码语言:javascript复制
import cv2
import filters
from managers import WindowManager, CaptureManager
import rects
from trackers import FaceTracker

class Cameo(object):

    def __init__(self):
        self._windowManager = WindowManager('Cameo',
                                            self.onKeypress)
        self._captureManager = CaptureManager(
            cv2.VideoCapture(0), self._windowManager, True)
        self._faceTracker = FaceTracker()
        self._shouldDrawDebugRects = False
        self._curveFilter = filters.BGRPortraCurveFilter()

    def run(self):
        """Run the main loop."""
        self._windowManager.createWindow()
        while self._windowManager.isWindowCreated:
            self._captureManager.enterFrame()
            frame = self._captureManager.frame

            self._faceTracker.update(frame)
            faces = self._faceTracker.faces
            rects.swapRects(frame, frame,
                            [face.faceRect for face in faces])

            filters.strokeEdges(frame, frame)
            self._curveFilter.apply(frame, frame)

            if self._shouldDrawDebugRects:
                self._faceTracker.drawDebugRects(frame)

            self._captureManager.exitFrame()
            self._windowManager.processEvents()

    def onKeypress(self, keycode):
        """Handle a keypress.

        space  -> Take a screenshot.
        tab    -> Start/stop recording a screencast.
        x      -> Start/stop drawing debug rectangles around faces.
        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 == 120: # x
            self._shouldDrawDebugRects = 
                not self._shouldDrawDebugRects
        elif keycode == 27: # escape
            self._windowManager.destroyWindow()

if __name__=="__main__":
   Cameo().run()

以下屏幕截图是来自 Cameo 的。 用户按下X后,脸部区域将被勾勒出轮廓:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8xnkSPy3-1681874125805)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv-cv-py/img/00006.jpg)]

以下屏幕截图来自 Cameo。 美国商人比尔·阿克曼(Bill Ackman)接管了作者的脸孔:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9SLgxicH-1681874125805)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv-cv-py/img/00007.jpg)]

在相机源之间复制人脸

对于两机版,让我们创建一个新类CameoDouble,它是Cameo的子类。 在初始化时,CameoDouble调用Cameo的构造器,还创建第二个CaptureManager。 在run()的主循环中,CameoDouble从两个摄像机获取新帧,然后获取两个帧的面部跟踪结果。 使用copyRect()将脸部从一帧复制到另一帧。 然后,显示目标框架,可以选择在其上方绘制调试矩形。 我们可以在cameo.py中实现CameoDouble,如下所示:


注意

对于某些型号的 MacBook,插入外部网络摄像头后,OpenCV 在使用内置摄像头时会遇到问题。特别是,在等待内置摄像头提供框架时,应用可能会死锁。 如果遇到此问题,请使用两个外接摄像机,而不要使用内置摄像机。


代码语言:javascript复制
class CameoDouble(Cameo):

    def __init__(self):
        Cameo.__init__(self)
        self._hiddenCaptureManager = CaptureManager(
            cv2.VideoCapture(1))

    def run(self):
        """Run the main loop."""
        self._windowManager.createWindow()
        while self._windowManager.isWindowCreated:
            self._captureManager.enterFrame()
            self._hiddenCaptureManager.enterFrame()
            frame = self._captureManager.frame
            hiddenFrame = self._hiddenCaptureManager.frame

            self._faceTracker.update(hiddenFrame)
            hiddenFaces = self._faceTracker.faces
            self._faceTracker.update(frame)
            faces = self._faceTracker.faces

            i = 0
            while i < len(faces) and i < len(hiddenFaces):
                rects.copyRect(
                    hiddenFrame, frame, hiddenFaces[i].faceRect,
                    faces[i].faceRect)
                i  = 1

            filters.strokeEdges(frame, frame)
            self._curveFilter.apply(frame, frame)

            if self._shouldDrawDebugRects:
                self._faceTracker.drawDebugRects(frame)

            self._captureManager.exitFrame()
            self._hiddenCaptureManager.exitFrame()
            self._windowManager.processEvents()

要运行CameoDouble而不是Cameo的,我们只需要修改if __name__=="__main__"块,如下所示:

代码语言:javascript复制
if __name__=="__main__":
    #Cameo().run() # uncomment for single camera
    CameoDouble().run() # uncomment for double camera

总结

现在,我们有两个版本的 Cameo。 一个版本可在单个相机供稿中跟踪面部,找到面部后,可通过复制和调整大小来交换它们。 另一种版本则跟踪两个相机源中的人脸,如果在每个源中找到人脸,则从一个源中复制人脸并调整其大小以替换另一个人脸。 此外,在这两个版本中,一个摄影机的提要均可见,并对其应用了效果。

这些版本的 Cameo 演示了我们在两章之前提出的基本功能。 用户可以将他或她的脸部移动到另一个身体上,并且可以对结果进行样式化以使其更统一。 但是,移植的脸仍然只是矩形切口。 到目前为止,还没有做出任何努力去掉矩形的非面部部分或对齐重叠的和下面的组件(例如眼睛)。 下一章将探讨一些更复杂的面部融合技术,尤其是使用深度视觉技术。

五、检测前景/背景区域和深度

本章介绍如何使用深度相机中的数据来识别前景和背景区域,这样我们就可以将效果限制为仅前景或背景。 作为前提条件,我们需要一个深度摄像头,例如 Microsoft Kinect,并且需要在支持深度摄像头的情况下构建 OpenCV。 有关构建说明,请参见第 1 章,“设置 OpenCV”。

创建模块

我们用于捕获和处理深度相机数据的代码可在Cameo.py外部重用。 因此,我们应该将其分成一个新模块。 让我们在与Cameo.py相同的目录中创建一个名为depth.py的文件。 我们需要在depth.py中使用以下import语句:

代码语言:javascript复制
import numpy

我们还需要修改我们先前存在的rects.py文件,以便将复制操作限制为矩形的非矩形子区域。 为了支持我们将要进行的更改,让我们在rects.py中添加以下导入语句:

代码语言:javascript复制
import numpy
import utils

最后,我们应用的新版本将使用与深度相关的功能。 因此,让我们在Cameo.py中添加以下import语句:

代码语言:javascript复制
import depth

现在,让我们更深入地研究深度主题。

从深度相机捕获帧

回到第 2 章,“处理文件,摄像机和 GUI”,我们讨论了计算机可以具有多个视频捕获设备,每个设备可以具有多个通道的概念。 假设给定的设备是立体相机。 每个通道可能对应于不同的镜头和传感器。 同样,每个通道可能对应于不同种类的数据,例如正常彩色图像与深度图。 使用 OpenCV 的VideoCapture类或包装器CaptureManager时,我们可以在初始化时选择一个设备,并且可以从该设备的每一帧读取一个或多个通道。 每个设备和通道均由整数标识。 不幸的是,设备和通道的编号是不直观的。 OpenCV 的 C 版本为某些设备和通道的标识符定义了一些常量。 但是,这些常量未在 Python 版本中定义。 为纠正这种情况,让我们在depth.py中添加以下定义:

代码语言:javascript复制
# Devices.
CV_CAP_OPENNI = 900 # OpenNI (for Microsoft Kinect)
CV_CAP_OPENNI_ASUS = 910 # OpenNI (for Asus Xtion)
# Channels of an OpenNI-compatible depth generator.
CV_CAP_OPENNI_DEPTH_MAP = 0 # Depth values in mm (CV_16UC1)
CV_CAP_OPENNI_POINT_CLOUD_MAP = 1 # XYZ in meters (CV_32FC3)
CV_CAP_OPENNI_DISPARITY_MAP = 2 # Disparity in pixels (CV_8UC1)
CV_CAP_OPENNI_DISPARITY_MAP_32F = 3 # Disparity in pixels (CV_32FC1)
CV_CAP_OPENNI_VALID_DEPTH_MASK = 4 # CV_8UC1
# Channels of an OpenNI-compatible RGB image generator.
CV_CAP_OPENNI_BGR_IMAGE = 5
CV_CAP_OPENNI_GRAY_IMAGE = 6

与深度有关的通道需要一些解释,如以下列表所示:

  • 深度图是灰度图像,其中每个像素值是从相机到表面的估计距离。 具体而言,来自CV_CAP_OPENNI_DEPTH_MAP通道的图像给出的距离为浮点数毫米。
  • 点云图是彩色图像,其中每种颜色对应于一个空间尺寸(xyz)。 具体而言,从相机的角度来看,CV_CAP_OPENNI_POINT_CLOUD_MAP通道会生成 BGR 图像,其中 B 为x(蓝色为右),G 为y(绿色为上),R 为z(红色为深)。 值以米为单位。
  • 视差图是灰度图像,其中每个像素值是表面的立体视差。 为了概念化立体视差,我们假设我们叠加了一个场景的两个图像,这些图像是从不同的角度拍摄的。 结果就像看到双重图像。 对于场景中任何一对孪生对象上的点,我们都可以以像素为单位测量距离。 该测量是立体差异。 与远处的物体相比,附近的物体表现出更大的立体视差。 因此,附近的物体在视差图中显得更亮。
  • 有效深度掩码显示给定像素处的深度信息是有效的(由非零值表示)还是无效的(由零值表示)。 例如,如果深度相机依靠红外照明器(红外闪光灯),则深度信息在被该光遮挡(阴影)的区域中无效。

以下屏幕截图显示了一个坐在猫雕塑后面的人的点云图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d5zRoneA-1681874125806)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv-cv-py/img/00008.jpg)]

以下屏幕截图显示了一个坐在猫雕塑后面的人的视差图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6GdrEunb-1681874125806)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv-cv-py/img/00009.jpg)]

以下屏幕截图显示了一个坐在猫雕塑后面的人的有效深度面具:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RXBSUbfn-1681874125806)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv-cv-py/img/00010.jpg)]

从视差图创建遮罩

出于Cameo和的目的,我们对视差图和有效的深度遮罩感兴趣。 它们可以帮助我们完善对面部区域的估计。

使用我们的FaceTracker函数和正常彩色图像,我们可以获得面部区域的矩形估计。 通过分析相应的视差图中的矩形区域,我们可以知道矩形中的某些像素离群值-太近或太远而不能真正成为人脸的一部分。 我们可以优化面部区域以排除这些异常值。 但是,我们仅应在有效深度掩码所指示的数据有效的地方应用此测试。

让我们编写一个函数来生成一个遮罩,该遮罩的值对于面部矩形的拒绝区域为0,对于接受区域为1。 此函数应使用视差图,有效的深度遮罩和矩形作为参数。 我们可以在depth.py中实现它,如下所示:

代码语言:javascript复制
def createMedianMask(disparityMap, validDepthMask, rect = None):
    """Return a mask selecting the median layer, plus shadows."""
    if rect is not None:
        x, y, w, h = rect
        disparityMap = disparityMap[y:y h, x:x w]
        validDepthMask = validDepthMask[y:y h, x:x w]
    median = numpy.median(disparityMap)
    return numpy.where((validDepthMask == 0) | 
                       (abs(disparityMap - median) < 12),
                       1.0, 0.0)

为了识别视差图中的离群值,我们首先使用numpy.median()来找到中值,它以数组作为参数。 如果数组的长度为奇数,则median()返回如果对数组进行排序,则该值将位于数组的中间。 如果数组的长度为偶数,则median()返回将最接近数组中间排序的两个值的平均值。

为了基于每个像素的布尔运算生成遮罩,我们将numpy.where()与三个参数一起使用。 作为第一个参数,where()接受一个数组,该数组的元素将评估为真还是假。 返回类似尺寸的输出数组。 只要输入数组中的元素为truewhere()函数的第二个参数都将分配给输出数组中的相应元素。 相反,无论输入数组中的哪个元素是falsewhere()函数的第三个参数都将分配给输出数组中的相应元素。

当像素的有效视差值与中位数视差值相差 12 或更多时,我们的实现会将像素视为离群值。 我只是通过实验选择了值 12。 以后根据您使用特定相机设置运行Cameo时遇到的结果,随时调整此值。

带遮罩的复制操作

作为上一章的一部分,我们将copyRect()编写为一种复制操作,将其自身限制为源图像和目标图像的给定矩形。 现在,我们想对该复制操作应用更多限制。 我们要使用与源矩形具有相同尺寸的给定遮罩。 我们将仅复制源矩形中掩码值不为零的那些像素。 其他像素应保留目标图像中的旧值。 可以使用我们最近了解的numpy.where()函数来简洁地表达具有条件数组和两个可能的输出值数组的逻辑。

让我们打开rects.py并编辑copyRect()以添加一个新参数mask。 该参数可能是None,在这种情况下,我们将退回到copy操作的旧实现。 否则,我们接下来确保mask和图像具有相同数量的通道。 我们假设mask具有一个通道,但是图像可能具有三个通道(BGR)。 我们可以使用numpy.arrayrepeat()reshape()方法向mask添加重复通道。 最后,我们使用where()执行copy操作。 完整的实现如下:

代码语言:javascript复制
def copyRect(src, dst, srcRect, dstRect, mask = None,
             interpolation = cv2.INTER_LINEAR):
    """Copy part of the source to part of the destination."""

    x0, y0, w0, h0 = srcRect
    x1, y1, w1, h1 = dstRect

    # Resize the contents of the source sub-rectangle.
    # Put the result in the destination sub-rectangle.
    if mask is None:
        dst[y1:y1 h1, x1:x1 w1] = 
            cv2.resize(src[y0:y0 h0, x0:x0 w0], (w1, h1),
                       interpolation = interpolation)
    else:
        if not utils.isGray(src):
            # Convert the mask to 3 channels, like the image.
            mask = mask.repeat(3).reshape(h0, w0, 3)
        # Perform the copy, with the mask applied.
        dst[y1:y1 h1, x1:x1 w1] = 
            numpy.where(cv2.resize(mask, (w1, h1),
                                   interpolation = 
                                   cv2.INTER_NEAREST),
                        cv2.resize(src[y0:y0 h0, x0:x0 w0], (w1, h1),
                                   interpolation = interpolation),
                        dst[y1:y1 h1, x1:x1 w1])

我们还需要修改swapRects()函数,该函数使用copyRect()执行矩形区域列表的循环交换。 对swapRects()的修改非常简单。 我们只需要添加一个新参数masks,它是一个掩码的列表,其元素将传递给相应的copyRect()调用。 如果给定的masksNone,则将None传递给每个copyRect()调用。 以下是完整的实现:

代码语言:javascript复制
def swapRects(src, dst, rects, masks = None,
              interpolation = cv2.INTER_LINEAR):
    """Copy the source with two or more sub-rectangles swapped."""

    if dst is not src:
        dst[:] = src

    numRects = len(rects)
    if numRects < 2:
        return

    if masks is None:
        masks = [None] * numRects

    # Copy the contents of the last rectangle into temporary storage.
    x, y, w, h = rects[numRects - 1]
    temp = src[y:y h, x:x w].copy()

    # Copy the contents of each rectangle into the next.
    i = numRects - 2
    while i >= 0:
        copyRect(src, dst, rects[i], rects[i 1], masks[i],
                 interpolation)
        i -= 1

    # Copy the temporarily stored content into the first rectangle.
    copyRect(temp, dst, (0, 0, w, h), rects[0], masks[numRects - 1],
             interpolation)

请注意,copyRect()中的掩码和swapRects()中的掩码都默认为None。 因此,这些函数的新版本与Cameo的先前版本向后兼容。

修改应用

对于Cameo的深度相机版本,让我们创建一个新类CameoDepth作为Cameo的子类。 初始化时, CameoDepth类会创建一个使用深度相机设备的CaptureManager类(对于 Microsoft Kinect 是CV_CAP_OPENNI还是对于 Asus Xtion 是CV_CAP_OPENNI_ASUS,这取决于我们的设置)。 在run()的主循环中,CameoDepth函数在每帧中获取视差图,有效的深度遮罩和正常的彩色图像。 正常彩色图像用于估计面部矩形,而视差图和有效深度遮罩用于使用createMedianMask()细化面部区域的估计。 使用copyRect()交换普通彩色图像中的脸部,并应用这些脸部的相应遮罩。 然后,显示目标框架,可以选择在其上方绘制调试矩形。 我们可以在cameo.py中实现CameoDepth,如下所示:

代码语言:javascript复制
class CameoDepth(Cameo):
    def __init__(self):
        self._windowManager = WindowManager('Cameo',
                                            self.onKeypress)
        device = depth.CV_CAP_OPENNI # uncomment for Microsoft Kinect
        #device = depth.CV_CAP_OPENNI_ASUS # uncomment for Asus Xtion
        self._captureManager = CaptureManager(
            cv2.VideoCapture(device), self._windowManager, True)
        self._faceTracker = FaceTracker()
        self._shouldDrawDebugRects = False
        self._curveFilter = filters.BGRPortraCurveFilter()
    def run(self):
        """Run the main loop."""
        self._windowManager.createWindow()
        while self._windowManager.isWindowCreated:
            self._captureManager.enterFrame()
            self._captureManager.channel = 
                depth.CV_CAP_OPENNI_DISPARITY_MAP
            disparityMap = self._captureManager.frame
            self._captureManager.channel = 
                depth.CV_CAP_OPENNI_VALID_DEPTH_MASK
            validDepthMask = self._captureManager.frame
            self._captureManager.channel = 
                depth.CV_CAP_OPENNI_BGR_IMAGE
            frame = self._captureManager.frame
            self._faceTracker.update(frame)
            faces = self._faceTracker.faces
            masks = [
                depth.createMedianMask(
                    disparityMap, validDepthMask, face.faceRect) 
                for face in faces
            ]
            rects.swapRects(frame, frame,
                            [face.faceRect for face in faces], masks)
            filters.strokeEdges(frame, frame)
            self._curveFilter.apply(frame, frame)
            if self._shouldDrawDebugRects:
                self._faceTracker.drawDebugRects(frame)
            self._captureManager.exitFrame()
            self._windowManager.processEvents()

要运行CameoDepth函数而不是CameoCameoDouble函数,我们只需要修改if __name__=="__main__"块,如下所示:

代码语言:javascript复制
if __name__=="__main__":
    #Cameo().run() # uncomment for single camera
    #CameoDouble().run() # uncomment for double camera
    CameoDepth().run() # uncomment for depth camera

以下是显示正在运行的CameoDepth类的屏幕截图。 请注意,我们的遮罩会按预期为复制区域提供一些不规则的边缘。 在脸部的左侧和右侧(与背景相遇的位置)比在顶部和底部(与相似深度的头发和颈部区域相遇)更成功:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kUDgIfNf-1681874125806)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv-cv-py/img/00011.jpg)]

总结

现在,我们有了一个使用深度相机,面部跟踪,复制操作,遮罩和图像过滤器的应用。 通过开发此应用,我们获得了利用 OpenCV,NumPy 和其他库的功能的实践。 我们还练习了将此功能包装在高级,可重用且面向对象的设计中。

恭喜你! 您现在具备使用 OpenCV 在 Python 中开发计算机视觉应用的技能。 尽管如此,总会有更多的东西要学习和做! 如果您喜欢使用 NumPy 和 OpenCV,请从 Packt Publishing 查看以下其他标题:

  • 《NumPy Cookbook》,伊万·伊德里斯
  • 《OpenCV 2 计算机视觉应用设计手册》,RobertLaganière,该手册在台式机上使用 OpenCV 的 C API
  • 《通过实用的计算机视觉项目掌握 OpenCV》,由多位作者撰写,其将 OpenCV 的 C API 用于多个平台
  • 即将出版的书《OpenCV for iOS How-to》,使用针对 iPhone 和 iPad 的 OpenCV 的 C API
  • 我即将出版的《OpenCV Android 应用编程》,使用了用于 Android 的 OpenCV 的 Java API

至此,我们结束了 OpenCV 的 Python 绑定之旅。 希望您能够将本书及其代码库用作奖励计算机视觉工作的起点。 让我知道您接下来要学习或发展的内容!

0 人点赞