Java-OpenCV3-计算机视觉-全-
Java OpenCV3 计算机视觉(全)
原文:
annas-archive.org/md5/f43ee261d3fc7444ba477040887fbb1f译者:飞龙
前言
在自动驾驶汽车成为现实的时代生活,可能会激发人们对计算机早期视觉工作原理的好奇心。进行面部识别以进行访问控制,自动按主题或人物组织我们的照片,以及从纸质扫描中自动识别字符,这些任务已成为我们生活中的常见任务。上述所有这些行动都已被列入所谓的计算机视觉研究领域。
作为一门科学学科,可以从图像中提取信息的系统背后的理论可以描述为计算机视觉,它已被用于从医学图像中提取有价值的测量值,以及帮助人类在所谓的半自动过程中描绘重要图像区域边界。
在提供简单易用的计算机视觉基础设施以帮助人们快速构建复杂的视觉应用的大背景下,创建了一个开源库:OpenCV。它专为实时应用设计,用 C++编写,包含数百个计算机视觉算法。
尽管 OpenCV 在 1999 年 1 月首次发布了 alpha 版本,但它直到 2013 年 2 月才通过绑定正式支持桌面 Java。由于这是计算机科学系以及 K-12 计算机相关课程中采用的最受欢迎的入门教学语言之一,因此拥有一个关于如何在 Java 环境中构建视觉应用的优秀参考非常重要。
本书涵盖了基本的 OpenCV 计算机视觉算法及其与 Java 的集成。由于 Swing GUI 小部件工具包被广泛用于构建 Java 中的 GUI,本书中,你将受益于处理此主题的章节,以及了解如何设置处理本地代码绑定的开发环境。此外,拉伸、缩小、扭曲和旋转等操作,以及寻找边缘、线条和圆等操作,都通过有趣且实用的示例项目在本书中进行了介绍。
由于 Kinect 设备已成为背景分割的强大工具,我们也在本章中对其进行了介绍。
另一个常见的与计算机视觉一起探索的热门主题是机器学习,本书中,你将找到创建自己的对象跟踪器以及使用 OpenCV 内置的人脸跟踪器的有用信息。
由于 Java 已被广泛用于 Web 应用,本书还涵盖了服务器端的计算机视觉应用,解释了图像上传与 OpenCV 集成的细节。
到本书结束时,你将具备如何使用 Java 与 OpenCV 从设置到服务器端的扎实背景知识;书中还简要介绍了基本计算机视觉主题。此外,你将获得几个完整项目的源代码,你可以从中扩展并添加自己的功能。
本书涵盖的内容
第一章,为 Java 设置 OpenCV,涵盖了库和开发环境的设置。本章涵盖了 Eclipse 和 NetBeans IDE,以及解释 Ant 和 Maven 构建工具的配置。
第二章,处理矩阵、文件、摄像头和 GUI,展示了如何在像素级别访问矩阵,以及如何从文件和网络摄像头中加载和显示图像。它还涵盖了 Swing 小部件工具包的支持以及如何使用 OpenCV。
第三章,图像滤波和形态学算子,讨论了从图像中去除噪声的过程以及形态学算子。它还解释了图像金字塔以及如洪水填充和图像阈值化等主题。
第四章,图像变换,解释了如何使用梯度滤波和 Sobel 滤波等重要的变换来找到边缘。此外,它还解释了线形和圆形 Hough 变换,这些变换不仅用于识别直线,还用于识别径向线。本章还解释了离散傅里叶分析和一些距离变换。
第五章,使用 Ada Boost 和 Haar 级联进行目标检测,演示了如何创建自己的分类器来查找某些对象,以及如何使用著名的面部检测分类器。
第六章,使用 Kinect 设备检测前景和背景区域以及深度,探讨了提取背景的重要问题。此外,它还解释了如何使用 Kinect 设备来获取深度信息。
第七章,服务器端 OpenCV,解释了如何使用 OpenCV 设置一个网络服务器应用程序。
你需要为这本书准备的内容
如果你是一名想要用 Java 创建计算机视觉应用的 Java 开发者、学生、研究人员或爱好者,那么这本书就是为你准备的。如果你是一名习惯于使用 OpenCV 的资深 C/C++开发者,你也会发现这本书在将你的应用迁移到 Java 时非常有用。
你只需要具备 Java 的基本知识,无需具备计算机视觉的先验理解,因为这本书将为你提供清晰的基本解释和示例。
这本书面向的对象
如果你是一名想要用 Java 创建计算机视觉应用的 C/C++开发者、学生、研究人员或爱好者,那么这本书就是为你准备的。如果你是一名习惯于使用 OpenCV 的资深 C/C++开发者,你也会发现这本书在将你的应用迁移到 Java 时非常有用。
您只需要具备 Java 的基本知识。不需要对计算机视觉有先前的了解,因为本书将为您提供清晰的解释和基本示例。
术语约定
在本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:“另一种获取源代码的方法是使用git工具。”
代码块以如下方式设置:
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
<mainClass>com.mycompany.app.App</mainClass>
</manifest>
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
imageView.addMouseListener(new MouseAdapter()
{
public void mousePressed(MouseEvent e)
{
Core.circle(image,new Point(e.getX(),e.getY()),20, new Scalar(0,0,255), 4);
updateView(image);
}
});
任何命令行输入或输出都如下所示:
sudo apt-get install build-essential cmake git libgtk2.0-dev pkg-config libavcodec-dev libavformat-dev libswscale-dev python-dev python-numpy libtbb2 libtbb-dev libjpeg-dev libpng-dev libtiff-dev libjasper-dev libdc1394-22-dev ant
新术语和重要词汇以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:“转到窗口 | 首选项,并在搜索框中输入 classpath 变量。”
注意
警告或重要提示将以这样的框显示。
小贴士
小贴士和技巧如下所示。
读者反馈
我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢什么或不喜欢什么。读者反馈对我们来说非常重要,因为它帮助我们开发出您真正能从中获得最大收益的书籍。
要向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件主题中提及书籍的标题。
如果您在某个领域有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南,网址为www.packtpub.com/authors。
客户支持
现在您已经是 Packt 图书的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。
下载示例代码
您可以从您在www.packtpub.com的账户中下载示例代码文件,适用于您购买的所有 Packt Publishing 书籍。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
下载本书中的彩色图像
我们还为您提供了一个包含本书中使用的截图/图表彩色图像的 PDF 文件。彩色图像将帮助您更好地理解输出中的变化。您可以从www.packtpub.com/sites/default/files/downloads/3972OS_ColorImages.pdf下载此文件。
错误清单
尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。
要查看之前提交的勘误表,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分。
盗版
互联网上对版权材料的盗版是一个持续存在的问题,跨越所有媒体。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过<copyright@packtpub.com>与我们联系,并提供疑似盗版材料的链接。
我们感谢您在保护我们的作者和我们为您提供有价值内容的能力方面的帮助。
询问
如果您在这本书的任何方面遇到问题,您可以通过<questions@packtpub.com>联系我们,我们将尽力解决问题。
第一章. 为 Java 设置 OpenCV
我相信您想要开始开发令人惊叹的计算机视觉应用程序。您可能已经听说过一个名为 OpenCV 的优秀的 C/C++ 计算机视觉库,它可以帮助您实现这一目标。但如果您想使用您对 Java 编程的了解来开发应用程序,我们有一个好消息要告诉您。自 2013 年 1 月 OpenCV 2.4.4 版本发布以来,Java 绑定已经正式开发。因此,您不仅可以在桌面 Java 中使用它们,还可以用于 Scala 开发。
本章将立即为您设置 OpenCV 开发环境。由于 Java 开发者大多数情况下习惯于使用 Eclipse、NetBeans、Apache Ant 和 Maven 等工具,我们将介绍如何使用 Java 开发者更熟悉的开发环境创建一个简单的 OpenCV 应用程序。
在本章中,我们将进行以下操作:
-
获取具有桌面 Java 支持的 OpenCV
-
讨论关于 Java 本地接口(JNI)的细节
-
配置 Eclipse 和 NetBeans 以支持 OpenCV
-
创建 Apache Ant 和 Maven OpenCV 项目
到本章结束时,用户应该在操作系统上运行一个 OpenCV for Java 安装,可以轻松地链接到 Eclipse、NetBeans、Apache Ant 或 Maven,这些是最常用的 Java 工具和构建系统。
获取用于 Java 开发的 OpenCV
在使用 OpenCV 进行 Java 开发时,首先要注意的是 OpenCV 是一个需要使用操作系统特定编译器编译的 C++ 库。生成的本地代码是平台相关的。因此,原生 Linux 代码无法在 Windows 上运行,同样 Android 原生代码也无法在 OSX 上运行。这与为 Java 生成的字节码非常不同,Java 字节码可以在任何平台上由解释器执行。为了在 Java 虚拟机(JVM)中运行本地代码,需要所谓的 Java 本地接口(JNI)。这样,本地代码将需要为您的应用程序将要运行的每个平台而准备。
重要的是要理解 JNI 是一个本地编程接口。它允许在 JVM 内部运行的 Java 代码与其他用 C、C++和汇编等编程语言编写的应用程序和库进行交互。由于它架起了 Java 和其他语言之间的桥梁,它需要将这些语言的 datatypes 进行转换,以及创建一些样板代码。好奇的读者可以参考位于modules/java/generator文件夹中的gen_java.py脚本,该脚本自动化了大部分这项工作。幸运的 Windows 用户可以得到编译后的二进制文件,这意味着源 C++ OpenCV 代码,用 Windows 编译器编译成仅在 Windows 上运行的本地代码,来自 OpenCV 软件包。来自其他操作系统的用户将需要从源代码构建二进制文件,尽管也可以在 Windows 上这样做。为了下载编译后的二进制文件,我们应该从 OpenCV SourceForge 仓库获取版本 2.4.4 或更高版本的 OpenCV Windows 软件包,该仓库位于sourceforge.net/projects/opencvlibrary/files/。
注意
注意到用于 Java 开发的预构建文件位于opencv/build/java/。例如,如果你正在使用 3.0.0 版本的 OpenCV,你应该在opencv-300.jar中看到包含 Java 接口的文件,以及在 x86 和 x64 原生动态库中,其中包含 Java 绑定,分别在x86/opencv_java300.dll和x64/opencv_java300.dll中。
从源代码构建 OpenCV
在本节中,我们主要关注生成包含在 JAR 文件中的所有 OpenCV Java 类文件,以及 Java OpenCV 的原生动态库。这是一个自包含的库,与 JNI 一起工作,并且是运行 Java OpenCV 应用程序所必需的。
如果你正在使用 Linux 或 OSX,或者你想要在 Windows 上从源代码构建,那么为了获取 OpenCV 中提交的最新功能,你应该使用源代码。你可以访问 OpenCV 下载页面opencv.org/downloads.html,并选择适合你分发版的链接。
获取源代码的另一种方式是使用git工具。安装它的适当说明可以在git-scm.com/downloads找到。当使用git时,请使用以下命令:
git clone git://github.com/Itseez/opencv.git
cd opencv
git checkout 3.0.0-rc1
mkdir build
cd build
这些命令将访问 OpenCV 开发者的仓库,并从branch 3.0.0-rc1下载最新代码,这是 3.0.0 版本的候选发布版。
在获取源代码的任何一种方法中,你都需要构建工具来生成二进制文件。所需的软件包如下:
-
CMake 2.6 或更高版本:这是一个跨平台的开源构建系统。你可以从
www.cmake.org/cmake/resources/software.html下载它。 -
Python 2.6 或更高版本,以及 python-dev 和 python-numpy:这是用于运行 Java 构建脚本的 Python 语言。你可以从
www.python.org/getit/下载 Python,并从sourceforge.net/projects/numpy/files/NumPy下载这些包。 -
C/C++ 编译器:这些编译器是生成原生代码所必需的。在 Windows 上,你可以从
www.visualstudio.com/downloads/安装免费的 Microsoft Visual Studio Community 或 Express 版本。这些编译器也支持 Visual Studio Professional 版本以及 2010 年以上版本,应该可以正常工作。你也可以让它与 MinGW 一起工作,可以从sourceforge.net/projects/mingw/files/Installer/下载。在 Linux 上,建议你在 Ubuntu 或 Debian 上使用 Gnu C 编译器(GCC),例如通过简单的sudo apt-get install build-essential命令。如果你在 Mac 上工作,你应该使用 XCode。 -
Java 开发工具包 (JDK):JDK 是生成 JAR 文件所必需的,这对于每个 Java OpenCV 程序都是必需的。推荐的版本从 Oracle 的 JDK 6、7 或 8 开始,可以从
www.oracle.com/technetwork/java/javase/downloads/index-jsp-138363.html下载。请按照链接中的操作系统特定说明进行安装。 -
Apache Ant:这是一个纯 Java 构建工具。在
ant.apache.org/bindownload.cgi寻找二进制发行版。确保你正确设置了ANT_HOME变量,正如安装说明中指出的ant.apache.org/manual/index.html。
为了在 Linux 发行版(如 Ubuntu 或 Debian)中安装这些软件,用户应执行以下命令:
sudo apt-get install build-essential cmake git libgtk2.0-dev pkg-config libavcodec-dev libavformat-dev libswscale-dev python-dev python-numpy libtbb2 libtbb-dev libjpeg-dev libpng-dev libtiff-dev libjasper-dev libdc1394-22-dev ant
一旦安装了所有这些包,你就可以准备构建库了。确保你位于 build 目录中,如果你已经遵循了前面的 Git 指令,你应该已经做到了。如果你从 OpenCV 下载中下载了源文件,你的构建父目录应该包含 CMakeLists.txt 以及 3rdparty、apps、cmake、data、doc、include、modules、platforms、samples 和 test 文件夹。
CMake 是一个构建工具,它将生成你特定的编译器解决方案文件。然后你应该使用你的编译器来生成二进制文件。确保你位于 build 目录中,因为这应该遵循最后的 cd build 命令。如果你使用 Linux,运行以下命令:
cmake -DBUILD_SHARED_LIBS=OFF
如果你使用 Windows,运行以下命令:
cmake -DBUILD_SHARED_LIBS=OFF -G "Visual Studio 10"
注意,使用 DBUILD_SHARED_LIBS=OFF 标志非常重要,因为它将指示 CMake 在一组静态库上构建 OpenCV。这样,它将为 Java 编译一个不依赖于其他库的单个动态链接库。这使得部署你的 Java 项目更容易。
注意
如果你正在 Windows 上使用其他编译器,请输入 cmake –help,它将显示所有可用的生成器。
如果你想使用 MinGW makefiles,只需将 CMake 命令更改为以下命令:
cmake -DBUILD_SHARED_LIBS=OFF -G "MinGW Makefiles"
在通过 CMake 生成项目文件时需要注意的一个关键点是 java 是将要构建的模块之一。你应该会看到一个如图所示的屏幕:

如果你看不到 java 作为待构建模块之一,如以下截图所示,你应该检查几个方面,例如 Ant 是否正确配置。同时确保你已经设置了 ANT_HOME 环境变量,并且 Python 已经正确配置。通过在 Python 壳中简单地输入 numpy import * 来检查 NumPy 是否已安装,并检查是否有任何错误:

如果你对 Python 和 Java 的安装有疑问,向下滑动以检查它们的配置。它们应该类似于下一张截图:

一切配置正确后,就是开始编译源代码的时候了。在 Windows 上,输入以下命令:
msbuild /m OpenCV.sln /t:Build /p:Configuration=Release /v:m
注意,你可能会收到一个错误消息,说 'msbuild' 不是内部或外部命令,也不是可操作的程序或批处理文件。这发生在你没有设置 msbuild 路径时。为了正确设置它,打开 Visual Studio 并在 工具 菜单中点击 Visual Studio 命令提示符。这将提供一个完全工作的命令提示符,可以访问 msbuild。请参考以下截图以获得更清晰的说明:

如果你使用的是较新的 Visual Studio 版本,请按 Windows 键并输入VS2012 命令提示符。这应该会设置你的环境变量。
为了在 Linux 上开始构建,只需输入以下命令:
make -j8
上述命令将编译具有 Java 支持的 OpenCV 库。请注意,-j8 标志告诉 make 使用八个工作线程并行运行,这从理论上讲可以加快构建速度。
提示
下载示例代码
你可以从www.packtpub.com下载示例代码文件,这是你购买的所有 Packt 出版物的代码。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给你。
整个过程将持续几分钟,直到生成一个包含 Java 接口的 JAR 文件,该文件位于 bin/opencv-300.jar。包含 Java 绑定的原生动态链接库在 lib/libopencv_java300.so 或 bin/Release/opencv_java300.dll 中生成,具体取决于您的操作系统。这些文件将在我们创建第一个 OpenCV 应用程序时使用。
备注
关于如何在您的平台上编译 OpenCV 的更多详细信息,请查找 docs.opencv.org/doc/tutorials/introduction/table_of_content_introduction/table_of_content_introduction.html。
恭喜!你现在已经完成了成为使用 OpenCV 的优秀开发者的一半旅程!
Eclipse 中的 Java OpenCV 项目
在任何 IDE 中使用 OpenCV 都非常简单。只需将 OpenCV JAR,即 opencv-300.jar 添加到您的类路径即可。但是,因为它依赖于原生代码,所以您需要指出动态链接库——Linux 中的 so,Windows 中的 .dll 和 MacOSX 中的 dylib。
-
在 Eclipse 中,转到 文件 | 新建 | Java 项目。
![Eclipse 中的 Java OpenCV 项目]()
-
给新项目起一个描述性的名称,例如
SimpleSample。在 包资源管理器 中选择项目,转到 项目 菜单并点击 属性。在 Java 构建路径 选项卡中,转到 库 选项卡,然后点击右侧的 添加库… 按钮,如图所示:![Eclipse 中的 Java OpenCV 项目]()
-
在 添加库 对话框中,选择 用户库,然后点击 下一步。
-
现在,点击 用户库… 按钮。
-
点击 新建…。适当地命名您的库,例如,
opencv-3.0.0。现在是引用 JAR 文件的时候了。 -
点击 添加 JARs…。
-
在你的文件系统中选择
opencv-300.jar文件;它应该在opencv\build\java文件夹中。然后,指向原生库位置,如以下截图所示:![Eclipse 中的 Java OpenCV 项目]()
-
现在,通过点击窗口右侧的 编辑… 按钮选择 原生库位置,并设置您的原生库位置文件夹,例如,
opencv\build\java\x64\。 -
现在 OpenCV 已经正确配置,只需在您的 添加库 对话框中选择它,按 完成。
注意,你的项目现在指向了 OpenCV JAR。你还可以从 包资源管理器 中浏览主类,如图所示:

在 NetBeans 配置 部分之后,可以找到创建一个简单的 OpenCV 应用程序的源代码。
NetBeans 配置
如果您更习惯使用 NetBeans,配置过程与 Eclipse 几乎相同:
-
选择文件 | 新建项目...。在项目选项卡中,选择Java 应用程序,然后单击下一步。为新项目提供一个合适的名称,然后单击完成。
![NetBeans 配置]()
-
现在,在您的库文件夹上右键单击,然后单击添加库...,如图所示:
![NetBeans 配置]()
-
由于我们之前没有进行过这个过程,OpenCV 库将不存在。在窗格的右侧单击创建...按钮。它将打开一个对话框,要求输入库名称——命名为
OpenCV——以及库类型,对于此选项,您应保留默认选项类库。在下一屏幕中,在类路径选项卡中,单击添加 JAR/Folder...,如图所示:![NetBeans 配置]()
-
现在指向您的库,即
opencv-300.jar文件所在的位置——通常在opencv/build/java/。由于您的库已正确配置,请在添加库对话框中选择它。 -
需要提供的最后一个细节是库的本地文件路径。在项目选项卡中右键单击您的项目名称,然后选择属性。在树中的运行项下,在虚拟机选项下,通过在文本框中输入
-Djava.library.path=C:\Users\baggio\Downloads\opencv\build\java\x64来设置库路径。![NetBeans 配置]()
确保将给定的路径更改为您的 OpenCV 安装位置,并且它指向包含本地库的文件夹,即 Windows 中的opencv_java300.dll或 Linux 中的libopencv_java300.so。现在,将SimpleSample类代码添加到您的项目中,如指示。运行示例并确保没有错误发生。
一个简单的 Java OpenCV 应用程序
是时候创建一个简单的应用程序来展示我们现在可以使用 OpenCV 编译和执行 Java 代码了。创建一个新的 Java 类,包含一个Main方法,并粘贴以下代码。它简单地创建一个 5 x 10 的 OpenCV 矩阵,设置其一些行和列,并将结果打印到标准输出。
确保通过调用System.loadlibrary("opencv_java300")加载正确的动态链接库。由于您可能以后想更改库版本,更好的方法是使用Core.NATIVE_LIBARAY_NAME常量,这将输出正确的库名称。您也可以在本书的chapter1代码库中找到此文件,位于ant/src目录下。
import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.core.CvType;
import org.opencv.core.Scalar;
class SimpleSample {
static{ System.loadLibrary(Core.NATIVE_LIBRARY_NAME); }
public static void main(String[] args) {
System.out.println("Welcome to OpenCV " + Core.VERSION);
Mat m = new Mat(5, 10, CvType.CV_8UC1, new Scalar(0));
System.out.println("OpenCV Mat: " + m);
Mat mr1 = m.row(1);
mr1.setTo(new Scalar(1));
Mat mc5 = m.col(5);
mc5.setTo(new Scalar(5));
System.out.println("OpenCV Mat data:\n" + m.dump());
}
}
根据 Oracle 的文档,它指出,类可以有任意数量的静态初始化块。并且它们可以出现在类体的任何位置。运行时系统保证静态初始化块按照它们在源代码中出现的顺序被调用。
你应该确保所有对 OpenCV 库的调用都由单个 System.loadLibrary 调用 precede,以便加载动态库。否则,你将收到一个 java.lang.UnsatisfiedLinkError: org.opencv.core.Mat.n_Mat(IIIDDDD)J 错误。这通常发生在静态块中。
如果一切顺利,你应在控制台看到以下输出:
Welcome to OpenCV 3.0.0-rc1
OpenCV Mat: Mat [ 5*10*CV_8UC1, isCont=true, isSubmat=false, nativeObj=0x2291b70, dataAddr=0x229bbd0 ]
OpenCV Mat data:
[ 0, 0, 0, 0, 0, 5, 0, 0, 0, 0;
1, 1, 1, 1, 1, 5, 1, 1, 1, 1;
0, 0, 0, 0, 0, 5, 0, 0, 0, 0;
0, 0, 0, 0, 0, 5, 0, 0, 0, 0;
0, 0, 0, 0, 0, 5, 0, 0, 0, 0]
使用 Ant 构建你的项目
如果你想要依赖 Apache Ant 进行构建而不是使用 IDE,OpenCV 示例中提供了一个 build.xml 文件。你可以在本章的存储库中找到此文件。以下是其内容:
<project name="SimpleSample" basedir="." default="rebuild-run">
<property name="src.dir" value="src"/>
<property name="lib.dir" value="${ocvJarDir}"/>
<path id="classpath">
<fileset dir="${lib.dir}" includes="**/*.jar"/>
</path>
<property name="build.dir" value="build"/>
<property name="classes.dir" value="${build.dir}/classes"/>
<property name="jar.dir" value="${build.dir}/jar"/>
<property name="main-class" value="${ant.project.name}"/>
<target name="clean">
<delete dir="${build.dir}"/>
</target>
<target name="compile">
<mkdir dir="${classes.dir}"/>
<javac includeantruntime="false" srcdir="${src.dir}" destdir="${classes.dir}" classpathref="classpath"/>
</target>
<target name="jar" depends="compile">
<mkdir dir="${jar.dir}"/>
<jar destfile="${jar.dir}/${ant.project.name}.jar" basedir="${classes.dir}">
<manifest>
<attribute name="Main-Class" value="${main-class}"/>
</manifest>
</jar>
</target>
<target name="run" depends="jar">
<java fork="true" classname="${main-class}">
<sysproperty key="java.library.path" path="${ocvLibDir}"/>
<classpath>
<path refid="classpath"/>
<path location="${jar.dir}/${ant.project.name}.jar"/>
</classpath>
</java>
</target>
<target name="rebuild" depends="clean,jar"/>
<target name="rebuild-run" depends="clean,run"/>
</project>
这是一个基本的 build.xml Ant 文件,它定义了诸如清理、编译、打包 .jar 文件、运行、重新构建和重新构建运行等任务。它期望你的源代码位于一个名为 src 的同级文件夹中。请确保之前提供的 SimpleSample.java 源代码位于此目录中。
使用 Ant 编译和运行项目很简单。只需输入以下命令:
ant -DocvJarDir=path/to/dir/containing/opencv-300.jar -DocvLibDir=path/to/dir/containing/opencv_java300/native/library
如果你已经下载并解压了预构建的二进制文件,请使用以下命令代替:
ant -DocvJarDir=X:\opencv3.0.0\opencv\build\java -DocvLibDir=X:\opencv3.00\opencv\build\java\x64
Ant build.xml 成功运行的外观如下所示:

提供的 build.xml 文件可以用于构建你的 Java OpenCV 应用程序。为了使用它,请确保项目名称与你的主类名称匹配。如果你的主类位于 package com.your.company 中,并且名为 MainOpenCV,你应该将 build.xml 的第一行从 <project name="SimpleSample" basedir="." default="rebuild-run"> 更改为 <project name="com.your.company.MainOpenCV" basedir="." default="rebuild-run">。
你还可以将 ocvJarDir 和 ocvLibDir 属性硬编码,这样在调用 Ant 时就不必输入它们。对于 ocvJarDir,只需将 <property name="lib.dir" value="${ocvJarDir}"/> 命令更改为 <property name="lib.dir" value="X:\opencv2.47\opencv\build\java"/>。
Java OpenCV Maven 配置
Apache Maven 是一个更复杂的构建自动化工具,主要用于 Java 项目。它不仅描述了软件是如何构建的,还描述了它如何依赖于其他库。其项目通过一个名为 pom.xml 的项目对象模型进行配置。Maven 依赖通常位于 Maven 2 中央仓库。如果在那里找不到,您需要添加其他仓库。您还可以创建一个本地仓库并将自己的依赖项添加到那里。在撰写本书时,没有公开的 Java OpenCV 依赖项。因此,我们将不仅涵盖在本地仓库中安装 Java OpenCV Maven 依赖项的过程,还将介绍如何使用本书的 Maven 仓库进行 OpenCV 3.0.0 版本的 Windows 构建。如果 OpenCV 开发者托管公共 Maven 仓库,则需要做些小的修改。您只需找到官方 OpenCV JAR 的 groupId、artifactId 和 version 并将它们放入您的 pom.xml 文件中即可。
为了使您的项目依赖于任何库,您只需在您的 pom.xml 文件中提供三个字段即可。它们是 groupId、artifactId 和 version。将项目依赖于不在中央 Maven 仓库中托管的库的推荐方法是使用简单的命令安装它们,例如 mvn install:install-file -Dfile=non-maven-proj.jar -DgroupId=some.group -DartifactId=non-maven-proj -Dversion=1 -Dpackaging=jar。
在下一节中,我们将向您展示如何使用 Packt 仓库进行 Windows 构建并随后提供如何在您的本地仓库中安装它们的详细信息,以防您需要这样做。
创建指向 Packt 仓库的 Windows Java OpenCV Maven 项目
本节展示了如何创建一个基本的 Maven 项目以及如何自定义它,以便添加 OpenCV 依赖项。除此之外,它还将生成一个 Eclipse 项目,以便读者可以轻松地在 Windows 中生成项目。这里的一个主要优势是,无需手动构建或下载 OpenCV 库。
虽然 Maven 的学习曲线可能比直接在您喜欢的 IDE 中创建项目要陡峭一些,但从长远来看,这是值得的。使用 Maven 的最好之处在于,您根本不需要安装 OpenCV,因为所有依赖项,包括本地文件,都会自动下载。我们将在以下简单步骤中向您展示如何操作:
-
从原型构建项目:为您的项目创建一个空文件夹。让我们将其命名为
D:\mvnopencv。在该文件夹中,输入以下命令:mvn archetype:generate -DgroupId=com.mycompany.app -DartifactId=my-opencv-app -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false让我们将其分解为几个部分。
mvn archetype:generate命令告诉 Maven 运行来自 archetype 插件的generate goal命令。从文档中,我们看到generate goal从一个模板创建 Maven 项目;它会要求用户从模板目录中选择一个模板,并从远程仓库检索它。一旦检索到,它将被处理以创建一个可工作的 Maven 项目。这样,我们推断出-DarchetypeArtifactId=maven-archetype-quickstart参数是选定的模板。这将生成一个具有以下结构的 Java 项目:my-opencv-app |-- pom.xml `-- src |-- main | `-- java | `-- com | `-- company | `-- app | `-- App.java `-- test `-- java `-- com `-- company `-- app `-- AppTest.java注意
注意到
-DgroupId=com.mycompany.app -DartifactId=my-opencv-app属性将填充pom.xml并提供项目树的一部分。 -
添加 OpenCV 依赖项:由于这是一个从通用 Maven 模板生成的项目,我们应该对其进行自定义,使其看起来像一个 Java OpenCV 项目。为了做到这一点,我们需要添加我们的依赖项。在
D:\mvnopencv\my-opencv-app中打开生成的pom.xml文件。我们首先需要添加 Java OpenCV 依赖项。由于在撰写本书时它们不存在于 Maven 中央仓库中,你还需要指向一个在线仓库。我们为 Windows x86 和 Windows 64 位提供了原生文件。为了添加 Packt Maven 仓库,只需将以下行添加到你的pom.xml文件中:<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <repositories> <repository> <id>javaopencvbook</id> <url>https://raw.github.com/JavaOpenCVBook/code/maven2/</url> </repository> </repositories> <modelVersion>4.0.0</modelVersion> … </project>现在,也添加 OpenCV 依赖项。为了编译你的代码,你只需要添加 OpenCV JAR 依赖项。如果你还想执行它,你还需要 Windows 原生文件。这些文件已经打包在
opencvjar-runtime-3.0.0-natives-windows-x86.jar中,适用于 32 位架构。对于 64 位架构,这些文件打包在opencvjar-runtime-3.0.0-natives-windows-x86_64.jar中。在junit依赖项附近,添加以下内容:<dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.javaopencvbook</groupId> <artifactId>opencvjar</artifactId> <version>3.0.0</version> </dependency> <dependency> <groupId>org.javaopencvbook</groupId> <artifactId>opencvjar-runtime</artifactId> <version>3.0.0</version> <classifier>natives-windows-x86_64</classifier> </dependency> </dependencies>注意到设置为 opencvjar-runtime 的 classifier 属性。它设置为
natives-windows-x86_64。这是你应该用于 64 位平台的值。如果你想要 32 位平台的版本,只需使用natives-windows-x86。 -
配置构建插件:
opencvjar-runtime依赖项仅包括.dll、.so等文件。这些文件将在执行mvn package命令时提取到你的目标位置。但是,这只会发生在你添加了maven-nativedependencies-plugin的情况下。此外,在创建可分发 JAR 时,将所有 JAR 库复制到你的/lib文件夹中也很重要。这将由maven-dependency-plugin处理。最后一个细节是在创建 JAR 时指定主类,这是由maven-jar-plugin执行的。所有构建插件配置应添加如下:<build> <plugins> <plugin> <artifactId>maven-jar-plugin</artifactId> <version>2.4</version> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> <classpathPrefix>lib/</classpathPrefix> <mainClass>com.mycompany.app.App</mainClass> </manifest> </archive> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <version>2.1</version> <executions> <execution> <id>copy-dependencies</id> <phase>package</phase> <goals> <goal>copy-dependencies</goal> </goals> <configuration> <outputDirectory>${project.build.directory}/lib</outputDirectory> <overWriteReleases>false</overWriteReleases> <overWriteSnapshots>false</overWriteSnapshots> <overWriteIfNewer>true</overWriteIfNewer> </configuration> </execution> </executions> </plugin> <plugin> <groupId>com.googlecode.mavennatives</groupId> <artifactId>maven-nativedependencies-plugin</artifactId> <version>0.0.7</version> <executions> <execution> <id>unpacknatives</id> <phase>generate-resources</phase> <goals> <goal>copy</goal> </goals> </execution> </executions> </plugin> </plugins> </build>你可以在本章示例代码的
chapter1/maven-sample目录中看到最终的pom.xml文件。 -
创建一个包:现在,你应该通过创建一个包来检查一切是否正确。只需输入以下命令:
mvn package前面的步骤应该会下载所有插件和依赖项,从原型中编译你的
App.java文件,在target文件夹中生成你的my-opencv-app-1.0-SNAPSHOT.jar,以及将所有依赖库复制到你的target/lib文件夹中;检查junit、opencvjar和opencvjar-runtimeJAR 文件。此外,本地库被提取到target/natives文件夹中,因此可以在那里找到opencv_java300.dll。你的编译类也可以在target/classes文件夹中找到。其他生成的文件夹与你的测试相关。 -
自定义你的代码:现在,我们将更改源文件以使用简单的 OpenCV 函数。导航到
D:\mvnopencv\my-opencv-app\src\main\java\com\mycompany\app并编辑App.java文件。只需添加以下代码:package com.mycompany.app; import org.opencv.core.Core; import org.opencv.core.Mat; import org.opencv.core.CvType; import org.opencv.core.Scalar; public class App { static{ System.loadLibrary(Core.NATIVE_LIBRARY_NAME); } public static void main(String[] args) { System.out.println("Welcome to OpenCV " + Core.VERSION); Mat m = new Mat(5, 10, CvType.CV_8UC1, new Scalar(0)); System.out.println("OpenCV Mat: " + m); Mat mr1 = m.row(1); mr1.setTo(new Scalar(1)); Mat mc5 = m.col(5); mc5.setTo(new Scalar(5)); System.out.println("OpenCV Mat data:\n" + m.dump()); } }这与我们在
App类中放入的SimpleSample相同的代码。现在我们只需要运行它。记住通过运行以下命令重新编译它:mvn package -
执行你的代码:执行生成的 JAR 文件,通过
-Djava.library.path属性指向/native文件夹中的本地文件。这应该像输入以下命令一样简单:D:\mvnopencv\my-opencv-app>java -Djava.library.path=target\natives -jar target\my-opencv-app-1.0-SNAPSHOT.jar干得好!现在你应该得到与运行
SimpleSample类相同的输出。如果你想通过.bat文件执行你的项目,只需在名为run.bat的文件中输入前面的命令,例如,并将其保存到D:\mvnopencv\my-opencv-app文件夹中。 -
生成一个 Eclipse 项目:现在,你将能够利用一些 Maven 功能,例如通过简单地输入以下命令来创建 Eclipse 项目:
mvn eclipse:eclipse
为了将项目导入到 Eclipse 中,打开你的工作空间,然后转到文件 | 导入...。然后,选择现有项目到工作空间,在选择根目录单选按钮中点击下一步 | 浏览...,并浏览到D:\mvnopencv\my-opencv-app。它应该将此文件夹识别为 Eclipse 项目。然后只需点击完成。
如果你现在想运行你的项目,请注意这里有两条警告。默认情况下,Eclipse 不识别 Maven。因此,你会看到一个错误,告诉你“项目无法构建,直到解决构建路径错误”,“未绑定的类路径变量:'M2_REPO/org/javaopencvbook/opencvjar/3.0.0/opencvjar-3.0.0.jar'在项目'my-opencv-app'中”。
这个错误仅仅意味着你的 M2_REPO 变量没有定义。转到 窗口 | 首选项,在搜索框中输入 classpath 变量。在树中选择它将带出定义此变量的选项卡。点击 新建...,将出现 新建变量条目 对话框。在 名称 输入中,将其命名为 M2_REPO,在 路径 输入中,选择 文件夹... 并浏览到你的 Maven 仓库。这应该位于一个类似于 C:/Users/baggio/.m2/ 的文件夹中。点击 确定,然后在 首选项 对话框中再次点击 确定。它将要求完全重建。点击 是,然后错误应该会消失。
如果你尝试通过右键点击运行 | Java 应用程序来运行你的 App.java 类,它应该会给你以下异常:Exception in thread "main" java.lang.UnsatisfiedLinkError: no opencv_java300 in java.library.path。
这仅仅意味着 Eclipse 没有找到你的本地文件。修复它就像展开你的项目并定位到 引用库 | opencvjar-3.0.0.jar 一样简单。右键点击它并选择 属性。在左侧选择 本地库,在 位置 路径中,点击 工作空间...,my-opencv-app | target | natives。记住,这个文件夹将只在你之前运行了 mvn package 命令时存在。再次运行 App 类,它应该会工作。
创建一个指向本地仓库的 Java OpenCV Maven 项目
上一节中给出的相同说明也适用于此处。唯一的区别是,你不需要将任何额外的仓库添加到你的 pom.xml 中,因为它们将位于你的本地仓库中,并且你必须安装并创建 Packt' 仓库中的所有 JAR 文件。我们假设你已经获得了 opencv-300.jar 和你架构所需的本地文件,也就是说,如果你在 Linux 上,你已经有了编译好的 opencv_java300.so。
为了将你的工件放入本地仓库,你必须使用 install 插件的 goal install-file。首先,你应该安装 opencv jar 文件。它应该位于你的 build 目录中,在一个看起来像 D:\opencv\build\bin 的文件夹中。在那个文件夹中,输入以下命令:
mvn install:install-file -Dfile=opencvjar-3.0.0.jar -DgroupId=opencvjar -DartifactId=opencvjar -Dversion=3.0.0 -Dpackaging=jar
确保你在 pom.xml 的依赖项中引用它时使用相同的 groupId 和 artifactId。现在,为了安装本地文件,几乎将使用相同的程序。在安装本地文件之前,建议将其转换为 .jar 文件。如果你使用 Linux,只需从 opencv_java300.so 创建一个 ZIP 文件并将其重命名为 opencv_java300.jar。实际上,JAR 文件是一个遵循某些标准的 ZIP 文件。在你创建了你的 JAR 文件后,就是时候将其安装到你的本地 Maven 仓库中了。只需输入以下命令:
mvn install:install-file -Dfile=opencvjar -runtime-natives-linux-x86.jar -DgroupId=opencvjar -DartifactId=opencvjar-runtime -Dversion=3.0.0 -Dpackaging=jar -Dclassifier=natives-linux-x86
注意 natives-linux-x86 分类器。这对于指定依赖项的架构非常重要。在输入它之后,你应该已经安装了所有依赖项。现在,只需简单地更新您的 pom.xml 文件,将其引用为 groupId opencvjar 而不是 org.javaopencvbook。遵循上一节的说明应该会使您准备好从本地仓库使用 Maven。
摘要
本章提供了几种不同的方法来为 Java 设置 OpenCV,即通过安装编译后的二进制文件或从源代码编译。它还指向了在 Eclipse 和 NetBeans IDE 中进行主要配置以及使用构建工具(如 Ant 和 Maven)的说明。用户应该准备好轻松地在自己的 Java 项目中使用 OpenCV。
下一章将更深入地探讨 OpenCV,并解决基本任务,例如通过矩阵处理图像、读取图像文件、从摄像头获取帧以及为您的计算机视觉应用程序创建漂亮的 Swing GUI。
第二章:处理矩阵、文件、摄像头和 GUI
本章将使你能够执行计算机视觉中所需的基本操作,例如处理矩阵、打开文件、从摄像头捕获视频、播放视频以及为原型应用程序创建 GUI。
本章将涵盖以下主题:
-
基本矩阵操作
-
像素操作
-
如何从文件中加载和显示图像
-
如何从摄像头捕获视频
-
视频播放
-
Swing GUI 与 OpenCV 的集成
到本章结束时,你应该能够通过加载图像并创建用于操作它们的良好 GUI 来启动这个计算机视觉应用程序。
基本矩阵操作
从计算机视觉的背景来看,我们可以将图像视为数值矩阵,它代表其像素。对于灰度图像,我们通常将值分配为从 0(黑色)到 255(白色),中间的数字表示两者的混合。这些通常是 8 位图像。因此,矩阵中的每个元素都对应于灰度图像上的每个像素,列数代表图像宽度,行数代表图像高度。为了表示彩色图像,我们通常将每个像素视为红色、绿色和蓝色三种基本颜色的组合。因此,矩阵中的每个像素由一个颜色三元组表示。
注意
重要的是要注意,使用 8 位,我们得到 2 的 8 次方 (2**⁸),即 256。因此,我们可以表示从 0 到 255 的范围,包括用于 8 位灰度图像的黑白级别值。除此之外,我们还可以将这些级别表示为浮点数,并使用 0.0 表示黑色和 1.0 表示白色。
OpenCV 有多种表示图像的方法,因此你可以通过位数来定制强度级别,考虑是否需要有符号、无符号或浮点数据类型,以及通道数。OpenCV 的约定可以通过以下表达式看到:
CV_<bit_depth>{U|S|F}C(<number_of_channels>)
在这里,U 代表无符号,S 代表有符号,F 代表浮点。例如,如果需要一个 8 位无符号单通道图像,数据类型表示将是 CV_8UC1,而由 32 位浮点数表示的彩色图像的数据类型定义将为 CV_32FC3。如果省略通道数,则默认为 1。我们可以在以下列表中看到根据每个位深度和数据类型的范围:
-
CV_8U: 这些是范围从 0 到 255 的 8 位无符号整数 -
CV_8S: 这些是范围从 -128 到 127 的 8 位有符号整数 -
CV_16U: 这些是范围从 0 到 65,535 的 16 位无符号整数 -
CV_16S: 这些是范围从 -32,768 到 32,767 的 16 位有符号整数 -
CV_32S: 这些是范围从 -2,147,483,648 到 2,147,483,647 的 32 位有符号整数 -
CV_32F: 这些是范围从-FLT_MAX到FLT_MAX的 32 位浮点数,包括INF和NAN值 -
CV_64F: 这些是范围从-DBL_MAX到DBL_MAX的 64 位浮点数,包括INF和NAN值
你通常从加载图像开始项目,但了解如何处理这些值是很重要的。确保你导入了 org.opencv.core.CvType 和 org.opencv.core.Mat。矩阵也有几个构造函数可用,例如:
Mat image2 = new Mat(480,640,CvType.CV_8UC3);
Mat image3 = new Mat(new Size(640,480), CvType.CV_8UC3);
前两个构造函数都将构建一个适合 640 像素宽和 480 像素高的图像的矩阵。请注意,宽度对应于列,高度对应于行。还要注意带有 Size 参数的构造函数,它期望宽度和高度的顺序。如果你想要检查一些矩阵属性,可以使用 rows()、cols() 和 elemSize() 方法:
System.out.println(image2 + "rows " + image2.rows() + " cols " + image2.cols() + " elementsize " + image2.elemSize());
前一行输出的结果是:
Mat [ 480*640*CV_8UC3, isCont=true, isSubmat=false, nativeObj=0xceeec70, dataAddr=0xeb50090 ]rows 480 cols 640 elementsize 3
isCont 属性告诉我们这个矩阵在表示图像时是否使用额外的填充,以便在某些平台上进行硬件加速;然而,我们目前不会详细讨论它。isSubmat 属性指的是这个矩阵是否由另一个矩阵创建,以及它是否引用另一个矩阵的数据。nativeObj 对象指的是原生对象地址,这是一个 Java Native Interface (JNI) 的细节,而 dataAddr 指向内部数据地址。元素大小以字节数衡量。
另一个矩阵构造函数是传递一个标量作为其元素之一。这个语法的样子如下:
Mat image = new Mat(new Size(3,3), CvType.CV_8UC3, new Scalar(new double[]{128,3,4}));
这个构造函数将使用三元组 {128, 3, 4} 初始化矩阵的每个元素。打印矩阵内容的一个非常有用方法是使用 Mat 的辅助方法 dump()。其输出将类似于以下内容:
[128, 3, 4, 128, 3, 4, 128, 3, 4;
128, 3, 4, 128, 3, 4, 128, 3, 4;
128, 3, 4, 128, 3, 4, 128, 3, 4]
重要的是要注意,在创建具有指定大小和类型的矩阵时,它也会立即为其内容分配内存。
像素操作
像素操作通常需要一个人访问图像中的像素。有几种方法可以做到这一点,每种方法都有其优点和缺点。一个直接的方法是使用 put(row, col, value) 方法。例如,为了用 values {1, 2, 3} 填充我们前面的矩阵,我们将使用以下代码:
for(int i=0;i<image.rows();i++){
for(int j=0;j<image.cols();j++){
image.put(i, j, new byte[]{1,2,3});
}
}
小贴士
注意,在字节数组 {1, 2, 3} 中,对于我们的矩阵,1 代表蓝色通道,2 代表绿色,3 代表红色通道,因为 OpenCV 在内部以 BGR (蓝色、绿色和红色)格式存储其矩阵。
对于小矩阵,以这种方式访问像素是可以的。唯一的问题是大型图像 JNI 调用的开销。记住,即使是 640 x 480 像素的小图像也有 307,200 个像素,如果我们考虑彩色图像,它在一个矩阵中有 921,600 个值。想象一下,为每个 307,200 个像素进行过载调用可能需要大约 50 毫秒。另一方面,如果我们在一个 Java 侧上操作整个矩阵,然后在一个调用中将其复制到本地侧,它将需要大约 13 毫秒。
如果你想在 Java 侧操作像素,请执行以下步骤:
-
在字节数组中分配与矩阵相同大小的内存。
-
将图像内容放入该数组中(可选)。
-
操作字节数组内容。
-
进行单个
put调用,将整个字节数组复制到矩阵中。
一个简单的例子将迭代所有图像像素并将蓝色通道设置为 0,这意味着我们将设置模 3 等于 0 的每个元素的值为 0,即{0, 3, 6, 9, …},如下面的代码片段所示:
public void filter(Mat image){
int totalBytes = (int)(image.total() * image.elemSize());
byte buffer[] = new byte[totalBytes];
image.get(0, 0,buffer);
for(int i=0;i<totalBytes;i++){
if(i%3==0) buffer[i]=0;
}
image.put(0, 0, buffer);
}
首先,我们通过将图像的总像素数(image.total)乘以字节数(image.elemenSize)来找出图像中的字节数。然后,我们使用该大小构建一个字节数组。我们使用get(row, col, byte[])方法将矩阵内容复制到我们最近创建的字节数组中。然后,我们迭代所有字节并检查与蓝色通道相关的条件(i%3==0)。记住,OpenCV 将颜色内部存储为{Blue, Green, Red}。我们最终再次调用 JNI 的image.put,将整个字节数组复制到 OpenCV 的本地存储。以下图像是 Mromanchenko 上传的,许可协议为 CC BY-SA 3.0,展示了此过滤器的示例:

注意,Java 没有无符号字节数据类型,所以在处理它时要小心。安全的方法是将它转换为整数,并使用与0xff的按位与操作符(&)。一个简单的例子是int unsignedValue = myUnsignedByte & 0xff;。现在,unsignedValue可以在 0 到 255 的范围内进行检查。
从文件加载和显示图像
大多数计算机视觉应用都需要从某处检索图像。如果你需要从文件中获取它们,OpenCV 提供了几个图像文件加载器。不幸的是,一些加载器依赖于有时不随操作系统一起提供的编解码器,这可能导致它们无法加载。从文档中我们可以看到,以下文件得到了一些限制的支持:
-
Windows 位图:
*.bmp,*.dib -
JPEG 文件:
*.jpeg,*.jpg,*.jpe -
JPEG 2000 文件:
*.jp2 -
可移植网络图形:
*.png -
可移植图像格式:
*.pbm,*.pgm,*.ppm -
Sun 光栅:
*.sr,*.ras -
TIFF 文件:
*.tiff,*.tif
注意,Windows 位图、可移植图像格式和 Sun 光栅格式在所有平台上都受支持,但其他格式则取决于一些细节。在 Microsoft Windows 和 Mac OS X 上,OpenCV 总是能够读取 jpeg、png 和 tiff 格式。在 Linux 上,OpenCV 将寻找操作系统提供的编解码器,如文档所述,因此请记住安装相关的包(例如,在 Debian 和 Ubuntu 中不要忘记开发文件,例如 "libjpeg-dev"),以获得编解码器支持,或者在 CMake 中打开 OPENCV_BUILD_3RDPARTY_LIBS 标志,正如 imread 的官方文档中指出的。
imread 方法提供了一种通过文件访问图像的方式。使用 Imgcodecs.imread(文件名)并检查读取的图像的 dataAddr() 是否与零不同,以确保图像已正确加载,也就是说,文件名已正确输入且其格式被支持。
打开文件的一个简单方法可能看起来像以下代码所示。确保你已导入 org.opencv.imgcodecs.Imgcodecs 和 org.opencv.core.Mat:
public Mat openFile(String fileName) throws Exception{
Mat newImage = Imgcodecs.imread(fileName);
if(newImage.dataAddr()==0){
throw new Exception ("Couldn't open file "+fileName);
}
return newImage;
}
使用 Swing 显示图像
OpenCV 开发者习惯于使用 OpenCV 提供的简单跨平台 GUI,称为 HighGUI,以及一个方便的方法 imshow。它可以轻松构建一个窗口并在其中显示图像,这对于创建快速原型来说很方便。由于 Java 拥有一个流行的 GUI API,称为 Swing,我们最好使用它。此外,Java 2.4.7.0 版本之前没有提供 imshow 方法。另一方面,创建这样的功能相当简单。请参考 chapter2/swing-imageshow 中的参考代码。
让我们把工作分解为两个类:App 和 ImageViewer。App 类将负责加载文件,而 ImageViewer 将负责显示它。应用程序的工作很简单,只需使用 Imgcodecs 的 imread 方法,如下所示:
package org.javaopencvbook;
import java.io.File;
…
import org.opencv.imgcodecs.Imgcodecs;
public class App
{
static{ System.loadLibrary(Core.NATIVE_LIBRARY_NAME); }
public static void main(String[] args) throws Exception {
String filePath = "src/main/resources/images/cathedral.jpg";
Mat newImage = Imgcodecs.imread(filePath);
if(newImage.dataAddr()==0){
System.out.println("Couldn't open file " + filePath);
} else{
ImageViewer imageViewer = new ImageViewer();
imageViewer.show(newImage, "Loaded image");
}
}
}
注意,App 类只会读取 Mat 对象中的示例图像文件,并且它会调用 ImageViewer 方法来显示它。现在,让我们看看 ImageViewer 的 show 方法是如何工作的:
package org.javaopencvbook.util;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Image;
import java.awt.image.BufferedImage;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JScrollPane;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.WindowConstants;
import org.opencv.core.Mat;
import org.opencv.imgproc.Imgproc;
public class ImageViewer {
private JLabel imageView;
public void show(Mat image){
show(image, "");
}
public void show(Mat image,String windowName){
setSystemLookAndFeel();
JFrame frame = createJFrame(windowName);
Image loadedImage = toBufferedImage(image);
imageView.setIcon(new ImageIcon(loadedImage));
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
private JFrame createJFrame(String windowName) {
JFrame frame = new JFrame(windowName);
imageView = new JLabel();
final JScrollPane imageScrollPane = new JScrollPane(imageView);
imageScrollPane.setPreferredSize(new Dimension(640, 480));
frame.add(imageScrollPane, BorderLayout.CENTER);
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
return frame;
}
private void setSystemLookAndFeel() {
try {
UIManager.setLookAndFeel (UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (UnsupportedLookAndFeelException e) {
e.printStackTrace();
}
}
public Image toBufferedImage(Mat matrix){
int type = BufferedImage.TYPE_BYTE_GRAY;
if ( matrix.channels() > 1 ) {
type = BufferedImage.TYPE_3BYTE_BGR;
}
int bufferSize = matrix.channels()*matrix.cols()*matrix.rows();
byte [] buffer = new byte[bufferSize];
matrix.get(0,0,buffer); // get all the pixels
BufferedImage image = new BufferedImage(matrix.cols(),matrix.rows(), type);
final byte[] targetPixels = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
System.arraycopy(buffer, 0, targetPixels, 0, buffer.length);
return image;
}
}
注意 show 和 toBufferedImage 方法。Show 将尝试将 Swing 的外观和感觉设置为默认的本地外观,这是装饰性的。然后,它将创建一个包含 JScrollPane 和 JLabel 的 JFrame。然后它将调用 toBufferedImage,该函数将 OpenCV Mat 对象转换为 AWT 的 BufferedImage。这种转换是通过创建一个存储矩阵内容的字节数组来完成的。通过将通道数乘以列数和行数来分配适当的大小。matrix.get 方法将所有元素放入字节数组中。最后,通过 getDataBuffer() 和 getData() 方法访问图像的栅格数据缓冲区。然后通过对 System.arraycopy 方法的快速系统调用进行填充。生成的图像随后被分配给 JLabel,然后它很容易地显示出来。请注意,此方法期望一个存储为单通道无符号 8 位或三通道无符号 8 位的矩阵。如果你的图像存储为浮点数,你应该在调用此方法之前使用以下代码将其转换,假设你需要转换的图像是一个名为 originalImage 的 Mat 对象:
Mat byteImage = new Mat();
originalImage.convertTo(byteImage, CvType.CV_8UC3);
这样,你可以从你的转换后的 byteImage 属性中调用 toBufferedImage 方法。
图像查看器可以轻松地安装在任何 Java OpenCV 项目中,并且它将帮助你显示图像以进行调试。这个程序的输出可以在下一张屏幕截图中看到:

从摄像头捕获视频
从网络摄像头捕获帧的过程非常复杂,它涉及到硬件细节以及大量的解码或解压缩算法。幸运的是,OpenCV 将所有这些封装在一个简单而强大的类中,称为 VideoCapture。这个类不仅可以从网络摄像头抓取图像,还可以读取视频文件。如果需要更高级的相机访问,你可能想使用它的专用驱动程序。
你可以将视频流视为一系列图片,并且你可以检索每个 Mat 中的图像并按你喜欢的方式处理它。为了使用 VideoCapture 类捕获网络摄像头流,你需要使用 VideoCapture(int device) 构造函数实例化它。请注意,构造函数参数指的是 camera 索引,如果你有多个摄像头。所以,如果你有一个内置摄像头和一个 USB 摄像头,并且创建一个 videocapture 对象,例如 new VideoCapture(1),那么这个对象将指向你的内置摄像头,而 new VideoCapture(0) 将指向你刚刚插入的 USB 摄像头或相反。在尝试在 OpenCV 中捕获图像之前,确保摄像头在制造商测试应用程序中工作,并检查摄像头驱动程序是否也已安装。
在实例化你的VideoCapture类之后,使用isOpened()方法检查它是否已成功实例化。如果访问相机时出现问题,这将返回false。不幸的是,不会有更多的信息,所以请仔细检查你的驱动程序。现在一切正常,调用read()方法以循环检索每个捕获的帧。请注意,此方法结合了VideoCapture grab()和retrieve()方法。grab()方法仅捕获下一个帧,这很快,而retrieve()方法解码并返回捕获的帧。当同步很重要或使用多个相机时,这些方法更有意义,因为这将更容易捕获尽可能接近的帧,首先通过为所有相机调用grab(),然后调用retrieve()。如果在使用read()方法时出现问题,即相机断开连接,则该方法返回false。
在使用VideoCapture类时,你需要记住的一个重要点是设置所需的相机分辨率。这可以通过set()属性设置方法实现,它需要Videoio.CAP_PROP_FRAME_WIDTH和Videoio.CAP_PROP_FRAME_HEIGHT参数。如果你想设置 640 x 480 的分辨率,你需要进行两次调用,如下所示:
VideoCapture capture = new VideoCapture(0);
capture.set(Videoio.CAP_PROP_FRAME_WIDTH,640);
capture.set(Videoio.CAP_PROP_FRAME_HEIGHT,480);
在尝试设置新分辨率之前,检查你的设备功能。如果你设置了一个相机无法处理的分辨率,这可能会导致相机挂起或回退到一个可以捕获图像的分辨率。
本章示例代码中可用的videocapture项目展示了如何检索网络摄像头流并将其显示在屏幕上,这与前面的swing-imageshow示例中的情况类似。在这个项目中,toBufferedImage方法已经被重构为一个ImageProcessor类,该类仅处理从VideoCapture类检索的Mat到BufferedImage的转换,这是在 Swing 中显示图像所必需的。main类也非常简单;它只构建一个窗口,实例化一个VideoCapture类,设置其属性,并进入主循环。这将从相机捕获一个帧,将其转换为BufferedImage并在JLabel中显示,如下面的代码所示:
package org.javaopencvbook;
import java.awt.Image;
import java.io.File;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import org.javaopencvbook.utils.ImageProcessor;
import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.videoio.Videoio;
import org.opencv.videoio.VideoCapture;
public class App
{
static{ System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
}
private JFrame frame;
private JLabel imageLabel;
public static void main(String[] args) {
App app = new App();
app.initGUI();
app.runMainLoop(args);
}
private void initGUI() {
frame = new JFrame("Camera Input Example");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(400,400);
imageLabel = new JLabel();
frame.add(imageLabel);
frame.setVisible(true);
}
private void runMainLoop(String[] args) {
ImageProcessor imageProcessor = new ImageProcessor();
Mat webcamMatImage = new Mat();
Image tempImage;
VideoCapture capture = new VideoCapture(0);
capture.set(Videoio.CAP_PROP_FRAME_WIDTH,320);
capture.set(Videoio.CAP_PROP_FRAME_HEIGHT,240);
if( capture.isOpened()){
while (true){
capture.read(webcamMatImage);
if( !webcamMatImage.empty() ){
tempImage= imageProcessor.toBufferedImage(webcamMatImage);
ImageIcon imageIcon = new ImageIcon(tempImage, "Captured video");
imageLabel.setIcon(imageIcon);
frame.pack(); //this will resize the window to fit the image
}
else{
System.out.println(" -- Frame not captured -- Break!");
break;
}
}
}
else{
System.out.println("Couldn't open capture.");
}
}
}
注意,调用frame.pack()将实现捕获的帧大小,并根据它调整窗口大小。以下截图显示了前面代码的成功执行:

注意,当你打开VideoCapture设备时,它可能不会优雅地释放进程,所以当你关闭它时,你的 Java 应用程序可能仍在运行。作为最后的手段,你可能需要根据你的平台杀死你的进程。在 Windows 上,这就像打开任务管理器一样简单,你可以通过按CTRL + ALT + DEL打开它,并定位你的 Java 进程。为了做到这一点,OS X 用户需要按CMD + ALT + ESC,而 Linux 用户只需发出一个kill命令。对于故障排除,如果你在使用了一段时间后遇到启动捕获设备的问题,重新连接你的 USB 插头可以使它工作。
视频播放
计算机视觉中另一个重要的 I/O 任务是能够打开和处理视频文件。幸运的是,OpenCV 可以通过VideoCapture类轻松处理视频。与之前使用设备号构建不同,我们需要使用文件路径来创建它。我们还可以使用空构造函数,并让open(String filename)方法负责指向文件。
本章源代码中提供的videoplayback项目结构与之前解释的swing-imageshow项目相同。它只是在初始化VideoCapture实例时有所不同:
VideoCapture capture = new VideoCapture("src/main/resources/videos/tree.avi");
我们还在每帧之间加入了 50ms 的延迟,这样整个视频就不会播放得太快。还有可以用来操作InterruptedException的代码。请注意,视频文件不会以与视频播放器设备中相同的速度播放。这是因为capture.read(webcamMatImage);方法尽可能地快速调用。你还可以在代码中添加延迟,使其播放速度比正常速度慢。尽管这在本节中没有涉及,但当你使用CV_CAP_PROP_FPS参数调用VideoCapture类的get方法时,应该返回视频每秒的帧数,这样你就可以以原始帧率播放它。
如果你的视频没有加载,可能是因为未安装的编解码器问题。尝试安装它或寻找其他编解码器,以便结束这个错误。另一种方法是使用工具将你的视频转换为支持的编解码器。也可能的情况是,opencv_ffmpeg300动态链接库从你的路径环境变量中丢失。尝试将其复制到你的项目主文件夹或添加到你的路径变量中。这应该会起作用。确保你的 java.library.path 指向包含此库的文件夹,就像你配置项目以查找原生 OpenCV 库一样,如第一章中所述,为 Java 设置 OpenCV。
Swing GUI 与 OpenCV 的集成
在调试或实验计算机视觉项目时,拥有丰富的图形用户界面非常重要,因为一些任务可能需要大量的调整。这样,处理滑块、按钮、标签和鼠标事件应该是任何计算机视觉研究者的基本技能。幸运的是,你可以在 Swing 中以相对简单的方式使用所有这些组件。在本节中,我们将介绍创建一个应用程序的重要部分,该应用程序通过滑块在多个级别上加载并模糊图像。此应用程序还利用鼠标事件来突出图像中的细节,以及一个漂亮的按钮来点击并清除所有内容。下一张截图为我们提供了一个关于应用程序工作原理的好主意。代码可以在本书代码包中的opencv-gui项目中找到。

加载图像的代码对我们来说并不陌生,可以在使用 Swing 显示图像部分找到。我们将更仔细地关注setupSlider()、setupImage()和setupButton()方法。阅读setupSlider方法,我们将在稍后详细讨论它:
private void setupSlider(JFrame frame) {
JLabel sliderLabel = new JLabel("Blur level", JLabel.CENTER);
sliderLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
int minimum = 0;
int maximum = 10;
int initial =0;
JSlider levelSlider = new JSlider(JSlider.HORIZONTAL, minimum, maximum, initial);
levelSlider.setMajorTickSpacing(2);
levelSlider.setMinorTickSpacing(1);
levelSlider.setPaintTicks(true);
levelSlider.setPaintLabels(true);
levelSlider.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
JSlider source = (JSlider)e.getSource();
int level = (int)source.getValue();
Mat output = imageProcessor.blur(image, level);
updateView(output);
}
});
frame.add(sliderLabel);
frame.add(levelSlider);
}
注意,滑块只是一个JSlider类,我们需要通过构造函数设置其最小值、最大值和初始值。我们还需要设置它是垂直还是水平滑块。一些外观细节,如主刻度和副刻度间距以及是否绘制标签和刻度,也进行了设置。滑块中的一个关键方法是它提供的匿名类中的stateChanged监听器,这基本上是用户更改滑块时发生的事情。在我们的情况下,我们将根据滑块设置的次数模糊图像。这是通过我们实现的ImageProcessor类完成的,该类基本上调用Imgproc.blur方法,这是一个非常简单的过滤器,它只计算一定数量相邻像素的平均值。滑块的值通过调用source.getValue()获得。
另一个重要的任务是响应用户的鼠标点击事件。这是通过向我们的JLabel图像视图添加MouseListener来实现的。以下是setupImage方法:
private void setupImage(JFrame frame) {
JLabel mouseWarning = new JLabel("Try clicking on the image!", JLabel.CENTER);
mouseWarning .setAlignmentX(Component.CENTER_ALIGNMENT);
mouseWarning.setFont(new Font("Serif", Font.PLAIN, 18));
frame.add(mouseWarning);
imageView = new JLabel();
final JScrollPane imageScrollPane = new JScrollPane(imageView);
imageScrollPane.setPreferredSize(new Dimension(640, 480));
imageView.addMouseListener(new MouseAdapter()
{
public void mousePressed(MouseEvent e)
{
Imgproc.circle(image,new Point(e.getX(),e.getY()),20, new Scalar(0,0,255), 4);
updateView(image);
}
});
frame.add(imageScrollPane);
}
在前面的代码中实现的mousePressed()方法负责响应用户的所有鼠标按下事件。我们可以通过getX()和getY()事件方法获取局部坐标。请注意,我们调用Imgproc.circle,这是一个 OpenCV 函数,它将在所需的矩阵中绘制一个圆,在所需的位置,并且我们可以定义其半径、颜色和粗细。
在本例中最后探索的 GUI 组件是通过JButton组件创建的按钮,该组件实现了actionPerformed接口。因为我们之前已经存储了原始图像,所以只需将原始图像复制回来就可以轻松清除图像:
private void setupButton(JFrame frame) {
JButton clearButton = new JButton("Clear");
clearButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
image = originalImage.clone();
updateView(originalImage);
}
});
clearButton.setAlignmentX(Component.CENTER_ALIGNMENT);
frame.add(clearButton);
}
摘要
哇!本章涵盖了大量的细节,但我们终于掌握了计算机视觉完整应用程序的开发。我们触及了 OpenCV 的核心结构主题,即用于基本像素操作的 Mat 类,以及它与 Swing 的 BufferedImage 类的紧密关系。除此之外,我们还涵盖了打开图像文件并在 Swing 应用程序中显示它们等重要任务。使用 VideoCapture 类涵盖了实时视频流的重要领域,它展示了如何从摄像头以及视频文件中获取帧。最后,我们通过在 Java 中处理鼠标事件,创建了一个具有滑块、标签、按钮的丰富图形用户界面应用程序。
使用 Java OpenCV API 的工作基础已经奠定,我们现在可以继续进入下一章,本章将涉及图像处理的核心操作,例如使用平滑滤波器去除噪声,使用形态学运算符隔离元素,使用桶填充进行分割,图像金字塔,以及阈值化的基本任务。务必查看这些内容。
第三章. 图像滤波器和形态学算子
在学习设置 OpenCV 的 Java 基础和图形用户界面处理之后,现在是时候探索图像处理中的核心算子了。其中一些来自信号处理,我们称它们为滤波器,因为它们通常帮助您从图像中去除噪声。重要的是要知道,几种数字滤波器有它们的光学对应物。其他算子在处理二值图像时扮演着有用的角色,例如形态学算子,它可以帮助您隔离区域或将它们中的某些部分粘合在一起。我们还将详细介绍著名的桶填充工具,它在分割中非常有用。在处理大图像时,了解图像金字塔如何帮助您在不丢失重要信息的情况下减小图像大小并提高性能是很重要的。我们将以分割中最简单且最有用的技术之一结束本章,即应用阈值来分离区域,以及研究一个不会受到光照问题影响太多的动态阈值。
在本章中,我们将介绍:
-
平滑
-
形态学算子
-
洪水填充
-
图像金字塔
-
阈值化
到本章结束时,您将能够对图像执行多个过滤过程,例如去除噪声、生长、收缩和填充某些区域,以及根据给定的标准判断某些像素是否适合。
平滑
就像在一维信号中一样,我们在图像中总是容易受到一些噪声的影响,在我们对图像进行主要工作之前,我们通常会对它们应用一些预处理滤波器。我们可以将噪声视为图像对象中不存在的颜色或亮度信息的随机变化,这可能是由于数码相机或扫描仪的传感器和电路的不理想造成的。本节使用低通滤波器核的思想来平滑我们的图像。这些滤波器移除了高频内容,如边缘和噪声,尽管一些技术允许边缘不被模糊。我们将介绍 OpenCV 中可用的四种主要图像滤波器:平均滤波、高斯滤波、中值滤波和双边滤波。
注意
二维核 卷积是一种数学卷积形式。输出图像是通过扫描给定图像的每个像素并对其应用核算子来计算的,为每个操作结果生成一个输出像素。例如,核算子可以是一个由 1 组成的 3x3 矩阵,除以 9。这样,每个输出像素将是输入图像中每个像素的 9 个相邻像素的平均值,从而得到一个平均输出图像。
平均
大多数模糊技术将使用 2D 核卷积来过滤图像。最简单的方法是使用一个 3 x 3 的核,它总共有 9 个像素。假设我们想要 9 个像素的平均值,我们只需要将它们相加然后除以 9。这是通过以下核的卷积来实现的:

为了应用此转换,我们将使用 Imgproc 的blur函数。其语法如下:
public static void blur(Mat src, Mat dst, Size ksize)
参数很简单,就是源图像、目标图像和核大小,对于我们的 3 x 3 核来说,就像new Size(3.0, 3.0)一样简单。你可以选择性地添加Point锚点参数,如下所示:
public static void blur(Mat src, Mat dst, Size ksize, Point anchor, int borderType)
前一行将允许你将锚点以及一个int borderType整数变量放置在中心点之外。这个borderType参数让你定义当核的一部分在图像内部和外部时,你希望的行为。请注意,在第一行,前一个核将寻找将位于行顶部的值,因此 OpenCV 需要对这些值进行外推。有一些选项可用于外推边界。根据文档,我们有以下类型的边界,所有这些都可以从Core常量中获取,例如:Core.BORDER_REPLICATE。例如,考虑|作为图像的一个边界,并将abcdefgh作为像素值:
BORDER_REPLICATE: aaaaaa|abcdefgh|hhhhhhh
BORDER_REFLECT: fedcba|abcdefgh|hgfedcb
BORDER_REFLECT_101: gfedcb|abcdefgh|gfedcba
BORDER_WRAP: cdefgh|abcdefgh|abcdefg
BORDER_CONSTANT: 000000|abcdefgh|0000000
默认值是Core.BORDER_DEFAULT,它映射到Core.BORDER_REFLECT_101。有关如何使用此函数的更多信息,请查找本章imageFilter项目的源代码。以下是该主应用的截图,它允许你尝试这些过滤器中的每一个:

注意,此应用程序还提供了一些简单的高斯噪声,其概率密度函数等于正态分布,以查看每个滤波器的优点。
高斯
高斯背后的思想与平均滤波相同,只是对于每个像素不使用相同的权重,而是使用一个二维高斯函数作为核,该核给予中心像素最高的权重。以下图表显示了二维高斯曲线的行为:

为了使用此函数,请使用以下基本签名:
public static void GaussianBlur(Mat src,
Mat dst,
Size ksize,
double sigmaX [, double sigmaY])
Mat src和Mat dst参数很简单,因为它们描述了输入和输出图像。Size ksize参数描述了核的宽度和高度。因此,如果你想设置其大小,此参数必须是正数且为奇数,以便核可以是对称的并且有中心。如果你将此参数设置为零,大小将根据double sigmaX计算。Sigma 是其标准差,大约是高斯值的半最大值宽度,这意味着当其高度是最高高斯值的一半时,它是高斯值宽度的一半。可选地,你也可以提供第五个参数作为sigmaY,它是y轴的标准差。如果你不使用此参数,sigmaY将与sigmaX相等。此外,如果sigmaX和sigmaY都为零,它们将根据核的宽度和高度计算。getGaussianKernel函数在需要时返回所有高斯系数。GaussianBlur函数还可以提供一个第六个参数,这决定了边界的行为。此参数与平均部分中的int borderType参数的工作方式相同。
如何使用GaussianBlur的示例可以取自本章的示例imageFilter项目:
Imgproc.GaussianBlur(image, output, new Size(3.0, 3.0), 0);
前一行将 sigma 设置为0,并使函数通过以下公式根据核的大小来计算它:
sigma = 0.3*((ksize-1)*0.5 - 1) + 0.8
在这里,ksize是核的孔径大小,在我们的例子中将是3。
中值滤波
制作滤波器的另一个想法是选择核中的中值像素而不是平均值,这意味着选择位于强度排序像素行中间的像素。这是通过以下函数实现的:
public static void medianBlur(Mat src,
Mat dst,
int ksize)
Mat src和dst参数分别是输入和输出图像,而int ksize是核的孔径大小,它必须是奇数且大于 1。
有时,图像噪声非常高,它可能表现为大型的孤立异常点,这会导致平均值的明显偏移。为了克服这些问题,可以使用中值滤波器来忽略这些异常点。
双边滤波
虽然 median、Gaussian 和平均滤波器倾向于平滑噪声和边缘,但使用双边滤波的主要优点是它将保留它们,因为它们提供了重要的信息,例如,在医学成像中某些细胞边界的信息,这些信息不应该被过滤掉。这个滤波器的难点在于它在计算平均值时考虑了空间距离和像素强度差异,这意味着在计算输出图像时,它不会包括强度差异超过给定阈值的像素。注意本章的imageFilter示例项目在大理石棋盘上双边滤波的效果:

右侧图像展示了在保留边缘的同时过滤后的弹珠,这是使用其他过滤器时不会发生的情况。这种方法的一个缺点是,软纹理细节往往会丢失,就像在上一张图片第三行第二列的白色方块中那样。函数签名如下:
public static void bilateralFilter(Mat src, Mat dst, int d,
double sigmaColor,
double sigmaSpace,
[int borderType])
虽然Mat src和Mat dst分别是输入和输出图像,int d参数是考虑的邻域直径。如果它是非正的,直径将根据sigmaSpace参数计算。颜色空间中的滤波器 sigma 由double sigmaColor参数定义,这意味着对于更高的值,在计算像素的输出颜色时,会考虑邻域中更远的颜色,从而产生水彩效果。Double sigmaSpace是坐标空间中的 sigma 值,这意味着只要由于sigmaColor而没有跳过颜色,它们将具有与高斯相当的平均成分。记住,水彩效果在图像分割的第一步时可能非常有用。如果您需要控制边界类型,可以将int borderType参数作为最后一个参数添加,就像在之前的过滤器中那样。
当考虑强度差异以计算像素的新平均值时,会使用另一个高斯函数。请注意,由于这个额外步骤,在处理实时图像时,应该使用较小的核大小(例如,5)进行双边滤波,而对于离线应用,大小为 9 的核可能已经足够。请注意,当使用 3 x 3 邻域对大小为 3 的核进行卷积时,每个像素的卷积中只验证了 9 个像素。另一方面,当使用大小为 9 的核时,会验证 9 x 9 像素,这使得算法在搜索时大约需要 9 倍的时间。
形态学算子
一些图像操作被称为形态学操作,因为它们会改变底层对象的形状。我们将讨论腐蚀和膨胀,这些是在本节中也非常有用的形态学变换,以及一些派生变换。它们通常出现在隔离元素、去除噪声和连接图像中距离较远的元素的情况下。
这些算子通过将给定的核与图像进行卷积来工作。这个核通过一个锚点来描述,该锚点用于探测像素区域,这取决于其形状:

前面的图像显示了图像上的一个亮区,我们将称之为A。请注意,补区域是完全黑暗的。我们的内核由一个中心锚点的 3 x 3 块组成,描述为B。C区域是应用侵蚀形态学变换后的图像结果。请注意,这个操作发生在扫描图像的每个像素时,将内核锚点中心对准这些像素,然后在该内核区域内检索局部最小值。请注意,侵蚀会减少亮区。
相反的操作称为膨胀,这两种操作的区别在于,在膨胀中,它不是在内核区域内计算局部最小值,而是在该区域内计算局部最大值。这个操作将扩展一个 3 x 3 方形阻塞内核的亮区。
为了更好地了解这些算子的工作原理,一个不错的主意是尝试本章源代码中的morphology项目。它基本上是将 OpenCV 的官方 C++ morphology2示例翻译成 Java,并添加了一些小的 GUI 增强。请注意,在多通道图像的情况下,每个通道都是独立处理的。以下截图显示了正在运行的应用程序:

注意,我们的内核边界框是内核大小滑块参数的两倍加 1,因此,如果内核大小参数选择为 1,我们将有一个 3 x 3 的内核边界框。我们还用平方内核的术语描述了我们的示例,但它可以是任何形状,因此形状参数也是我们可以选择的。为了轻松创建这些内核,我们使用了 Imgproc 的getStructuringElement函数。这个函数将接受内核的形状、大小和零索引锚点位置作为其参数。内核形状可以是Imgproc.CV_SHAPE_RECT(用于矩形)、Imgproc.CV_SHAPE_ELLIPSE(用于椭圆)或Imgproc.CV_SHAPE_CROSS(用于十字形内核)。
我们将所有图像操作放入ImageProcessor类中,我们将在以下代码中突出显示:
public Mat erode(Mat input, int elementSize, int elementShape){
Mat outputImage = new Mat();
Mat element = getKernelFromShape(elementSize, elementShape);
Imgproc.erode(input,outputImage, element);
return outputImage;
}
public Mat dilate(Mat input, int elementSize, int elementShape) {
Mat outputImage = new Mat();
Mat element = getKernelFromShape(elementSize, elementShape);
Imgproc.dilate(input,outputImage, element);
return outputImage;
}
public Mat open(Mat input, int elementSize, int elementShape) {
Mat outputImage = new Mat();
Mat element = getKernelFromShape(elementSize, elementShape);
Imgproc.morphologyEx(input,outputImage, Imgproc.MORPH_OPEN, element);
return outputImage;
}
public Mat close(Mat input, int elementSize, int elementShape) {
Mat outputImage = new Mat();
Mat element = getKernelFromShape(elementSize, elementShape);
Imgproc.morphologyEx(input,outputImage, Imgproc.MORPH_CLOSE, element);
return outputImage;
}
private Mat getKernelFromShape(int elementSize, int elementShape) {
return Imgproc.getStructuringElement(elementShape, new Size(elementSize*2+1, elementSize*2+1), new Point(elementSize, elementSize) );
}
由于我们所有的方法都以相同的方式创建内核,我们已经提取了getKernelFromShape方法,它将简单地使用前面代码中描述的大小调用getStructuringElement函数。由于我们有一个自定义内核,我们将使用重载的Imgproc.erode函数,将输入图像、输出图像和内核作为第三个参数。以下截图是侵蚀函数在给定输入图像上的结果:

注意,这个算子经常被用来从图像中去除斑纹噪声,因为它会被腐蚀至无,而包含重要信息的较大区域实际上不会受到影响。请注意,平滑滤波器不会完全去除斑纹噪声,因为它们倾向于降低其幅度。此外,请注意,这些操作对核大小敏感,因此需要进行大小调整和一些实验。我们还可以查看以下截图中应用膨胀的结果:

注意,除了使区域变厚外,膨胀形态学变换在搜索连接组件(像素强度相似的大区域)方面也非常有用。当由于阴影、噪声或其他效果将大区域分割成更小的区域时,可能需要这样做。如前一个截图中的图像底部所示。应用膨胀将使它们连接成一个更大的元素。
我们还推导出了形态学变换,即开和闭。开是通过腐蚀后跟膨胀来定义的,而在闭合操作中,膨胀先发生。以下是一个开变换的截图:

这个操作通常在从二值图像中计数区域时使用。例如,我们可能用它来在计数之前分离彼此过于接近的区域。请注意,在我们的示例底部,只有较大的区域在操作中幸存下来,同时保持了原本分离的大区域之间的非连通性。另一方面,我们可以看到将闭合操作应用于同一图像的效果,如下面的截图所示:

检查这是否会连接邻近的区域。根据核大小,它可能在连接组件算法中用来减少由噪声生成的片段。与腐蚀和膨胀不同,开闭形态学变换倾向于保留其感兴趣区域的面积。
浮点填充
另一个非常重要的分割算法是浮点填充,也称为区域增长。你们中已经使用过流行计算机图形程序的人,比如 Microsoft Paint 或 GIMP,可能已经使用过桶填充或画桶工具,它用颜色填充一个区域。尽管乍一看这可能看起来是一个非常简单的算法,但它有一个非常有趣的实现,并且有几个参数可以使它很好地分割图像。
算法背后的思想是从一个给定的点——所谓的种子点——开始,检查连接组件,即具有相似颜色或亮度的区域,然后检查这个特定点的邻居。这些邻居可以是 4 个(北、南、东和西)或 8 个(北、东北、东、东南、南、西南、西和西北),它们会检查一个条件,然后递归地,对每个满足条件的邻居执行相同的程序。如果条件为真,它自然会将该点添加到给定的连接组件中。我们通常寻找与种子点或其邻居点相似的像素,这取决于哪种洪水填充模式将运行。我们称像素与种子点比较时为固定范围,而像素与邻居像素比较时为浮动范围。此条件还接受较低差异loDiff和较高差异upDiff参数,这些参数根据src(x',y') – loDiff < src (x,y) < src(x',y') + upDiff方程进入条件。在这个方程中,src(x,y)是测试的x,y坐标处的像素值,以检查它是否属于与种子点相同的域,而src(x',y')是在浮动范围中操作的灰度图像中已知属于该组件的像素之一。如果我们有一个固定范围的洪水填充,方程变为src(seed.x,seed.y) – loDiff < src (x,y) < src(seed.x,seed.y) + upDiff,其中seed.x和seed.y是种子点的坐标。此外,请注意,在彩色图像的情况下,每个像素的分量都会与条件进行比较,而loDiff和highDiff是三维标量。总的来说,如果新像素的亮度或颜色足够接近其邻居之一,该邻居已属于连接组件,或者在浮动范围洪水填充的情况下足够接近种子点的属性,则该像素将被添加到域中。
洪水填充的签名如下:
public static int floodFill(Mat image,
Mat mask,
Point seedPoint,
Scalar newVal,
Rect rect,
Scalar loDiff,
Scalar upDiff,
int flags)
Mat image参数是要执行洪水填充的图像的输入/输出Mat,而Mat mask是一个比Mat image高 2 行、宽 2 列的单通道 8 位矩阵,出于性能考虑。Point seedpoint参数包含种子点的坐标,而Rect rect是一个输出矩形,包含分割区域的最小边界框。Scalar loDiff和upDiff参数在先前的条件中已讨论。int flags参数包含算法操作模式的选项。包含floodFill方法门面类的源代码可在本章的floodfill项目中找到。以下是该应用的截图:

在前面的截图左侧,有一个类似于第二章中解释的JLabel,用于加载图像,但这个有一个MouseListener,它会将捕获的点击发送到FloodFillFacade类。在前面截图的右侧,当Mask单选按钮被选中时,会显示掩码。算法操作模式是通过Range radio按钮选择的,可以是相对的(检查邻居的条件)、固定的(条件与种子点比较)或空的(当loDiff和hiDiff都为零时)。还有一个用于 4 或 8 个邻居的连通性单选按钮,而下限和上限阈值分别对应于loDiff和hiDiff参数。
虽然FloodFillFacade的大多数字段只是getters和setters,但标志配置是你需要注意的事情。请注意,门面只是一个创建对更大部分代码的简化接口的对象,使其更容易使用。以下是FloodFillFacade的一些重要部分:
public class FloodFillFacade {
public static final int NULL_RANGE = 0;
public static final int FIXED_RANGE = 1;
public static final int FLOATING_RANGE = 2;
private boolean colored = true;
private boolean masked = true;
private int range = FIXED_RANGE;
private Random random = new Random();
private int connectivity = 4;
private int newMaskVal = 255;
private int lowerDiff = 20;
private int upperDiff = 20;
public int fill(Mat image, Mat mask, int x, int y) {
Point seedPoint = new Point(x,y);
int b = random.nextInt(256);
int g = random.nextInt(256);
int r = random.nextInt(256);
Rect rect = new Rect();
Scalar newVal = isColored() ? new Scalar(b, g, r) : new Scalar(r*0.299 + g*0.587 + b*0.114);
Scalar lowerDifference = new Scalar(lowerDiff,lowerDiff,lowerDiff);
Scalar upperDifference = new Scalar(upperDiff,upperDiff,upperDiff);
if(range == NULL_RANGE){
lowerDifference = new Scalar (0,0,0);
upperDifference = new Scalar (0,0,0);
}
int flags = connectivity + (newMaskVal << 8) +
(range == FIXED_RANGE ? Imgproc.FLOODFILL_FIXED_RANGE : 0);
int area = 0;
if(masked){
area = Imgproc.floodFill(image, mask, seedPoint, newVal, rect, lowerDifference, upperDifference, flags);
}
else{
area = Imgproc.floodFill(image, new Mat(), seedPoint, newVal, rect, lowerDifference, upperDifference, flags);
}
return area;
}
...
}
在这里,首先,创建newVal作为要填充到连通组件中的新颜色。使用 Java 随机类生成颜色,如果是灰度图像,则将其转换为灰度。然后,我们设置lowerDifference和higherDifference标量,它们将根据之前描述的方程使用。然后,定义flags变量。请注意,连通性设置在低位,而newMaskVal向左移动 8 位。此参数是在使用掩码时用于填充掩码的颜色。然后,如果需要固定范围的洪水填充,则设置其标志。然后,我们可以从掩码或未掩码的洪水填充版本中进行选择。注意new Mat(),当不使用掩码时传递。观察seedPoint参数是由我们的MouseListener提供的坐标构建的。
图像金字塔
图像金字塔简单来说是由对原始图像进行下采样得到的一系列图像集合,使得每个图像的面积是其前一个图像的四分之一。它主要应用于图像分割,因为它可以在低分辨率下生成非常具有意义的图像表示,从而使得耗时算法可以在其上运行。这使得我们能够将此结果映射回金字塔中的更高分辨率图像,并使得在那里细化结果成为可能。此外,通过高斯差分可以生成拉普拉斯近似。请注意,拉普拉斯图像是会显示其边缘的图像。
为了生成下采样图像,我们将其称为高斯金字塔中的 i+1 层(Gi+1),我们首先使用高斯核对 Gi 进行卷积,就像在高斯滤波中一样,然后移除所有偶数行和列。然后,我们得到一个面积是上一层四分之一的图像。在下采样之前进行平均很重要,因为这样可以从奇数行和列中捕获信息。获取下采样图像的函数具有以下签名:
public static void pyrDown(Mat src, Mat dst ,[Size dstsize, int borderType])
Mat src 和 Mat dst 参数是输入和输出图像。请注意,输出图像的宽度将是 (src.width+1)/2,高度是 (src.height+1)/2,其中 / 表示整数除法。当处理奇数维度时,你应该小心,因为从下采样图像生成的上采样图像将不具有相同的维度。以一个 11 x 11 的图像为例。当你使用 pyrDown 时,它将变成一个 6 x 6 的图像。如果你对其进行上采样,它将变成一个 12 x 12 的图像,因此你不能将其添加或从原始图像中减去。请注意,当使用 pyrDown 时,使用了一个 5 x 5 的高斯核。如果你想,pyrDown 函数通过 Size dstsize 和 int borderType 属性进行了重载。dstsize 属性将允许你定义输出图像大小,但你必须满足以下条件:
|dstsize.width * 2 – src.cols| < 2
|dstsize.height * 2 – src.rows| < 2
这意味着在决定输出图像大小时,你不会有太多的自由度。此外,borderType 与 平滑 部分中给出的考虑相同。
另一方面,pyrUp 函数将上采样一个图像然后对其进行模糊处理。首先,它将在偶数位置注入零行和列,然后与从金字塔下操作中相同的核进行卷积。请注意,pyrDown 是一种会丢失信息的转换,因此 pyrUp 无法恢复原始图像。其用法如下:
public static void pyrUp(Mat src, Mat dst)
它的参数与 pyrDown 参数类似。
如果你想构建拉普拉斯算子,请注意这可以通过以下方程实现:

UP 是上采样操作,⊗G5 是与 5 x 5 高斯核的卷积。由于 pyrUp 已经实现为上采样后跟高斯模糊,我们所需做的只是对原始图像进行下采样,然后上采样,最后从原始图像中减去它。这可以通过以下代码实现,如本章的 imagePyramid 示例所示:
Mat gp1 = new Mat();
Imgproc.pyrDown(image, gp1);
Imgproc.pyrUp(gp1, gp1);
Core.subtract(image, gp1, gp1);
在前面的代码中,我们假设 image 是我们正在处理的图像。在上采样并从图像中减去时,请小心,因为如果原始图像的维度是奇数,它们的维度将不同。Core.subtract 函数简单地从一个图像中减去另一个图像,如下面的截图所示:

为了看到一些使用金字塔的代码示例,请考虑查看本章的 imagePyramid 项目。前面的截图显示了正在运行拉普拉斯滤波器的应用程序。还可以通过按钮来感受金字塔的工作方式。
阈值处理
将灰度图像分割的最简单方法之一是使用阈值技术。它基本上会将低于给定值的像素视为感兴趣的对象的一部分,而将其他像素视为不属于它的一部分。尽管它可能受到光照问题以及物体内部变化引起的问题的影响,但在对页面扫描中的文本进行 OCR 或校准相机时寻找棋盘图案时,这可能是足够的。此外,一些更有趣的方法,如自适应阈值,也可以在受非均匀光照影响的图像中产生良好的结果。
基本阈值化是通过 Imgproc 的 threshold 函数实现的,其签名如下:
public static double threshold(Mat src,
Mat dst,
double thresh,
double maxval,
int type)
Mats src 和 dst 参数是输入和输出矩阵,而 thresh 是用于阈值化图像的级别。maxval 只在 Binary 和 Binary_Inv 模式下使用,这将在以下表格中解释。type 是用于描述阈值化类型的 Imgproc 常量,如下表所示,当在下一个条件下测试时,源像素值大于给定的阈值:
| 阈值类型 | 当为真时的输出 | 当为假时的输出 |
|---|---|---|
CV_THRESH_BINARY |
maxval |
0 |
CV_THRESH_BINARY_INV |
0 |
maxval |
CV_THRESH_BINARY |
threshold |
source value |
CV_TOZERO |
source value |
0 |
CV_TOZERO_INV |
0 |
source value |
以下图表将帮助您轻松理解前面的表格:

在进行阈值处理时,通过例如滑动条等工具尝试几个不同的值是很重要的。本章中的示例项目 threshold 使得改变函数的参数和测试结果变得非常简单。以下展示了项目的截图:

注意,尽管苹果在分割时可能是一个简单的问题,但在应用二值阈值方法时,苹果几乎完全被识别,除了中间线上方的光照点,这些点的像素明显高于 205 级,因为它们几乎是纯白色,这将是 255 级。此外,苹果下方的阴影区域也被识别为属于它的一部分。除了这些小问题外,它使用简单,通常将是任何计算机视觉应用步骤中的一部分。
这种类型分割的另一种有趣的方法与使用动态阈值值有关。而不是使用给定的值,阈值被计算为围绕每个像素的平方块的均值减去一个给定的常数。这种方法通过 OpenCV 中的adaptiveThreshold函数实现,该函数具有以下签名:
public static void adaptiveThreshold(Mat src,
Mat dst,
double maxValue,
int adaptiveMethod,
int thresholdType,
int blockSize,
double C)
Mat src和dst参数分别是输入和输出矩阵。Maxvalue的使用方式与普通阈值函数相同,这在前面章节中有描述。自适应方法可以是ADAPTIVE_THRESH_MEAN_C或ADAPTIVE_THRESH_GAUSSIAN_C。前者将计算平均值作为块中像素值总和除以像素数,而后者将使用高斯加权平均。BlockSize是用于平均值的blockSize乘以blockSize的正方形区域,其值必须是奇数且大于 1。C常数是从平均值中减去的值,用于组成动态阈值。注意使用blocksize为13和常数C为6时,对同一图像使用自适应阈值得到的结果:

注意,阴影区域现在要好得多,尽管苹果的不规则纹理可能会引起其他问题。示例代码使用了二值和ADAPTIVE_THRESH_MEAN_C自适应阈值,但将其更改为高斯阈值只需更改类型参数。
摘要
本章解释了在任何计算机视觉项目中都需要的基本图像处理操作的原理和实践。我们首先讨论了使用简单平均或高斯加权的滤波器,以及中值滤波器,并讨论了有趣的双边滤波器,它保持了边缘。然后,我们探讨了重要的形态学算子,如腐蚀、膨胀、开运算和闭运算,这些算子在隔离元素、去除噪声和连接图像中距离较远的元素时出现。随后,我们通过洪水填充讨论了众所周知的油漆桶操作。然后,我们探讨了时间和处理节省的图像金字塔,通过在每个层中将图像面积减少到四分之一来加快分割速度。最后,我们解释了重要的图像分割技术——阈值,并测试了自适应阈值。
在下一章中,我们将重点关注重要的图像变换,这将使我们能够在图像中找到边缘、线条和圆。然后,你将学习拉伸、收缩、扭曲和旋转操作,这将由傅里叶变换跟随,傅里叶变换是一种将图像从空间域转换为频率域的好工具。最后,我们将检查积分图像,它可以提高某些人脸跟踪算法。
第四章:图像变换
本章介绍了将图像转换为数据不同表示的方法,以涵盖计算机视觉和图像处理的重要问题。这些方法的一些示例是用于寻找图像边缘的伪影以及帮助我们找到图像中线条和圆的变换。在本章中,我们介绍了拉伸、收缩、扭曲和旋转操作。一个非常有用且著名的变换是傅里叶变换,它将信号在时域和频率域之间转换。在 OpenCV 中,你可以找到离散傅里叶变换(DFT)和离散余弦变换(DCT)。本章我们还介绍了一种与积分图像相关的变换,它允许快速求和子区域,这在人脸追踪算法中是一个非常有用的步骤。此外,本章还将介绍距离变换和直方图均衡化。
我们将涵盖以下主题:
-
梯度和 Sobel 导数
-
拉普拉斯和 Canny 变换
-
线和圆 Hough 变换
-
几何变换:拉伸、收缩、扭曲和旋转
-
离散傅里叶变换 (DFT) 和离散余弦变换 (DCT)
-
整数图像
-
距离变换
-
直方图均衡化
到本章结束时,你将学会一些变换,这些变换将使你能够在图像中找到边缘、线和圆。此外,你将能够拉伸、收缩、扭曲和旋转图像,以及将域从空间域转换为频率域。本章还将涵盖用于人脸追踪的其他重要变换。最后,距离变换和直方图均衡化也将被详细探讨。
梯度与 Sobel 导数
计算机视觉的一个关键构建块是寻找边缘,这与在图像中寻找导数的近似密切相关。从基本的微积分中我们知道,导数显示了给定函数或输入信号随某个维度的变化。当我们找到导数的局部最大值时,这将产生信号变化最大的区域,对于图像来说可能意味着边缘。希望有一种简单的方法可以通过核卷积来近似离散信号的导数。卷积基本上意味着将某些变换应用于图像的每一部分。最常用于微分的是 Sobel 滤波器 [1],它适用于水平、垂直甚至任何阶数的混合偏导数。
为了近似水平导数的值,以下 Sobel 核矩阵与输入图像进行卷积:

这意味着,对于每个输入像素,将计算其右上角邻居的值加上两倍其右侧邻居的值,加上其右下角邻居的值,减去其左上角邻居的值,减去其左侧邻居的值,减去其左下角邻居的值,从而得到一个结果图像。为了在 OpenCV 中使用此算子,您可以按照以下签名调用 Imgproc 的Sobel函数:
public static void Sobel(Mat src, Mat dst, int ddepth, int dx,int dy)
src参数是输入图像,dst是输出。Ddepth是输出图像的深度,当将其设置为-1时,它将与源图像具有相同的深度。dx和dy参数将告诉我们每个方向中的顺序。当将dy设置为0,dx设置为1时,我们使用的核就是前面矩阵中提到的核。本章的示例项目kernels展示了这些算子的可定制外观,如下面的截图所示:

拉普拉斯和 Canny 变换
另一个非常有用的算子用于查找边缘的是拉普拉斯变换。而不是依赖于一阶导数,OpenCV 的拉普拉斯变换实现了以下函数的离散算子:

当使用有限差分方法和 3x3 窗口时,矩阵可以近似为以下核的卷积:

前述函数的签名如下:
Laplacian(Mat source, Mat destination, int ddepth)
虽然源和目标矩阵是简单的参数,但ddepth是目标矩阵的深度。当您将此参数设置为-1时,它将与源图像具有相同的深度,尽管在应用此算子时您可能需要更多的深度。此外,此方法还有重载版本,它接收窗口大小、缩放因子和加法标量。
除了使用拉普拉斯方法外,您还可以使用 Canny 算法,这是一种由计算机科学家 John F. Canny 提出的优秀方法,他优化了边缘检测以实现低错误率、单次识别和正确定位。为了实现这一点,Canny 算法应用高斯滤波器以过滤噪声,通过 Sobel 计算强度梯度,抑制虚假响应,并应用双阈值,随后通过滞后抑制弱和不连续的边缘。有关更多信息,请参阅这篇论文[2]。该方法签名如下:
Canny(Mat image, Mat edges, double threshold1, double threshold2, int apertureSize, boolean L2gradient)
image参数是输入矩阵,edges是输出图像,threshold1是阈值化过程的第一阈值(小于此值的值将被忽略),而threshold2是阈值化过程的高阈值(高于此值的值将被视为强边缘,而较小的值和高于低阈值的值将检查与强边缘的连接)。孔径大小用于计算梯度时的 Sobel 算子,而boolean告诉我们使用哪个范数来计算梯度。您还可以查看源代码,以了解如何在内核的项目示例中使用此算子。
直线和圆的霍夫变换
如果您需要在图像中找到直线或圆,可以使用霍夫变换,因为它们非常有用。在本节中,我们将介绍 OpenCV 方法来从您的图像中提取它们。
原始霍夫线变换背后的思想是,二值图像中的任何点都可能是一组线条的一部分。假设每条直线都可以用直线方程 y = mx + b 来参数化,其中 m 是直线的斜率,b 是这条线的 y 轴截距。现在,我们可以迭代整个二值图像,存储每个 m 和 b 参数并检查它们的累积。m 和 b 参数的局部最大值将产生在图像中出现的直线的方程。实际上,我们不是使用斜率和 y 轴截距点,而是使用极坐标直线表示。
由于 OpenCV 不仅支持标准的霍夫变换,还支持渐进概率霍夫变换,其中两个函数分别是Imgproc.HoughLines和Imgproc.HoughLinesP。有关详细信息,请参阅[3]。这些函数的签名如下:
HoughLines(Mat image, Mat lines, double rho, double theta, int threshold)
HoughLinesP(Mat image, Mat lines, double rho, double theta, int threshold)
本章的hough项目展示了使用它们的示例。以下是从Imgproc.HoughLines检索线条的代码:
Mat canny = new Mat();
Imgproc.Canny(originalImage, canny, 10, 50, aperture, false);
image = originalImage.clone();
Mat lines = new Mat();
Imgproc.HoughLines(canny, lines, 1, Math.PI/180, lowThreshold);
注意,我们需要在边缘图像上应用霍夫变换;因此,前两行代码将处理此问题。然后,原始图像被克隆以供显示,并在第四行创建一个Mat对象以保持线条。在最后一行,我们可以看到HoughLines的应用。
Imgproc.HoughLines中的第三个参数指的是累加器在像素中的距离分辨率,第四个参数是累加器在弧度中的角度分辨率。第五个参数是累加器阈值,这意味着只有获得超过指定数量票数的线才会被返回。lowThreshold变量与示例应用程序中的缩放滑块相关联,以便用户可以对其进行实验。重要的是要注意,线被返回在lines矩阵中,该矩阵有两列,其中每条线返回极坐标的rho和theta参数。这些坐标分别指的是图像左上角与线的旋转之间的距离以及弧度。根据此示例,您将了解如何从返回的矩阵中绘制线。您可以在以下截图中看到霍夫变换的工作原理:

除了标准的霍夫变换外,OpenCV 还提供了概率霍夫线变换以及圆形版本。这两个实现都在同一个Hough示例项目中进行了探索,以下截图显示了圆形版本的工作原理:

几何变换 – 拉伸、收缩、扭曲和旋转
在处理图像和计算机视觉时,您通常会需要使用已知的几何变换来预处理图像,例如拉伸、收缩、旋转和扭曲。后者与非均匀缩放相同。这些变换可以通过将源点与 2 x 3 矩阵相乘来实现,并且它们在将矩形转换为平行四边形时被称为仿射变换。因此,它们具有要求目标具有平行边的限制。另一方面,3 x 3 矩阵的乘法表示透视变换。它们提供了更多的灵活性,因为它们可以将二维四边形映射到另一个四边形。以下截图显示了这一概念的一个非常有用的应用。
在这里,我们将找出哪个透视变换将建筑侧面的透视视图映射到其正面视图:

注意,该问题的输入是建筑物的透视照片,如图像左侧所示,以及突出显示的四边形形状的四个角点。输出在右侧,显示了如果观察者从建筑侧面看会看到的内容。
由于仿射变换是透视变换的子集,因此我们将重点关注后者。本例中可用的代码位于本章的 warps 项目中。这里使用的主要方法是 Imgproc 中的 warpPerspective。它对输入图像应用透视变换。以下是 warpPerspective 方法的签名:
public static void warpPerspective(Mat src, Mat dst, Mat M, Size dsize)
Mat src 参数自然是输入图像,即前一张截图中的左侧图像,而 dst Mat 是右侧的图像;在使用方法之前,请确保初始化此参数。这里不太直观的参数是 Mat M,它是变换矩阵。为了计算它,你可以使用 Imgproc 中的 getPerspectiveTransform 方法。该方法将从两组相关的四个二维点(源点和目标点)计算透视矩阵。在我们的例子中,源点是截图左侧突出显示的点,而目标点是右侧图像的四个角点。这些点可以通过 MatOfPoint2f 类存储,该类存储 Point 对象。getPerspectiveTransform 方法的签名如下:
public static Mat getPerspectiveTransform(Mat src, Mat dst)
Mat src 和 Mat dst 与之前提到的 MatOfPoint2f 类相同,它是 Mat 的子类。
在我们的例子中,我们添加了一个鼠标监听器来检索用户点击的点。需要注意的一个细节是,这些点按照以下顺序存储:左上角、右上角、左下角和右下角。在示例应用程序中,可以通过图像上方的四个单选按钮选择当前修改的点。点击和拖动监听器的操作已经被添加到代码中,因此两种方法都可以工作。
离散傅里叶变换和离散余弦变换
在处理图像分析时,如果你能够将图像从空间域(即以 x 和 y 坐标表示的图像)转换为频率域——图像分解为高频和低频成分——这将非常有用,这样你就能看到并操作频率参数。这在图像压缩中可能很有用,因为已知人类视觉对高频信号的敏感度不如对低频信号。通过这种方式,你可以将图像从空间域转换为频率域,并移除高频成分,从而减少表示图像所需的内存,进而压缩图像。下一张图像可以更好地展示图像频率。
为了将图像从空间域转换为频率域,可以使用离散傅里叶变换。由于我们可能需要将其从频率域转换回空间域,因此可以应用另一个变换,即逆离散傅里叶变换。
DFT 的正式定义如下:

f(i,j) 的值是空间域中的图像,而 F(k,l) 是频率域中的图像。请注意,F(k,l) 是一个复函数,这意味着它有一个实部和虚部。因此,它将由两个 OpenCV Mat 对象或具有两个通道的 Mat 对象表示。分析 DFT 最简单的方法是绘制其幅度并取其对数,因为 DFT 的值可以处于不同的数量级。
例如,这是一个脉冲模式,它是一个可以从零(表示为黑色)到顶部(表示为白色)的左侧信号,其傅里叶变换幅度及其右侧应用的对数:

回顾先前的 DFT 变换,我们可以将 F(k,l) 视为将空间图像的每个点与一个与频率域相关的基函数相乘,然后求和的结果。记住,基函数是正弦函数,它们具有递增的频率。这样,如果某些基函数的振荡速率与信号相同,它们将能够相加成一个很大的数,这在傅里叶变换图像上会显示为一个白点。另一方面,如果给定的频率在图像中不存在,振荡与图像的乘积将导致一个很小的数,这在傅里叶变换图像中不会被注意到。
从方程中还可以观察到,F(0,0) 将产生一个始终为 1 的基本函数。这样,F(0,0) 就简单地指代了空间图像中所有像素的总和。我们还可以检查 F(N-1, N-1) 是否对应于与图像最高频率相关的基函数。请注意,前面的图像基本上有一个直流分量,这将是图像的均值,可以从离散傅里叶变换图像中间的白点中检查出来。此外,左侧的图像可以看作是一系列脉冲,因此它将在 x 轴上有一个频率,这可以通过右侧傅里叶变换图像中靠近中心点的两个点来注意到。然而,我们需要使用多个频率来近似脉冲形状。这样,在右侧图像的 x-轴上可以看到更多的点。下面的截图提供了更多的见解,并有助于您理解傅里叶分析:

现在,我们将再次检查 DFT 图像中心的直流电平,右侧作为一个明亮的中心点。此外,我们还可以以对角线模式检查多个频率。可以检索的重要信息之一是空间变化的方向,这在 DFT 图像中清晰地显示为明亮的点。
现在是时候编写一些代码了。以下代码展示了如何为应用 DFT 腾出空间。记住,从前面的截图可以看出,DFT 的结果是复数。此外,我们需要将它们存储为浮点值。为此,我们首先将我们的三通道图像转换为灰度,然后转换为浮点。之后,我们将转换后的图像和一个空的Mat对象放入一个 mats 列表中,通过使用Core.merge函数将它们合并为一个单一的Mat对象,如下所示:
Mat gray = new Mat();
Imgproc.cvtColor(originalImage, gray, Imgproc.COLOR_RGB2GRAY);
Mat floatGray = new Mat();
gray.convertTo(floatGray, CvType.CV_32FC1);
List<Mat> matList = new ArrayList<Mat>();
matList.add(floatGray);
Mat zeroMat = Mat.zeros(floatGray.size(), CvType.CV_32F);
matList.add(zeroMat);
Mat complexImage = new Mat();
Core.merge(matList, complexImage);
现在,应用原地离散傅里叶变换(DFT)变得容易:
Core.dft(complexImage,complexImage);
为了获取一些有意义的信息,我们将打印图像,但首先,我们必须获取其幅度。为了获取它,我们将使用我们在学校学到的标准方法,即获取数字实部和虚部平方和的平方根。
再次,OpenCV 有一个用于此目的的函数,即Core.magnitude,其签名是magnitude(Mat x, Mat y, Mat magnitude),如下面的代码所示:
List<Mat> splitted = new ArrayList<Mat>();
Core.split(complexImage,splitted);
Mat magnitude = new Mat();
Core.magnitude(splitted.get(0), splitted.get(1), magnitude);
在使用Core.magnitude之前,请注意使用Core.split在拆分的 mats 中解包 DFT 的过程。
由于值可能处于不同的数量级,因此将值转换为对数尺度非常重要。在进行此操作之前,需要将矩阵中的所有值加 1,以确保在应用log函数时不会得到负值。除此之外,OpenCV 已经有一个处理对数运算的函数,即Core.log:
Core.add(Mat.ones(magnitude.size(), CvType.CV_32F), magnitude, magnitude);
Core.log(magnitude, magnitude);
现在,是时候将图像移至中心,这样更容易分析其频谱。执行此操作的代码简单,如下所示:
int cx = magnitude.cols()/2;
int cy = magnitude.rows()/2;
Mat q0 = new Mat(magnitude,new Rect(0, 0, cx, cy));
Mat q1 = new Mat(magnitude,new Rect(cx, 0, cx, cy));
Mat q2 = new Mat(magnitude,new Rect(0, cy, cx, cy));
Mat q3 = new Mat(magnitude ,new Rect(cx, cy, cx, cy));
Mat tmp = new Mat();
q0.copyTo(tmp);
q3.copyTo(q0);
tmp.copyTo(q3);
q1.copyTo(tmp);
q2.copyTo(q1);
tmp.copyTo(q2);
作为最后一步,对图像进行归一化非常重要,这样它才能以更好的方式被看到。在我们对其进行归一化之前,它应该被转换为 CV_8UC1 格式:
magnitude.convertTo(magnitude, CvType.CV_8UC1);
Core.normalize(magnitude, magnitude,0,255, Core.NORM_MINMAX, CvType.CV_8UC1);
当处理实值数据时,使用 DFT 通常只需要计算 DFT 的一半,就像图像一样。这样,可以使用一个称为离散余弦变换的类似概念。如果您需要,可以通过Core.dct来调用它。
积分图像
一些人脸识别算法,如 OpenCV 的人脸检测算法,大量使用以下图像中所示的特征:

这些被称为 Haar-like 特征,它们是白色区域像素总和减去黑色区域像素总和的计算结果。您可能会觉得这种特征有点奇怪,但在人脸检测的训练中,它可以构建成一个极其强大的分类器,仅使用这两个特征,如下面的图像所示:

事实上,仅使用前两个特征的分类器可以调整到检测给定面部训练数据库中的 100%,而只有 40% 的误报。从图像中取出所有像素的总和以及计算每个区域的总和可能是一个漫长的过程。然而,这个过程必须针对给定输入图像中的每一帧进行测试,因此快速计算这些特征是我们需要满足的要求。
首先,让我们定义一个积分图像和为以下表达式:

例如,如果以下矩阵代表我们的图像:

积分图像将类似于以下内容:

这里的技巧源于以下属性:

这意味着为了找到由点 (x1,y1)、(x2,y1)、(x2,y2) 和 (x1,y2) 界定的给定矩形的和,你只需要使用点 (x2,y2) 的积分图像,但你还需要从 (x1-1,y2) 到 (x2,y1-1) 的点中减去。此外,由于 (x1-1, y1-1) 的积分图像被减去了两次,我们只需要加一次。
以下代码将生成前面的矩阵并使用 Imgproc.integral 创建积分图像:
Mat image = new Mat(3,3 ,CvType.CV_8UC1);
Mat sum = new Mat();
byte[] buffer = {0,2,4,6,8,10,12,14,16};
image.put(0,0,buffer);
System.out.println(image.dump());
Imgproc.integral(image, sum);
System.out.println(sum.dump());
这个程序的输出类似于前面矩阵中 A 和 Sum A 的输出。
由于初始行和列的零值,需要验证输出是一个 4 x 4 矩阵,因为这些零值被用来使计算更高效。
距离变换
简而言之,对一个图像应用距离变换将生成一个输出图像,其像素值将是输入图像中零值像素的最短距离。基本上,它们将具有给定的距离度量下的最短距离到背景。以下截图展示了人体轮廓会发生什么:

由 J E Theriot 的人体轮廓
这种变换在获取给定分割图像的拓扑骨架以及产生模糊效果的过程中非常有用。这种变换的另一个有趣应用是在分割重叠对象时,与分水岭一起使用。
通常,距离变换应用于由 Canny 滤波器生成的边缘图像。我们将使用 Imgproc 的 distanceTransform 方法,这在 distance 项目中可以看到,该项目可以在本章的源代码中找到。以下是此示例程序中最重要的一些行:
protected void processOperation() {
Imgproc.Canny(originalImage, image, 220, 255, 3, false);
Imgproc.threshold(image, image, 100, 255, Imgproc.THRESH_BINARY_INV );
Imgproc.distanceTransform(image, image, Imgproc.CV_DIST_L2, 3);
image.convertTo(image, CvType.CV_8UC1);
Core.multiply(image, new Scalar(20), image);
updateView();
}
首先,对输入图像应用 Canny 边缘检测器过滤器。然后,使用THRESH_BINARY_INV阈值的转换将边缘变为黑色,豆子变为白色。只有在此之后,才应用距离变换。第一个参数是输入图像,第二个参数是输出矩阵,第三个参数指定了距离是如何计算的。在我们的例子中,CVDIST_L2表示欧几里得距离,而其他距离,如CVDIST_L1或CVDIST_L12等也存在。由于distanceTransform的输出是一个单通道 32 位浮点图像,需要进行转换。最后,我们应用Core.multiply来增强对比度。
以下截图为您提供了整个过程的良好理解:

直方图均衡化
人类视觉系统对图像中的对比度非常敏感,这是不同物体颜色和亮度的差异。此外,人眼是一个神奇的系统,可以在 10[16]个光级上感受到强度[4]。难怪一些传感器可能会搞乱图像数据。
在分析图像时,绘制它们的直方图非常有用。它们简单地显示了数字图像的亮度分布。为了做到这一点,您需要计算具有确切亮度的像素数量,并将其作为分布图绘制出来。这使我们能够深入了解图像的动态范围。
当相机图片以非常窄的光范围捕捉时,在阴影区域或其他局部对比度较差的区域看细节变得困难。幸运的是,有一种技术可以均匀分布强度,称为直方图均衡化。以下图像显示了在应用直方图均衡化技术前后,同一图片及其相应的直方图:

注意,位于上方直方图最右侧的光值很少使用,而中间范围的光值过于集中。在整个范围内扩展这些值会产生更好的对比度,并且细节更容易被感知。直方图均衡化图像更好地利用了产生更好对比度的强度。为了完成这项任务,可以使用累积分布来重新映射直方图,使其类似于均匀分布。然后,只需检查原始直方图的点通过累积高斯分布等方法映射到均匀分布的位置即可。
现在,好事是所有这些细节都封装在一个简单的 OpenCV equalizeHist函数调用中。以下是本章histogram项目中的示例:
protected void processOperation() {
Imgproc.cvtColor(originalImage, grayImage, Imgproc.COLOR_RGB2GRAY);
Imgproc.equalizeHist(grayImage, image);
updateView();
}
这段代码只是将图像转换为单通道图像;然而,只要将每个通道单独处理,你就可以在彩色图像上使用equalizeHist。Imgproc.equalizeHist方法按照之前提到的概念输出校正后的图像。
参考文献
-
《用于图像处理的 3x3 各向同性梯度算子》,在 1968 年斯坦福人工项目演讲中提出,作者:I. Sobel 和 G. Feldman。
-
《边缘检测的计算方法》,IEEE Trans. Pattern Analysis and Machine Intelligence,作者:Canny, J.
-
使用渐进概率 Hough 变换进行线的鲁棒检测,CVIU 78 1,作者:Matas, J. 和 Galambos, C.,以及 Kittler, J.V. 第 119-137 页(2000 年)。
-
高级高动态范围成像:理论与实践,CRC Press,作者:Banterle, Francesco;Artusi, Alessandro;Debattista, Kurt;Chalmers, Alan。
摘要
本章涵盖了计算机视觉日常应用的关键方面。我们从重要的边缘检测器开始,通过 Sobel、Laplacian 和 Canny 边缘检测器,你获得了如何找到它们的经验。然后,我们看到了如何使用 Hough 变换来找到直线和圆。之后,通过一个交互式示例,我们探讨了几何变换拉伸、收缩、扭曲和旋转。接着,我们探讨了如何使用离散傅里叶分析将图像从空间域转换到频域。之后,我们展示了通过使用积分图像快速计算 Haar 特征的小技巧。然后,我们探讨了重要的距离变换,并在解释直方图均衡化后结束本章。
现在,准备好深入机器学习算法,因为我们将介绍如何在下一章中检测人脸。你还将学习如何创建自己的对象检测器,并理解监督学习是如何工作的,以便更好地训练你的分类树。
第五章。使用 Ada Boost 和 Haar 级联进行对象检测
本章展示了 OpenCV 的一个非常有趣的功能——在图像或视频流中检测人脸。在后一种情况下,我们称之为人脸跟踪。为了做到这一点,本章深入探讨了机器学习算法,特别是带有提升的有监督学习。我们将涵盖Viola-Jones 分类器及其理论,以及如何使用 OpenCV 附带的人脸训练分类器的详细说明。
在本章中,我们将涵盖以下主题:
-
提升理论
-
Viola-Jones 分类器
-
人脸检测
-
学习新对象
到本章结束时,你将能够通过提升和 Viola-Jones 分类器理解人脸分类器背后的理论。你还将了解如何使用简单的分类器。此外,你将能够为不同的对象创建自己的对象分类器。
提升理论
在图像中检测人脸的问题可以用更简单的方式提出。我们可以通过几个较小的窗口迭代整个图像,并创建一个分类器,该分类器将告诉我们一个窗口是否是人脸。正确识别人脸的窗口将是人脸检测的坐标。
现在,究竟什么是分类器,以及它是如何构建的呢?在机器学习中,分类问题已经被深入探索,并被提出为基于先前训练的已知类别成员资格的集合,确定给定观察属于哪个类别。这可能是像在水果分类应用中,如果给定的图像属于香蕉、苹果或葡萄类别,例如。在人脸检测的情况下,有两个类别——人脸和非人脸。
本节描述了一个元算法,它基本上是一个模板算法,用于使用一组弱学习器创建一个强大的分类器。这些弱学习器是基于某些特征的分类器,尽管它们不能将整个集合分为两类,但它们对某些集合做得很好。比如说,一个弱学习器可能是一个寻找胡子的分类器,以判断给定的人脸是否为男性。即使它可能不会在集合中找到所有男性,但它会对有胡子的那些人做得很好。
AdaBoost
AdaBoosting,即自适应提升,实际上不是一个算法,而是一个元算法,它将帮助我们构建分类器。它的主要任务是构建一个由弱分类器组成的强大分类器,这些弱分类器只是偶然地更好。它的最终形式是给定分类器的加权组合,如下面的方程所示:

符号运算符在括号内的表达式为正时将返回+1,否则返回-1。请注意,它是一个二元分类器,输出为是或否,或者可能是属于或不属于,或者简单地是+1或-1。因此,
是分配给给定分类器
在T个分类器集中对给定输入x的权重。
例如,在一组人中,我们想知道任何给定的人p是男性还是女性。假设我们有一些弱分类器,它们是好的猜测,例如:
-
:如果身高超过 5 英尺 9 英寸(约 175 厘米),那么这个人就是男性或女性。当然,有些女性的身高超过男性,但平均来看,男性通常更高。 -
:如果一个人有长发,那么这个人就是女性或男性。同样,有几个长头发的男性,但平均来看,女性通常有更长的头发。 -
:如果一个人有胡须,那么这个人就是男性或女性。在这里,我们可能会错误地将刮胡子的男性分类。
假设我们有一组随机的人群:
| Name/Feature | Height (h1) | Hair (h2) | Beard (h3) | Gender (f(x)) |
|---|---|---|---|---|
| Katherine | 1.69 | Long | Absent | Female |
| Dan | 1.76 | Short | Absent | Male |
| Sam | 1.80 | Short | Absent | Male |
| Laurent | 1.83 | Short | Present | Male |
| Sara | 1.77 | Short | Absent | Female |
分类器h1将正确分类三个人,而h2将正确分类四个人,h3将正确分类三个人。然后我们会选择h2,这是最好的,因为它最小化了加权错误,并设置其 alpha 值。然后我们会增加错误分类数据(Sara)的权重,并减少其他所有数据(Katherine、Dan、Sam 和 Laurent)的权重。然后我们会在新的分布上寻找最好的分类器。现在 Sara 处于关键位置,要么选择h2或h3,这取决于错误,因为h1以更高的权重错误地将 Sara 分类。然后我们会继续对T个弱分类器进行操作,在我们的例子中是 3 个。
AdaBoost 的算法如下:

幸运的是,OpenCV 已经实现了 boosting。以下示例可以在第五章的boost项目中找到,它展示了如何处理Boost类,以及前面的示例。我们首先创建一个名为data的 5 x 3 矩阵。这个矩阵存储我们的训练数据集,将被Boost用于创建分类器。然后,我们像前面的表格一样输入矩阵。第一列是高度。头发和胡须被赋予值一或零。当头发短时,我们将其设置为zero,当头发长时,我们将其设置为one。如果胡须存在,其值为one否则为zero。这些值使用 Mat 的put函数设置。请注意,男性或女性的事实并没有进入data矩阵,因为这是我们想要的分类器的输出。这样,就创建了一个 5 x 1 列矩阵responses。它简单地存储zero表示女性和one表示男性。
然后,实例化一个Boost类,并通过CvBoostParams的 setter 设置训练参数。我们使用setBoostType方法将 boost 类型设置为离散 Adaboost,传递Boost.DISCRETE作为参数。其他 boosting 的变体被称为实 Adaboost、LogitBoost和温和 Adaboost。setWeakCount方法设置使用的弱分类器的数量。在我们的案例中,它是3。下一个设置告诉如果节点中的样本数量小于此参数,则节点将不会分裂。实际上,默认值是10,它不会与如此小的数据集一起工作,因此将其设置为4以便它能够与这个数据集一起工作。重要的是要注意,Boost 是从 DTrees 派生的,它与决策树相关。这就是为什么它使用节点术语。
在设置参数之后,通过train方法使用data和responses矩阵来训练 boost 分类器。以下是此方法签名:
public boolean train(Mat trainData, int tflag, Mat responses)
这是具有特征的trainData训练矩阵,而responses矩阵是包含分类数据的矩阵。tflag参数将告诉特征是放在行还是列中。
之后,预测是一个简单的问题,就是创建一个新的行矩阵,包含输入参数高度、头发大小和胡须存在,并将其传递给Boost的predict函数。它的输出将把输入分类为男性或女性:
public class App
{
static{ System.loadLibrary(Core.NATIVE_LIBRARY_NAME); }
public static void main(String[] args) throws Exception {
Mat data = new Mat(5, 3, CvType.CV_32FC1, new Scalar(0));
data.put(0, 0, new float[]{1.69f, 1, 0});
data.put(1, 0, new float[]{1.76f, 0, 0});
data.put(2, 0, new float[]{1.80f, 0, 0});
data.put(3, 0, new float[]{1.77f, 0, 0});
data.put(4, 0, new float[]{1.83f, 0, 1});
Mat responses = new Mat(5, 1, CvType.CV_32SC1, new Scalar(0));
responses.put(0,0, new int[]{0,1,1,0,1});
Boost boost = Boost.create();
boost.setBoostType(Boost.DISCRETE);
boost.setWeakCount(3);
boost.setMinSampleCount(4);
boost.train(data, Ml.ROW_SAMPLE, responses);
//This will simply show the input data is correctly classified
for(int i=0;i<5;i++){
System.out.println("Result = " + boost.predict(data.row(i)));
}
Mat newPerson = new Mat(1,3,CvType.CV_32FC1, new Scalar(0));
newPerson.put(0, 0, new float[]{1.60f, 1,0});
System.out.println(newPerson.dump());
System.out.println("New (woman) = " + boost.predict(newPerson));
newPerson.put(0, 0, new float[]{1.8f, 0,1});
System.out.println("New (man) = " + boost.predict(newPerson));
newPerson.put(0, 0, new float[]{1.7f, 1,0});
System.out.println("New (?) = " + boost.predict(newPerson));
}
}
然后是级联分类器的检测和训练
有些人可能会想知道 OpenCV 是如何检测人脸的,因为这对于几个月大的婴儿来说是一个非常简单的任务,而对于告诉计算机如何完成这项任务来说则看起来相当复杂。我们将问题分为两部分——目标检测,即在分类器说的时候应用分类器并检索对象位置,以及训练一个新的分类器来学习新的、应该主要是刚性的对象。
OpenCV 级联分类器最初实现了一种称为Viola-Jones检测器的面部检测技术,最初由 Paul Viola 和 Michael Jones 开发,它使用所谓的 Haar-like 特征,以 Alfréd Haar 小波命名。这些特征基于原始图像值矩形区域的和与差的阈值。后来,这个分类器也允许使用局部二值模式(LBP)特征,与 Haar-like 特征相比是整数值;这导致训练时间更快,但质量相似。
虽然在 OpenCV 中使用级联分类器相当直接,但了解它是如何工作的对于理解使用边界很重要。作为一个经验法则,它应该能够在纹理一致且大部分刚性的物体上工作得很好。级联分类器被提供了一组大小和直方图均衡化的图像,这些图像被标记为包含或不包含感兴趣的对象。分类器通过几个较小的窗口迭代,这些窗口覆盖整个图像,因此它很少会找到一个对象。例如,团体照片中只有几个坐标有面部,而图像的其余部分应该被标记为没有面部。由于它应该最大化拒绝,OpenCV 级联分类器使用一种 AdaBoost 分类器形式的拒绝级联,这意味着非对象补丁应该尽可能早地被丢弃。
特征阈值可以用作弱分类器,通过 AdaBoost 构建强分类器,正如我们在本章所学。在计算一个特征之后,我们可以决定这个问题:这个值是否高于或低于给定的阈值? 如果答案是true,那么对象是一个面部,例如,否则它就不是。我们通常使用单个特征来做这个决定,但这个数字可以在训练中设置。使用 AdaBoost,我们构建分类器作为弱分类器的加权求和,如下所示:

在这里,
是与每个特征i相关联的函数,如果特征值高于某个阈值,则返回+1,如果低于阈值,则返回-1。提升用于正确量化与特征相关的每个权重
。Viola-Jones 分类器将树的每个节点构建为加权求和的信号,就像在函数F中一样。一旦这个函数被设置,它就会为 Viola-Jones 分类器提供一个节点,然后使用级联中更高层次的所有存活数据来训练下一个节点,依此类推。最终的树看起来类似于这个:

检测
OpenCV 已经包含了一些预训练的级联分类器,可以直接使用。其中,我们可以找到正面和侧面面部检测器,以及眼睛、身体、嘴巴、鼻子、下半身和上半身检测器。在本节中,我们将介绍如何使用它们。完整的源代码可以在本章的cascade项目中找到。
以下代码展示了如何加载一个训练好的级联:
private void loadCascade() {
String cascadePath = "src/main/resources/cascades/lbpcascade_frontalface.xml";
faceDetector = new CascadeClassifier(cascadePath);
}
大部分操作发生在objdetect包中的CascadeClassifier类中。这个类封装了级联加载和对象检测。具有字符串的构造函数已经从给定路径加载了级联。如果您想推迟级联名称,可以使用空构造函数和load方法。
runMainLoop方法,此处未展示,它将简单地从网络摄像头中抓取一张图片并将其传递给detectAndDrawFace方法,该方法将初始化的分类器投入使用。以下为detectAndDrawFace方法:
private void detectAndDrawFace(Mat image) {
MatOfRect faceDetections = new MatOfRect();
faceDetector.detectMultiScale(image, faceDetections);
for (Rect rect : faceDetections.toArray()) {
Core.rectangle(image, new Point(rect.x, rect.y), new Point(rect.x + rect.width, rect.y + rect.height), new Scalar(0, 255, 0));
}
}
首先,我们实例化faceDetections对象,它是一个MatOfRect容器(一个用于Rect的特殊容器)。然后,我们运行detectMultiScale方法,将接收到的图像和MatOfRect作为参数传递。这就是运行级联检测器的地方。算法将使用滑动窗口扫描图像,为每个窗口运行级联分类器。它还会以不同的图像比例运行此过程。默认情况下,它将每次尝试将图像比例减少 1.1。如果至少有三个检测发生,默认情况下,在三个不同的比例下,坐标被认为是命中,它将成为faceDetections数组的一部分,并添加到检测对象的宽度和高度中。
for循环简单地遍历返回的矩形,并在原始图像上用绿色绘制它们。
训练
虽然 OpenCV 已经打包了几个级联分类器,但可能需要检测一些特定对象或对象类别。创建自定义级联分类器并不简单,因为它需要成千上万张图像,其中应该去除所有差异。例如,如果正在创建面部分类器,所有图像的眼睛都应该对齐。在本节中,我们将描述使用 OpenCV 创建级联分类器的过程。
为了训练级联,OpenCV 提供了一些工具。它们可以在opencv/build/x86/vc11/bin目录中找到。opencv_createsamples和opencv_traincascade可执行文件用于准备正样本的训练数据集以及生成级联分类器。
为了更好地展示这个过程,我们包括了来自 UIUC 图像数据库中用于车辆检测的文件,这些文件由 Shivani Agarwal、Aatif Awan 和 Dan Roth 收集。这些文件可以在“第五章”的cardata目录中找到。以下说明依赖于位于此文件夹中才能工作。
注意
正样本 – 包含目标图像的图片
负样本是任意图像,不得包含旨在检测的对象。
要创建自己的级联分类器,收集目标对象的数百张图片,确保这些图片展示了足够的变化,以便给出被检测对象类别的良好概念。
然后,使用 opencv_createsamples 工具准备正样本和测试样本的训练数据集。这将生成一个具有 .vec 扩展名的二进制文件,其中包含从给定标记数据集中生成的正样本。没有应用扭曲;它们仅被调整到目标样本的大小并存储在 vec-file 输出中。读者应发出以下命令:
opencv_createsamples -info cars.info -num 550 -w 48 -h 24 -vec cars.vec.
前面的命令将读取 cars.info 文件,该文件中的每一行包含一个图像的路径,后面跟着一个数字 n。这个数字是图像中存在的对象实例的数量。随后是 n 个对象的边界 矩形 (x, y, width, height) 坐标。以下是有效行的示例:
images/image1.jpg 1 90 100 45 45
images/image2.jpg 2 200 300 50 50 100 30 25 25
参数 -w 和 -h 指定要生成的输出样本的宽度和高度。这应该足够小,以便在后续目标检测中搜索图像中的对象时,图像中对象的大小将大于这个大小。-num 参数告诉这些样本的数量。
为了为给定的 .vec 文件创建分类器,使用 opencv_traincascade 工具。此应用程序将读取通过 -vec 参数提供的文件中的正样本以及通过 -bg 参数提供的文件中的某些负样本。负样本文件简单地指向每行中的一个图像,这些图像是任意的,并且不得包含旨在检测的对象。为了使用此工具,发出以下命令:
opencv_traincascade -data data -vec cars.vec -bg cars-neg.info -numPos 500 -numNeg 500 -numStages 10 -w 48 -h 24 -featureType LBP
参数 -numPos 和 -numNeg 用于指定每个分类器阶段训练中使用的正样本和负样本的数量,而 -numStages 指定要训练的级联阶段数量。最后的 -featureType 参数设置要使用哪种类型的特征,可以选择 Haar 类特征或 LBP。正如之前所述,LBP 特征是整数值,与 Haar 特征不同,因此使用 LBP 将会使得检测和训练更快,但它们的品质可以相同,这取决于训练。还可以使用更多参数来微调训练,例如误报率、最大树深度和最小命中率。读者应参考文档了解这些设置。现在,关于训练时间,即使在快速机器上,也可能需要从几小时到几天。但是,如果您不想等待最终结果,并且急于检查分类器的工作情况,可以使用以下命令获取中间分类器 XML 文件:
convert_cascade --size="48x24" haarcascade haarcascade-inter.xml
在这里,48和24分别是可能检测到的最小宽度和高度,类似于opencv_traincascade命令中的–w和–h。
一旦发出前面的命令,就会在作为-data参数传递的文件夹中创建一个名为cascade.xml的文件。在训练成功后,可以安全地删除此文件夹中创建的其他文件。现在,可以通过CascadeClassifier类加载并使用它,正如前面检测部分所描述的那样。只需使用此文件代替示例中给出的lbpcascade_frontalface.xml文件即可。
下面的截图显示了使用训练好的级联分类器正确检测到的玩具汽车以及一个错误检测,这是一个假阳性:

参考文献
请参考视频,OpenCV 教程:训练自己的检测器,Packt Publishing,(www.packtpub.com/application-development/opencv-computer-vision-application-programming-video),由 Sebastian Montabone 编写。
摘要
本章向读者提供了几个有趣的概念。我们不仅介绍了提升理论的坚实基础,还通过一个实际例子进行了实践。接着,我们还涵盖了 OpenCV 的 Viola-Jones 级联分类器,并应用了实际操作方法,通过CascadeClassifier类使用分类器。之后,我们提供了一个完整的、真实世界的例子,用于创建一个新的汽车分类器,它可以适应你偏好的任何主要刚性的物体。
在下一章中,我们将通过帧差分和背景平均等纯图像处理方法研究并实践背景减法领域,以及有趣的 Kinect 设备用于深度图。
第六章. 使用 Kinect 设备检测前景和背景区域及深度
在视频安全应用领域,人们常常需要注意到帧之间的差异,因为那里是动作发生的地方。在其他领域,将对象从背景中分离出来也非常重要。本章展示了实现这一目标的一些技术,并比较了它们的优缺点。检测前景或背景区域的一种完全不同的方法是使用深度设备,如Kinect。本章还讨论了如何使用该设备实现这一目标。
在本章中,我们将涵盖:
-
背景减法
-
帧差分
-
平均背景法
-
高斯混合法
-
等高线查找
-
Kinect 深度图
到本章结束时,你将拥有几种解决查找前景/背景区域问题的方法,无论是通过直接图像处理还是使用与深度兼容的设备,如 Kinect。
背景减法
当与监控摄像头一起工作时,很容易看出大多数帧保持静止,而移动的对象,即我们感兴趣的对象,是随时间变化最大的区域。背景减法被定义为从静态摄像头检测移动对象的方法,也称为前景检测,因为我们主要对前景对象感兴趣。
为了执行一些有价值的背景减法,重要的是要考虑到变化的亮度条件,并始终注意更新我们的背景模型。尽管一些技术将背景减法的概念扩展到其字面意义之外,例如高斯混合方法,但它们仍然被这样命名。
为了比较以下章节中的所有解决方案,我们将提出一个有用的接口,称为VideoProcessor。此接口由一个简单的名为process的方法组成。整个接口在以下代码片段中给出:
public interface VideoProcessor {
public Mat process(Mat inputImage);
}
注意,我们将在以下背景处理器中实现此接口,这样我们可以轻松地更改它们并比较它们的结果。在此上下文中,Mat inputImage指的是正在处理的视频序列中的当前帧。
所有与背景减法相关的代码都可以在background项目中找到,该项目位于chapter6参考代码中。
我们的主要应用由两个窗口组成。其中一个简单地回放输入视频或网络摄像头流,而另一个显示应用了实现VideoProcessor接口的背景减法器的输出。这样,我们的主循环看起来大致如下代码:
while (true){
capture.read(currentImage);
if( !currentImage.empty() ){
foregroundImage = videoProcessor.process(currentImage);
... update Graphical User Interfaces ...
Thread.sleep(10);
}
}
注意,在成功检索图像后,我们将其传递给我们的 VideoProcessor 并更新我们的窗口。我们还暂停了 10 毫秒,以便视频播放不会看起来像快进。这个 10 毫秒的延迟不是记录的帧延迟,它被使用是因为这里的重点不是以原始文件相同的速度播放。为了尝试不同的减法方法,我们只需更改我们的 VideoProcessor 类的实例化。
帧差分
为了检索前景对象,简单地考虑一个简单的背景减法应该是直截了当的。一个简单的解决方案可能看起来像以下代码行:
Core.absdiff(backgroundImage,inputImage , foregroundImage);
这个函数简单地从 backgroundImage 中减去 inputImage 的每个像素,并将绝对值写入 foregroundImage。只要我们初始化了背景为 backgroundImage,并且我们清楚地从对象中分离出来,这就可以作为一个简单的解决方案。
下面是背景减法视频处理器代码:
public class AbsDifferenceBackground implements VideoProcessor {
private Mat backgroundImage;
public AbsDifferenceBackground(Mat backgroundImage) {
this.backgroundImage = backgroundImage;
}
public Mat process(Mat inputImage) {
Mat foregroundImage = new Mat();
Core.absdiff(backgroundImage,inputImage , foregroundImage);
return foregroundImage;
}
}
主要方法,process,实际上非常简单。它仅应用绝对差分方法。唯一需要记住的细节是在构造函数中初始化背景图像,它应该对应整个背景没有前景对象。
我们可以在以下图像中看到应用普通背景减法的结果;重要的是要检查背景中的移动树叶是否被正确移除,因为这是一个弱背景建模。此外,记得移动视频播放示例窗口,因为它可能会覆盖背景移除示例窗口:

背景平均方法
上一节中背景减法器的问题在于背景通常会因为光照和其他效果而改变。另一个事实是背景可能不容易获得,或者背景的概念可能改变,例如,当有人在视频监控应用中留下行李时。行李可能是第一帧的前景对象,但之后,它应该被遗忘。
一个有趣的算法用于处理这些问题,它使用了运行平均的概念。它不是总是使用第一帧作为清晰的背景,而是通过计算其移动平均来不断更新它。考虑以下方程,它将被执行,更新每个像素从旧的平均值,并考虑从最近获取的图像中的每个像素:

注意,
是新的像素值;
是时间 t-1 的平均背景值,这将是最后一帧;
是背景的新值;而
是学习率。
幸运的是,OpenCV 已经有了 accumulateWeighted 函数,它为我们执行最后一个方程。现在让我们看看在检查 RunningAverageBackground 类的 process 方法时,平均背景过程是如何在 RunningAverageBackground 类中实现的:
public Mat process(Mat inputImage) {
Mat foregroundThresh = new Mat();
// Firstly, convert to gray-level image, yields good results with performance
Imgproc.cvtColor(inputImage, inputGray, Imgproc.COLOR_BGR2GRAY);
// initialize background to 1st frame, convert to floating type
if (accumulatedBackground.empty())
inputGray.convertTo(accumulatedBackground, CvType.CV_32F);
// convert background to 8U, for differencing with input image
accumulatedBackground.convertTo(backImage,CvType.CV_8U);
// compute difference between image and background
Core.absdiff(backImage,inputGray,foreground);
// apply threshold to foreground image
Imgproc.threshold(foreground,foregroundThresh, threshold,255, Imgproc.THRESH_BINARY_INV);
// accumulate background
Mat inputFloating = new Mat();
inputGray.convertTo(inputFloating, CvType.CV_32F);
Imgproc.accumulateWeighted(inputFloating, accumulatedBackground,learningRate, foregroundThresh);
return negative(foregroundThresh);
}
private Mat negative(Mat foregroundThresh) {
Mat result = new Mat();
Mat white = foregroundThresh.clone();
white.setTo(new Scalar(255.0));
Core.subtract(white, foregroundThresh, result);
return result;
}
首先,我们将输入图像转换为灰度级别,因为我们将以这种方式存储平均背景,尽管我们也可以使用三个通道来实现。然后,如果累积背景尚未开始,我们必须将其设置为第一个输入图像的浮点格式。然后我们从累积背景中减去最近获取的帧,从而得到我们的前景图像,我们稍后会对其进行阈值处理以去除小的光照变化或噪声。
注意这次我们使用的是 Imgproc.THRESH_BINARY_INV,它将所有高于给定阈值的像素转换为黑色,从而为前景对象生成黑色像素,为背景生成白色像素。
这样,我们就可以使用这张图像作为掩模,在以后使用 accumulateWeighted 方法时只更新背景像素。在下一行,我们只转换 inputImage 为 inputFloating,这样我们就可以以浮点格式拥有它。然后我们使用 accumulateWeighted 应用我们的注释方程进行运行平均值。最后,我们反转图像并返回作为白色像素的前景对象。
在以下图像中,我们可以看到对背景中移动的叶子的更好建模。尽管阈值处理使得将这些结果与简单的背景减法进行比较变得困难,但很明显,许多移动的叶子已经被移除。此外,大部分手也被清除。可以通过调整阈值参数来获得更好的结果,如下面的截图所示:

高斯混合方法
尽管我们可以通过前面的想法获得非常好的结果,但文献中已经提出了更多先进的方法。1999 年,Grimson 提出的一种伟大方法是使用不止一个运行平均值,而是更多平均值,这样如果像素在两个轨道点之间波动,这两个运行平均值都会被计算。如果它不符合任何一个,则被认为是前景。
此外,Grimson 的方法还保留了像素的方差,这是衡量一组数字分布程度的统计量。有了平均值和方差,我们可以计算出高斯模型,并测量一个概率以供考虑,从而得到一个高斯混合模型(MOG)。当背景中的树枝和叶子移动时,这可以非常有用。
不幸的是,Grimson 的方法在开始时学习速度较慢,并且无法区分移动的阴影和移动的对象。因此,KaewTraKulPong 和 Bowden 发布了一种改进技术来解决这些问题。这个技术已经在 OpenCV 中实现,并且通过BackgroundSubtractorMOG2类使用它非常简单。
为了展示高斯混合方法的有效性,我们实现了一个基于BackgroundSubtractorMOG2的VideoProcessor。其完整代码如下:
public class MixtureOfGaussianBackground implements VideoProcessor {
privateBackgroundSubtractorMOG2 mog= org.opencv.video.Video. createBackgroundSubtractorMOG2();
private Mat foreground = new Mat();
private double learningRate = 0.01;
public Mat process(Mat inputImage) {
mog.apply(inputImage, foreground, learningRate);
return foreground;
}
}
注意,我们只需要实例化BackgroundSubtractorMOG2类并使用apply方法,传递输入帧、输出图像以及一个学习率,这个学习率将告诉算法以多快的速度学习新的背景。除了无参数的工厂方法外,还有一个具有以下签名的工厂方法:
Video.createBackgroundSubtractorMOG2 (int history, double varThreshold, boolean detectShadows)
在这里,history是历史长度,varThreshold是像素与模型之间的平方马氏距离的阈值,用于决定像素是否被背景模型很好地描述,如果detectShadows为true,则算法将检测并标记阴影。如果我们不使用空构造函数设置参数,则默认使用以下值:
-
defaultHistory = 500; -
varThreshold = 16; -
detectShadows = true;
尝试调整这些值以在执行背景减法时寻找更好的结果。

在前面的截图中,我们可以清楚地看到一个非常出色的背景去除结果,且几乎无需任何定制。尽管一些叶子仍然在去除的背景结果中造成噪声,但我们仍可以看到大量手部被正确地识别为前景。可以应用一个简单的开形态算子来去除一些噪声,如下面的截图所示:

轮廓查找
当处理从背景中去除的二值图像时,将像素转换为有用的信息非常重要,例如通过将它们组合成一个对象或让用户非常清楚地看到。在这种情况下,了解连通组件的概念非常重要,连通组件是在二值图像中连接的像素集,以及 OpenCV 用于找到其轮廓的函数。
在本节中,我们将检查findContours函数,该函数提取图像中连通组件的轮廓以及一个辅助函数,该函数将在图像中绘制轮廓,即drawContours。findContours函数通常应用于经过阈值处理以及一些 Canny 图像变换的图像。在我们的例子中,使用了阈值。
findContours函数具有以下签名:
public static void findContours(Mat image,
java.util.List<MatOfPoint> contours,
Mat hierarchy,
int mode,
int method)
它使用 Suzuki 在其论文《通过边界跟踪对数字化二值图像进行拓扑结构分析》中描述的算法实现。第一个参数是输入图像。确保您在目标图像的副本上工作,因为此函数会更改图像。此外,请注意,图像的 1 像素边界不被考虑。找到的轮廓存储在MatOfPoints列表中。这是一个简单地以矩阵形式存储点的结构。
Mat hierarchy是一个可选的输出向量,为每个找到的轮廓设置。它们代表同一层次级别中下一个和前一个轮廓的 0 基于索引,第一个子轮廓和父轮廓,分别用hierarcy[i][0]、hierarcy[i][1]、hierarcy[i][2]和hierarcy[i][3]元素表示,对于给定的i轮廓。如果没有与这些值对应的轮廓,它们将是负数。
mode参数处理层次关系是如何建立的。如果您对此不感兴趣,可以将其设置为Imgproc.RETR_LIST。在检索轮廓时,method参数控制它们是如何近似的。如果将Imgproc.CHAIN_APPROX_NONE设置为,则存储所有轮廓点。另一方面,当使用Imgproc.CHAIN_APPROX_SIMPLE为此值时,通过仅使用它们的端点压缩水平、垂直和对角线线。还有其他近似方法可用。
为了绘制获取到的轮廓轮廓或填充它们,使用 Imgproc 的drawContours函数。此函数具有以下签名:
public static void drawContours(Mat image,
java.util.List<MatOfPoint> contours,
int contourIdx,
Scalar color)
Mat image是目标图像,而MatOfPoint轮廓列表是在调用findContours时获得的。contourIdx属性是要绘制的属性,而color是绘制所需的颜色。还有重载函数,用户可以选择厚度、线型、层次结构最大级别和偏移量。
在决定哪些轮廓有趣时,一个有用的函数可以帮助做出这个决定,那就是找到轮廓面积。OpenCV 通过Imgproc.contourArea实现此功能。此函数可以在chapter6源代码的示例connected项目中找到。此应用程序接受一个图像作为输入,对其运行阈值,然后使用它来查找轮廓。本节讨论的函数有几个测试选项,例如是否填充轮廓或根据找到的面积绘制轮廓。以下是此应用程序的屏幕截图:

在处理轮廓时,围绕它们绘制形状也很重要,以便进行测量或突出显示找到的内容。示例应用程序还提供了一些代码,说明如何围绕轮廓绘制边界框、圆形或凸包。让我们看看主要的drawContours()函数,该函数在按下按钮时被调用:
protected void drawContours() {
Mat contourMat = binary.clone();
List<MatOfPoint> contours = new ArrayList<MatOfPoint>();
int thickness = (fillFlag.equals(onFillString))?-1:2;
Imgproc.findContours(contourMat, contours, new Mat(),
Imgproc.CHAIN_APPROX_NONE,Imgproc.CHAIN_APPROX_SIMPLE);
for(int i=0;i<contours.size();i++){
MatOfPoint currentContour = contours.get(i);
double currentArea = Imgproc.contourArea(currentContour);
if(currentArea > areaThreshold){
Imgproc.drawContours(image, contours, i, new Scalar(0,255,0), thickness);
if(boundingBoxString.equals(enclosingType)){
drawBoundingBox(currentContour);
}
else if (circleString.equals(enclosingType)){
drawEnclosingCircle(currentContour);
}
else if (convexHullString.equals(enclosingType)){
drawConvexHull(currentContour);
}
}
else{
Imgproc.drawContours(image, contours, i, new Scalar(0,0,255), thickness);
}
}
updateView();
}
我们首先克隆我们的目标二进制镜像,这样我们就不会改变它。然后,我们初始化MatOfPoint结构并定义厚度标志。接下来,我们运行findContours,忽略输出层次矩阵。现在是时候在for循环中迭代轮廓了。我们使用Imgproc.contourArea辅助函数进行面积估计。基于此,如果它是之前通过滑块定义的areaThreshold,则使用drawContours函数将其绘制为绿色;否则,将其绘制为红色。代码中一个有趣的部分是形状绘制函数,具体描述如下:
private void drawBoundingBox(MatOfPoint currentContour) {
Rect rectangle = Imgproc.boundingRect(currentContour);
Imgproc.rectangle(image, rectangle.tl(), rectangle.br(), new Scalar(255,0,0),1);
}
private void drawEnclosingCircle(MatOfPoint currentContour) {
float[] radius = new float[1];
Point center = new Point();
MatOfPoint2f currentContour2f = new MatOfPoint2f();
currentContour.convertTo(currentContour2f, CvType.CV_32FC2);
Imgproc.minEnclosingCircle(currentContour2f, center, radius);
Imgproc.circle(image, center, (int) radius[0], new Scalar(255,0,0));
}
private void drawConvexHull(MatOfPoint currentContour) {
MatOfInt hull = new MatOfInt();
Imgproc.convexHull(currentContour, hull);
List<MatOfPoint> hullContours = new ArrayList<MatOfPoint>();
MatOfPoint hullMat = new MatOfPoint();
hullMat.create((int)hull.size().height,1,CvType.CV_32SC2);
for(int j = 0; j < hull.size().height ; j++){
int index = (int)hull.get(j, 0)[0];
double[] point = new double[] {
currentContour.get(index, 0)[0], currentContour.get(index, 0)[1]
};
hullMat.put(j, 0, point);
}
hullContours.add(hullMat);
Imgproc.drawContours(image, hullContours, 0, new Scalar(128,0,0), 2);
}
绘制边界框很简单;只需调用Imgproc.boundingRect()以识别形状周围的矩形。然后,调用 Imgproc 的rectangle函数方法来绘制矩形本身。
由于存在minEnclosingCircle函数,绘制包围圆也很容易。唯一的注意事项是将MatOfPoint转换为MatOfPoint2f,这可以通过调用 Contour 的convertTo方法实现。Imgproc 的circle函数负责绘制它。
从计算几何的角度来看,找到凸包是一个相当重要的问题。它可以看作是在一组点周围放置一个橡皮筋并检查它最终形成的形状。幸运的是,OpenCV 也通过 Imgproc 的convexHull函数处理这个问题。注意,在前面的代码中drawConvexHull的第一行和第二行,创建了MatOfInt,并调用convexHull,将当前轮廓和这个矩阵作为参数传递。这个函数将返回凸包索引在MatOfInt中。我们可以根据这些索引的坐标从原始轮廓绘制线条。另一个想法是使用 OpenCV 的drawContour函数。为了做到这一点,你需要构建一个新的轮廓。这是在代码中的以下行中完成的,直到调用drawContour。
Kinect 深度图
从本章的开头到现在,我们一直专注于尝试使用普通摄像头对场景背景进行建模的背景减法方法,然后是应用帧差分。
注意
虽然据报道 Kinect 与 Linux 和 OSX 兼容,但本节仅涉及在 OpenCV 2.4.7 版本上对 Windows 的设置。
在本节中,我们将采用不同的方法。我们将设置我们希望我们的物体被认为是前景和背景的距离,这意味着通过选择一个深度参数来移除背景。不幸的是,这不能通过一次拍摄使用单个普通相机来完成,因此我们需要一个能够告诉我们物体深度的传感器,或者尝试从立体视觉中确定深度,但这超出了本章的范围。多亏了游戏玩家和来自世界各地的许多努力,这种设备已经变成了商品,它被称为Kinect。可以尝试使用两个相机并尝试从立体视觉中获得深度,但结果可能不如使用 Kinect 传感器的结果那么好。以下是它的样子:

使 Kinect 真正区别于普通相机的是,它包括一个红外发射器和红外传感器,能够投影和感应结构光模式。它还包含一个普通的 VGA 相机,以便将深度数据合并到其中。结构光背后的想法是,当将已知的像素模式投影到物体上时,该模式的变形允许计算机视觉系统从这些模式中计算深度和表面信息。如果使用能够注册红外线的相机来记录发射的 Kinect 模式,可以看到类似以下图像:

虽然它可能看起来像一组随机的点,但实际上它们是先前生成的伪随机模式。这些模式可以被识别,并且可以计算出深度与视差的关系,从而推断深度。如果需要,可以通过研究结构光概念来获取更多信息。
应该意识到这种方法的影响。因为它依赖于主动红外投影,一些户外效果,如直射日光,会混淆传感器,因此不建议户外使用。用户还应该意识到深度范围是从 0.8 米到 4.0 米(大约从 2.6 英尺到 13.1 英尺)。与红外投影相关的某些阴影也可能使结果看起来不如应有的好,并在图像中产生一些噪声。尽管存在所有这些问题,但它是在近场背景移除方面最好的结果之一。
Kinect 设置
使用 Kinect 应该是直接的,但我们需要考虑两个重要方面。首先,我们需要确保所有设备驱动软件都已正确安装以便使用。然后,我们需要检查 OpenCV 是否已编译为支持 Kinect。不幸的是,如果你已经从sourceforge.net/projects/opencvlibrary/files/下载了 2.4.7 版本的预编译二进制文件,如第一章开头所述,为 Java 设置 OpenCV,开箱即用的支持不包括在内。我们将在接下来的章节中简要描述设置说明。
重要的是要注意,不仅 Xbox 360 Kinect 设备是商业化的,而且 Windows Kinect 也是。目前,如果你正在创建使用 Kinect 的商业应用程序,你应该选择 Windows Kinect,尽管 Xbox 360 Kinect 可以与提供的驱动程序一起使用。
驱动程序设置
OpenCV 对 Kinect 的支持依赖于 OpenNI 和 PrimeSensor Module for OpenNI。OpenNI 框架是一个开源 SDK,用于开发 3D 感知中间件库和应用。不幸的是,OpenNI.org网站仅在 2014 年 4 月 23 日之前可用,但 OpenNI 源代码可在 Github 上找到,地址为github.com/OpenNI/OpenNI和github.com/OpenNI/OpenNI2。在本节中,我们将重点关注使用版本 1.5.7.10。
虽然构建二进制的说明 readily available,但我们可以使用本书代码库中提供的安装程序。
在安装 OpenNI 库之后,我们还需要安装 Kinect 驱动程序。这些驱动程序可在github.com/avin2/SensorKinect/找到,安装程序位于github.com/avin2/SensorKinect/tree/unstable/Bin。
当将你的 Xbox 360 Kinect 设备插入 Windows 时,你应该在你的设备管理器中看到以下截图:

确保所有三个 Kinect 设备——音频、摄像头和电机——都显示正确。
注意
可能发生的一个注意事项是,如果用户忘记为 XBox 360 Kinect 设备连接电源,则可能只有Kinect 电机会显示,因为它们没有足够的能量。此外,你将无法在 OpenCV 应用程序中检索帧。请记住连接电源,你应该会没事的。
OpenCV Kinect 支持
在确保 OpenNI 和 Kinect 驱动程序已经正确安装后,你需要检查 OpenCV 的 Kinect 支持。幸运的是,OpenCV 提供了一个非常有用的函数来检查这一点。它被称为Core.getBuildInformation()。这个函数显示了在 OpenCV 编译期间启用的哪些选项的重要信息。为了检查 Kinect 支持,只需使用System.out.println(Core.getBuildInformation());将此函数的输出打印到控制台,并查找类似于以下内容的视频 I/O 部分:

这意味着 OpenNI 和 Kinect 支持尚未启用。
-
现在,根据第一章 为 Java 设置 OpenCV,而不是输入:
cmake -DBUILD_SHARED_LIBS=OFF ..记得添加
WITH_OPENNI标志,如下代码行所示:cmake -DBUILD_SHARED_LIBS=OFF .. -D WITH_OPENNI使用 CMake 的 GUI 时,不要使用前面的代码,确保勾选这个选项。检查输出是否类似于以下截图:
![OpenCV Kinect 支持]()
确保将 OPENNI 路径指向你的 OpenNI 正确安装文件夹。重新构建库,现在你的
opencv_java247.dll将带有 Kinect 支持构建。 -
现在再次尝试检查你的
Core.getBuildInformation()。OpenNI 的可用性将在你的 Java 控制台中显示,如下所示:Video I/O: Video for Windows: YES DC1394 1.x: NO DC1394 2.x: NO FFMPEG: YES (prebuilt binaries) codec: YES (ver 55.18.102) format: YES (ver 55.12.100) util: YES (ver 52.38.100) swscale: YES (ver 2.3.100) gentoo-style: YES OpenNI: YES (ver 1.5.7, build 10) OpenNI PrimeSensor Modules: YES (C:/Program Files (x86)/PrimeSense/Sensor/Bin/XnCore.dll) PvAPI: NO GigEVisionSDK: NO DirectShow: YES Media Foundation: NO XIMEA: NO
另一种方法是使用我们配置的 Maven 仓库。我们已经将运行时依赖项添加到本书的 Maven 仓库中,仅适用于 Windows x86,配置非常简单。只需遵循第一章 为 Java 设置 OpenCV 中的 Java OpenCV Maven 配置部分,然后,在添加普通 OpenCV 依赖项opencvjar-runtime而不是添加依赖项时,使用以下依赖项:
<dependency>
<groupId>org.javaopencvbook</groupId>
<artifactId>opencvjar-kinect-runtime</artifactId>
<version>2.4.7</version>
<classifier>natives-windows-x86</classifier>
</dependency>
完整的 POM 文件可以在本章的 Kinect 项目源代码中找到。
确保你检查了一些注意事项,例如不要混合 32 位和 64 位的驱动程序和库,以及 Java 运行时。如果是这种情况,你可能收到“在 AMD 64 位平台上无法加载 IA 32 位.dll”的错误信息,例如。另一个问题来源是忘记为 Kinect XBox 360 连接电源,这将导致它只能加载 Kinect 电机。
现在我们确认 OpenNI 和 Kinect 驱动程序已经正确安装,以及 OpenCV 的 OpenNI 支持,我们就可以继续到下一节了。
Kinect 深度应用
该应用程序专注于 Kinect 的深度感应信息以及 OpenCV API 的 OpenNI 深度传感器,这意味着它不会涵盖一些知名的 Kinect 功能,如骨骼追踪(在头部、脊椎中心、肩膀、手腕、手、膝盖、脚等重要身体部位放置节点)、手势追踪、麦克风录音或倾斜设备。尽管我们只涉及深度感应,但这却是 Kinect 最神奇的功能之一。
该应用程序背后的基本思想是从深度信息中分割图像,并将其与背景图像结合。我们将从 Kinect 设备捕获 RGB 帧并检索其深度图。通过滑块,你可以选择你想要的分割深度。基于此,通过简单的阈值生成掩码。现在,结合 RGB 帧和深度用于叠加背景图像,产生类似于色键合成的效果,当然,不需要绿色屏幕背景。这个过程可以在以下屏幕截图中看到:

我们应该注意到,在 OpenCV 2.4.7 版本中,Java API 不支持 Kinect 深度感应,但这建立在VideoCapture之上,因此只需要对常量进行一些小的修改。为了简单起见,这些常量位于主App类中,但它们应该重构为一个只处理 OpenNI 常量的类。请查找本章中的项目kinect以检查源代码。
为了处理深度感应图像,我们需要遵循以下简单指南:
VideoCapture capture = new VideoCapture(CV_CAP_OPENNI);
capture.grab();
capture.retrieve( depthMap, CV_CAP_OPENNI_DEPTH_MAP);
capture.retrieve(colorImage, CV_CAP_OPENNI_BGR_IMAGE);
我们将使用与第二章中相同,即处理矩阵、文件、摄像头和 GUI所使用的VideoCapture类,用于摄像头输入,具有相同的接口,传递常量CV_CAP_OPENNI来告诉它从 Kinect 检索帧。这里的区别在于,我们不会使用read方法,而是将获取帧的步骤拆分,然后分别检索深度图像或捕获的帧。请注意,这是通过首先调用grab方法,然后调用retrieve方法来完成的,参数为CV_CAP_OPENNI_DEPTH_MAP和CV_CAP_OPENNI_BGR_IMAGE。确保将它们发送到不同的矩阵中。请注意,所有这些常量都是从 OpenCV 源代码树中的opencv\modules\highgui\include\opencv2\highgui路径下的highgui_c.h文件中提取的。我们只将与 Kinect 的视差图和 RGB 图像一起工作,但也可以使用CV_CAP_OPENNI_DEPTH_MAP常量以毫米为单位接收深度值作为CV_16UC1矩阵,或者使用CV_CAP_OPENNI_POINT_CLOUD_MAP以CV_32FC3矩阵的形式接收点云图,其中值是米为单位的 XYZ 坐标。
我们的主要循环由以下代码组成:
while(true){
capture.grab();
capture.retrieve( depthMap, CV_CAP_OPENNI_DISPARITY_MAP);
disparityImage = depthMap.clone();
capture.retrieve(colorImage, CV_CAP_OPENNI_BGR_IMAGE);
workingBackground = resizedBackground.clone();
Imgproc.threshold(disparityImage, disparityThreshold, gui.getLevel(), 255.0f, Imgproc.THRESH_BINARY);
maskedImage.setTo(new Scalar(0,0,0));
colorImage.copyTo(maskedImage,disparityThreshold);
maskedImage.copyTo(workingBackground,maskedImage);
renderOutputAccordingToMode(disparityImage, disparityThreshold,
colorImage, resizedBackground, workingBackground, gui);
}
首先,我们调用grab方法从 Kinect 获取下一帧。然后,我们检索深度图和彩色图像。因为我们之前已经在resizedBackground中加载了我们的背景,所以我们只需将其克隆到workingBackground。随后,我们根据滑块级别对差异图像进行阈值处理。这将使远离我们期望深度的像素变黑,而我们仍然想要的像素变白。现在是时候清除我们的掩码并将其与彩色图像组合了。
摘要
本章真正涵盖了处理背景去除的几个领域以及由此问题产生的某些细节,例如需要使用连通组件来找到它们的轮廓。首先,确立了背景去除本身的问题。然后,分析了如帧差分这样的简单算法。之后,介绍了更有趣的算法,例如平均背景和高斯混合(MOG)。
在使用算法处理背景去除问题后,探索了关于连通组件的见解。解释了核心 OpenCV 算法,如findContours和drawContours。还分析了轮廓的一些属性,例如它们的面积以及凸包。
本章以如何使用 Kinect 的深度传感器设备作为背景去除工具的完整解释结束,针对 OpenCV 2.4.7。在设备设置上的深度指令之后,开发了一个完整的应用程序,使处理深度感应传感器 API 变得清晰。
好吧,现在到了下一章从桌面应用程序跳转到 Web 应用程序的时候了。在那里,我们将介绍如何设置基于 OpenCV 的 Web 应用程序的细节,处理图像上传,并基于 Tomcat Web 服务器创建一个不错的增强现实应用程序。这将很有趣,只是要注意看 Einstein 的截图。
第七章:服务器端的 OpenCV
随着互联网越来越互动,一个令人感兴趣的主题是如何处理服务器端的图像处理,以便你可以创建处理 OpenCV 的 Web 应用程序。由于 Java 是开发 Web 应用程序时选择的语言之一,本章展示了整个应用程序的架构,允许用户上传图像,并在检测到的面部上方添加一顶 fedora 帽子,使用的是本书中学到的技术。
在本章中,我们将涵盖以下主题:
-
设置 OpenCV Web 应用程序
-
混合现实
-
图像上传
-
处理 HTTP 请求
到本章结束时,你将了解如何创建一个完整的带有图像处理的 Web 应用程序,从用户那里获取输入,在服务器端处理图像,并将处理后的图像返回给用户。
设置 OpenCV Web 应用程序
由于本章涵盖了使用 Java OpenCV 开发 Web 应用程序的开发,因此在转向服务器端时,解决一些差异是很重要的。首先,需要告诉 Web 容器(通常是 Tomcat、Jetty、JBoss 或 Websphere)关于本地库的位置。其他细节涉及加载本地代码。这应该在 Web 服务器启动时立即发生,并且不应再次发生。
使用 Web 架构的优势是显著的。由于某些图像处理任务计算密集,它们可能会在短时间内迅速耗尽设备的电池,因此,将它们转移到云上的更强大的硬件上可以减轻本地处理。此外,用户无需安装除网络浏览器之外的其他任何东西,服务器端发生的更新也非常方便。
另一方面,也有一些缺点。如果,不是在管理员基础设施上托管 Web 应用程序,而是打算在在线 Java 服务器上托管它,那么应该清楚它是否允许运行本地代码。在撰写本文时,Google 的 App Engine 不允许这样做,但很容易在 Amazon EC2 或 Google 的 Compute Engine 上设置一个 Linux 服务器,使其顺利运行,尽管这不会在本书中涵盖。另一件需要考虑的事情是,一些计算机视觉应用程序需要实时运行,例如,以每秒 20 帧的速度,这在 Web 架构中是不切实际的,因为上传时间过长,这类应用程序应该本地运行。
为了创建我们的 Web 应用程序,我们将按照以下步骤进行:
-
创建一个基于 Maven 的 Web 应用程序。
-
添加 OpenCV 依赖项。
-
运行 Web 应用程序。
-
将项目导入 Eclipse。
在以下章节中,我们将详细说明这些步骤。
创建一个基于 Maven 的 Web 应用程序
在 Java 中创建 Web 应用程序有几种方法。Spring MVC、Apache Wicket 和 Play Framework 等都是不错的选择。此外,在这些框架之上,我们可以使用 JavaServer Faces、PrimeFaces 或 RichFaces 作为基于组件的用户界面为这些 Web 应用程序。然而,对于本章,我们不会涉及所有这些技术,而是只使用 servlets 来供你选择框架。你应该注意到,servlet 简单来说是一个用于扩展服务器功能的 Java 类,这通常用于处理或存储通过 HTML 表单提交的数据。servlet API 自 1997 年以来一直存在,因此已被广泛使用,关于它的书籍和示例有很多。尽管本章专注于 Servlet 2.x 以保持简单,但我们仍需意识到 API 是同步的,并且对于将接收多个客户端的应用程序,可能更好的选择是使用异步的 Servlet 3.x。
尽管任何 IDE 都可以通过向导轻松生成 Web 应用程序——例如,在 Eclipse 中导航到 文件 | 新建 | 项目… | Web | 动态 Web 项目——但我们将专注于使用 Maven 来启动它,因为我们可以轻松获取原生依赖项。只要它根据 第一章 中的说明正确安装,Maven 就可以通过使用原型来设置 Web 应用程序。这是通过以下命令实现的:
mvn archetype:generate -DgroupId=com.mycompany.app -DartifactId=my-webapp -DarchetypeArtifactId=maven-archetype-webapp -DinteractiveMode=false
此命令将从 archetype 插件调用 generate 目标。将 archetype 视为一个项目模板。这个 Maven 插件将根据我们通过 -DarchetypeArtifactId=maven-archetype-webapp 选项设置的 archetypeArtifactId 为 maven-archetype-webapp 从模板生成一个 Web 应用程序。另一个选项 DartifactId=my-webapp 将简单地设置 Web 应用程序的文件夹名称,如该选项中定义的那样,而 groupId 是 Maven 为项目提供的通用唯一标识符。
注意以下结构将被创建:

前面是一个 Web 应用的简单结构。你应该注意 web.xml 文件,它用于映射 servlets,以及 index.jsp,这是一个简单的 Java 服务器页面文件。到目前为止,你应该能够轻松地在 Tomcat 等服务器上运行这个 Web 应用程序。只需输入以下命令:
cd my-webapp
mvn tomcat:run
现在,如果你访问地址 http://localhost:8080/my-webapp/,浏览器中应该看到以下响应:

注意,这意味着我们已经成功创建了一个 Web 项目,我们正在通过 Tomcat Web 容器运行它,并且它可以通过 localhost 服务器,在端口 8080,通过名称 my-webapp 访问。您可以在 index.jsp 中看到 Hello World! 消息。在下一节中,您将自定义 pom 文件以添加 OpenCV 依赖项。
添加 OpenCV 依赖项
由于 Web 应用程序存档已经为我们创建了一个项目结构,我们将为生成的 pom.xml 添加 OpenCV 依赖项。如果您打开它,您将看到以下代码:
<project
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mycompany.app</groupId>
<artifactId>my-webapp</artifactId>
<packaging>war</packaging>
<version>1.0-SNAPSHOT</version>
<name>my-webapp Maven Webapp</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>my-webapp</finalName>
</build>
</project>
注意,唯一的依赖项是 junit。现在将以下内容添加到依赖项标签中:
<dependency>
<groupId>org.javaopencvbook</groupId>
<artifactId>opencvjar</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.javaopencvbook</groupId>
<artifactId>opencvjar-runtime</artifactId>
<version>3.0.0</version>
<classifier>natives-windows-x86</classifier>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
前两个依赖项,opencvjar 和 opencvjar-runtime,与在第一章中讨论的相同,即为 Java 设置 OpenCV。现在,对 javax.servlet-api 的依赖指的是 3.0.1 版本的 servlet API,它用于使文件上传更加容易。除了使用这些依赖项之外,所有其他配置都在第一章中提到,为 Java 设置 OpenCV,例如添加 JavaOpenCVBook 仓库、maven-jar-plugin、maven-dependency-plugin 和 maven-nativedependencies-plugin。
唯一的新插件是 tomcat7,因为我们需要使用来自 servlet 3.0 的文件上传 API。为了添加 tomcat7 插件,在 pom.xml 中查找 <plugins> 部分,并添加以下代码:
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<port>9090</port>
<path>/</path>
</configuration>
</plugin>
除了能够从 Maven 运行 tomcat7 之外,它还将配置端口 9090 作为我们服务器的默认端口,但您可以使用另一个端口。最终的 pom.xml 文件可以在本章的源代码项目中找到。运行 mvn package 命令将显示项目设置一切正常。在下一节中,我们将通过从 .jsp 文件中的简单 OpenCV 调用检查所有过程。
运行 Web 应用程序
现在所有依赖项都已设置,运行我们的 Web 应用程序应该很简单。但有一个细节需要注意。由于我们的应用程序依赖于本地代码,即 opencv_java300.dll 文件或共享对象,我们应该在运行 Tomcat 服务器之前将其放在 Java 库路径中。根据您的部署策略,有几种方法可以做到这一点,但一种简单的方法是通过 MAVEN_OPTS 环境变量设置路径。您应该在终端中输入以下命令:
set MAVEN_OPTS=-Djava.library.path=D:/your_path/my-webapp/target/natives
请记住将 your_path 更改为您设置项目的地方,即 my-webapp 的父文件夹。为了检查应用程序服务器是否可以正确加载 OpenCV 本地库,我们将设置一个简单的 servlet,该 servlet 能够输出正确的已安装版本。将您在 my-webapp\src\main\webapp 文件夹中生成的 index.jsp 文件更改为以下代码:
<html>
<body>
<h2>OpenCV Webapp Working!</h2>
<%@ page import = "org.opencv.core.Core" %>
Core.VERSION: <%= Core.VERSION %>
</body>
</html>
现在,通过输入 mvn tomcat7:run 运行您的服务器。尝试在您的网页浏览器中加载您的应用程序,地址为 http://localhost:9090,您应该会看到输出您加载的 OpenCV 版本的页面。尽管这段代码实际上并没有加载本地库,因为 Core.VERSION 可以从纯 Java JAR 中检索,但将业务代码(真正进行图像处理的代码)与表示代码(我们刚刚编辑的 Java 服务器页面)混合在一起并不是一个好的做法。为了处理图像处理,我们将集中代码在一个只处理它的 servlet 中。
将项目导入到 Eclipse
现在项目已经使用 Maven 设置好了,将其导入到 Eclipse 应该很容易。只需发出以下 Maven 命令:
mvn eclipse:eclipse -Dwtpversion=2.0
记得添加 -Dwtpversion=2.0 标志以支持 WTP 版本 2.0,这是 Eclipse 的 Web 工具平台。如果您没有按照 第一章 中所述设置 M2_REPO,有一个简单的技巧可以自动完成它。输入以下命令:
mvn -Declipse.workspace="YOUR_WORKSPACE_PATH" eclipse:configure-workspace
如果您的 Eclipse 工作区位于 C:\Users\baggio\workspace,则应将 YOUR_WORKSPACE_PATH 路径更改为类似 C:\Users\baggio\workspace 的路径。
在 Eclipse 中,通过 File | Import | General | Existing Projects 导航到工作区,并指向您的 my-webapp 文件夹。请注意,您的 Eclipse 应该有 WTP 支持。如果您收到 "Java compiler level does not match the version of the installed Java project facet" 信息,只需右键单击它,然后在 Quick Fix 菜单中选择 Change Java Project Facet version to Java 1.8。现在您可以通过右键单击项目,导航到 Run as | Run on Server,选择 Apache | Tomcat v7.0 Server,然后点击 Next 来运行它。如果您没有现有的 Tomcat 7 安装,请选择 Download and Install,如下一张截图所示:

选择一个文件夹用于您的 Tomcat7 安装,然后点击 Next 和 Finish。现在,您可以直接从 Eclipse 运行您的应用程序,通过在您的项目上右键单击并选择 Run as | Run on Server。如果您收到 "java.lang.UnsatisfiedLinkError: no opencv_java300 in java.library.path" 错误,请右键单击您的项目,选择 "Run As ->Run Configurations...",然后在 Arguments 选项卡中,在 VM arguments 文本框中添加 -Djava.library.path="C:\path_to_your\target\natives"。点击 "Apply",然后通过转到 Server 选项卡并右键单击您的 Tomcat7 执行 -> Restart 来重启您的服务器。
混合现实网络应用程序
我们将要开发的一个网络应用程序将在给定图像中检测到的头部上方绘制费德里亚帽。为了做到这一点,用户通过一个简单的表单上传图片,然后它在内存中转换为 OpenCV 矩阵。转换后,在矩阵上运行一个寻找脸部的级联分类器。应用简单的缩放和平移来估计帽子的位置和大小。然后在每个检测到的脸部指定位置绘制一个透明的费德里亚帽图像。然后通过将混合现实图片提供给用户,通过 HTTP 返回结果。请注意,所有处理都在服务器端进行,因此客户端只需上传和下载图片,这对于依赖电池的客户端非常有用,例如智能手机。
注意
混合现实(MR),有时也称为混合现实(包括增强现实和增强虚拟性),是指现实世界和虚拟世界的融合,产生新的环境和可视化,其中物理对象和数字对象在实时共存和交互。它不仅仅发生在物理世界或虚拟世界中,而是现实和虚拟的混合,包括增强现实和增强虚拟性。
来源:Fleischmann, Monika; Strauss, Wolfgang (eds.) (2001). "CAST01//Living in Mixed Realities" 国际会议关于艺术、科学和技术传播的论文集,弗劳恩霍夫 IMK 2001,第 401 页。ISSN 1618–1379(印刷版),ISSN 1618–1387(网络版)。
这个网络应用程序可以分解为几个更简单的步骤:
-
图片上传。
-
图片处理。
-
响应图片。
以下部分将详细说明这些步骤。
图片上传
首先,我们将把我们的模拟 Java 服务器页面转换成一个需要用户选择本地文件的表单,类似于以下截图所示:

以下代码显示了完整的 Java 服务器页面。注意表单元素,它表明它将在 servlet 的doPost部分调用post方法,并要求网络服务器接受表单中包含的数据以进行存储。enctype= "multipart/form-data"表示不会对字符进行编码,正如在"text/plain"加密类型中可以看到的,它将空格转换为+符号。另一个重要属性是action="upload"。它确保表单中编码的数据被发送到"/upload" URL。类型为"file"的输入元素简单地作为对操作系统文件对话框的调用,该对话框弹出并允许用户指定文件位置。最后,当按钮被点击时,类型为"submit"的输入元素处理带有表单数据的请求发送:
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>File Upload</title>
</head>
<body>
<center>
<h1>File Upload</h1>
<form method="post" action="upload"
enctype="multipart/form-data">
Select file to upload: <input type="file" name="file" size="60" /><br />
<br /> <input type="submit" value="Upload" />
</form>
</center>
</body>
</html>
当按下提交按钮时,一个字节流被发送到服务器,服务器将它们转发到名为Upload的 servlet。请注意,从/upload URL 映射到Upload servlet 的操作发生在/src/main/webapp/WEB-INF/web.xml文件中,如下所示:
<web-app>
<servlet>
<servlet-name>Upload</servlet-name>
<servlet-class>org.javaopencvbook.webapp.UploadServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>Upload</servlet-name>
<url-pattern>/upload</url-pattern>
</servlet-mapping>
</web-app>
注意,当用户从表单中点击提交按钮时,映射的 servlet 类UploadServlet的doPost方法被调用。这个方法是这个 Web 应用的核心,我们将在下面的代码中详细查看它:
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
loadCascade();
Mat image = receiveImage(request);
Mat overlay = loadOverlayImage();
detectFaceAndDrawHat(image, overlay);
writeResponse(response, image);
}
在doPost方法中的主要操作首先是通过加载 OpenCV 库开始的,正如前几章所看到的,然后加载稍后用于人脸检测的级联分类器。为了简洁起见,初始化在这里进行,但在实际代码中,你应该使用ServletContextListener来初始化它。然后,receiveImage方法处理从上传接收字节并将其转换为 OpenCV 矩阵。因此,其他方法负责加载费多拉帽子的图像并检测人脸,以便可以通过detectFaceAndDrawHat方法绘制叠加图像。最后,writeResponse方法响应请求。我们将在下面的代码中更详细地介绍receiveImage:
private Mat receiveImage(HttpServletRequest request) throws IOException, ServletException {
byte[] encodedImage = receiveImageBytes(request);
return convertBytesToMatrix(encodedImage);
}
注意,receiveImage只是从上传请求的receiveImageBytes中抓取字节,然后将其转换为矩阵。以下是receiveImageBytes的代码:
private byte[] receiveImageBytes(HttpServletRequest request)
throws IOException, ServletException {
InputStream is = (InputStream) request.getPart("file").getInputStream();
BufferedInputStream bin = new BufferedInputStream(is);
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
int ch =0;
while((ch=bin.read())!=-1) {
buffer.write(ch);
}
buffer.flush();
bin.close();
byte[] encodedImage = buffer.toByteArray();
return encodedImage;
}
这是接收上传的默认代码。它从表单的"文件"字段中访问,并通过request.getPart("file").getInputStream()获取其流。然后创建一个缓冲区,只要上传中有数据,所有来自输入流的数据都通过write()方法写入。然后,通过ByteArrayOutputStream类的toByteArray()方法返回字节数组。由于此时我们接收到的只是一堆字节,因此需要解码图像格式并将其转换为 OpenCV 矩阵。幸运的是,已经有一个方法可以做到这一点,即来自Imgcodecs包的imdecode方法,其签名如下:
public static Mat imdecode(Mat buf, int flags)
buf参数是一个Mat缓冲区,我们将从字节数组中创建它,而flags是一个选项,用于将返回的Mat缓冲区转换为灰度或彩色,例如。
解码的完整代码可以在以下行中看到:
private Mat convertBytesToMatrix(byte[] encodedImage) {
Mat encodedMat = new Mat(encodedImage.length,1,CvType.CV_8U);
encodedMat.put(0, 0,encodedImage);
Mat image = Imgcodecs.imdecode(encodedMat, Imgcodecs.CV_LOAD_IMAGE_ANYCOLOR);
return image;
}
现在已经完成了,我们已经接收到了用户的图像上传,并且它已被转换为我们的熟知的Mat类。现在是时候创建混合现实了。
图像处理
在本节中,我们将描述如何处理接收到的图像,以便在其上方绘制图像文件。现在,级联分类器就像在上一章中一样运行。注意 XML 级联文件的位置很重要。在整个代码中,我们使用了一个名为getResourcePath的辅助函数,并且我们使用了将所有资源存储在src/main/resources/文件夹中的约定。这样,辅助函数的工作方式类似于以下代码:
private String getResourcePath(String path) {
String absoluteFileName = getClass().getResource(path).getPath();
absoluteFileName = absoluteFileName.replaceFirst("/", "");
return absoluteFileName;
}
使用此函数,可以通过以下调用加载级联:
private void loadCascade() {
String cascadePath = getResourcePath("/cascades/lbpcascade_frontalface.xml");
faceDetector = new CascadeClassifier(cascadePath);
}
在级联被正确加载后,我们一切准备就绪,现在是时候解释如何估计帽子的位置了。当运行人脸分类器时,我们不仅对脸的位置有很好的了解,而且对脸的边界矩形也有很好的了解。我们将使用这个宽度来估计帽子的宽度。我们可以假设帽子的宽度将是脸的边界矩形宽度的三倍。这样,我们仍然需要保持帽子的宽高比。这是通过一个简单的三比规则来实现的,如下所示:

现在虚拟帽子的尺寸已经定义,我们仍然需要估计其位置。从一些测试中,我们可以推断出,对于大多数图片来说,在脸的边界矩形上方 60%的位置应该是合适的。现在,我们有了帽子的尺寸和位置。最后,我们不再使用帽子的宽度是脸宽度的三倍,而是使用 2.3 倍脸宽度的值似乎效果更好。以下代码显示了在detectFaceAndDrawHat方法中设置感兴趣区域(ROI)以绘制礼帽所使用的数学方法。当帽子尺寸超出边界时,对其进行简单调整。
double hatGrowthFactor = 2.3;
int hatWidth = (int) (rect.width *hatGrowthFactor);
int hatHeight = (int) (hatWidth * overlay.height() / overlay.width());
int roiX = rect.x - (hatWidth-rect.width)/2;
int roiY = (int) (rect.y - 0.6*hatHeight);
roiX = roiX<0 ? 0 : roiX;
roiY = roiY< 0? 0 :roiY;
hatWidth = hatWidth+roiX > image.width() ? image.width() -roiX : hatWidth;
hatHeight = hatHeight+roiY > image.height() ? image.height() - roiY : hatHeight;
以下截图为我们提供了宽度和绘制礼帽叠加过程的概述:

是时候画帽子了!这应该就像在图片中找到帽子的位置并复制子矩阵一样简单。不过,我们需要小心,确保正确地绘制透明像素,不要超出图片范围。Mat 的copyTo方法用于将子矩阵复制到另一个矩阵中。此方法还接受一个掩码 Mat 参数,其中非零元素指示必须复制的矩阵元素。请注意,帽子图像本身被作为掩码参数传递,并且它实际上工作得很好,因为所有透明像素在所有通道中都变为零,而所有其他像素都将具有某些值,就像一个掩码一样。调整礼帽大小并将其复制到主图像的代码如下:
Mat resized = new Mat();
Size size = new Size(hatWidth,hatHeight);
Imgproc.resize(overlay,resized, size);
Mat destinationROI = image.submat( roi );
resized.copyTo( destinationROI , resized);
响应图像
我们已经成功接收到了一张图像,并在识别到的面部上绘制了帽子。现在,是时候将结果发送回用户了。我们通过将响应的内容类型设置为image/jpeg(例如)来完成这项工作。然后我们使用在头部中定义的相同格式来编码我们的响应——如果它是 jpeg,我们将使用 JPEG 进行编码——并将字节写入我们的响应 servlet 对象:
private void writeResponse(HttpServletResponse response, Mat image) throws IOException {
MatOfByte outBuffer = new MatOfByte();
Imgcodecs.imencode(".jpg", image, outBuffer);
response.setContentType("image/jpeg");
ServletOutputStream out;
out = response.getOutputStream();
out.write(outBuffer.toArray());
}
输入图像和输出结果如下截图所示。在我们的增强现实网络应用程序中,一些费多拉帽被分配给了爱因斯坦和他的朋友们。左侧的照片是上传的图像,而右侧的照片显示了在检测到的面部上绘制的帽子。根据我们的循环,帽子的绘制顺序与检测到的面部的返回顺序相同。这样,我们无法保证正确的 Z 顺序,即帽子绘制在另一个帽子之上,尽管我们可以尝试从面部大小中推断它。以下图片展示了这一点:

摘要
在本章中,我们将我们的计算机视觉应用程序发送到了服务器端的世界。我们开始介绍使用 Maven 配置简单 servlet 基于的 Web 应用程序的基础,它为我们提供了一个通用应用程序结构。然后我们将 OpenCV 依赖项添加到我们的pom.xml配置文件中,就像在标准的 OpenCV 桌面应用程序中使用的那样。然后我们检查了其他运行时配置,因为我们使用 Maven 部署了我们的 Web 服务器。
在解决了每个 Web 应用程序配置方面的问题后,我们继续开发我们的混合现实应用程序,该应用程序探讨了图像上传的细节,将其转换为 OpenCV Mat 对象,然后向客户端发送处理后的图像的响应。
看起来,创建基本计算机视觉应用的各个方面都已经涵盖了。我们处理了为 Java 设置 OpenCV,然后学习了如何处理矩阵。然后我们接触到了创建 Java Swing 桌面应用程序的基础,并使用图像处理算法进行过滤、改变图像形态和进行基本阈值处理。你还学习了每个计算机视觉研究人员工具箱中的工具,例如霍夫变换来寻找线和圆以及特殊的核卷积。我们还涵盖了重要的傅里叶变换和变换操作。然后我们深入到机器学习,并使用了方便的 OpenCV 级联,你还学习了如何创建新的对象分类器。除此之外,我们还研究了某些背景移除方法,并测试了令人难以置信的 Kinect 设备以执行基于深度的处理。最后,我们用完整的服务器端示例完成了这本书,现在,你可以为你的计算机视觉项目信赖 Java 了!









:如果身高超过 5 英尺 9 英寸(约 175 厘米),那么这个人就是男性或女性。当然,有些女性的身高超过男性,但平均来看,男性通常更高。
:如果一个人有长发,那么这个人就是女性或男性。同样,有几个长头发的男性,但平均来看,女性通常有更长的头发。
:如果一个人有胡须,那么这个人就是男性或女性。在这里,我们可能会错误地将刮胡子的男性分类。
浙公网安备 33010602011771号