原文:
annas-archive.org/md5/a67615bbb4c21c6cbe00ed412173f8c0译者:飞龙
前言
OpenCV 3 是一个最先进的计算机视觉库,用于各种图像和视频处理操作。一些更令人瞩目的未来功能,如人脸识别或对象跟踪,使用 OpenCV 3 轻松实现。学习计算机视觉算法、模型和 OpenCV API 背后的基本概念,将能够开发各种现实世界应用,包括安全和监控工具。
从基本的图像处理操作开始,本书将带您踏上一段探索高级计算机视觉概念的旅程。计算机视觉是一个快速发展的科学,其在现实世界中的应用正在爆炸式增长,因此本书将吸引计算机视觉新手以及想要了解全新 OpenCV 3.0.0 的专家。
本书涵盖内容
第一章,设置 OpenCV,解释了如何在不同的平台上使用 Python 设置 OpenCV 3,还将解决常见问题。
第二章,处理文件、摄像头和 GUI,介绍了 OpenCV 的 I/O 功能。它还将讨论项目概念以及该项目面向对象设计的起点。
第三章,使用 OpenCV 3 处理图像,介绍了改变图像所需的一些技术,例如检测图像中的肤色、锐化图像、标记主题的轮廓以及使用线段检测器检测人行横道。
第四章,深度估计和分割,展示了如何使用深度相机的数据来识别前景和背景区域,这样我们就可以将效果限制在仅前景或背景。
第五章,检测和识别人脸,介绍了 OpenCV 的一些人脸检测功能,以及定义特定类型可跟踪对象的数据文件。
第六章,使用图像描述符检索图像和搜索,展示了如何借助 OpenCV 检测图像特征,并利用这些特征进行匹配和搜索图像。
第七章,检测和识别对象,介绍了检测和识别对象的概念,这是计算机视觉中最常见的挑战之一。
第八章,跟踪对象,探讨了对象跟踪的广泛主题,这是在摄像头的帮助下在电影或视频流中定位移动对象的过程。
第九章,使用 OpenCV 的神经网络简介,将向你介绍 OpenCV 中的人工神经网络,并展示其在实际应用中的使用。
你需要为这本书准备什么
你只需要一台相对较新的电脑,因为第一章将指导你安装所有必要的软件。强烈推荐使用网络摄像头,但不是必需的。
本书面向对象
本书面向具有 Python 实际知识背景的程序员,以及希望使用 OpenCV 库探索计算机视觉主题的人。不需要具备计算机视觉或 OpenCV 的先验经验。建议有编程经验。
术语约定
在本书中,你将找到多种文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称将如下所示:“我们可以通过使用include指令来包含其他上下文。”
代码块将如下设置:
import cv2
import numpy as np
img = cv2.imread('images/chess_board.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
gray = np.float32(gray)
dst = cv2.cornerHarris(gray, 2, 23, 0.04)
当我们希望引起你对代码块中特定部分的注意时,相关的行或项目将以粗体显示:
img = cv2.imread('images/chess_board.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
gray = np.float32(gray)
dst = cv2.cornerHarris(gray, 2, 23, 0.04)
任何命令行输入或输出将如下所示:
mkdir build && cd build
cmake D CMAKE_BUILD_TYPE=Release -DOPENCV_EXTRA_MODULES_PATH=<opencv_contrib>/modules D CMAKE_INSTALL_PREFIX=/usr/local ..
make
新术语和重要词汇将以粗体显示。屏幕上显示的单词,例如在菜单或对话框中,将在文本中如下所示:“在 Windows Vista / Windows 7 / Windows 8 上,点击开始菜单。”
注意
警告或重要提示将以这样的框显示。
提示
小贴士和技巧如下所示。
读者反馈
我们始终欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢或不喜欢什么。读者反馈对我们来说很重要,因为它帮助我们开发出你真正能从中获得最大收益的标题。
要向我们发送一般反馈,只需发送电子邮件至 <feedback@packtpub.com>,并在邮件主题中提及本书的标题。
如果你在某个领域有专业知识,并且对撰写或参与书籍的编写感兴趣,请参阅我们的作者指南,网址为 www.packtpub.com/authors。
客户支持
现在,你已经是 Packt 图书的骄傲拥有者,我们有一些事情可以帮助你从购买中获得最大收益。
下载示例代码
你可以从你的账户中下载示例代码文件,网址为 www.packtpub.com,适用于你购买的所有 Packt 出版图书。如果你在其他地方购买了这本书,你可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给你。
错误清单
尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。
要查看之前提交的勘误表,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分。
侵权
互联网上版权材料的侵权是一个持续存在的问题,跨越所有媒体。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现任何形式的非法复制我们的作品,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过<copyright@packtpub.com>与我们联系,并提供涉嫌侵权材料的链接。
我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面的帮助。
询问
如果您在这本书的任何方面遇到问题,您可以通过<questions@packtpub.com>联系我们,我们将尽力解决问题。
第一章。设置 OpenCV
你选择这本书,可能已经对 OpenCV 有了一定的了解。也许,你听说过一些科幻般的功能,比如人脸检测,并对此产生了兴趣。如果是这样,你做出了完美的选择。OpenCV 代表 开源计算机视觉。它是一个免费的计算机视觉库,允许你操纵图像和视频,以完成各种任务,从显示网络摄像头的视频流到可能教会机器人识别现实生活中的物体。
在这本书中,你将学习如何利用 Python 编程语言充分发挥 OpenCV 的巨大潜力。Python 是一种优雅的语言,学习曲线相对平缓,功能非常强大。本章是快速设置 Python 2.7、OpenCV 以及其他相关库的指南。设置完成后,我们还将查看 OpenCV 的 Python 示例脚本和文档。
注意
如果你希望跳过安装过程,直接进入操作,你可以下载我在 techfort.github.io/pycv/ 提供的 虚拟机(VM)。
此文件与 VirtualBox 兼容,这是一个免费使用的虚拟化应用程序,允许你构建和运行虚拟机。我构建的虚拟机基于 Ubuntu Linux 14.04,并安装了所有必要的软件,以便你可以立即开始编码。
这个虚拟机需要至少 2 GB 的 RAM 才能平稳运行,所以请确保为虚拟机分配至少 2 GB(但理想情况下,超过 4 GB)的 RAM,这意味着你的主机机器至少需要 6 GB 的 RAM 才能维持其运行。
本章涵盖了以下相关库:
-
NumPy:这个库是 OpenCV Python 绑定的依赖项。它提供了包括高效数组在内的数值计算功能。
-
SciPy:这个库是一个与 NumPy 密切相关的科学计算库。它不是 OpenCV 所必需的,但它在操纵 OpenCV 图像中的数据时很有用。
-
OpenNI:这个库是 OpenCV 的可选依赖项。它增加了对某些深度相机(如华硕 XtionPRO)的支持。
-
SensorKinect:这个库是一个 OpenNI 插件,也是 OpenCV 的可选依赖项。它增加了对微软 Kinect 深度相机的支持。
对于本书的目的,OpenNI 和 SensorKinect 可以被认为是可选的。它们在 第四章 中使用,深度估计和分割,但在其他章节或附录中并未使用。
注意
本书专注于 OpenCV 3,这是 OpenCV 库的新版主要发布。有关 OpenCV 的所有附加信息可在 opencv.org 获取,其文档可在 docs.opencv.org/master 获取。
选择和使用正确的设置工具
我们可以根据我们的操作系统和想要进行多少配置来选择各种设置工具。让我们概述一下 Windows、Mac、Ubuntu 和其他类 Unix 系统的工具。
Windows 上的安装
Windows 没有预装 Python。然而,有预编译的 Python、NumPy、SciPy 和 OpenCV 的安装向导可用。或者,我们可以从源代码构建。OpenCV 的构建系统使用 CMake 进行配置,并使用 Visual Studio 或 MinGW 进行编译。
如果我们想要支持深度相机,包括 Kinect,我们首先应该安装 OpenNI 和 SensorKinect,它们作为预编译的二进制文件和安装向导提供。然后,我们必须从源代码构建 OpenCV。
注意
预编译版的 OpenCV 不支持深度相机。
在 Windows 上,OpenCV 2 对 32 位 Python 的支持优于 64 位 Python;然而,由于今天大多数销售的计算机都是 64 位系统,我们的说明将参考 64 位。所有安装程序都有 32 位版本,可以从与 64 位相同的网站下载。
以下步骤中的一些涉及编辑系统的PATH变量。这项任务可以在控制面板的环境变量窗口中完成。
-
在 Windows Vista / Windows 7 / Windows 8 上,点击开始菜单并启动控制面板。现在,导航到系统和安全 | 系统 | 高级系统设置。点击**环境变量…**按钮。
-
在 Windows XP 上,点击开始菜单并导航到控制面板 | 系统。选择高级选项卡。点击**环境变量…**按钮。
-
现在,在系统变量下,选择Path并点击**编辑…**按钮。
-
按指示进行更改。
-
要应用更改,点击所有确定按钮(直到我们回到控制面板的主窗口)。
-
然后,注销并重新登录(或者重新启动)。
使用二进制安装程序(不支持深度相机)
如果您愿意,可以选择单独安装 Python 及其相关库;然而,有一些 Python 发行版包含安装程序,可以设置整个 SciPy 堆栈(包括 Python 和 NumPy),这使得设置开发环境变得非常简单。
其中一个发行版是 Anaconda Python(可在09c8d0b2229f813c1b93c95ac804525aac4b6dba79b00b39d1d3.r79.cf1.rackcdn.com/Anaconda-2.1.0Windows-x86_64.exe下载)。一旦下载了安装程序,运行它并记得按照前面的步骤将 Anaconda 安装路径添加到您的PATH变量中。
这里是设置 Python7、NumPy、SciPy 和 OpenCV 的步骤:
-
从
www.python.org/ftp/python/2.7.9/python-2.7.9.amd64.msi下载并安装 32 位 Python 2.7.9。 -
从
www.lfd.uci.edu/~gohlke/pythonlibs/#numpyhttp://sourceforge.net/projects/numpy/files/NumPy/1.6.2/numpy-1.6.2-win32-superpack-python2.7.exe/download下载并安装 NumPy 1.6.2(注意,由于 Windows 上缺少 NumPy 所依赖的 64 位 Fortran 编译器,在 Windows 64 位上安装 NumPy 有点棘手。前一个链接中的二进制文件是非官方的)。 -
从
www.lfd.uci.edu/~gohlke/pythonlibs/#scipyhttp://sourceforge.net/projects/scipy/files/scipy/0.11.0/scipy-0.11.0win32-superpack-python2.7.exe/download下载并安装 SciPy 11.0(这与 NumPy 相同,这些都是社区安装程序)。 -
从
github.com/Itseez/opencv下载 OpenCV 3.0.0 的自解压 ZIP 文件。运行此 ZIP 文件,并在提示时输入目标文件夹,我们将称之为<unzip_destination>。将创建一个子文件夹<unzip_destination>\opencv。 -
将
<unzip_destination>\opencv\build\python\2.7\cv2.pyd复制到C:\Python2.7\Lib\site-packages(假设我们将 Python 2.7 安装在默认位置)。如果您使用 Anaconda 安装了 Python 2.7,请使用 Anaconda 安装文件夹而不是默认的 Python 安装。现在,新的 Python 安装可以找到 OpenCV。 -
如果我们想要默认使用新的 Python 安装运行 Python 脚本,则需要执行一个最终步骤。编辑系统的
PATH变量,并追加;C:\Python2.7(假设我们将 Python 2.7 安装在默认位置)或您的 Anaconda 安装文件夹。删除任何以前的 Python 路径,例如;C:\Python2.6。注销并重新登录(或者重新启动)。
使用 CMake 和编译器
Windows 不自带任何编译器或 CMake。我们需要安装它们。如果我们想要支持包括 Kinect 在内的深度相机,我们还需要安装 OpenNI 和 SensorKinect。
假设我们已经通过二进制文件(如前所述)或源代码安装了 32 位 Python 2.7、NumPy 和 SciPy。现在,我们可以继续安装编译器和 CMake,可选地安装 OpenNI 和 SensorKinect,然后从源代码构建 OpenCV:
-
从
www.cmake.org/files/v3.1/cmake-3.1.2-win32-x86.exe下载并安装 CMake 3.1.2。在运行安装程序时,选择“将 CMake 添加到系统 PATH 以供所有用户使用”或“将 CMake 添加到当前用户的系统 PATH”。不用担心没有 64 位版本的 CMake,因为 CMake 只是一个配置工具,它本身不执行任何编译。相反,在 Windows 上,它创建可以与 Visual Studio 打开的工程文件。 -
从
www.visualstudio.com/products/free-developer-offers-vs.aspx?slcid=0x409&type=web or MinGW下载并安装 Microsoft Visual Studio 2013(如果您在 Windows 7 上工作,请选择桌面版)。注意,您需要使用您的 Microsoft 账户进行登录,如果您没有,您可以在现场创建一个。安装软件,安装完成后重新启动。
对于 MinGW,从
sourceforge.net/projects/mingw/files/Installer/mingw-get-setup.exe/download和sourceforge.net/projects/mingw/files/OldFiles/mingw-get-inst/mingw-get-inst-20120426/mingw-get-inst-20120426.exe/download获取安装程序。在运行安装程序时,确保目标路径不包含空格,并且包含可选的 C++ 编译器。编辑系统的PATH变量,并追加;C:\MinGW\bin(假设 MinGW 安装在默认位置)。重新启动系统。 -
可选,从 OpenNI 在 GitHub 主页提供的链接中下载并安装 OpenNI 1.5.4.0。
github.com/OpenNI/OpenNI。 -
您可以从
github.com/avin2/SensorKinect/blob/unstable/Bin/SensorKinect093-Bin-Win32-v5.1.2.1.msi?raw=true(32 位)下载并安装 SensorKinect 0.93。对于 64 位 Python,从github.com/avin2/SensorKinect/blob/unstable/Bin/SensorKinect093-Bin-Win64-v5.1.2.1.msi?raw=true(64 位)下载设置文件。请注意,这个存储库已经停用超过三年了。 -
从
github.com/Itseez/opencv下载 OpenCV 3.0.0 的自解压 ZIP 文件。运行自解压 ZIP 文件,当提示时,输入任何目标文件夹,我们将称之为<unzip_destination>。然后创建一个子文件夹,<unzip_destination>\opencv。 -
打开命令提示符,使用以下命令创建一个新文件夹,我们的构建将放在那里:
> mkdir<build_folder>更改
build文件夹的目录:> cd <build_folder> -
现在,我们已经准备好配置我们的构建。为了理解所有选项,我们可以阅读
<unzip_destination>\opencv\CMakeLists.txt中的代码。然而,出于本书的目的,我们只需要使用那些将为我们提供带有 Python 绑定的发布构建的选项,以及可选的通过 OpenNI 和 SensorKinect 的深度相机支持。 -
打开 CMake(
cmake-gui)并指定 OpenCV 源代码的位置以及您希望构建库的文件夹。点击配置。选择要生成的项目。在这种情况下,选择 Visual Studio 12(对应于 Visual Studio 2013)。CMake 完成项目配置后,将输出一个构建选项列表。如果您看到红色背景,这意味着可能需要重新配置项目:CMake 可能会报告它未能找到某些依赖项。OpenCV 的许多依赖项是可选的,所以现在不必过于担心。注意
如果构建未完成或您遇到问题,请尝试安装缺失的依赖项(通常作为预构建的二进制文件提供),然后从这一步重新构建 OpenCV。
您可以选择/取消选择构建选项(根据您在机器上安装的库),然后再次点击配置,直到获得清晰的背景(白色)。
-
在此过程结束时,您可以点击生成,这将创建一个
OpenCV.sln文件在您选择的构建文件夹中。然后,您可以导航到<build_folder>/OpenCV.sln并使用 Visual Studio 2013 打开该文件,然后继续构建项目,ALL_BUILD。您需要构建 OpenCV 的调试和发布版本,因此请先以调试模式构建库,然后选择发布并重新构建它(F7是启动构建的键)。 -
在此阶段,您将在 OpenCV 构建目录中有一个
bin文件夹,其中将包含所有生成的.dll文件,这将允许您将 OpenCV 包含到您的项目中。或者,对于 MinGW,运行以下命令:
> 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>\Sensor\Bin。或者,对于 MinGW,运行以下命令:
> mingw32-make -
将
<build_folder>\lib\Release\cv2.pyd(来自 Visual Studio 构建)或<build_folder>\lib\cv2.pyd(来自 MinGW 构建)复制到<python_installation_folder>\site-packages。 -
最后,编辑系统的
PATH变量,并追加;<build_folder>/bin/Release(对于 Visual Studio 构建)或;<build_folder>/bin(对于 MinGW 构建)。重新启动您的系统。
在 OS X 上安装
以前的一些 Mac 版本预装了由 Apple 定制的 Python 2.7 版本,用于满足系统的内部需求。然而,这种情况已经改变,标准的 OS X 版本现在都带有标准的 Python 安装。在 python.org 上,您还可以找到与新的 Intel 系统和旧 PowerPC 兼容的通用二进制文件。
注意
您可以从 www.python.org/downloads/release/python-279/ 获取此安装程序(参考 Mac OS X 32 位 PPC 或 Mac OS X 64 位 Intel 链接)。从下载的 .dmg 文件安装 Python 将简单地覆盖您当前系统上的 Python 安装。
对于 Mac,获取标准 Python 2.7、NumPy、SciPy 和 OpenCV 有几种可能的方法。所有方法最终都需要使用 Xcode 开发者工具从源代码编译 OpenCV。然而,根据方法的不同,这项任务可以通过第三方工具以各种方式自动化。我们将通过使用 MacPorts 或 Homebrew 来查看这些方法。这些工具可以执行 CMake 可以执行的所有操作,并且帮助我们解决依赖关系,并将我们的开发库与系统库分开。
提示
我推荐使用 MacPorts,尤其是如果您想通过 OpenNI 和 SensorKinect 编译具有深度相机支持的 OpenCV。相关的补丁和构建脚本,包括我维护的一些,已经为 MacPorts 准备好。相比之下,Homebrew 目前还没有提供编译具有深度相机支持的 OpenCV 的现成解决方案。
在继续之前,让我们确保 Xcode 开发者工具已正确设置:
-
从 Mac App Store 或
developer.apple.com/xcode/downloads/下载并安装 Xcode。在安装过程中,如果有安装 命令行工具 的选项,请选择它。 -
打开 Xcode 并接受许可协议。
-
如果安装程序没有提供安装 命令行工具 的选项,则需要执行最后一步。导航到 Xcode | 首选项 | 下载,然后点击 命令行工具 旁边的 安装 按钮。等待安装完成并退出 Xcode。
或者,您可以通过运行以下命令(在终端中)来安装 Xcode 命令行工具:
$ xcode-select –install
现在,我们有了任何方法所需的编译器。
使用带有现成软件包的 MacPorts
我们可以使用 MacPorts 软件包管理器帮助我们设置 Python 2.7、NumPy 和 OpenCV。MacPorts 提供了终端命令,可以自动化下载、编译和安装各种开源软件(OSS)。MacPorts 还会根据需要安装依赖项。对于每件软件,依赖项和构建配方都定义在一个名为 Portfile 的配置文件中。MacPorts 存储库是 Portfiles 的集合。
从已经设置好 Xcode 及其命令行工具的系统开始,以下步骤将使用 MacPorts 为我们提供 OpenCV 安装:
-
从
www.macports.org/install.php下载并安装 MacPorts。 -
如果您想支持 Kinect 深度相机,您需要告诉 MacPorts 下载我编写的自定义 Portfiles 的位置。为此,编辑
/opt/local/etc/macports/sources.conf(假设 MacPorts 安装在默认位置)。在以下行rsync://rsync.macports.org/release/ports/ [default]之上添加以下行:http://nummist.com/opencv/ports.tar.gz保存文件。现在,MacPorts 知道它必须首先在我的在线仓库中搜索 Portfiles,然后是默认在线仓库。
-
打开终端并运行以下命令来更新 MacPorts:
$ sudo port selfupdate当提示时,输入您的密码。
-
现在(如果我们使用我的仓库),运行以下命令来安装具有 Python 2.7 绑定和深度相机支持的 OpenCV,包括 Kinect:
$ sudo port install opencv +python27 +openni_sensorkinect或者(无论是否使用我的仓库),运行以下命令来安装具有 Python 2.7 绑定和深度相机支持的 OpenCV,不包括 Kinect:
$ 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。在安装之前,我们可以输入以下命令来查看所有可用变体的完整列表:$ port variants opencv根据我们的定制需求,我们可以在
install命令中添加其他变体。为了获得更大的灵活性,我们可以编写自己的变体(如下一节所述)。 -
此外,运行以下命令来安装 SciPy:
$ sudo port install py27-scipy -
Python 安装的执行文件名为
python2.7。如果我们想将默认的python可执行文件链接到python2.7,请运行此命令:$ sudo port install python_select $ sudo port select python python27
使用 MacPorts 和您自己的自定义软件包
通过几个额外的步骤,我们可以更改 MacPorts 编译 OpenCV 或其他软件的方式。如前所述,MacPorts 的构建配方定义在名为 Portfiles 的配置文件中。通过创建或编辑 Portfiles,我们可以访问高度可配置的构建工具,如 CMake,同时还能享受 MacPorts 的功能,如依赖关系解析。
假设我们已安装 MacPorts。现在,我们可以配置 MacPorts 以使用我们编写的自定义 Portfiles:
-
在某个位置创建一个文件夹来存放我们的自定义 Portfiles。我们将把这个文件夹称为
<local_repository>。 -
编辑
/opt/local/etc/macports/sources.conf文件(假设 MacPorts 安装到默认位置)。在rsync://rsync.macports.org/release/ports/ [default]行上方,添加此行:file://<local_repository>例如,如果
<local_repository>是/Users/Joe/Portfiles,请添加以下行:file:///Users/Joe/Portfiles注意三重斜杠并保存文件。现在,MacPorts 知道它必须首先在
<local_repository>中搜索 Portfiles,然后是其默认在线仓库。 -
打开终端并更新 MacPorts 以确保我们拥有默认仓库的最新 Portfile:
$ sudo port selfupdate -
以默认仓库的
opencvPortfile 为例,让我们也复制目录结构,这决定了包在 MacPorts 中的分类方式:$ mkdir <local_repository>/graphics/ $ cp /opt/local/var/macports/sources/rsync.macports.org/release/ports/graphics/opencv <local_repository>/graphics或者,对于包含 Kinect 支持的示例,我们可以从
nummist.com/opencv/ports.tar.gz下载我的在线仓库,解压它,并将整个graphics文件夹复制到<local_repository>:$ cp <unzip_destination>/graphics <local_repository> -
编辑
<local_repository>/graphics/opencv/Portfile。注意,此文件指定了 CMake 配置标志、依赖项和变体。有关 Portfile 编辑的详细信息,请参阅guide.macports.org/#development。要查看与 OpenCV 相关的 CMake 配置标志,我们需要查看其源代码。从
github.com/Itseez/opencv/archive/3.0.0.zip下载源代码存档,将其解压到任何位置,并阅读<unzip_destination>/OpenCV-3.0.0/CMakeLists.txt。在对 Portfile 进行任何编辑后,请保存它。
-
现在,我们需要在本地仓库中生成一个索引文件,以便 MacPorts 可以找到新的 Portfile:
$ cd <local_repository> $ portindex -
从现在起,我们可以将我们的自定义
opencv文件视为任何其他 MacPorts 包。例如,我们可以按照以下方式安装它:$ sudo port install opencv +python27 +openni_sensorkinect注意,由于它们在
/opt/local/etc/macports/sources.conf中的列表顺序,我们的本地仓库的 Portfile 优先于默认仓库的 Portfile。
使用带有预装包的 Homebrew(不支持深度相机)
Homebrew 是另一个可以帮助我们的包管理器。通常,MacPorts 和 Homebrew 不应安装在同一台机器上。
从已经设置好 Xcode 及其命令行工具的系统开始,以下步骤将通过 Homebrew 为我们提供 OpenCV 安装:
-
打开终端并运行以下命令以安装 Homebrew:
$ ruby -e "$(curl -fsSkLraw.github.com/mxcl/homebrew/go)" -
与 MacPorts 不同,Homebrew 不会自动将其可执行文件放入
PATH。要这样做,创建或编辑~/.profile文件,并在代码顶部添加此行:export PATH=/usr/local/bin:/usr/local/sbin:$PATH保存文件并运行以下命令以刷新
PATH:$ source ~/.profile注意,现在由 Homebrew 安装的可执行文件优先于由系统安装的可执行文件。
-
要运行 Homebrew 的自我诊断报告,请运行以下命令:
$ brew doctor遵循它提供的任何故障排除建议。
-
现在,更新 Homebrew:
$ brew update -
运行以下命令安装 Python 2.7:
$ brew install python -
现在,我们可以安装 NumPy。Homebrew 对 Python 库包的选择有限,所以我们使用一个名为
pip的单独的包管理工具,它包含在 Homebrew 的 Python 中:$ pip install numpy -
SciPy 包含一些 Fortran 代码,因此我们需要一个合适的编译器。我们可以使用 Homebrew 来安装
gfortran编译器:$ brew install gfortran现在,我们可以安装 SciPy:
$ pip install scipy -
要在 64 位系统上安装 OpenCV(自 2006 年底以来所有新的 Mac 硬件),请运行以下命令:
$ brew install opencv
小贴士
下载示例代码
您可以从您在www.packtpub.com的账户中下载您购买的所有 Packt Publishing 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
使用 Homebrew 和自定义包
Homebrew 使得编辑现有的包定义变得容易:
$ brew edit opencv
包定义实际上是 Ruby 编程语言中的脚本。有关编辑它们的提示,可以在 Homebrew Wiki 页面github.com/mxcl/homebrew/wiki/Formula-Cookbook上找到。脚本可以指定 Make 或 CMake 配置标志,以及其他内容。
要查看哪些 CMake 配置标志与 OpenCV 相关,我们需要查看其源代码。从github.com/Itseez/opencv/archive/3.0.0.zip下载源代码存档,将其解压缩到任何位置,并阅读<unzip_destination>/OpenCV-2.4.3/CMakeLists.txt。
在对 Ruby 脚本进行编辑后,保存它。
定制的包可以像普通包一样处理。例如,它可以按照以下方式安装:
$ brew install opencv
在 Ubuntu 及其衍生版上安装
首先最重要的是,这里有一个关于 Ubuntu 操作系统版本的快速说明:Ubuntu 有一个 6 个月的发布周期,其中每个发布都是主要版本(撰写本文时为 14)的.04 或.10 小版本。然而,每两年,Ubuntu 会发布一个被归类为长期支持(LTS)的版本,这将通过 Canonical(Ubuntu 背后的公司)为您提供五年的支持。如果您在企业环境中工作,安装 LTS 版本无疑是明智的。目前可用的最新版本是 14.04。
Ubuntu 预装了 Python 2.7。标准的 Ubuntu 仓库包含没有深度相机支持的 OpenCV 2.4.9 包。在撰写本文时,OpenCV 3 尚未通过 Ubuntu 仓库提供,因此我们必须从源代码构建它。幸运的是,大多数 Unix-like 和 Linux 系统已经预装了所有必要的软件,可以从头开始构建项目。从源代码构建时,OpenCV 可以通过 OpenNI 和 SensorKinect 支持深度相机,它们作为预编译的二进制文件和安装脚本提供。
使用 Ubuntu 仓库(不支持深度相机)
我们可以使用apt包管理器通过运行以下命令安装 Python 及其所有必要的依赖项:
> sudo apt-get install build-essential
> sudo apt-get install cmake git libgtk2.0-dev pkg-config libavcodecdev libavformat-dev libswscale-dev
> sudo apt-get install python-dev python-numpy libtbb2 libtbb-dev libjpeg-dev libpng-dev libtiff-dev libjasper-dev libdc1394-22-dev
同样,我们也可以使用 Ubuntu 软件中心,它是apt包管理器的图形前端。
从源代码构建 OpenCV
现在我们已经安装了整个 Python 栈和cmake,我们可以构建 OpenCV。首先,我们需要从github.com/Itseez/opencv/archive/3.0.0-beta.zip下载源代码。
在终端中解压归档并将其移动到解压文件夹中。
然后,运行以下命令:
> mkdir build
> cd build
> cmake -D CMAKE_BUILD_TYPE=Release -D CMAKE_INSTALL_PREFIX=/usr/local ..
> make
> make install
安装完成后,您可能想查看 OpenCV 的 Python 示例,位于<opencv_folder>/opencv/samples/python和<script_folder>/opencv/samples/python2。
在其他类 Unix 系统上的安装
对于 Ubuntu(如前所述)的方法可能适用于任何从 Ubuntu 14.04 LTS 或 Ubuntu 14.10 衍生出来的 Linux 发行版,如下所示:
-
Kubuntu 14.04 LTS 或 Kubuntu 14.10
-
Xubuntu 14.04 LTS 或 Xubuntu 14.10
-
Linux Mint 17
在 Debian Linux 及其衍生版本中,apt包管理器的工作方式与 Ubuntu 相同,尽管可用的包可能不同。
在 Gentoo Linux 及其衍生版本中,Portage 包管理器与 MacPorts(如前所述)类似,尽管可用的包可能不同。
在 FreeBSD 衍生版本上,安装过程再次类似于 MacPorts;实际上,MacPorts 源自 FreeBSD 采用的ports安装系统。请参考出色的 FreeBSD 手册www.freebsd.org/doc/handbook/以了解软件安装过程的概述。
在其他类 Unix 系统中,包管理器和可用的包可能不同。请查阅您的包管理器文档,并搜索名称中包含 opencv 的包。请记住,OpenCV 及其 Python 绑定可能被拆分为多个包。
此外,寻找系统提供商、仓库维护者或社区发布的任何安装说明。由于 OpenCV 使用相机驱动程序和媒体编解码器,在多媒体支持较差的系统上,使所有功能正常工作可能很棘手。在某些情况下,可能需要重新配置或重新安装系统包以实现兼容性。
如果有 OpenCV 的包可用,请检查它们的版本号。本书建议使用 OpenCV 3 或更高版本。此外,请检查这些包是否提供 Python 绑定和通过 OpenNI 和 SensorKinect 的深度相机支持。最后,检查开发者社区中是否有人报告了使用这些包的成功或失败情况。
如果我们想从源代码自定义构建 OpenCV,可能有助于参考之前讨论的 Ubuntu 安装脚本,并将其适应到另一个系统上的包管理器和包。
安装 Contrib 模块
与 OpenCV 2.4 不同,一些模块包含在名为opencv_contrib的仓库中,该仓库可在github.com/Itseez/opencv_contrib找到。我强烈建议安装这些模块,因为它们包含 OpenCV 中没有的额外功能,例如人脸识别模块。
下载完成后(无论是通过zip还是git,我推荐使用git,这样您可以通过简单的git pull命令保持更新),您可以重新运行cmake命令,包括构建带有opencv_contrib模块的 OpenCV,如下所示:
cmake -DOPENCV_EXTRA_MODULES_PATH=<opencv_contrib>/modules <opencv_source_directory>
因此,如果您已遵循标准程序并在 OpenCV 下载文件夹中创建了一个构建目录,您应该运行以下命令:
mkdir build && cd build
cmake -D CMAKE_BUILD_TYPE=Release -DOPENCV_EXTRA_MODULES_PATH=<opencv_contrib>/modules -D CMAKE_INSTALL_PREFIX=/usr/local ..
make
运行示例
运行几个示例脚本是测试 OpenCV 是否正确设置的好方法。这些示例包含在 OpenCV 的源代码存档中。
在 Windows 上,我们应该已经下载并解压了 OpenCV 的自解压 ZIP 文件。在<unzip_destination>/opencv/samples中找到示例。
在 Unix-like 系统上,包括 Mac,从github.com/Itseez/opencv/archive/3.0.0.zip下载源代码存档,并将其解压到任何位置(如果我们还没有这样做)。在<unzip_destination>/OpenCV-3.0.0/samples中找到示例。
一些示例脚本需要命令行参数。然而,以下脚本(以及其他一些脚本)可以在没有任何参数的情况下运行:
-
python/camera.py:此脚本显示一个网络摄像头流(假设已经插入了网络摄像头)。 -
python/drawing.py:此脚本绘制一系列形状,例如屏幕保护程序。 -
python2/hist.py:此脚本显示一张照片。按A、B、C、D或E查看照片的变体以及相应的颜色或灰度值直方图。 -
python2/opt_flow.py(Ubuntu 包中缺失):此脚本显示带有叠加光流可视化(例如运动方向)的网络摄像头流。例如,慢慢在摄像头前挥手以查看效果。按1或2进行不同的可视化。
要退出脚本,请按Esc(不是窗口的关闭按钮)。
如果我们遇到ImportError: No module named 'cv2.cv'的消息,那么这意味着我们正在从不知道 OpenCV 的 Python 安装中运行脚本。这种情况有两个可能的解释:
-
OpenCV 安装过程中可能有一些步骤失败或被遗漏。返回并检查这些步骤。
-
如果机器上有多个 Python 安装,我们可能使用了错误的 Python 版本来启动脚本。例如,在 Mac 上,可能的情况是 OpenCV 是为 MacPorts Python 安装的,但我们使用的是系统的 Python 来运行脚本。返回并回顾有关编辑系统路径的安装步骤。此外,尝试使用如下命令手动从命令行启动脚本:
$ python python/camera.py您还可以使用以下命令:
$ python2.7 python/camera.py作为选择不同 Python 安装的另一种可能方法,尝试编辑示例脚本以删除
#!行。这些行可能明确地将脚本与错误的 Python 安装关联起来(针对我们的特定设置)。
查找文档、帮助和更新
OpenCV 的文档可以在网上找到,网址为docs.opencv.org/。该文档包括 OpenCV 新 C++ API、新 Python API(基于 C++ API)、旧 C API 及其旧 Python API(基于 C API)的综合 API 参考。在查找类或函数时,请务必阅读有关新 Python API(cv2模块)的部分,而不是旧 Python API(cv模块)的部分。
该文档还可用作几个可下载的 PDF 文件:
-
API 参考:此文档可在
docs.opencv.org/modules/refman.html找到 -
教程:这些文档可在
docs.opencv.org/doc/tutorials/tutorials.html找到(这些教程使用 C++代码;教程代码的 Python 版本可在阿比德·拉赫曼·K.的仓库goo.gl/EPsD1找到)
如果您在飞机或其他没有互联网接入的地方编写代码,您肯定希望保留文档的离线副本。
如果文档似乎没有回答您的问题,请尝试与 OpenCV 社区交流。以下是一些您可以找到有帮助人士的网站:
-
OpenCV 论坛:
www.answers.opencv.org/questions/ -
大卫·米兰·埃斯克里瓦的博客(本书的审稿人之一):
blog.damiles.com/ -
阿比德·拉赫曼·K.的博客(本书的审稿人之一):
www.opencvpython.blogspot.com/ -
阿德里安·罗斯布鲁克的网站(本书的审稿人之一):
www.pyimagesearch.com/ -
乔·米尼奇诺为此书的网站(本书的作者):
techfort.github.io/pycv/ -
乔·豪斯为此书的网站(本书第一版的作者):
nummist.com/opencv/
最后,如果你是一位希望尝试最新(不稳定)OpenCV 源代码中的新功能、错误修复和示例脚本的进阶用户,请查看项目的仓库:github.com/Itseez/opencv/。
摘要
到目前为止,我们应该已经安装了一个可以完成本书中描述的项目所需所有功能的 OpenCV。根据我们采取的方法,我们可能还拥有一套可用的工具和脚本,可用于重新配置和重建 OpenCV 以满足我们未来的需求。
我们知道在哪里可以找到 OpenCV 的 Python 示例。这些示例涵盖了本书范围之外的不同功能范围,但它们作为额外的学习辅助工具是有用的。
在下一章中,我们将熟悉 OpenCV API 的最基本功能,即显示图像、视频,通过摄像头捕获视频,以及处理基本的键盘和鼠标输入。
第二章:处理文件、摄像头和 GUI
安装 OpenCV 并运行示例很有趣,但在这个阶段,我们想亲自尝试。本章介绍了 OpenCV 的 I/O 功能。我们还讨论了项目概念以及这个项目的面向对象设计的开始,我们将在随后的章节中详细阐述。
通过从查看 I/O 能力和设计模式开始,我们将以制作三明治的方式构建我们的项目:从外到内。面包切片和涂抹,或者端点和胶水,在填充或算法之前。我们选择这种方法是因为计算机视觉主要是外向的——它考虑的是我们计算机之外的现实世界——我们希望通过一个公共接口将我们后续的所有算法工作应用到现实世界中。
基本 I/O 脚本
大多数 CV 应用程序需要获取图像作为输入。大多数也会生成图像作为输出。一个交互式 CV 应用程序可能需要一个摄像头作为输入源和一个窗口作为输出目标。然而,其他可能的源和目标包括图像文件、视频文件和原始字节。例如,原始字节可能通过网络连接传输,或者如果我们将过程图形纳入我们的应用程序,它们可能由算法生成。让我们看看这些可能性中的每一个。
读取/写入图像文件
OpenCV 提供了imread()和imwrite()函数,支持各种静态图像的文件格式。支持的格式因系统而异,但应始终包括 BMP 格式。通常,PNG、JPEG 和 TIFF 也应包括在支持的格式中。
让我们探索在 Python 和 NumPy 中图像表示的结构。
不论格式如何,每个像素都有一个值,但区别在于像素的表示方式。例如,我们可以通过简单地创建一个 2D NumPy 数组从头开始创建一个黑色方形图像:
img = numpy.zeros((3,3), dtype=numpy.uint8)
如果我们将此图像打印到控制台,我们将获得以下结果:
array([[0, 0, 0],
[0, 0, 0],
[0, 0, 0]], dtype=uint8)
每个像素由一个单一的 8 位整数表示,这意味着每个像素的值在 0-255 范围内。
现在让我们使用cv2.cvtColor将此图像转换为蓝绿红(BGR):
img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
让我们观察图像是如何变化的:
array([[[0, 0, 0],
[0, 0, 0],
[0, 0, 0]],
[[0, 0, 0],
[0, 0, 0],
[0, 0, 0]],
[[0, 0, 0],
[0, 0, 0],
[0, 0, 0]]], dtype=uint8)
如您所见,每个像素现在由一个包含三个元素的数组表示,其中每个整数分别代表 B、G 和 R 通道。其他颜色空间,如 HSV,将以相同的方式表示,尽管值范围不同(例如,HSV 颜色空间的色调值范围为 0-180)以及通道数量不同。
您可以通过检查shape属性来检查图像的结构,该属性返回行、列和通道数(如果有多个通道)。
考虑以下示例:
>>> img = numpy.zeros((3,3), dtype=numpy.uint8)
>>> img.shape
上述代码将打印(3,3)。如果您然后将图像转换为 BGR,形状将是(3,3,3),这表明每个像素有三个通道。
图像可以从一种文件格式加载并保存到另一种格式。例如,让我们将图像从 PNG 转换为 JPEG:
import cv2
image = cv2.imread('MyPic.png')
cv2.imwrite('MyPic.jpg', image)
注意
我们使用的 OpenCV 功能大多位于 cv2 模块中。你可能会遇到其他依赖 cv 或 cv2.cv 模块的 OpenCV 指南,这些是旧版本。Python 模块被称为 cv2 并不是因为它是 OpenCV 2.x.x 的 Python 绑定模块,而是因为它引入了一个更好的 API,它利用面向对象编程,而不是之前的 cv 模块,后者遵循更过程化的编程风格。
默认情况下,即使文件使用灰度格式,imread() 也返回 BGR 颜色格式的图像。BGR 代表与 红-绿-蓝(RGB)相同的颜色空间,但字节顺序相反。
可以选择指定 imread() 的模式为以下枚举之一:
-
IMREAD_ANYCOLOR = 4 -
IMREAD_ANYDEPTH = 2 -
IMREAD_COLOR = 1 -
IMREAD_GRAYSCALE = 0 -
IMREAD_LOAD_GDAL = 8 -
IMREAD_UNCHANGED = -1
例如,让我们将 PNG 文件作为灰度图像加载(在此过程中丢失任何颜色信息),然后将其保存为灰度 PNG 图像:
import cv2
grayImage = cv2.imread('MyPic.png', cv2.IMREAD_GRAYSCALE)
cv2.imwrite('MyPicGray.png', grayImage)
为了避免不必要的麻烦,在使用 OpenCV 的 API 时,至少使用图像的绝对路径(例如,Windows 上的 C:\Users\Joe\Pictures\MyPic.png 或 Unix 上的 /home/joe/pictures/MyPic.png),路径必须是相对的,除非它是绝对路径。图像的路径,除非是绝对路径,否则相对于包含 Python 脚本的文件夹,所以在前面的例子中,MyPic.png 必须与你的 Python 脚本在同一文件夹中,否则找不到图像。
无论模式如何,imread() 都会丢弃任何 alpha 通道(透明度)。imwrite() 函数要求图像以 BGR 或灰度格式存在,并且每个通道需要支持一定数量的位,该位数为输出格式所能支持。例如,bmp 需要每个通道 8 位,而 PNG 允许每个通道 8 或 16 位。
在图像和原始字节之间进行转换
从概念上讲,一个字节是一个介于 0 到 255 之间的整数。在所有今天的实时图形应用中,一个像素通常由每个通道一个字节表示,尽管其他表示也是可能的。
OpenCV 图像是一个 .array 类型的 2D 或 3D 数组。一个 8 位灰度图像是一个包含字节的 2D 数组。一个 24 位 BGR 图像是一个 3D 数组,它也包含字节值。我们可以通过使用表达式来访问这些值,例如 image[0, 0] 或 image[0, 0, 0]。第一个索引是像素的 y 坐标或行,0 表示顶部。第二个索引是像素的 x 坐标或列,0 表示最左边。如果适用,第三个索引代表一个颜色通道。
例如,在一个 8 位灰度图像中,如果左上角有一个白色像素,image[0, 0] 是 255。对于一个 24 位 BGR 图像,如果左上角有一个蓝色像素,image[0, 0] 是 [255, 0, 0]。
注意
作为使用表达式(如 image[0, 0] 或 image[0, 0] = 128)的替代,我们可以使用表达式,如 image.item((0, 0)) 或 image.setitem((0, 0), 128)。后者的表达式对于单像素操作更有效率。然而,正如我们将在后续章节中看到的,我们通常希望对图像的大块区域进行操作,而不是单个像素。
假设图像每个通道有 8 位,我们可以将其转换为标准的 Python bytearray,它是一维的:
byteArray = bytearray(image)
相反,如果 bytearray 中的字节顺序适当,我们可以将其转换并重塑为 numpy.array 类型,得到一个图像:
grayImage = numpy.array(grayByteArray).reshape(height, width)
bgrImage = numpy.array(bgrByteArray).reshape(height, width, 3)
作为更完整的示例,让我们将包含随机字节的 bytearray 转换为灰度图像和 BGR 图像:
import cv2
import numpy
import os
# Make an array of 120,000 random bytes.
randomByteArray = bytearray(os.urandom(120000))
flatNumpyArray = numpy.array(randomByteArray)
# Convert the array to make a 400x300 grayscale image.
grayImage = flatNumpyArray.reshape(300, 400)
cv2.imwrite('RandomGray.png', grayImage)
# Convert the array to make a 400x100 color image.
bgrImage = flatNumpyArray.reshape(100, 400, 3)
cv2.imwrite('RandomColor.png', bgrImage)
运行此脚本后,我们应该在脚本目录中有一对随机生成的图像,RandomGray.png 和 RandomColor.png。
注意
在这里,我们使用 Python 的标准 os.urandom() 函数生成随机原始字节,然后将其转换为 NumPy 数组。请注意,也可以直接(并且更高效地)使用语句生成随机 NumPy 数组,例如 numpy.random.randint(0, 256, 120000).reshape(300, 400)。我们使用 os.urandom() 的唯一原因是为了帮助演示从原始字节到转换的过程。
使用 numpy.array 访问图像数据
现在你已经更好地理解了图像的形成方式,我们可以开始对其进行基本操作。我们知道,在 OpenCV 中加载图像最简单(也是最常见)的方法是使用 imread 函数。我们也知道这将返回一个图像,实际上是一个数组(二维或三维,取决于你传递给 imread() 的参数)。
y.array 结构针对数组操作进行了很好的优化,并允许进行某些在普通 Python 列表中不可用的批量操作。这类 .array 类型特定的操作在 OpenCV 中的图像处理中非常有用。让我们从最基本的例子开始,逐步探索图像处理:假设你想操作 BGR 图像中坐标为 (0, 0) 的像素,并将其转换为白色像素。
import cv
import numpy as np
img = cv.imread('MyPic.png')
img[0,0] = [255, 255, 255]
如果你使用标准的 imshow() 调用显示图像,你将在图像的左上角看到一个白色点。当然,这并不很有用,但它展示了可以完成的事情。现在让我们利用 numpy.array 的能力,以比普通 Python 数组快得多的速度对数组进行转换操作。
假设你想要改变特定像素的蓝色值,例如,坐标为(150,120)的像素。numpy.array类型提供了一个非常方便的方法,item(),它接受三个参数:x(或左)位置、y(或顶)以及数组中(x,y)位置的索引(记住,在 BGR 图像中,某个位置的数是一个包含 B、G 和 R 值的三个元素的数组,顺序如下)并返回索引位置的值。另一个itemset()方法将特定像素的特定通道的值设置为指定的值(itemset()接受两个参数:一个包含三个元素(x、y 和索引)的元组以及新值)。
在这个例子中,我们将(150,120)处的蓝色值从其当前值(127)更改为任意值 255:
import cv
import numpy as np
img = cv.imread('MyPic.png')
print img.item(150, 120, 0) // prints the current value of B for that pixel
img.itemset( (150, 120, 0), 255)
print img.item(150, 120, 0) // prints 255
记住,我们使用numpy.array做这件事有两个原因:numpy.array是一个针对这类操作进行了高度优化的库,而且我们通过 NumPy 优雅的方法而不是第一个示例中的原始索引访问获得了更易读的代码。
这段特定的代码本身并没有做什么,但它开启了一个可能性的世界。然而,建议你使用内置的过滤器和方法来操作整个图像;上述方法仅适用于小区域。
现在,让我们看看一个非常常见的操作,即操作通道。有时,你可能想要将特定通道(B、G 或 R)的所有值置零。
小贴士
使用循环来操作 Python 数组在运行时间上非常昂贵,应该尽量避免。使用数组索引允许高效地操作像素。这是一个昂贵且缓慢的操作,特别是如果你在操作视频时,你会发现输出会有抖动。然后,一个名为索引的功能就派上用场了。将图像中所有 G(绿色)值设置为0就像使用以下代码一样简单:
import cv
import as np
img = cv.imread('MyPic.png')
img[:, :, 1] = 0
这是一段相当令人印象深刻且易于理解的代码。相关行是最后一行,它基本上指示程序从所有行和列中获取所有像素,并将结果值的三元素数组的索引一设置为0。如果你显示这张图片,你会注意到绿色完全消失。
通过使用 NumPy 的数组索引访问原始像素,我们可以做许多有趣的事情;其中之一是定义感兴趣区域(ROI)。一旦定义了区域,我们可以执行一系列操作,例如,将此区域绑定到一个变量上,然后甚至定义第二个区域并将第一个区域的值赋给它(在图像中将一部分图像复制到另一个位置):
import cv
import numpy as np
img = cv.imread('MyPic.png')
my_roi = img[0:100, 0:100]
img[300:400, 300:400] = my_roi
确保两个区域在大小上是一致的非常重要。如果不是,NumPy 会(正确地)抱怨两个形状不匹配。
最后,我们可以从numpy.array中获得一些有趣的细节,例如使用此代码获取图像属性:
import cv
import numpy as np
img = cv.imread('MyPic.png')
print img.shape
print img.size
print img.dtype
这三个属性按此顺序排列:
-
形状:NumPy 返回一个包含宽度、高度以及如果图像是彩色的则包含通道数的元组。这对于调试图像类型很有用;如果图像是单色或灰度,则不会包含通道值。
-
大小:此属性指图像的像素大小。
-
数据类型:此属性指用于图像的数据类型(通常是未签名的整数类型及其支持的字节数,即
uint8)。
总而言之,强烈建议您在处理 OpenCV 时熟悉 NumPy,特别是numpy.array,因为它是使用 Python 进行图像处理的基础。
读取/写入视频文件
OpenCV 提供了VideoCapture和VideoWriter类,支持各种视频文件格式。支持的格式因系统而异,但应始终包括 AVI。通过其read()方法,VideoCapture类可以轮询新帧,直到达到视频文件的末尾。每个帧都是以 BGR 格式的图像。
相反,可以将图像传递给VideoWriter类的write()方法,该方法将图像追加到VideoWriter中的文件。让我们看看一个例子,从 AVI 文件中读取帧并使用 YUV 编码写入另一个文件:
import cv2
videoCapture = cv2.VideoCapture('MyInputVid.avi')
fps = videoCapture.get(cv2.CAP_PROP_FPS)
size = (int(videoCapture.get(cv2.CAP_PROP_FRAME_WIDTH)),
int(videoCapture.get(cv2.CAP_PROP_FRAME_HEIGHT)))
videoWriter = cv2.VideoWriter(
'MyOutputVid.avi', cv2.VideoWriter_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.VideoWriter_fourcc('I','4','2','0'):此选项是不压缩的 YUV 编码,4:2:0 色度子采样。这种编码广泛兼容,但生成的文件很大。文件扩展名应为.avi。 -
cv2.VideoWriter_fourcc('P','I','M','1'):此选项是 MPEG-1。文件扩展名应为.avi。 -
cv2.VideoWriter_fourcc('X','V','I','D'):此选项是 MPEG-4,如果您希望生成的视频大小为平均大小,这是一个首选选项。文件扩展名应为.avi。 -
cv2.VideoWriter_fourcc('T','H','E','O'):此选项是 Ogg Vorbis。文件扩展名应为.ogv。 -
cv2.VideoWriter_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.CAP_PROP_FRAME_WIDTH)),
int(cameraCapture.get(cv2.CAP_PROP_FRAME_HEIGHT)))
videoWriter = cv2.VideoWriter(
'MyOutputVid.avi', cv2.VideoWriter_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
cameraCapture.release()
很不幸,VideoCapture 类的 get() 方法并不能返回相机帧率的准确值;它总是返回 0。在官方文档docs.opencv.org/modules/highgui/doc/reading_and_writing_images_and_video.html中写道:
“当查询
VideoCapture类后端不支持的一个属性时,返回值0。”
这通常发生在只支持基本功能的驱动程序的系统上。
为了为相机创建一个合适的 VideoWriter 类,我们不得不要么对帧率做出假设(就像我们在之前的代码中所做的那样),要么使用计时器来测量它。后者方法更好,我们将在本章后面讨论。
相机的数量及其顺序当然是系统相关的。不幸的是,OpenCV 并没有提供查询相机数量或其属性的方法。如果使用无效的索引来构造 VideoCapture 类,该类将不会输出任何帧;其 read() 方法将返回 (false, None)。为了避免尝试从未正确打开的 VideoCapture 中检索帧,一个很好的方法是使用 VideoCapture.isOpened 方法,它返回一个布尔值。
当我们需要同步一组相机或多头相机(如立体相机或 Kinect)时,read() 方法是不合适的。这时,我们使用 grab() 和 retrieve() 方法代替。对于一组相机,我们使用以下代码:
success0 = cameraCapture0.grab()
success1 = cameraCapture1.grab()
if success0 and success1:
frame0 = cameraCapture0.retrieve()
frame1 = cameraCapture1.retrieve()
在窗口中显示图像
OpenCV 中最基本的一个操作就是显示图像。这可以通过 imshow() 函数实现。如果你来自任何其他 GUI 框架的背景,你可能会认为调用 imshow() 来显示图像就足够了。这仅部分正确:图像会被显示,然后立即消失。这是设计上的考虑,以便在处理视频时能够不断刷新窗口框架。以下是一个显示图像的非常简单的示例代码:
import cv2
import numpy as np
img = cv2.imread('my-image.png')
cv2.imshow('my image', img)
cv2.waitKey()
cv2.destroyAllWindows()
imshow() 函数接受两个参数:我们想要在其中显示图像的窗口名称,以及图像本身。当我们探讨在窗口中显示帧时,我们将更详细地讨论 waitKey()。
命名为 destroyAllWindows() 的函数会销毁 OpenCV 创建的所有窗口。
在窗口中显示相机帧
OpenCV 允许使用 namedWindow()、imshow() 和 destroyWindow() 函数创建、重绘和销毁命名窗口。此外,任何窗口都可以通过 waitKey() 函数捕获键盘输入,通过 setMouseCallback() 函数捕获鼠标输入。让我们看看一个示例,其中我们展示了实时相机输入的帧:
import cv2
clicked = False
def onMouse(event, x, y, flags, param):
global clicked
if event == cv2.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')
cameraCapture.release()
waitKey() 的参数是等待键盘输入的毫秒数。返回值是 -1(表示没有按键被按下)或一个 ASCII 键码,例如 27 对应于 Esc。有关 ASCII 键码的列表,请参阅 www.asciitable.com/。此外,请注意,Python 提供了一个标准函数 ord(),可以将字符转换为它的 ASCII 键码。例如,ord('a') 返回 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。回调的事件参数是以下动作之一:
-
cv2.EVENT_MOUSEMOVE: 此事件表示鼠标移动 -
cv2.EVENT_LBUTTONDOWN: 此事件表示左键按下 -
cv2.EVENT_RBUTTONDOWN: 这表示右键按下 -
cv2.EVENT_MBUTTONDOWN: 这表示中间按钮按下 -
cv2.EVENT_LBUTTONUP: 这表示左键释放 -
cv2.EVENT_RBUTTONUP: 此事件表示右键释放 -
cv2.EVENT_MBUTTONUP: 此事件表示中间按钮释放 -
cv2.EVENT_LBUTTONDBLCLK: 此事件表示左键被双击 -
cv2.EVENT_RBUTTONDBLCLK: 这表示右键被双击 -
cv2.EVENT_MBUTTONDBLCLK: 这表示中间按钮被双击
鼠标回调的标志参数可能是以下事件的位运算组合:
-
cv2.EVENT_FLAG_LBUTTON: 此事件表示左键被按下 -
cv2.EVENT_FLAG_RBUTTON: 此事件表示右键被按下 -
cv2.EVENT_FLAG_MBUTTON: 此事件表示中间按钮被按下 -
cv2.EVENT_FLAG_CTRLKEY: 此事件表示按下 Ctrl 键 -
cv2.EVENT_FLAG_SHIFTKEY: 此事件表示按下 Shift 键 -
cv2.EVENT_FLAG_ALTKEY: 此事件表示按下 Alt 键
不幸的是,OpenCV 不提供处理窗口事件的方法。例如,当窗口的关闭按钮被点击时,我们无法停止我们的应用程序。由于 OpenCV 有限的的事件处理和 GUI 功能,许多开发者更喜欢将其与其他应用程序框架集成。在本章的后面部分,我们将设计一个抽象层,以帮助将 OpenCV 集成到任何应用程序框架中。
Project Cameo(人脸追踪和图像处理)
OpenCV 通常通过一种类似于食谱的方法来研究,它涵盖了大量的算法,但没有关于高级应用程序开发的内容。在一定程度上,这种方法是可以理解的,因为 OpenCV 的潜在应用非常多样。OpenCV 被用于广泛的领域:照片/视频编辑器、动作控制游戏、机器人的 AI,或者记录参与者眼动行为的心理学实验。在如此不同的用例中,我们真的能够研究出一套有用的抽象吗?
我相信我们可以,而且越早开始创建抽象,越好。我们将围绕单个应用程序来结构化我们对 OpenCV 的研究,但在每个步骤中,我们将设计这个应用程序的一个组件,使其可扩展和可重用。
我们将开发一个交互式应用程序,该应用程序在实时摄像头输入上执行面部跟踪和图像操作。这类应用程序涵盖了 OpenCV 的广泛功能,并挑战我们创建一个高效、有效的实现。
具体来说,我们的应用程序将执行实时面部融合。给定两个摄像头输入流(或者,可选地,预先录制的视频输入),应用程序将把一个流中的面部叠加到另一个流中的面部上。将应用滤镜和扭曲,使这个混合场景看起来和感觉上统一。用户应该体验到参与现场表演的感觉,进入另一个环境和角色。这种用户体验在像迪士尼乐园这样的游乐园中很受欢迎。
在这样的应用程序中,用户会立即注意到缺陷,例如帧率低或跟踪不准确。为了获得最佳结果,我们将尝试使用传统成像和深度成像的几种方法。
我们将把我们的应用程序命名为 Cameo。在珠宝中,Cameo 是指一个人的小肖像,或者在电影中是指名人扮演的非常短暂的角色。
Cameo – 面向对象设计
Python 应用程序可以编写为纯过程式风格。这通常用于小型应用程序,例如我们之前讨论的基本 I/O 脚本。然而,从现在开始,我们将使用面向对象风格,因为它促进了模块化和可扩展性。
从我们对 OpenCV I/O 功能的概述中,我们知道所有图像都是相似的,无论它们的来源或目的地。无论我们如何获取图像流或将其发送到何处作为输出,我们都可以将相同的应用特定逻辑应用于这个流中的每一帧。在像 Cameo 这样的应用程序中,分离 I/O 代码和应用代码变得特别方便,因为它使用多个 I/O 流。
我们将创建名为CaptureManager和WindowManager的类,作为 I/O 流的高级接口。我们的应用程序代码可以使用CaptureManager读取新帧,并且可选地将每个帧派发到一个或多个输出,包括静态图像文件、视频文件和窗口(通过WindowManager类)。WindowManager类允许我们的应用程序代码以面向对象的方式处理窗口和事件。
CaptureManager和WindowManager都是可扩展的。我们可以实现不依赖于 OpenCV 进行 I/O 的版本。实际上,附录 A,与 Pygame 集成,使用 Python 的 OpenCV 计算机视觉,使用了一个WindowManager子类。
使用 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()
return self._frame
@property
def isWritingImage (self):
return self._imageFilename is not None
@property
def isWritingVideo(self):
return self._videoFilename is not None
注意,大多数的member变量都是非公开的,这可以通过变量名前的下划线前缀来表示,例如self._enteredFrame。这些非公开变量与当前帧的状态以及任何文件写入操作相关。正如之前讨论的,应用程序代码只需要配置一些事情,这些事情作为构造函数参数和可设置的公共属性来实现:摄像头通道、窗口管理器以及是否镜像摄像头预览的选项。
本书假设读者对 Python 有一定程度的熟悉;然而,如果你对那些@注解(例如,@property)感到困惑,请参考 Python 文档中关于decorators的部分,这是语言的一个内置特性,允许一个函数被另一个函数包装,通常用于在应用程序的多个地方应用用户定义的行为(参考docs.python.org/2/reference/compound_stmts.html#grammar-token-decorator))。
注意
Python 没有私有成员变量的概念,单下划线前缀(_)只是一个约定。
根据这个约定,在 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.VideoWriter_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.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.CAP_PROP_FRAME_WIDTH)),
int(self._capture.get(
cv2.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,我们的当前实现是足够的。
使用managers.WindowManager管理窗口和键盘
正如我们所见,OpenCV 提供了创建窗口、销毁窗口、显示图像和处理事件的函数。这些函数不是窗口类的成员方法,而是需要将窗口的名称作为参数传递。由于这个接口不是面向对象的,它不符合 OpenCV 的一般风格。此外,它可能与我们可能最终想要使用的其他窗口或事件处理接口不兼容。
为了面向对象和适应性,我们将此功能抽象成一个具有createWindow()、destroyWindow()、show()和processEvents()方法的WindowManager类。作为一个属性,WindowManager类有一个名为keypressCallback的函数对象,该对象(如果非None)在processEvents()中响应任何按键时被调用。keypressCallback对象必须接受一个单一参数,例如 ASCII 键码。
让我们在managers.py中添加以下WindowManager的实现:
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 集成,使用 Python 的 OpenCV 计算机视觉展示了使用 Pygame 的窗口处理和事件框架实现的WindowManager子类,而不是使用 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类时传递了True给shouldMirrorPreview。
到目前为止,我们除了镜像预览之外,没有以任何方式操作帧。我们将在第三章,过滤图像中开始添加更多有趣的效果。
摘要
到目前为止,我们应该有一个显示摄像头视频流、监听键盘输入,并且(在命令下)记录截图或屏幕录制的应用程序。我们现在准备通过在每一帧的开始和结束之间插入一些图像过滤代码(第三章,过滤图像)来扩展应用程序。可选地,我们也准备集成其他摄像头驱动程序或应用程序框架(附录 A,与 Pygame 集成,使用 Python 的 OpenCV 计算机视觉),除了 OpenCV 支持的那些。
我们现在也拥有了处理图像和理解通过 NumPy 数组进行图像操作原理的知识。这为理解下一个主题,过滤图像,奠定了完美的基础。
第三章:使用 OpenCV 3 处理图像
在处理图像的过程中,迟早你会发现自己需要修改图像:无论是应用艺术滤镜、扩展某些部分、剪切、粘贴,还是你心中能想到的其他任何操作。本章将介绍一些修改图像的技术,到本章结束时,你应该能够执行诸如在图像中检测肤色、锐化图像、标记主题轮廓以及使用线段检测器检测人行横道等任务。
在不同色彩空间之间转换
OpenCV 中实际上有数百种方法与色彩空间的转换相关。一般来说,在现代计算机视觉中,三种色彩空间最为常见:灰度、BGR 和色调、饱和度、明度(HSV)。
-
灰度是一种能够有效消除颜色信息,转换为灰度色调的色彩空间:这种色彩空间在中间处理中极为有用,例如人脸检测。
-
BGR 是蓝绿红色彩空间,其中每个像素是一个包含三个元素的数组,每个值代表蓝色、绿色和红色:网络开发者对颜色的类似定义应该很熟悉,只是颜色的顺序是 RGB。
-
在 HSV 色彩空间中,色调代表一种颜色调,饱和度是颜色的强度,而明度则表示其暗度(或光谱另一端的亮度)。
关于 BGR 的简要说明
当我最初开始处理 BGR 色彩空间时,发现有些事情并不符合预期:[0 255 255]的值(没有蓝色,全绿色和全红色)产生了黄色。如果你有艺术背景,你甚至不需要拿起画笔和画布就能看到绿色和红色混合成一种泥泞的棕色。这是因为计算机中使用的颜色模型被称为加色模型,并且与光有关。光的行为与颜料(遵循减色颜色模型)不同,并且——由于软件在以显示器为媒介的计算机上运行,而显示器会发出光——参考的颜色模型是加色模型。
傅里叶变换
在 OpenCV 中对图像和视频应用的大部分处理都涉及到以某种形式的概念——傅里叶变换。约瑟夫·傅里叶是一位 18 世纪的法国数学家,他发现了许多数学概念并使之流行,他的工作主要集中在研究热量和数学中的波形规律。特别是,他观察到所有波形都是不同频率的简单正弦波的叠加。
换句话说,你周围观察到的所有波形都是其他波形的总和。这个概念在处理图像时非常有用,因为它允许我们识别图像中信号(如图像像素)变化很大的区域,以及变化不那么剧烈的区域。然后我们可以任意标记这些区域作为噪声或感兴趣的区域,背景或前景等。这些就是构成原始图像的频率,我们有能力将它们分离,以便理解图像并推断有趣的数据。
注意
在 OpenCV 的上下文中,实现了一些算法,使我们能够处理图像并理解其中的数据,这些算法也在 NumPy 中重新实现,使我们的工作更加容易。NumPy 有一个快速傅里叶变换(FFT)包,其中包含fft2()方法。此方法允许我们计算图像的离散傅里叶变换(DFT)。
让我们使用傅里叶变换来考察图像的幅度谱概念。图像的幅度谱是另一个图像,它以变化的形式表示原始图像:可以想象成将图像中的所有最亮的像素拖到中心。然后,你逐渐向外工作,直到所有最暗的像素都被推到边缘。立即,你将能够看到图像中包含了多少亮暗像素以及它们分布的百分比。
傅里叶变换的概念是许多用于常见图像处理操作(如边缘检测或线形和形状检测)的算法的基础。
在详细探讨这些内容之前,让我们先看看两个概念,这两个概念与傅里叶变换结合,构成了上述处理操作的基础:高通滤波器和低通滤波器。
高通滤波器
高通滤波器(HPF)是一种检查图像某个区域的滤波器,根据与周围像素强度的差异来增强某些像素的强度。
以以下核为例:
[[0, -0.25, 0],
[-0.25, 1, -0.25],
[0, -0.25, 0]]
注意
核是一组应用于源图像某个区域的权重,用于生成目标图像的单个像素。例如,ksize为7意味着在生成每个目标像素时考虑了49 (7 x 7)个源像素。我们可以将核想象成一块磨砂玻璃在源图像上移动,并让源的光线通过扩散混合。
在计算中心像素与所有直接邻居像素强度差异之和后,如果发现强度变化很大,则中心像素的强度将被增强(或不会增强)。换句话说,如果一个像素与周围像素不同,它将被增强。
这在边缘检测中特别有效,其中使用了一种称为高增强滤波器的高通滤波器。
高通滤波器和低通滤波器都使用一个名为 radius 的属性,它扩展了参与滤波器计算的邻居区域。
让我们通过一个 HPF 的例子来了解:
import cv2
import numpy as np
from scipy import ndimage
kernel_3x3 = np.array([[-1, -1, -1],
[-1, 8, -1],
[-1, -1, -1]])
kernel_5x5 = np.array([[-1, -1, -1, -1, -1],
[-1, 1, 2, 1, -1],
[-1, 2, 4, 2, -1],
[-1, 1, 2, 1, -1],
[-1, -1, -1, -1, -1]])
注意
注意,这两个滤波器的总和为 0,其原因在 边缘检测 部分有详细解释。
img = cv2.imread("../images/color1_small.jpg", 0)
k3 = ndimage.convolve(img, kernel_3x3)
k5 = ndimage.convolve(img, kernel_5x5)
blurred = cv2.GaussianBlur(img, (11,11), 0)
g_hpf = img - blurred
cv2.imshow("3x3", k3)
cv2.imshow("5x5", k5)
cv2.imshow("g_hpf", g_hpf)
cv2.waitKey()
cv2.destroyAllWindows()
在初始导入之后,我们定义了一个 3x3 的核和一个 5x5 的核,然后以灰度形式加载图像。通常,大多数图像处理都是使用 NumPy 完成的;然而,在这个特定的情况下,我们想要“卷积”一个图像和一个给定的核,而 NumPy 只接受一维数组。
这并不意味着不能使用 NumPy 实现深度数组的卷积,只是这可能会稍微复杂一些。相反,ndimage(它是 SciPy 的一部分,因此您应按照 第一章 中 设置 OpenCV 的说明进行安装),通过其 convolve() 函数使这变得简单,该函数支持 cv2 模块使用的经典 NumPy 数组来存储图像。
我们使用我们定义的两个卷积核应用两个 HPF。最后,我们还实现了一种通过应用低通滤波器并计算与原始图像的差异来获取 HPF 的微分方法。您将注意到第三种方法实际上效果最好,所以让我们也详细说明低通滤波器。
低通滤波器
如果一个 HPF 增强了像素的强度,考虑到它与邻居的差异,那么一个 低通滤波器(LPF)如果与周围像素的差异低于某个阈值,将会平滑该像素。这在去噪和模糊中得到了应用。例如,最流行的模糊/平滑滤波器之一,高斯模糊,就是一个低通滤波器,它衰减高频信号的强度。
创建模块
就像我们的 CaptureManager 和 WindowManager 类一样,我们的过滤器应该在 Cameo 之外可重用。因此,我们应该将过滤器分离到它们自己的 Python 模块或文件中。
让我们在 cameo.py 同一目录下创建一个名为 filters.py 的文件。在 filters.py 中,我们需要以下 import 语句:
import cv2
import numpy
import utils
让我们在同一目录下也创建一个名为 utils.py 的文件。它应该包含以下 import 语句:
import cv2
import numpy
import scipy.interpolate
我们将在 filters.py 中添加滤波函数和类,而更通用的数学函数将放在 utils.py 中。
边缘检测
边缘在人类和计算机视觉中都起着重要作用。作为人类,我们只需通过看到背光轮廓或粗糙草图,就能轻松识别许多物体类型及其姿态。确实,当艺术强调边缘和姿态时,它往往传达出原型的概念,比如罗丹的《思想者》或乔·舒斯特的《超人》。软件也可以对边缘、姿态和原型进行推理。我们将在后面的章节中讨论这些推理。
OpenCV 提供了许多边缘检测过滤器,包括Laplacian()、Sobel()和Scharr()。这些过滤器本应将非边缘区域变为黑色,而将边缘区域变为白色或饱和颜色。然而,它们容易将噪声误识别为边缘。这种缺陷可以通过在尝试寻找边缘之前对图像进行模糊来减轻。OpenCV 还提供了许多模糊过滤器,包括blur()(简单平均)、medianBlur()和GaussianBlur()。边缘检测和模糊过滤器的参数各不相同,但总是包括ksize,这是一个表示过滤器核宽度和高度的奇数整数。
对于模糊,让我们使用medianBlur(),它在去除数字视频噪声方面非常有效,尤其是在彩色图像中。对于边缘检测,让我们使用Laplacian(),它在灰度图像中产生粗壮的边缘线。在应用Laplacian()之前,但在应用medianBlur()之后,我们应该将图像从 BGR 转换为灰度。
一旦我们得到了Laplacian()的结果,我们可以将其反转以在白色背景上得到黑色边缘。然后,我们可以将其归一化(使其值范围从 0 到 1)并与源图像相乘以加深边缘。让我们在filters.py中实现这种方法:
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_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。在我的网络摄像头中,我发现blurKsize值为7和edgeKsize值为5看起来最佳。不幸的是,当ksize较大,如7时,medianBlur()会变得很昂贵。
小贴士
如果你在运行strokeEdges()时遇到性能问题,尝试减小blurKsize的值。要关闭模糊,将其设置为小于3的值。
自定义核 – 变得复杂
正如我们刚才看到的,OpenCV 的许多预定义过滤器使用核。记住,核是一组权重,这些权重决定了每个输出像素是如何从输入像素的邻域中计算出来的。核的另一个术语是卷积矩阵。它在某个区域内混合或卷积像素。同样,基于核的过滤器可能被称为卷积过滤器。
OpenCV 提供了一个非常通用的filter2D()函数,它可以应用我们指定的任何核或卷积矩阵。为了了解如何使用此函数,让我们首先学习卷积矩阵的格式。它是一个行和列都是奇数的二维数组。中心元素对应于一个感兴趣的像素,其他元素对应于该像素的邻居。每个元素包含一个整数或浮点值,这是一个应用于输入像素值的权重。考虑以下示例:
kernel = numpy.array([[-1, -1, -1],
[-1, 9, -1],
[-1, -1, -1]])
在这里,感兴趣像素的权重为9,其直接邻居的权重各为-1。对于感兴趣像素,输出颜色将是其输入颜色的九倍减去所有八个相邻像素的输入颜色。如果感兴趣像素与其邻居已经有点不同,这种差异会变得更加明显。结果是,当邻居之间的对比度增加时,图像看起来会更锐利。
继续我们的示例,我们可以将这个卷积矩阵分别应用于源图像和目标图像,如下所示:
cv2.filter2D(src, -1, kernel, dst)
第二个参数指定了目标图像的每个通道的深度(例如,对于每个通道 8 位,使用cv2.CV_8U)。一个负值(如这里所示)表示目标图像具有与源图像相同的深度。
注意
对于彩色图像,请注意filter2D()对每个通道应用内核是相同的。要使用不同通道的不同内核,我们还需要使用split()和merge()函数。
基于这个简单的示例,让我们向filters.py中添加两个类。一个类,VConvolutionFilter,将代表一般的卷积滤镜。一个子类,SharpenFilter,将代表我们的锐化滤镜。让我们编辑filters.py以实现这两个新类,如下所示:
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)
注意,权重之和为1。这应该在我们想要保持图像整体亮度不变时始终成立。如果我们稍微修改锐化核,使其权重之和为0,那么我们就有了一个将边缘变为白色、非边缘变为黑色的边缘检测核。例如,让我们将以下边缘检测滤镜添加到filters.py中:
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,并且在整个邻域内应为正值。例如,我们可以简单地取邻域的平均值,如下所示:
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中的实现示例:
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并添加以下摘录中加粗的行:
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
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 的颜色:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-ocv3-cv-py/img/image00194.jpeg
使用 Canny 进行边缘检测
OpenCV 还提供了一个非常方便的函数,称为 Canny(以算法的发明者 John F. Canny 命名),它不仅因其有效性而广受欢迎,而且因其实现简单而广受欢迎,因为它在 OpenCV 程序中是一行代码:
import cv2
import numpy as np
img = cv2.imread("../images/statue_small.jpg", 0)
cv2.imwrite("canny.jpg", cv2.Canny(img, 200, 300))
cv2.imshow("canny", cv2.imread("canny.jpg"))
cv2.waitKey()
cv2.destroyAllWindows()
结果是边缘的非常清晰的识别:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-ocv3-cv-py/img/image00195.jpeg
Canny 边缘检测算法相当复杂但也很有趣:它是一个五步过程,使用高斯滤波器对图像进行降噪,计算梯度,对边缘应用非最大 抑制(NMS),对所有检测到的边缘进行双重阈值以消除假阳性,最后分析所有边缘及其相互之间的连接,以保留真实边缘并丢弃较弱的边缘。
轮廓检测
计算机视觉中的另一个重要任务是轮廓检测,这不仅因为检测图像或视频帧中包含的主题轮廓的明显方面,还因为与识别轮廓相关的导数操作。
这些操作包括计算边界多边形、近似形状以及通常计算感兴趣区域,这大大简化了与图像数据的交互,因为使用 NumPy 可以轻松地用数组切片定义矩形区域。在探索对象检测(包括人脸)和对象跟踪的概念时,我们将大量使用这项技术。
让我们按顺序进行,首先通过一个示例熟悉 API:
import cv2
import numpy as np
img = np.zeros((200, 200), dtype=np.uint8)
img[50:150, 50:150] = 255
ret, thresh = cv2.threshold(img, 127, 255, 0)
image, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
color = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
img = cv2.drawContours(color, contours, -1, (0,255,0), 2)
cv2.imshow("contours", color)
cv2.waitKey()
cv2.destroyAllWindows()
首先,我们创建一个 200x200 像素大小的空黑色图像。然后,我们利用 ndarray 在切片上赋值的能力,在图像的中心放置一个白色方块。
我们然后对图像进行阈值处理,并调用findContours()函数。此函数有三个参数:输入图像、层次结构类型和轮廓近似方法。此函数中有几个特别有趣的方面:
-
函数会修改输入图像,因此建议使用原始图像的副本(例如,通过传递
img.copy())。 -
其次,函数返回的层次结构树非常重要:
cv2.RETR_TREE将检索图像中轮廓的整个层次结构,使您能够建立轮廓之间的“关系”。如果您只想检索最外层的轮廓,请使用cv2.RETR_EXTERNAL。这在您想要消除完全包含在其他轮廓中的轮廓时特别有用(例如,在大多数情况下,您不需要检测另一个相同类型的对象内的对象)。
findContours函数返回三个元素:修改后的图像、轮廓及其层次结构。我们使用轮廓在图像的颜色版本上绘制(这样我们可以在绿色上绘制轮廓),并最终显示它。
结果是一个用绿色绘制轮廓的白色正方形。虽然简单,但有效地展示了概念!让我们继续看更有意义的例子。
轮廓 – 包围盒、最小面积矩形和最小包围圆
找到正方形的轮廓是一个简单的任务;不规则、倾斜和旋转的形状最能发挥 OpenCV 的cv2.findContours实用函数的作用。让我们看看下面的图像:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-ocv3-cv-py/img/image00196.jpeg
在实际应用中,我们最感兴趣的是确定主题的包围盒、其最小包围矩形和其圆。结合cv2.findContours函数和一些其他 OpenCV 实用工具,这使得实现这一点变得非常容易:
import cv2
import numpy as np
img = cv2.pyrDown(cv2.imread("hammer.jpg", cv2.IMREAD_UNCHANGED))
ret, thresh = cv2.threshold(cv2.cvtColor(img.copy(), cv2.COLOR_BGR2GRAY) , 127, 255, cv2.THRESH_BINARY)
image, contours, hier = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for c in contours:
# find bounding box coordinates
x,y,w,h = cv2.boundingRect(c)
cv2.rectangle(img, (x,y), (x+w, y+h), (0, 255, 0), 2)
# find minimum area
rect = cv2.minAreaRect(c)
# calculate coordinates of the minimum area rectangle
box = cv2.boxPoints(rect)
# normalize coordinates to integers
box = np.int0(box)
# draw contours
cv2.drawContours(img, [box], 0, (0,0, 255), 3)
# calculate center and radius of minimum enclosing circle
(x,y),radius = cv2.minEnclosingCircle(c)
# cast to integers
center = (int(x),int(y))
radius = int(radius)
# draw the circle
img = cv2.circle(img,center,radius,(0,255,0),2)
cv2.drawContours(img, contours, -1, (255, 0, 0), 1)
cv2.imshow("contours", img)
在初始导入之后,我们加载图像,然后在原始图像的灰度版本上应用二值阈值。通过这样做,我们在灰度副本上进行所有查找轮廓的计算,但我们绘制在原始图像上,以便利用颜色信息。
首先,让我们计算一个简单的包围盒:
x,y,w,h = cv2.boundingRect(c)
这是一个相当直接的轮廓信息转换为(x, y)坐标的转换,加上矩形的宽度和高度。绘制这个矩形是一个简单的任务,可以使用以下代码完成:
cv2.rectangle(img, (x,y), (x+w, y+h), (0, 255, 0), 2)
其次,让我们计算包围主题的最小面积:
rect = cv2.minAreaRect(c)
box = cv2.boxPoints(rect)
box = np.int0(box)
这里使用的机制特别有趣:OpenCV 没有直接从轮廓信息计算最小矩形顶点坐标的功能。相反,我们计算最小矩形面积,然后计算这个矩形的顶点。请注意,计算出的顶点是浮点数,但像素是用整数访问的(您不能访问像素的一部分),因此我们需要进行这种转换。接下来,我们绘制这个盒子,这为我们提供了一个介绍cv2.drawContours函数的绝佳机会:
cv2.drawContours(img, [box], 0, (0,0, 255), 3)
首先,这个函数——就像所有绘图函数一样——会修改原始图像。其次,它在其第二个参数中接受一个轮廓数组,因此您可以在一次操作中绘制多个轮廓。因此,如果您有一组代表轮廓多边形的点,您需要将这些点包装成一个数组,就像我们在前面的例子中处理我们的盒子一样。这个函数的第三个参数指定了我们要绘制的轮廓数组的索引:值为-1将绘制所有轮廓;否则,将绘制轮廓数组(第二个参数)中指定索引处的轮廓。
大多数绘图函数将绘图颜色和厚度作为最后两个参数。
我们将要检查的最后一个边界轮廓是最小包围圆:
(x,y),radius = cv2.minEnclosingCircle(c)
center = (int(x),int(y))
radius = int(radius)
img = cv2.circle(img,center,radius,(0,255,0),2)
cv2.minEnclosingCircle 函数的唯一特殊性在于它返回一个包含两个元素的元组,其中第一个元素本身也是一个元组,表示圆心的坐标,第二个元素是这个圆的半径。在将这些值转换为整数后,画圆就变得相当简单了。
最终在原始图像上的结果看起来像这样:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-ocv3-cv-py/img/image00197.jpeg
等高线 – 凸等高线和 Douglas-Peucker 算法
大多数时候,当处理等高线时,主题将具有最多样化的形状,包括凸形。凸形是指在这个形状内部有两个点,它们之间的连线会超出形状本身的轮廓。
OpenCV 提供的第一个用于计算形状近似边界多边形的工具是 cv2.approxPolyDP。这个函数有三个参数:
-
等高线
-
一个 epsilon 值,表示原始等高线和近似多边形之间的最大差异(值越低,近似值越接近原始等高线)
-
一个布尔标志,表示多边形是封闭的
epsilon 值对于获得有用的等高线至关重要,因此让我们了解它代表什么。epsilon 是近似多边形周长和原始等高线周长之间的最大差异。这个差异越低,近似多边形就越接近原始等高线。
当我们已经有了一个精确表示的等高线时,你可能想知道为什么我们还需要一个近似的 polygon。答案是,多边形是一系列直线,能够在区域内定义多边形以便进一步的操作和处理,这在许多计算机视觉任务中至关重要。
现在我们已经知道了 epsilon 是什么,我们需要获取等高线周长信息作为参考值。这可以通过 OpenCV 的 cv2.arcLength 函数获得:
epsilon = 0.01 * cv2.arcLength(cnt, True)
approx = cv2.approxPolyDP(cnt, epsilon, True)
实际上,我们是在指示 OpenCV 计算一个近似的多边形,其周长只能在一个 epsilon 比率内与原始等高线不同。
OpenCV 还提供了一个 cv2.convexHull 函数来获取凸形状的处理后的等高线信息,这是一个简单的单行表达式:
hull = cv2.convexHull(cnt)
让我们将原始等高线、近似的多边形等高线和凸包结合在一个图像中,以观察它们之间的差异。为了简化问题,我已经将等高线应用到一张黑底图像上,这样原始主题不可见,但其轮廓是可见的:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-ocv3-cv-py/img/image00198.jpeg
如您所见,凸包包围了整个主题,近似多边形是最内层的多边形形状,两者之间是原始等高线,主要由弧线组成。
线和圆检测
检测边缘和轮廓不仅是常见且重要的任务,它们还构成了其他复杂操作的基础。线和形状检测与边缘和轮廓检测是相辅相成的,因此让我们看看 OpenCV 如何实现这些。
线和形状检测背后的理论基于一种称为 Hough 变换的技术,由 Richard Duda 和 Peter Hart 发明,他们扩展(推广)了 Paul Hough 在 20 世纪 60 年代初的工作。
让我们看看 OpenCV 的 Hough 变换 API。
线检测
首先,让我们使用 HoughLines 和 HoughLinesP 函数检测一些线,这是通过 HoughLines 函数调用来完成的。这两个函数之间的唯一区别是,一个使用标准 Hough 变换,另一个使用概率 Hough 变换(因此名称中的 P)。
概率版本之所以被称为概率版本,是因为它只分析点的一个子集,并估计这些点全部属于同一条线的概率。这个实现是标准 Hough 变换的优化版本,在这种情况下,它计算量更小,执行速度更快。
让我们来看一个非常简单的例子:
import cv2
import numpy as np
img = cv2.imread('lines.jpg')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray,50,120)
minLineLength = 20
maxLineGap = 5
lines = cv2.HoughLinesP(edges,1,np.pi/180,100,minLineLength,maxLineGap)
for x1,y1,x2,y2 in lines[0]:
cv2.line(img,(x1,y1),(x2,y2),(0,255,0),2)
cv2.imshow("edges", edges)
cv2.imshow("lines", img)
cv2.waitKey()
cv2.destroyAllWindows()
这个简单脚本的关键点——除了 HoughLines 函数调用之外——是设置最小线长度(较短的线将被丢弃)和最大线间隙,这是在两条线段开始被视为单独的线之前,线中最大间隙的大小。
还要注意,HoughLines 函数需要一个单通道二值图像,该图像通过 Canny 边缘检测滤波器处理。Canny 不是严格的要求;一个去噪后只代表边缘的图像,是 Hough 变换的理想来源,因此你会发现这是一种常见的做法。
HoughLinesP 的参数如下:
-
我们想要处理的图像。
-
线的几何表示,
rho和theta,通常为1和np.pi/180。 -
阈值,表示低于此阈值的线将被丢弃。Hough 变换使用一个由桶和投票组成的系统,每个桶代表一条线,因此任何获得
<阈值>投票的线将被保留,其余的将被丢弃。 -
我们之前提到的
MinLineLength和MaxLineGap。
圆检测
OpenCV 还有一个用于检测圆的功能,称为 HoughCircles。它的工作方式与 HoughLines 非常相似,但 minLineLength 和 maxLineGap 是用于丢弃或保留线的参数,而 HoughCircles 有圆心之间的最小距离、最小和最大半径等参数。下面是一个必看的示例:
import cv2
import numpy as np
planets = cv2.imread('planet_glow.jpg')
gray_img = cv2.cvtColor(planets, cv2.COLOR_BGR2GRAY)
img = cv2.medianBlur(gray_img, 5)
cimg = cv2.cvtColor(img,cv2.COLOR_GRAY2BGR)
circles = cv2.HoughCircles(img,cv2.HOUGH_GRADIENT,1,120,
param1=100,param2=30,minRadius=0,maxRadius=0)
circles = np.uint16(np.around(circles))
for i in circles[0,:]:
# draw the outer circle
cv2.circle(planets,(i[0],i[1]),i[2],(0,255,0),2)
# draw the center of the circle
cv2.circle(planets,(i[0],i[1]),2,(0,0,255),3)
cv2.imwrite("planets_circles.jpg", planets)
cv2.imshow("HoughCirlces", planets)
cv2.waitKey()
cv2.destroyAllWindows()
下面是结果的可视表示:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-ocv3-cv-py/img/image00199.jpeg
检测形状
使用霍夫变换检测形状仅限于圆形;然而,当我们讨论approxPolyDP时,我们已经隐式地探索了检测任何形状的方法。这个函数允许对多边形进行近似,所以如果你的图像包含多边形,它们将被相当准确地检测到,这结合了cv2.findContours和cv2.approxPolyDP的使用。
摘要
到目前为止,你应该已经对颜色空间、傅里叶变换以及 OpenCV 提供的用于处理图像的几种类型的过滤器有了很好的理解。
你还应该熟练地检测边缘、线条、圆形以及一般形状。此外,你应该能够找到轮廓并利用它们提供关于图像中包含的主题的信息。这些概念将作为探索下一章主题的理想背景。
第四章:深度估计与分割
本章向您展示如何使用深度相机的数据来识别前景和背景区域,以便我们可以将效果限制在仅前景或仅背景。作为先决条件,我们需要一个深度相机,例如微软 Kinect,并且我们需要构建支持我们深度相机的 OpenCV。有关构建说明,请参阅第一章,设置 OpenCV。
本章我们将处理两个主要主题:深度估计和分割。我们将通过两种不同的方法来探索深度估计:首先,使用深度相机(本章第一部分的先决条件),例如微软 Kinect;然后,使用立体图像,对于普通相机就足够了。有关如何构建支持深度相机的 OpenCV 的说明,请参阅第一章,设置 OpenCV。本章的第二部分是关于分割,这是一种允许我们从图像中提取前景对象的技术。
创建模块
捕获和处理深度相机数据的代码将在Cameo.py外部可重用。因此,我们应该将其分离到一个新的模块中。让我们在Cameo.py相同的目录下创建一个名为depth.py的文件。在depth.py中,我们需要以下import语句:
import numpy
我们还需要修改现有的rects.py文件,以便我们的复制操作可以限制在矩形的非矩形子区域内。为了支持我们将要进行的更改,让我们向rects.py添加以下import语句:
import numpy
import utils
最后,我们应用的新版本将使用与深度相关的功能。因此,让我们向Cameo.py添加以下import语句:
import depth
现在,让我们更深入地探讨深度主题。
从深度相机捕获帧
在第二章,处理文件、相机和 GUI中,我们讨论了计算机可以拥有多个视频捕获设备,并且每个设备可以有多个通道的概念。假设一个给定的设备是立体相机。每个通道可能对应不同的镜头和传感器。此外,每个通道可能对应不同类型的数据,例如正常彩色图像与深度图。OpenCV 的 C++版本定义了一些用于某些设备和通道标识符的常量。然而,这些常量在 Python 版本中并未定义。
为了解决这个问题,让我们在depth.py中添加以下定义:
# Devices.CAP_OPENNI = 900 # OpenNI (for Microsoft Kinect)CAP_OPENNI_ASUS = 910 # OpenNI (for Asus Xtion)
# Channels of an OpenNI-compatible depth generator.CAP_OPENNI_DEPTH_MAP = 0 # Depth values in mm (16UC1)CAP_OPENNI_POINT_CLOUD_MAP = 1 # XYZ in meters (32FC3)CAP_OPENNI_DISPARITY_MAP = 2 # Disparity in pixels (8UC1)CAP_OPENNI_DISPARITY_MAP_32F = 3 # Disparity in pixels (32FC1)CAP_OPENNI_VALID_DEPTH_MASK = 4 # 8UC1
# Channels of an OpenNI-compatible RGB image generator.CAP_OPENNI_BGR_IMAGE = 5CAP_OPENNI_GRAY_IMAGE = 6
深度相关通道需要一些解释,如下所示列表中所述:
-
深度图是一种灰度图像,其中每个像素值代表从相机到表面的估计距离。具体来说,来自
CAP_OPENNI_DEPTH_MAP通道的图像将距离表示为毫米的浮点数。 -
点云图是一个彩色图像,其中每个颜色对应于一个(x,y 或 z)空间维度。具体来说,
CAP_OPENNI_POINT_CLOUD_MAP通道产生一个 BGR 图像,其中 B 是 x(蓝色是右侧),G 是 y(绿色是上方),R 是 z(红色是深度),从相机的视角来看。这些值以米为单位。 -
视差图是一个灰度图像,其中每个像素值是表面的立体视差。为了概念化立体视差,让我们假设我们叠加了从不同视角拍摄的同一场景的两个图像。结果将类似于看到双重图像。对于场景中任何一对孪生物体上的点,我们可以测量像素距离。这种测量是立体视差。靠近的物体表现出比远处的物体更大的立体视差。因此,靠近的物体在视差图中看起来更亮。
-
一个有效的深度掩码显示给定像素处的深度信息是否被认为是有效的(通过非零值表示)或无效的(通过零值表示)。例如,如果深度相机依赖于红外照明器(红外闪光灯),那么从该光源被遮挡(阴影)的区域中的深度信息是无效的。
以下截图显示了一个坐在猫雕塑后面的男人的点云图:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-ocv3-cv-py/img/image00200.jpeg
以下截图显示了一个坐在猫雕塑后面的男人的视差图:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-ocv3-cv-py/img/image00201.jpeg
以下截图显示了坐在猫雕塑后面的男人的有效深度掩码:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-ocv3-cv-py/img/image00202.jpeg
从视差图创建掩码
对于 Cameo 的目的,我们感兴趣的是视差图和有效深度掩码。它们可以帮助我们细化我们对面部区域的估计。
使用 FaceTracker 函数和正常彩色图像,我们可以获得面部区域的矩形估计。通过分析相应的视差图中的这样一个矩形区域,我们可以知道矩形内的某些像素是异常值——太近或太远,实际上不可能是面部的一部分。我们可以细化面部区域以排除这些异常值。然而,我们只应在数据有效的地方应用此测试,正如有效深度掩码所示。
让我们编写一个函数来生成一个掩码,其值对于被拒绝的面部矩形区域为 0,对于接受的区域为 1。这个函数应该接受视差图、有效深度掩码和一个矩形作为参数。我们可以在 depth.py 中实现它如下:
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() 接收一个数组,其元素被评估为真或假。返回一个具有相同维度的输出数组。在输入数组中的任何元素为 true 时,where() 函数的第二参数被分配给输出数组中的相应元素。相反,在输入数组中的任何元素为 false 时,where() 函数的第三参数被分配给输出数组中的相应元素。
我们的实现将具有有效视差值且与中值视差值偏差 12 或更多的像素视为异常值。我仅通过实验选择了 12 这个值。请根据您使用特定相机设置运行 Cameo 时遇到的结果自由调整此值。
对复制操作进行掩码处理
作为前一章工作的部分,我们将 copyRect() 编写为一个复制操作,该操作限制自己仅限于源和目标图像的给定矩形。现在,我们想要进一步限制这个复制操作。我们想要使用一个与源矩形具有相同尺寸的给定掩码。
我们将只复制源矩形中掩码值为非零的像素。其他像素将保留其从目标图像中的旧值。这种逻辑,使用条件数组以及两个可能的输出值数组,可以用我们最近学习的 numpy.where() 函数简洁地表达。
让我们打开 rects.py 并编辑 copyRect() 以添加一个新的掩码参数。这个参数可能是 None,在这种情况下,我们将回退到我们旧的复制操作实现。否则,我们接下来确保掩码和图像具有相同数量的通道。我们假设掩码有一个通道,但图像可能有三个通道(BGR)。我们可以使用 numpy.array 的 repeat() 和 reshape() 方法向掩码添加重复的通道。
最后,我们使用 where() 执行复制操作。完整的实现如下:
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() 调用中。如果给定的 masks 参数值为 None,我们将 None 传递给每个 copyRect() 调用。
以下代码展示了这一实现的完整内容:
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() 中的 masks 参数默认为 None。因此,我们这些函数的新版本与我们的 Cameo 旧版本向后兼容。
使用普通相机进行深度估计
深度相机是一种捕捉图像并估计物体与相机之间距离的神奇小设备,但是,深度相机是如何检索深度信息的?此外,是否可以使用普通相机重现同样的计算?
深度相机,如 Microsoft Kinect,使用一个传统的相机结合一个红外传感器,这有助于相机区分相似的对象并计算它们与相机的距离。然而,并不是每个人都能接触到深度相机或 Kinect,尤其是在你刚开始学习 OpenCV 时,你可能不会投资昂贵的设备,直到你觉得自己技能已经磨炼得很好,你对这个主题的兴趣也得到了确认。
我们的设置包括一个简单的相机,这很可能是集成在我们的机器中,或者是一个连接到我们电脑的摄像头。因此,我们需要求助于不那么花哨的方法来估计物体与相机之间的距离差异。
在这种情况下,几何学将提供帮助,特别是极线几何,它是立体视觉的几何学。立体视觉是计算机视觉的一个分支,它从同一主题的两个不同图像中提取三维信息。
极线几何是如何工作的呢?从概念上讲,它从相机向图像中的每个对象绘制想象中的线条,然后在第二张图像上做同样的事情,并基于对应对象的线条交点来计算物体的距离。以下是这个概念的一个表示:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-ocv3-cv-py/img/image00203.jpeg
让我们看看 OpenCV 是如何应用极线几何来计算所谓的视差图,这基本上是图像中检测到的不同深度的表示。这将使我们能够提取图片的前景并丢弃其余部分。
首先,我们需要从不同的视角拍摄同一主题的两个图像,但要注意,图片是从与物体等距离的位置拍摄的,否则计算将失败,视差图将没有意义。
那么,让我们继续一个例子:
import numpy as np
import cv2
def update(val = 0):
# disparity range is tuned for 'aloe' image pair
stereo.setBlockSize(cv2.getTrackbarPos('window_size', 'disparity'))
stereo.setUniquenessRatio(cv2.getTrackbarPos('uniquenessRatio', 'disparity'))
stereo.setSpeckleWindowSize(cv2.getTrackbarPos('speckleWindowSize', 'disparity'))
stereo.setSpeckleRange(cv2.getTrackbarPos('speckleRange', 'disparity'))
stereo.setDisp12MaxDiff(cv2.getTrackbarPos('disp12MaxDiff', 'disparity'))
print 'computing disparity...'
disp = stereo.compute(imgL, imgR).astype(np.float32) / 16.0
cv2.imshow('left', imgL)
cv2.imshow('disparity', (disp-min_disp)/num_disp)
if __name__ == "__main__":
window_size = 5
min_disp = 16
num_disp = 192-min_disp
blockSize = window_size
uniquenessRatio = 1
speckleRange = 3
speckleWindowSize = 3
disp12MaxDiff = 200
P1 = 600
P2 = 2400
imgL = cv2.imread('images/color1_small.jpg')
imgR = cv2.imread('images/color2_small.jpg')
cv2.namedWindow('disparity')
cv2.createTrackbar('speckleRange', 'disparity', speckleRange, 50, update)
cv2.createTrackbar('window_size', 'disparity', window_size, 21, update)
cv2.createTrackbar('speckleWindowSize', 'disparity', speckleWindowSize, 200, update)
cv2.createTrackbar('uniquenessRatio', 'disparity', uniquenessRatio, 50, update)
cv2.createTrackbar('disp12MaxDiff', 'disparity', disp12MaxDiff, 250, update)
stereo = cv2.StereoSGBM_create(
minDisparity = min_disp,
numDisparities = num_disp,
blockSize = window_size,
uniquenessRatio = uniquenessRatio,
speckleRange = speckleRange,
speckleWindowSize = speckleWindowSize,
disp12MaxDiff = disp12MaxDiff,
P1 = P1,
P2 = P2
)
update()
cv2.waitKey()
在这个例子中,我们取同一主题的两个图像,并计算一个视差图,用较亮的颜色显示地图中靠近相机的点。用黑色标记的区域代表视差。
首先,我们像往常一样导入numpy和cv2。
让我们先暂时跳过update函数的定义,看看主要代码;这个过程相当简单:加载两个图像,创建一个StereoSGBM实例(StereoSGBM代表半全局块匹配,这是一种用于计算视差图的算法),并创建一些滑块来调整算法的参数,然后调用update函数。
update函数将滑块值应用于StereoSGBM实例,然后调用compute方法,该方法生成视差图。总的来说,相当简单!以下是第一个我使用的图像:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-ocv3-cv-py/img/image00204.jpeg
这是第二个:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-ocv3-cv-py/img/image00205.jpeg
你看:这是一个既好又容易解释的视差图。
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-ocv3-cv-py/img/image00206.jpeg
StereoSGBM使用的参数如下(摘自 OpenCV 文档):
| 参数 | 描述 |
|---|---|
minDisparity | 此参数表示可能的最小视差值。通常为零,但有时校正算法可以移动图像,因此需要相应地调整此参数。 |
numDisparities | 此参数表示最大视差减去最小视差。结果值始终大于零。在当前实现中,此参数必须是 16 的倍数。 |
windowSize | 此参数表示匹配块的大小。它必须是一个大于或等于 1 的奇数。通常,它应该在 3-11 的范围内。 |
P1 | 此参数表示控制视差平滑度的第一个参数。参见下一点。 |
P2 | 此参数表示控制视差平滑度的第二个参数。值越大,视差越平滑。P1是相邻像素之间视差变化加减 1 的惩罚。P2是相邻像素之间视差变化超过 1 的惩罚。算法要求P2 > P1。请参见stereo_match.cpp示例,其中显示了某些合理的P1和P2值(例如8*number_of_image_channels*windowSize*windowSize和32*number_of_image_channels*windowSize*windowSize,分别)。 |
disp12MaxDiff | 此参数表示左右视差检查允许的最大差异(以整数像素为单位)。将其设置为非正值以禁用检查。 |
preFilterCap | 此参数表示预滤波图像像素的截断值。算法首先计算每个像素的 x 导数,并通过[-preFilterCap, preFilterCap]区间剪辑其值。结果值传递给 Birchfield-Tomasi 像素成本函数。 |
uniquenessRatio | 此参数表示最佳(最小)计算成本函数值相对于第二最佳值应“获胜”的百分比边缘。通常,5-15 范围内的值就足够好了。 |
speckleWindowSize | 此参数表示考虑其噪声斑点和无效化的平滑视差区域的最大大小。将其设置为0以禁用斑点滤波。否则,将其设置为 50-200 范围内的某个值。 |
speckleRange | 此参数指代每个连通组件内的最大视差变化。如果你进行斑点滤波,将参数设置为正值;它将隐式地乘以 16。通常,1 或 2 就足够了。 |
使用前面的脚本,你可以加载图像并调整参数,直到你对StereoSGBM生成的视差图满意为止。
使用 Watershed 和 GrabCut 算法进行对象分割
计算视差图对于检测图像的前景非常有用,但StereoSGBM并非完成此任务的唯一算法,实际上,StereoSGBM更多的是从二维图片中收集三维信息,而不是其他。然而,GrabCut是完成此目的的完美工具。GrabCut 算法遵循一系列精确的步骤:
-
定义一个包含图片主题(s)的矩形。
-
位于矩形外部区域自动定义为背景。
-
背景中的数据用作参考,以区分用户定义矩形内的背景区域和前景区域。
-
高斯混合模型(GMM)对前景和背景进行建模,并将未定义的像素标记为可能的背景和前景。
-
图像中的每个像素通过虚拟边与周围的像素虚拟连接,每个边根据其与周围像素在颜色上的相似性获得成为前景或背景的概率。
-
每个像素(或算法中概念化的节点)连接到前景节点或背景节点,你可以想象成这样:https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-ocv3-cv-py/img/image00207.jpeg
-
在节点连接到任一终端(背景或前景,也称为源和汇)之后,属于不同终端的节点之间的边被切断(算法中著名的切割部分),这使得图像部分的分离成为可能。此图充分代表了该算法:https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-ocv3-cv-py/img/image00208.jpeg
使用 GrabCut 进行前景检测的示例
让我们来看一个例子。我们从一个美丽的天使雕像的图片开始。
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-ocv3-cv-py/img/image00209.jpeg
我们想要抓住我们的天使并丢弃背景。为此,我们将创建一个相对简短的脚本,该脚本将实例化 GrabCut,执行分离,然后将生成的图像与原始图像并排显示。我们将使用matplotlib,这是一个非常有用的 Python 库,它使得显示图表和图像变得非常简单:
import numpy as np
import cv2
from matplotlib import pyplot as plt
img = cv2.imread('images/statue_small.jpg')
mask = np.zeros(img.shape[:2],np.uint8)
bgdModel = np.zeros((1,65),np.float64)
fgdModel = np.zeros((1,65),np.float64)
rect = (100,50,421,378)
cv2.grabCut(img,mask,rect,bgdModel,fgdModel,5,cv2.GC_INIT_WITH_RECT)
mask2 = np.where((mask==2)|(mask==0),0,1).astype('uint8')
img = img*mask2[:,:,np.newaxis]
plt.subplot(121), plt.imshow(img)
plt.title("grabcut"), plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(cv2.cvtColor(cv2.imread('images/statue_small.jpg'), cv2.COLOR_BGR2RGB))
plt.title("original"), plt.xticks([]), plt.yticks([])
plt.show()
这段代码实际上非常直接。首先,我们加载我们想要处理的图像,然后我们创建一个与加载的图像形状相同的零填充掩码:
import numpy as np
import cv2
from matplotlib import pyplot as plt
img = cv2.imread('images/statue_small.jpg')
mask = np.zeros(img.shape[:2],np.uint8)
然后,我们创建零填充的前景和背景模型:
bgdModel = np.zeros((1,65),np.float64)
fgdModel = np.zeros((1,65),np.float64)
我们可以用数据填充这些模型,但我们将使用一个矩形来初始化 GrabCut 算法,以识别我们想要隔离的主题。因此,背景和前景模型将基于初始矩形之外的区域来确定。这个矩形在下一行定义:
rect = (100,50,421,378)
现在来到有趣的部分!我们运行 GrabCut 算法,指定空模型和掩码,以及我们将使用矩形来初始化操作:
cv2.grabCut(img,mask,rect,bgdModel,fgdModel,5,cv2.GC_INIT_WITH_RECT)
你也会注意到fgdModel后面有一个整数,这是算法将在图像上运行的迭代次数。你可以增加这些迭代次数,但像素分类会收敛到一个点,实际上,你只是在增加迭代次数而没有获得任何更多的改进。
之后,我们的掩码将改变,包含介于 0 和 3 之间的值。值0和2将被转换为零,1-3 将被转换为 1,并存储到mask2中,然后我们可以使用它来过滤掉所有零值像素(理论上留下所有前景像素):
mask2 = np.where((mask==2)|(mask==0),0,1).astype('uint8')
img = img*mask2[:,:,np.newaxis]
代码的最后部分显示了并排的图像,这是结果:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-ocv3-cv-py/img/image00210.jpeg
这是一个相当令人满意的结果。你会注意到天使的胳膊下留下了一块背景区域。可以通过触摸笔触来应用更多迭代;这种技术在samples/python2目录下的grabcut.py文件中有很好的说明。
使用 Watershed 算法进行图像分割
最后,我们简要了解一下 Watershed 算法。该算法被称为 Watershed,因为其概念化涉及水。想象一下图像中低密度(几乎没有变化)的区域为山谷,高密度(变化很多)的区域为山峰。开始往山谷中注水,直到两个不同山谷的水即将汇合。为了防止不同山谷的水汇合,你建立一道屏障来保持它们分离。形成的屏障就是图像分割。
作为一名意大利人,我喜欢食物,我最喜欢的东西之一就是一份美味的意面配以香蒜酱。所以,这是香蒜酱最重要的成分罗勒的图片:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-ocv3-cv-py/img/image00211.jpeg
现在,我们想要分割图像,将罗勒叶从白色背景中分离出来。
再次,我们导入numpy、cv2和matplotlib,然后导入我们的罗勒叶图像:
import numpy as np
import cv2
from matplotlib import pyplot as plt
img = cv2.imread('images/basil.jpg')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
在将颜色转换为灰度后,我们对图像进行阈值处理。这个操作有助于将图像分为黑白两部分:
ret, thresh = cv2.threshold(gray,0,255,cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)
接下来,我们通过应用morphologyEx变换来从图像中去除噪声,这是一个由膨胀和侵蚀图像以提取特征的操作:
kernel = np.ones((3,3),np.uint8)
opening = cv2.morphologyEx(thresh,cv2.MORPH_OPEN,kernel, iterations = 2)
通过膨胀morphology变换的结果,我们可以获得图像中几乎肯定是背景的区域:
sure_bg = cv2.dilate(opening,kernel,iterations=3)
相反,我们可以通过应用distanceTransform来获得确切的背景区域。在实践中,在所有最可能成为前景的区域中,一个点离背景“边界”越远,它成为前景的可能性就越高。一旦我们获得了图像的distanceTransform表示,我们就应用一个阈值,以高度数学的概率确定这些区域是否为前景:
dist_transform = cv2.distanceTransform(opening,cv2.DIST_L2,5)
ret, sure_fg = cv2.threshold(dist_transform,0.7*dist_transform.max(),255,0)
在这个阶段,我们有一些确切的背景和前景。那么,中间区域怎么办?首先,我们需要确定这些区域,这可以通过从背景中减去确切的背景来完成:
sure_fg = np.uint8(sure_fg)
unknown = cv2.subtract(sure_bg,sure_fg)
现在我们有了这些区域,我们可以构建我们著名的“障碍物”来阻止水合并。这是通过connectedComponents函数完成的。当我们分析 GrabCut 算法时,我们瞥了一眼图论,并将图像视为由边连接的节点集合。给定确切的背景区域,这些节点中的一些将连接在一起,但也有一些不会。这意味着它们属于不同的水谷,它们之间应该有一个障碍物:
ret, markers = cv2.connectedComponents(sure_fg)
现在我们将背景区域的值加 1,因为我们只想让未知区域保持在0:
markers = markers+1
markers[unknown==255] = 0
最后,我们打开大门!让水落下,我们的障碍物用红色绘制:
markers = cv2.watershed(img,markers)
img[markers == -1] = [255,0,0]
plt.imshow(img)
plt.show()
现在,让我们展示结果:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-ocv3-cv-py/img/image00212.jpeg
不言而喻,我现在很饿!
摘要
在本章中,我们学习了如何从二维输入(视频帧或图像)中收集三维信息。首先,我们考察了深度相机,然后是极线几何和立体图像,因此我们现在能够计算视差图。最后,我们探讨了两种最流行的图像分割方法:GrabCut 和 Watershed。
本章带我们进入了从图像中解释信息的世界,我们现在准备好探索 OpenCV 的另一个重要特性:特征描述符和关键点检测。
第五章. 识别人脸
在众多使计算机视觉成为一个迷人主题的原因中,计算机视觉使许多听起来非常未来主义的任务成为现实。其中一项功能就是人脸检测。OpenCV 内置了执行人脸检测的功能,这在现实世界的各种环境中几乎有无限的应用,从安全到娱乐。
本章介绍了 OpenCV 的一些人脸检测功能,以及定义特定类型可追踪对象的数据文件。具体来说,我们研究了 Haar 级联分类器,这些分类器通过分析相邻图像区域之间的对比度来确定给定的图像或子图像是否与已知类型匹配。我们考虑了如何将多个 Haar 级联分类器组合成一个层次结构,以便一个分类器识别父区域(在我们的目的中是脸部),而其他分类器识别子区域(眼睛、鼻子和嘴巴)。
我们还简要地探讨了矩形这个谦逊但重要的主题。通过绘制、复制和调整矩形图像区域的大小,我们可以对我们正在追踪的图像区域进行简单的操作。
到本章结束时,我们将把人脸追踪和矩形操作集成到 Cameo 中。最后,我们将实现一些面对面互动!
概念化 Haar 级联
当我们谈论分类对象和追踪它们的位置时,我们究竟希望精确到什么程度?构成物体可识别部分的是什么?
照片图像,即使是来自网络摄像头的,也可能包含大量的细节,以满足我们(人类)观看的愉悦。然而,图像细节在光照、视角、观看距离、相机抖动和数字噪声变化方面往往是不稳定的。此外,即使是物理细节的真实差异,也可能对我们进行分类的目的不感兴趣。我在学校学到的是,在显微镜下,没有两片雪花看起来是相同的。幸运的是,作为一个加拿大孩子,我已经学会了如何在没有显微镜的情况下识别雪花,因为在大批量中,它们的相似性更为明显。
因此,在产生稳定的分类和追踪结果时,抽象图像细节的一些方法是有用的。这些抽象被称为特征,据说它们是从图像数据中提取出来的。尽管任何像素都可能影响多个特征,但特征的数量应该远少于像素。两个图像之间的相似程度可以根据图像对应特征的欧几里得距离来评估。
例如,距离可以定义为空间坐标或颜色坐标。Haar 类似特征是常用于实时人脸跟踪的一种特征类型。它们首次在论文 Robust Real-Time Face Detection 中被用于此目的,作者为 Paul Viola and Michael Jones,出版于 Kluwer Academic Publishers,2001 年(可在 www.vision.caltech.edu/html-files/EE148-2005-Spring/pprs/viola04ijcv.pdf 获取)。每个 Haar 类似特征描述了相邻图像区域之间的对比度模式。例如,边缘、顶点和细线各自生成独特的特征。
对于任何给定的图像,特征可能会根据区域的大小而变化;这可以称为 窗口大小。只有缩放不同的两个图像应该能够产生相似的特征,尽管窗口大小不同。因此,为多个窗口大小生成特征是有用的。这种特征集合被称为 级联。我们可以说 Haar 级联是尺度不变的,换句话说,对尺度变化具有稳健性。OpenCV 提供了一个分类器和跟踪器,用于处理期望以特定文件格式存在的尺度不变 Haar 级联。
OpenCV 中实现的 Haar 级联对旋转变化不稳健。例如,一个颠倒的脸不被认为是直立脸的相似,侧面看脸也不被认为是正面看脸的相似。一个更复杂且资源消耗更大的实现可以通过考虑图像的多个变换以及多个窗口大小来提高 Haar 级联对旋转的稳健性。然而,我们将局限于 OpenCV 中的实现。
获取 Haar 级联数据
一旦你有了 OpenCV 3 的源代码副本,你将找到一个名为 data/haarcascades 的文件夹。
这个文件夹包含 OpenCV 人脸检测引擎用于在静态图像、视频和摄像头流中检测人脸所使用的所有 XML 文件。
一旦找到 haarcascades,为你的项目创建一个目录;在这个文件夹中,创建一个名为 cascades 的子文件夹,并将以下文件从 haarcascades 复制到 cascades:
haarcascade_profileface.xml
haarcascade_righteye_2splits.xml
haarcascade_russian_plate_number.xml
haarcascade_smile.xml
haarcascade_upperbody.xml
如其名称所示,这些级联用于跟踪人脸、眼睛、鼻子和嘴巴。它们需要被检测对象的正面、直立视角。我们将在构建人脸检测器时使用它们。如果你对如何生成这些数据集感兴趣,请参阅 附录 B,为自定义目标生成 Haar 级联,使用 Python 的 OpenCV 计算机视觉。有了足够的耐心和一台强大的计算机,你可以制作自己的级联并为各种类型的对象进行训练。
使用 OpenCV 进行人脸检测
与你一开始可能想到的不同,在静态图像或视频流上执行人脸检测是一个极其相似的操作。后者只是前者的顺序版本:在视频中的人脸检测只是将人脸检测应用到从摄像头读入程序中的每一帧。自然地,许多概念都应用于视频人脸检测,例如跟踪,这在静态图像中不适用,但了解底层理论是相同的。
让我们继续检测一些人脸。
在静态图像上执行人脸检测
执行人脸检测的第一种也是最基本的方法是加载一张图像并在其中检测人脸。为了使结果在视觉上具有意义,我们将在原始图像上的人脸周围绘制矩形。
现在你已经将 haarcascades 包含在你的项目中,让我们继续创建一个基本的脚本来执行人脸检测。
import cv2
filename = '/path/to/my/pic.jpg'
def detect(filename):
face_cascade = cv2.CascadeClassifier('./cascades/haarcascade_frontalface_default.xml')
img = cv2.imread(filename)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
faces = face_cascade.detectMultiScale(gray, 1.3, 5)
for (x,y,w,h) in faces:
img = cv2.rectangle(img,(x,y),(x+w,y+h),(255,0,0),2)
cv2.namedWindow('Vikings Detected!!')
cv2.imshow('Vikings Detected!!', img)
cv2.imwrite('./vikings.jpg', img)
cv2.waitKey(0)
detect(filename)
让我们来看一下代码。首先,我们使用必要的 cv2 导入(你会发现这本书中的每个脚本都会这样开始,或者几乎相似)。其次,我们声明 detect 函数。
def detect(filename):
在这个函数中,我们声明一个 face_cascade 变量,它是一个用于人脸的 CascadeClassifier 对象,负责人脸检测。
face_cascade =
cv2.CascadeClassifier('./cascades/haarcascade_frontalface_default.xml')
然后,我们使用 cv2.imread 加载我们的文件,并将其转换为灰度图,因为人脸检测是在这个颜色空间中进行的。
下一步(face_cascade.detectMultiScale)是我们执行实际人脸检测的地方。
img = cv2.imread(filename)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
faces = face_cascade.detectMultiScale(gray, 1.3, 5)
传递的参数是 scaleFactor 和 minNeighbors,它们决定了在人脸检测过程的每一迭代中图像的百分比缩减,以及每个迭代中每个脸矩形保留的最小邻居数。一开始这可能看起来有点复杂,但你可以在官方文档中查看所有选项。
检测操作返回的值是一个表示脸矩形的元组数组。实用方法 cv2.rectangle 允许我们在指定的坐标处绘制矩形(x 和 y 代表左上坐标,w 和 h 代表脸矩形的宽度和高度)。
我们将通过遍历 faces 变量来绘制我们找到的所有人脸周围的蓝色矩形,确保我们使用原始图像进行绘制,而不是灰度版本。
for (x,y,w,h) in faces:
img = cv2.rectangle(img,(x,y),(x+w,y+h),(255,0,0),2)
最后,我们创建一个 namedWindow 实例,并在其中显示处理后的图像。为了防止图像窗口自动关闭,我们插入一个 waitKey 调用,按下任意键时关闭窗口。
cv2.namedWindow('Vikings Detected!!')
cv2.imshow('Vikings Detected!!', img)
cv2.waitKey(0)
就这样,我们已经在图像中检测到了一整队维京人,如下面的截图所示:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-ocv3-cv-py/img/image00213.jpeg
在视频上执行人脸检测
现在我们有一个很好的基础来理解如何在静态图像上执行人脸检测。如前所述,我们可以在视频的各个帧上重复此过程(无论是摄像头流还是视频)并执行人脸检测。
脚本将执行以下任务:它将打开摄像头流,读取一帧,检查该帧中的人脸,扫描检测到的人脸内的眼睛,然后将在脸部周围绘制蓝色矩形,在眼睛周围绘制绿色矩形。
-
让我们创建一个名为
face_detection.py的文件,并首先导入必要的模块:import cv2 -
在此之后,我们声明一个名为
detect()的方法,它将执行人脸检测。def detect(): face_cascade = cv2.CascadeClassifier('./cascades/haarcascade_frontalface_default.xml') eye_cascade = cv2.CascadeClassifier('./cascades/haarcascade_eye.xml') camera = cv2.VideoCapture(0) -
在
detect()方法内部,我们首先需要加载 Haar 级联文件,以便 OpenCV 可以执行人脸检测。由于我们在本地的cascades/文件夹中复制了级联文件,我们可以使用相对路径。然后,我们打开一个VideoCapture对象(摄像头流)。VideoCapture构造函数接受一个参数,表示要使用的摄像头;zero表示第一个可用的摄像头。while (True): ret, frame = camera.read() gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) -
接下来,我们捕获一帧。
read()方法返回两个值:一个布尔值表示读取帧操作的成功,以及帧本身。我们捕获帧,然后将其转换为灰度。这是一个必要的操作,因为 OpenCV 中的人脸检测是在灰度颜色空间中进行的:faces = face_cascade.detectMultiScale(gray, 1.3, 5) -
与单个静态图像示例类似,我们在帧的灰度版本上调用
detectMultiScale。for (x,y,w,h) in faces: img = cv2.rectangle(frame,(x,y),(x+w,y+h),(255,0,0),2) roi_gray = gray[y:y+h, x:x+w] eyes = eye_cascade.detectMultiScale(roi_gray, 1.03, 5, 0, (40,40))注意
在眼检测中还有一些额外的参数。为什么?
detectMultiScale方法的签名接受一些可选参数:在检测人脸的情况下,默认选项已经足够检测到人脸。然而,眼睛是人脸的一个较小特征,我胡须或鼻子的自阴影以及画面中的随机阴影都会触发假阳性。通过将眼睛搜索限制在最小尺寸 40x40 像素,我能够排除所有假阳性。继续测试这些参数,直到你的应用程序达到你期望的性能水平(例如,你可以尝试指定特征的最大尺寸,或者增加缩放因子和邻居数量)。
-
与静态图像示例相比,这里有一个额外的步骤:我们创建一个与脸矩形对应的感兴趣区域,并在该矩形内进行“眼检测”。这很有意义,因为你不希望在人脸之外寻找眼睛(至少对于人类来说是这样!)。
for (ex,ey,ew,eh) in eyes: cv2.rectangle(img,(ex,ey),(ex+ew,ey+eh),(0,255,0),2) -
再次,我们遍历结果眼睛元组,并在它们周围绘制绿色矩形。
cv2.imshow("camera", frame) if cv2.waitKey(1000 / 12) & 0xff == ord("q"): break camera.release() cv2.destroyAllWindows() if __name__ == "__main__": detect() -
最后,我们在窗口中显示结果帧。如果一切顺利,如果相机视野内有任何人脸,你将在其脸部周围看到一个蓝色矩形,在每个眼睛周围看到一个绿色矩形,如图所示:https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-ocv3-cv-py/img/image00214.jpeg
执行人脸识别
检测人脸是 OpenCV 的一个非常棒的功能,也是构成更高级操作(人脸识别)的基础。什么是人脸识别?它是指一个程序,在给定一个图像或视频流的情况下,能够识别一个人的能力。实现这一目标的一种方法(也是 OpenCV 采用的方法)是通过“训练”程序,给它提供一组分类图片(一个面部数据库),然后对这些图片进行识别操作。
这是 OpenCV 及其人脸识别模块遵循的过程来识别人脸。
人脸识别模块的另一个重要特性是每个识别都有一个置信度分数,这允许我们在实际应用中设置阈值以限制错误读取的数量。
让我们从一开始;要操作人脸识别,我们需要识别的面孔。你可以通过两种方式来做这件事:自己提供图像或获取免费的人脸数据库。互联网上有许多人脸数据库:
-
耶鲁 人脸数据库(Yalefaces):
vision.ucsd.edu/content/yale-face-database -
AT&T:
www.cl.cam.ac.uk/research/dtg/attarchive/facedatabase.html -
扩展耶鲁或耶鲁 B:
www.cl.cam.ac.uk/research/dtg/attarchive/facedatabase.html
要在这些样本上操作人脸识别,你将不得不在一个包含样本人物面孔的图像上运行人脸识别。这可能是一个教育过程,但我发现它不如提供自己的图像那么令人满意。事实上,我可能和许多人有同样的想法:我想知道我能否编写一个程序,以一定程度的置信度识别我的面孔。
生成人脸识别的数据
因此,让我们编写一个脚本来生成这些图像。我们只需要一些包含不同表情的图像,但我们必须确保样本图像符合某些标准:
-
图像将以
.pgm格式的灰度图形式存在 -
正方形形状
-
所有相同大小的图像(我使用了 200 x 200;大多数免费提供的集合都小于这个大小)
这是脚本本身:
import cv2
def generate():
face_cascade = cv2.CascadeClassifier('./cascades/haarcascade_frontalface_default.xml')
eye_cascade = cv2.CascadeClassifier('./cascades/haarcascade_eye.xml')
camera = cv2.VideoCapture(0)
count = 0
while (True):
ret, frame = camera.read()
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
faces = face_cascade.detectMultiScale(gray, 1.3, 5)
for (x,y,w,h) in faces:
img = cv2.rectangle(frame,(x,y),(x+w,y+h),(255,0,0),2)
f = cv2.resize(gray[y:y+h, x:x+w], (200, 200))
cv2.imwrite('./data/at/jm/%s.pgm' % str(count), f)
count += 1
cv2.imshow("camera", frame)
if cv2.waitKey(1000 / 12) & 0xff == ord("q"):
break
camera.release()
cv2.destroyAllWindows()
if __name__ == "__main__":
generate()
这个练习相当有趣的地方在于,我们将基于我们新获得的知识来生成样本图像,这些知识是关于如何在视频流中检测人脸的。实际上,我们正在做的是检测人脸,裁剪灰度帧的该区域,将其调整大小为 200x200 像素,并以特定的文件夹(在我的情况下,是jm;你可以使用你的首字母)中的.pgm格式保存。
我插入了一个变量 count,因为我们需要为图像设置递增的名称。运行脚本几秒钟,改变几次表情,并检查脚本中指定的目标文件夹。您将找到许多您的面部图像,它们被灰度化、调整大小并以 <count>.pgm 格式命名。
现在我们来尝试在视频流中识别我们的面部。这应该很有趣!
识别面部
OpenCV 3 提供了三种主要的面部识别方法,基于三种不同的算法:Eigenfaces、Fisherfaces和局部二值模式直方图(LBPH)。本书的范围不包括深入探讨这些方法之间理论差异的细节,但我们可以提供一个概念的高级概述。
我将为您提供以下链接,以详细描述算法:
-
主成分分析(PCA):Jonathon Shlens 提供了一个非常直观的介绍,可在
arxiv.org/pdf/1404.1100v1.pdf找到。该算法由 K. Pearson 在 1901 年发明,原始论文《On Lines and Planes of Closest Fit to Systems of Points in Space》可在stat.smmu.edu.cn/history/pearson1901.pdf找到。 -
Eigenfaces:论文《Eigenfaces for Recognition》,作者 M. Turk 和 A. Pentland,1991 年,可在
www.cs.ucsb.edu/~mturk/Papers/jcn.pdf找到。 -
Fisherfaces:开创性论文《THE USE OF MULTIPLE MEASUREMENTS IN TAXONOMIC PROBLEMS》,作者 R.A. Fisher,1936 年,可在
onlinelibrary.wiley.com/doi/10.1111/j.1469-1809.1936.tb02137.x/pdf找到。 -
局部二值模式:描述此算法的第一篇论文是《Performance evaluation of texture measures with classification based on Kullback discrimination of distributions》,作者 T. Ojala、M. Pietikainen 和 D. Harwood,可在
ieeexplore.ieee.org/xpl/articleDetails.jsp?arnumber=576366&searchWithin%5B%5D=%22Authors%22%3A.QT.Ojala%2C+T..QT.&newsearch=true找到。
首先,所有方法都遵循一个类似的过程;它们都从一组分类观察(我们的面部数据库,包含每个个体的多个样本)中获取,并在其上进行“训练”,对图像或视频中检测到的面部进行分析,并确定两个要素:是否识别了主题,以及主题真正被识别的置信度度量,这通常被称为置信度分数。
主成分分析(Eigenfaces)执行所谓的 PCA,在所有与计算机视觉相关的数学概念中,这可能是最描述性的。它基本上识别一组特定观察的主成分(再次,你的面部数据库),计算当前观察(在图像或帧中检测到的面部)与数据集之间的发散度,并产生一个值。值越小,面部数据库与检测到的面部之间的差异越小;因此,0 的值是一个完全匹配。
鱼脸(Fisherfaces)源自 PCA 并发展了这一概念,应用了更复杂的逻辑。虽然计算量更大,但它往往比 Eigenfaces 产生更准确的结果。
LBPH(局部二值模式直方图)大致上(再次,从非常高的层面)将检测到的面部划分为小的单元,并将每个单元与模型中的对应单元进行比较,为每个区域生成匹配值的直方图。由于这种灵活的方法,LBPH 是唯一允许模型样本面部和检测到的面部具有不同形状和大小的面部识别算法。就一般而言,我个人认为这是最准确的算法,但每种算法都有其优势和劣势。
准备训练数据
现在我们有了数据,我们需要将这些样本图片加载到我们的面部识别算法中。所有面部识别算法在其 train() 方法中接受两个参数:一个图像数组和一个标签数组。这些标签代表什么?它们是某个个体/面部的 ID,这样当执行面部识别时,我们不仅知道识别了这个人,而且知道——在我们的数据库中的许多人中——这个人是谁。
要做到这一点,我们需要创建一个逗号分隔值(CSV)文件,该文件将包含样本图片的路径,随后是那个人的 ID。以我的情况为例,我使用了之前的脚本生成了 20 张图片,它们位于文件夹 data/at/ 的子文件夹 jm/ 中,该文件夹包含所有个体的图片。
因此,我的 CSV 文件看起来是这样的:
jm/1.pgm;0
jm/2.pgm;0
jm/3.pgm;0
...
jm/20.pgm;0
注意
这些点都是缺失的数字。jm/ 实例表示子文件夹,而末尾的 0 值是我的面部 ID。
好的,在这个阶段,我们已经拥有了指导 OpenCV 识别我们面部所需的一切。
加载数据和识别面部
接下来,我们需要将这些两种资源(图像数组和 CSV 文件)加载到面部识别算法中,以便它可以训练以识别我们的面部。为此,我们构建一个函数,该函数读取 CSV 文件,并且对于文件的每一行——将对应路径的图像加载到图像数组中,并将 ID 加载到标签数组中。
def read_images(path, sz=None):
c = 0
X,y = [], []
for dirname, dirnames, filenames in os.walk(path):
for subdirname in dirnames:
subject_path = os.path.join(dirname, subdirname)
for filename in os.listdir(subject_path):
try:
if (filename == ".directory"):
continue
filepath = os.path.join(subject_path, filename)
im = cv2.imread(os.path.join(subject_path, filename), cv2.IMREAD_GRAYSCALE)
# resize to given size (if given)
if (sz is not None):
im = cv2.resize(im, (200, 200))
X.append(np.asarray(im, dtype=np.uint8))
y.append(c)
except IOError, (errno, strerror):
print "I/O error({0}): {1}".format(errno, strerror)
except:
print "Unexpected error:", sys.exc_info()[0]
raise
c = c+1
return [X,y]
执行主成分分析(Eigenfaces)识别
我们已经准备好测试面部识别算法了。以下是执行它的脚本:
def face_rec():
names = ['Joe', 'Jane', 'Jack']
if len(sys.argv) < 2:
print "USAGE: facerec_demo.py </path/to/images> [</path/to/store/images/at>]"
sys.exit()
[X,y] = read_images(sys.argv[1])
y = np.asarray(y, dtype=np.int32)
if len(sys.argv) == 3:
out_dir = sys.argv[2]
model = cv2.face.createEigenFaceRecognizer()
model.train(np.asarray(X), np.asarray(y))
camera = cv2.VideoCapture(0)
face_cascade = cv2.CascadeClassifier('./cascades/haarcascade_frontalface_default.xml')
while (True):
read, img = camera.read()
faces = face_cascade.detectMultiScale(img, 1.3, 5)
for (x, y, w, h) in faces:
img = cv2.rectangle(img,(x,y),(x+w,y+h),(255,0,0),2)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
roi = gray[x:x+w, y:y+h]
try:
roi = cv2.resize(roi, (200, 200), interpolation=cv2.INTER_LINEAR)
params = model.predict(roi)
print "Label: %s, Confidence: %.2f" % (params[0], params[1])
cv2.putText(img, names[params[0]], (x, y - 20), cv2.FONT_HERSHEY_SIMPLEX, 1, 255, 2)
except:
continue
cv2.imshow("camera", img)
if cv2.waitKey(1000 / 12) & 0xff == ord("q"):
break
cv2.destroyAllWindows()
有几行可能看起来有点神秘,所以让我们分析一下脚本。首先,有一个名字数组被声明了;这些是我存储在人脸数据库中的实际个人名字。将一个人识别为 ID 0 是很棒的,但在正确检测和识别到的人脸上打印 'Joe' 要戏剧性得多。
所以每当脚本识别到一个 ID 时,我们将在names数组中打印相应的名字,而不是 ID。
在此之后,我们按照前一个函数中描述的方式加载图像,使用cv2.createEigenFaceRecognizer()创建人脸识别模型,并通过传递图像和标签(ID)数组来训练它。请注意,Eigenface 识别器接受两个重要的参数,你可以指定:第一个是你想要保留的主成分数量,第二个是一个指定置信度阈值的浮点值。
接下来,我们重复与面部检测操作类似的过程。不过,这次,我们通过在检测到的任何面部上执行人脸识别来扩展帧的处理。
这分为两个步骤:首先,我们将检测到的人脸调整到期望的大小(在我的情况下,样本是 200x200 像素),然后我们在调整大小后的区域上调用predict()函数。
注意
这是一个简化的过程,它的目的是让你能够运行一个基本的应用程序并理解 OpenCV 3 中人脸识别的过程。实际上,你将应用一些额外的优化,例如正确对齐和旋转检测到的人脸,以最大限度地提高识别的准确性。
最后,我们获得识别结果,并且为了效果,我们在帧中绘制它:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-ocv3-cv-py/img/image00215.jpeg
使用 Fisherfaces 进行人脸识别
那 Fisherfaces 呢?过程没有太大变化;我们只需要实例化一个不同的算法。所以,我们的模型变量声明将如下所示:
model = cv2.face.createFisherFaceRecognizer()
Fisherface 与 Eigenfaces 具有相同的两个参数:要保留的 Fisherfaces 和置信度阈值。置信度高于此阈值的面孔将被丢弃。
使用 LBPH 进行人脸识别
最后,让我们快速看一下 LBPH 算法。同样,过程非常相似。然而,算法工厂接受的参数要复杂一些,因为它们按顺序指示:radius,neighbors,grid_x,grid_y,以及置信度阈值。如果你不指定这些值,它们将自动设置为 1,8,8,8,和 123.0。模型声明将如下所示:
model = cv2.face.createLBPHFaceRecognizer()
注意
注意,使用 LBPH,你不需要调整图像大小,因为网格的划分允许比较每个单元格中识别出的模式。
丢弃置信度分数的结果
predict()方法返回一个包含两个元素的数组:第一个元素是识别个体的标签,第二个是置信度分数。所有算法都提供了设置置信度分数阈值的选项,这衡量了识别的面与原始模型之间的距离,因此分数为 0 表示完全匹配。
可能会有这样的情况,你宁愿保留所有识别,然后进行进一步的处理,这样你可以提出自己的算法来估计识别的置信度分数;例如,如果你试图在视频中识别人,你可能想分析后续帧中的置信度分数,以确定识别是否成功。在这种情况下,你可以检查算法获得的置信度分数,并得出自己的结论。
注意
置信度分数在 Eigenfaces/Fisherfaces 和 LBPH 中完全不同。Eigenfaces 和 Fisherfaces 将产生(大约)在 0 到 20,000 范围内的值,任何低于 4-5,000 的分数都表示相当有信心的识别。
LBPH 的工作原理类似;然而,良好识别的参考值低于 50,任何高于 80 的值都被认为是低置信度分数。
正常的定制方法是在获得足够数量的具有令人满意的任意置信度分数的帧之前,不绘制识别面的矩形,但你完全自由地使用 OpenCV 的人脸识别模块来定制你的应用程序以满足你的需求。
摘要
到现在为止,你应该已经很好地理解了人脸检测和识别的工作原理,以及如何在 Python 和 OpenCV 3 中实现它们。
人脸检测和识别是计算机视觉中不断发展的分支,算法正在不断发展,并且随着对机器人和物联网的重视,它们将在不久的将来发展得更快。
目前,检测和识别的准确性高度依赖于训练数据的质量,所以请确保为你的应用程序提供高质量的面对面数据库,你将对自己的结果感到满意。
503

被折叠的 条评论
为什么被折叠?



