原文:
annas-archive.org/md5/b5d2ddddf00cdfdea66355a7259934ba译者:飞龙
第五章:使用 CUDA 支持的 OpenCV 入门
到目前为止,我们已经看到了所有与使用 CUDA 进行并行编程相关的概念,以及它是如何利用 GPU 进行加速的。从本章开始,我们将尝试使用 CUDA 中的并行编程概念来应用于计算机视觉应用。虽然我们已经处理过矩阵,但我们还没有处理过实际图像。基本上,处理图像类似于二维矩阵的操作。我们不会从头开始为 CUDA 中的计算机视觉应用开发整个代码,但我们将使用名为 OpenCV 的流行计算机视觉库。尽管这本书假设读者对使用 OpenCV 有一些熟悉,但本章回顾了在 C++中使用 OpenCV 的概念。本章描述了在 Windows 和 Ubuntu 上安装支持 CUDA 的 OpenCV 库。然后它描述了如何测试此安装并运行一个简单的程序。本章通过为它开发简单的代码来描述使用 OpenCV 处理图像和视频。本章还将比较具有 CUDA 支持的程序与没有 CUDA 支持的程序的性能。
本章将涵盖以下主题:
-
图像处理和计算机视觉简介
-
支持 CUDA 的 OpenCV 简介
-
在 Windows 和 Ubuntu 上安装支持 CUDA 的 OpenCV
-
使用 OpenCV 处理图像
-
使用 OpenCV 处理视频
-
图像上的算术和逻辑运算
-
颜色空间转换和图像阈值
-
CPU 和 GPU OpenCV 程序的性能比较
技术要求
本章需要具备图像处理和计算机视觉的基本理解。它需要熟悉基本的 C 或 C++编程语言以及前几章中解释的所有代码示例。本章中使用的所有代码都可以从以下 GitHub 链接下载:github.com/PacktPublishing/Hands-On-GPU-Accelerated-Computer-Vision-with-OpenCV-and-CUDA。代码可以在任何操作系统上执行,尽管它只在 Ubuntu 16.04 上进行了测试。
查看以下视频以查看代码的实际运行情况:
图像处理和计算机视觉简介
世界上的图像和视频数据量每天都在不断增加。随着移动设备用于捕捉图像和互联网用于发布图像的日益普及,每天都会产生大量的视频和图像数据。图像处理和计算机视觉在各个领域的许多应用中都得到了应用。医生使用 MRI 和 X 射线图像进行医学诊断。空间科学家和化学工程师使用图像进行太空探索和分子水平上各种基因的分析。图像可以用于开发自动驾驶车辆和视频监控应用。它们还可以用于农业应用和制造过程中的故障产品识别。所有这些应用都需要在计算机上以高速处理图像。我们不会探讨图像是如何通过相机传感器捕捉并转换为计算机存储的数字图像的。在这本书中,我们只涵盖计算机上的图像处理,假设它已经存储好了。
许多人将图像处理和计算机视觉这两个术语互换使用。然而,这两个领域之间是有区别的。图像处理关注通过修改像素值来提高图像的视觉质量,而计算机视觉关注从图像中提取重要信息。因此,在图像处理中,输入和输出都是图像,而在计算机视觉中,输入是图像,但输出是从该图像中提取的信息。两者都有广泛的应用,但图像处理主要在计算机视觉应用的预处理阶段使用。
图像以多维矩阵的形式存储。因此,在计算机上处理图像不过是操作这个矩阵。我们在前面的章节中看到了如何在 CUDA 中处理矩阵。在 CUDA 中读取、操作和显示图像的代码可能会变得非常长、繁琐且难以调试。因此,我们将使用一个包含所有这些功能 API 的库,并且可以利用 CUDA-GPU 加速处理图像的优势。这个库被称为 OpenCV,它是“开放计算机视觉”的缩写。在下一节中,我们将详细介绍这个库。
OpenCV 简介
OpenCV 是一个以计算效率为前提,并专注于实时性能的计算机视觉库。它用 C/C++编写,包含超过一百个有助于计算机视觉应用的功能。OpenCV 的主要优势是它是开源的,并且根据伯克利软件发行许可(BSD 许可)发布,这允许在研究和商业应用中免费使用 OpenCV。这个库提供了 C、C++、Java 和 Python 语言的接口,并且可以在所有操作系统上使用,如 Windows、Linux、macOS 和 Android,而无需修改任何一行代码。
这个库还可以利用多核处理、OpenGL 和 CUDA 进行并行处理。由于 OpenCV 轻量级,它也可以在树莓派等嵌入式平台上使用。这使得它在现实场景中部署计算机视觉应用到嵌入式系统中变得理想。我们将在接下来的几章中探讨这一点。这些特性使 OpenCV 成为计算机视觉开发者的默认选择。它拥有广泛的开发者和用户社区,不断帮助改进这个库。OpenCV 的下载量以百万计,并且每天都在增加。另一个流行的计算机视觉和图像处理工具是 MATLAB,因此你可能会想知道使用 OpenCV 而不是 MATLAB 的优势。以下表格显示了这两个工具的比较:
| 参数 | OpenCV | MATLAB |
|---|---|---|
| 程序速度 | 由于它是用 C/C++开发的,所以速度更快 | 低于 OpenCV |
| 资源需求 | OpenCV 是一个轻量级库,因此在硬盘和 RAM 方面都消耗很少的内存。一个普通的 OpenCV 程序将需要少于 100MB 的 RAM。 | MATLAB 非常庞大。最新版本的 MATLAB 安装可能需要在硬盘上占用超过 15GB 的空间,并且在使用时需要大量的 RAM(超过 1GB)。 |
| 可移植性 | OpenCV 可以在所有可以运行 C 语言的操作系统上运行。 | MATLAB 只能在 Windows、Linux 和 MAC 上运行。 |
| 成本 | 在商业或学术应用中使用 OpenCV 是完全免费的。 | MATLAB 是许可软件,因此你必须支付一大笔钱才能在学术或商业应用中使用它。 |
| 易用性 | 与其较少的文档和难以记忆的语法相比,OpenCV 相对较难使用。它也没有自己的开发环境。 | MATLAB 有自己的集成开发环境,内置帮助资源,这使得新程序员容易使用。 |
MATLAB 和 OpenCV 都有自己的优缺点。但当我们想在嵌入式应用中使用计算机视觉并利用并行处理时,OpenCV 是理想的选择。因此,在这本书中,我们将描述如何使用 GPU 和 CUDA 加速计算机视觉应用。OpenCV 提供了 C、C++、Python 和 Java 的 API。它是用 C/C++编写的,因此这些语言的 API 将是最快的。此外,CUDA 加速在 C/C++ API 中支持得更好,所以在这本书中,我们将使用 OpenCV 的 C/C++ API。在下一节中,我们将看到如何在各种操作系统上安装 OpenCV。
支持 CUDA 的 OpenCV 安装
使用 CUDA 安装 OpenCV 并不像你想象中那么简单。它涉及许多步骤。在本节中,我们将通过截图详细解释在 Windows 和 Ubuntu 上安装 OpenCV 的所有步骤,以便你可以轻松地设置你的环境。
在 Windows 上安装 OpenCV
本节解释了在 Windows 操作系统上安装带有 CUDA 的 OpenCV 所需的步骤。这些步骤在 Windows 10 操作系统上执行,但它们可以在任何 Windows 操作系统上工作。
使用预构建的二进制文件
对于 OpenCV,有一些预构建的二进制文件可供下载并直接用于您的程序中。它没有充分利用 CUDA,因此不建议在本书中使用。以下步骤描述了在 Windows 上不使用 CUDA 支持安装 OpenCV 的过程:
-
确保已安装 Microsoft Visual Studio 以编译 C 程序。
-
从
sourceforge.net/projects/opencvlibrary/files/opencv-win/下载 OpenCV 的最新版本。 -
双击下载的
.exe文件,并将其提取到您选择的文件夹中。在这里,我们将其提取到C://opencv文件夹。 -
通过右键单击我的电脑 | 高级设置 | 环境变量 | 新建来设置环境变量
OPENCV_DIR。将其值设置为C:\opencv\build\x64\vc14,如下截图所示。这里vc14将取决于 Microsoft Visual Studio 的版本:
现在,您可以使用此安装为 OpenCV 应用程序使用 C/C++。
从源代码构建库
如果您想编译带有 CUDA 支持的 OpenCV,请按照以下步骤进行安装:
-
OpenCV 带有 CUDA 将需要一个 C 编译器和 GPU 编译器。它需要 Microsoft Visual Studio 和最新的 CUDA 安装。安装它们的步骤在第一章,介绍 CUDA 和 CUDA 入门中有所介绍。因此,在继续之前,请检查它们是否已正确安装。
-
通过访问链接下载 OpenCV 最新版本的源代码:
github.com/opencv/opencv/. -
有些额外的模块不包括在 OpenCV 中,但它们可以在名为
opencv_contrib的额外模块中找到,这个模块可以与 OpenCV 一起安装。此模块中可用的功能不稳定;一旦它们变得稳定,它们就会被移动到实际的 OpenCV 源中。如果您想安装此模块,请从github.com/opencv/opencv_contrib下载。 -
从以下链接安装
cmake:cmake.org/download/。它是编译 OpenCV 库所需的。 -
将
opencv和opencv_contrib的 ZIP 文件提取到任何文件夹中。在这里,它们被提取到C://opencv和C://opencv_contrib文件夹中。 -
打开 CMake 以编译 OpenCV。在 CMake 中,您需要选择 OpenCV 源代码的路径,并选择此源代码将被构建的文件夹。如下截图所示:
- 然后点击配置。它将开始配置源。CMake 将尝试根据系统变量的路径设置定位尽可能多的包。配置过程如下面的截图所示:
- 如果某些包没有找到,您可以手动定位它们。为了配置具有 CUDA 支持的 OpenCV 安装,您必须检查如下截图所示的
WITH_CUDA变量,然后再次点击配置:
- 配置完成后,点击生成。这将根据您选择的 Visual Studio 版本创建 Visual Studio 项目文件。当生成完成后,窗口应该类似于以下截图:
- 前往
opencv文件夹的构建目录,并找到名为OpenCV.sln的 Visual Studio 项目,如下面的截图所示:
- 这将在 Microsoft Visual Studio 中打开项目。在解决方案资源管理器中,找到名为
ALL_BUILD的项目。右键单击它并构建。在 Visual Studio 中为调试和发布选项构建此项目。如下面的截图所示:
-
构建整个项目可能需要很长时间,尽管这会根据您的处理器和 Visual Studio 版本而有所不同。在构建操作成功完成后,您就可以在 C/C++项目中使用 OpenCV 库了。
-
通过右键单击我的电脑 | 高级系统设置 | 环境变量 | 新建来设置环境变量
OPENCV_DIR。将其值设置为C:\opencv\build\x64\vc14。在这里,vc14将取决于您使用的 Microsoft Visual Studio 版本:
您可以通过访问C://opencv/build/bin/Debug目录并运行任何.exe 应用程序来检查安装情况。
在 Linux 上安装具有 CUDA 支持的 OpenCV
本节涵盖了在 Linux 操作系统上安装具有 CUDA 支持的 OpenCV 的步骤。这些步骤已在 Ubuntu 16.04 上测试,但它们应该适用于任何 Unix 发行版:
- 具有 CUDA 的 OpenCV 将需要最新的 CUDA 安装。安装它的过程在第一章,介绍 CUDA 和 CUDA 入门中有所覆盖。所以在继续之前,请检查它是否已正确安装。您可以通过执行
nvidia-smi命令来检查 CUDA 工具包和 Nvidia 设备驱动程序的安装情况。如果您的安装正常工作,您应该看到如下类似的输出:
-
通过访问链接下载 OpenCV 最新版本的源代码:
github.com/opencv/opencv/。将其解压到opencv文件夹中。 -
有一些额外的模块不包括在 OpenCV 中,但它们在名为
opencv_contrib的额外模块中可用,可以与 OpenCV 一起安装。此模块中提供的函数是不稳定的;一旦它们变得稳定,它们就会被移动到实际的 OpenCV 源中。如果您想安装此模块,可以从github.com/opencv/opencv_contrib下载它。将其提取到与opencv文件夹相同的目录中的opencv_contrib文件夹。 -
打开
opencv文件夹并创建一个构建目录。然后进入这个新创建的build目录。这些步骤可以通过在命令提示符中执行以下命令来完成:
$ cd opencv
$ mkdir build
$ cd build
cmake命令用于编译具有 CUDA 支持的opencv。确保在此命令中将WITH_CUDA标志设置为 ON,并指定一个适当的路径,用于存储在opencv_contrib目录中下载并保存的额外模块。完整的cmake命令如下所示:
cmake -D CMAKE_BUILD_TYPE=RELEASE CMAKE_INSTALL_PREFIX=/usr/local WITH_CUDA=ON ENABLE_FAST_MATH=1 CUDA_FAST_MATH=1 -D WITH_CUBLAS=1 OPENCV_EXTRA_MODULES_PATH=../../opencv_contrib/modules BUILD_EXAMPLES=ON ..
它将开始配置和创建makefile。它将根据系统路径中的值定位所有额外模块。以下截图显示了具有所选 CUDA 安装的cmake命令的输出:
- 在配置成功后,CMake 将在构建目录中创建一个 makefile。要使用此 makefile 编译 OpenCV,请在命令窗口中执行命令
make -j8,如下所示:
- 编译成功后,要安装 OpenCV,您必须从命令行执行命令
sudo make install。以下是该命令的输出:
-
运行
sudo ldconfig命令以完成安装。它创建了必要的链接和缓存到opencv库。 -
您可以通过运行
opencv/samples/gpu文件夹中的任何示例来检查安装。
要在程序中使用 OpenCV,您必须包含opencv2/opencv.hpp头文件。此头文件将包含程序所需的全部其他头文件。因此,所有 OpenCV 程序都必须在顶部包含此头文件。
在 OpenCV 中处理图像
现在 OpenCV 已安装在系统上,我们可以开始使用它来处理图像。在本节中,我们将学习 OpenCV 中图像的表示方式,开发读取图像、显示图像和将图像保存到磁盘的程序。我们还将了解在 OpenCV 中创建合成图像的方法。我们还将使用 OpenCV 在图像上绘制不同的形状。此外,还将解释 OpenCV 的重要语法和功能。
OpenCV 中的图像表示
如前所述,图像不过是二维数组,因此它们应该在计算机内部以数组的形式存储以供处理。OpenCV 提供了一个 Mat 类,它实际上是一个用于存储图像的图像容器。Mat 对象可以通过以下两行分别创建并分配给图像:
Mat img;
img= imread("cameraman.tif");
在创建对象时,也可以定义图像的数据类型和二维数组的大小。图像的数据类型非常重要,因为它表示了指定单个像素值所使用的通道数和位数。灰度图像只有一个通道,而彩色图像是三个独立通道的组合:红色、绿色和蓝色。
单个像素使用的位数指定了离散灰度级别值的数量。一个 8 位图像可以具有介于 0 到 255 之间的灰度级别,而 16 位图像可以具有介于 0 到 65,535 之间的灰度级别。OpenCV 支持许多数据类型,默认为 CV_8U,表示单通道的 8 位无符号图像。它等同于 CV_8UC1。彩色图像可以指定为 CV_8UC3,表示具有三个通道的 8 位无符号图像。OpenCV 支持多达 512 个通道。五个或更多通道必须用圆括号定义,例如,CV_8UC(5) 表示具有五个通道的 8 位图像。OpenCV 还支持有符号数,因此数据类型也可以是 CV_16SC3,表示具有三个通道的 16 位有符号图像。
Mat 对象可以用来定义图像的大小。这也被称为图像的分辨率。它表示水平和垂直方向上的像素数量。通常,图像的分辨率是以宽度 x 高度来定义的。而在 Mat 对象中的数组大小应该以行 x 列数来定义。以下是一些使用 Mat 定义图像容器的示例:
Mat img1(6,6,CV_8UC1);
//This defines img1 object with size of 6x6, unsigned 8-bit integers and single channel.
Mat img2(256,256, CV_32FC1)
//This defines img2 object with size of 256x256, 32 bit floating point numbers and single channel.
Mat img3(1960,1024, CV_64FC3)
//This defines img3 object with size of 1960x1024, 64 bit floating point numbers and three channels.
图像的分辨率和大小将决定在磁盘上保存图像所需的空间。假设一个具有三个通道的彩色图像的大小为 1024 x 1024,那么它将占用 3 x 1024 x 1024 字节 = 3 MB 的磁盘空间。在下一节中,我们将看到如何使用这个 Mat 对象和 OpenCV 读取和显示图像。
读取和显示图像
在本节中,我们将尝试使用 C++ 和 OpenCV 开发读取和显示图像的代码。整个代码如下,然后逐行进行解释:
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main(int argc, char** argv)
{
// Read the image
Mat img = imread("images/cameraman.tif",0);
// Check for failure in reading an Image
if (img.empty())
{
cout << "Could not open an image" << endl;
return -1;
}
//Name of the window
String win_name = "My First Opencv Program";
// Create a window
namedWindow(win_name);
// Show our image inside the created window.
imshow(win_name, img);
// Wait for any keystroke in the window
waitKey(0);
//destroy the created window
destroyWindow(win_name);
return 0;
}
程序从包含标准输入输出和图像处理的头文件开始。
程序中使用来自 std 命名空间的功能,如 cout 和 endl,因此添加了 std 命名空间。所有 OpenCV 类和函数都是使用 cv 命名空间定义的。因此,为了使用 cv 命名空间中定义的函数,我们指定了 using namespace cv 这一行。如果省略该行,那么在 cv 命名空间中的每个函数都必须以下述方式使用:
Mat img = cv::imread("cameraman.tif")
main函数包含读取和显示图像的代码。在 OpenCV 中使用imread命令读取图像。它返回一个Mat对象。imread命令有两个参数。第一个参数是图像的名称及其路径。路径可以有两种指定方式。您可以在 PC 上指定图像的完整路径,或者从您的代码文件中指定图像的相对路径。在上面的示例中,使用了相对路径,其中图像位于与代码文件相同的目录下的 images 文件夹中。
第二个参数是可选的,用于指定图像是要以灰度图像还是彩色图像读取。如果图像要以彩色图像读取,则指定IMREAD_COLOR或1。如果图像要以灰度图像读取,则指定IMREAD_GRAYSCALE或0。如果要以保存的格式读取图像,则将IMREAD_UNCHANGED或-1作为第二个参数。如果图像作为彩色图像读取,imread命令将返回三个通道,从蓝色、绿色和红色(BGR格式)开始。如果第二个参数未提供,则默认值为IMREAD_COLOR,它将图像作为彩色图像读取。
如果图像无法读取或磁盘上不可用,则imread命令将返回一个Null Mat对象。如果发生这种情况,无需继续执行进一步图像处理代码,我们可以在此时通过通知用户关于错误来退出。这由if循环内的代码处理。
应创建一个用于显示图像的窗口。OpenCV 提供了一个名为namedWindow的函数来实现这一功能。它需要两个参数。第一个参数是窗口的名称,它必须是一个字符串。第二个参数指定要创建的窗口的大小。它可以取两个值:WINDOW_AUTOSIZE或WINDOW_NORMAL。如果指定了WINDOW_AUTOSIZE,则用户将无法调整窗口大小,图像将以原始大小显示。如果指定了WINDOW_NORMAL,则用户可以调整窗口大小。此参数是可选的,如果未指定,其默认值为WINDOW_AUTOSIZE。
要在创建的窗口中显示图像,使用imshow命令。此命令需要两个参数。第一个参数是使用namedWindow命令创建的窗口名称,第二个参数是要显示的图像变量。此变量必须是一个Mat对象。要显示多个图像,必须创建具有唯一名称的单独窗口。窗口的名称将作为图像窗口的标题出现。
imshow 函数应该有足够的时间在创建的窗口中显示一张图片。这是通过使用 waitKey 函数来实现的。因此,在所有 OpenCV 程序中,imshow 函数后面都应该跟 waitkey 函数,否则图片将不会显示。waitKey 是一个键盘绑定函数,它接受一个参数,即以毫秒为单位的时间。它将在指定的时间内等待按键,然后移动到下一行代码。如果没有指定参数或指定为 0,它将无限期地等待按键。只有在键盘上按下任何键时,它才会移动到下一行。我们还可以检测是否按下了特定的键,并根据按下的键做出某些决定。我们将在本章后面使用这个功能。
在程序终止之前,需要关闭为显示窗口创建的所有窗口。这可以通过使用 destroyAllWindows 函数来完成。它将关闭程序中通过 namedWindow 函数创建的所有窗口。有一个名为 destroyWindow 的函数,可以关闭特定的窗口。应将窗口名称作为参数提供给 destroyWindow 函数。
对于程序的执行,只需将代码复制并粘贴到 Windows 上的 Visual Studio 中,或者如果你在 Ubuntu 上使用,可以创建一个 cpp 文件。构建方法与 Visual Studio 中的正常 cpp 应用程序类似,所以这里不再重复。要在 Ubuntu 上执行,请在保存 cpp 文件的文件夹中从命令提示符执行以下命令:
For compilation:
$ g++ -std = c++11 image_read.cpp 'pkg_config --libs --cflags opencv' -o image_read
For execution:
$./image_read
前面程序的输出如下:
读取和显示彩色图像
在前面的程序中,imread 的第二个参数被指定为 0,这意味着它将以灰度图像的形式读取图像。假设你想读取任何彩色图像。为此,你可以按以下方式更改 imread 命令:
Mat img = imread("images/autumn.tif",1);
第二个参数被指定为 1,这意味着它将以 BGR 格式读取图像。重要的是要注意,OpenCV 的 imread 和 imshow 使用 BGR 格式来处理彩色图像,这与 MATLAB 和其他图像处理工具使用的 RGB 格式不同。更改 imread 后的输出如下:
即使是彩色图像,也可以通过将第二个参数指定为 0 来将其读取为灰度图像。这将隐式地将图像转换为灰度并读取。图像将看起来如下:
记住如何使用 imread 函数读取图像非常重要,因为它将影响你程序中的其他图像处理代码。
总结来说,在本节中,我们看到了如何使用 OpenCV 读取图像并显示它。在这个过程中,我们还了解了一些 OpenCV 中可用的重要函数。在下一节中,我们将看到如何使用 OpenCV 创建合成图像。
使用 OpenCV 创建图像
有时,我们可能需要创建自己的图像或在现有图像上绘制一些形状。或者,我们可能想在检测到的对象周围绘制边界框或在图像上显示标签。因此,在本节中,我们将看到如何创建空白灰度和彩色图像。我们还将看到在图像上绘制线条、矩形、椭圆、圆和文本的函数。
要创建一个大小为 256 x 256 的空黑色图像,可以使用以下代码:
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main(int argc, char** argv)
{
//Create blank black grayscale Image with size 256x256
Mat img(256, 256, CV_8UC1, Scalar(0));
String win_name = "Blank Image";
namedWindow(win_name);
imshow(win_name, img);
waitKey(0);
destroyWindow(win_name);
return 0;
}
代码与用于读取图像的代码大致相似,但在这里不是使用 imread 命令,而是仅使用 Mat 类的构造函数来创建图像。如前所述,我们可以在创建 Mat 对象时提供大小和数据类型。因此,在创建 img 对象时,我们提供了四个参数。前两个参数指定了图像的大小,第一个参数定义了行数(高度),第二个参数定义了列数(宽度)。第三个参数定义了图像的数据类型。我们使用了 CV_8UC1,这意味着一个单通道的 8 位无符号整数图像。最后一个参数指定了数组中所有像素的初始化值。
这里我们使用了 0,这是黑色的值。当程序执行时,它将创建一个大小为 256 x 256 的黑色图像,如下所示:
可以使用类似的代码创建任何颜色的空白图像,如下所示:
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main(int argc, char** argv)
{
//Create blank blue color Image with size 256x256
Mat img(256, 256, CV_8UC3, Scalar(255,0,0));
String win_name = "Blank Blue Color Image";
namedWindow(win_name);
imshow(win_name, img);
waitKey(0);
destroyWindow(win_name);
return 0;
}
在创建 Mat 对象时,不是使用 CV_8UC1 数据类型,而是使用 CV_8UC3,它指定了一个具有三个通道的 8 位图像。因此,单个像素有 24 位。第四个参数指定了起始像素值。它使用标量关键字和一个包含所有三个通道起始值的元组来指定。在这里,蓝色通道初始化为 255,绿色通道初始化为 0,红色通道初始化为 0。这将创建一个大小为 256 x 256 的蓝色图像。元组中值的组合将创建不同的颜色。前述程序的输出如下:
在空白图像上绘制形状
要开始在图像上绘制不同的形状,我们将首先使用以下命令创建一个任意大小的空白黑色图像:
Mat img(512, 512, CV_8UC3, Scalar(0,0,0));
此命令将创建一个大小为 512 x 512 的黑色图像。现在,我们将开始在这个图像上绘制不同的形状。
绘制线条
一条线可以通过两个点来指定:起点和终点。要在图像上绘制线条,必须指定这两个点。在图像上绘制线条的函数如下:
line(img,Point(0,0),Point(511,511),Scalar(0,255,0),7);
线函数有五个参数。第一个参数指定需要绘制线的图像,第二个和第三个参数分别定义起点和终点。这些点使用 Point 类构造函数定义,该构造函数以图像的 x 和 y 坐标为参数。第四个参数指定线的颜色。它指定为 B、G 和 R 值的元组。这里,取值为 (0,255,0),指定绿色。第五个参数是线的厚度。其值被设置为 7 像素宽。此函数还有一个可选的 linetype 参数。前面的函数将绘制一个从 (0,0) 到 (511,511) 的对角线绿色线,宽度为 7 像素。
绘制矩形
矩形可以使用两个极端对角点来指定。OpenCV 提供了一个在图像上绘制矩形的函数,其语法如下:
rectangle(img,Point(384,0),Point(510,128),Scalar(255,255,0),5);
矩形函数有五个参数。第一个参数是需要绘制矩形的图像。第二个参数是矩形的左上角点。第三个参数是矩形的右下角点。第四个参数指定边框的颜色。它指定为 (255,255,0),是蓝色和绿色的混合,产生青色。第五个参数是边框的厚度。如果第五个参数被指定为 -1,则形状将被填充。因此,前面的函数将使用两个极端点 (384,0) 和 (510,128),以青色绘制一个边框厚度为 5 像素的矩形。
绘制圆
圆可以通过中心和半径来指定。OpenCV 提供了一个在图像上绘制圆的函数,其语法如下:
circle(img,Point(447,63), 63, Scalar(0,0,255), -1);
圆函数有五个参数。第一个参数是需要绘制圆的图像。第二个参数指定该圆的中心点,第三个参数指定半径。第四个参数指定圆的颜色。取值为 (0,0,255),表示红色。第五个参数是边框的厚度。这里,它被设置为 -1,意味着圆将被红色填充。
绘制椭圆
OpenCV 提供了一个在图像上绘制椭圆的函数,其语法如下:
ellipse(img,Point(256,256),Point(100,100),0,0,180,255,-1);
椭圆函数有许多参数。第一个参数指定需要绘制椭圆的图像。第二个参数指定椭圆的中心。第三个参数指定椭圆将要绘制的框的大小。第四个参数指定椭圆需要旋转的角度。它被设置为 0 度。第五个和第六个参数指定椭圆需要绘制的角度范围。它被设置为 0 到 180 度。因此,只会绘制椭圆的一半。下一个参数指定椭圆的颜色,它被指定为 255. 它与 (255,0,0) 相同,表示蓝色。最后一个参数指定边框的粗细。它被设置为 -1,因此椭圆将被蓝色填充。
在图像上写文本
OpenCV 提供了一个在图像上写文本的函数,即 putText。该函数的语法如下:
putText( img, "OpenCV!", Point(10,500), FONT_HERSHEY_SIMPLEX, 3,Scalar(255, 255, 255), 5, 8 );
putText 函数有许多参数。第一个参数是要在图像上写文本的图像。第二个参数是作为字符串数据类型的文本,我们想要在图像上写的内容。第三个参数指定文本的左下角。第四个参数指定字体类型。OpenCV 中有许多可用的字体类型,你可以查看 OpenCV 文档。第五个参数指定字体的缩放比例。第六个参数是文本的颜色。它被设置为 (255,255,255),表示白色。第七个参数是文本的粗细,它被设置为 5,最后一个参数指定线型,它被设置为 8。
我们已经看到了在空黑图像上绘制形状的单独函数。以下代码显示了之前讨论的所有函数的组合:
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main(int argc, char** argv)
{
Mat img(512, 512, CV_8UC3, Scalar(0,0,0));
line(img,Point(0,0),Point(511,511),Scalar(0,255,0),7);
rectangle(img,Point(384,0),Point(510,128),Scalar(255,255,0),5);
circle(img,Point(447,63), 63, Scalar(0,0,255), -1);
ellipse(img,Point(256,256),Point(100,100),0,0,180,255,-1);
putText( img, "OpenCV!", Point(10,500), FONT_HERSHEY_SIMPLEX, 3,Scalar(255, 255, 255), 5, 8 );
String win_name = "Shapes on blank Image";
namedWindow(win_name);
imshow(win_name, img);
waitKey(0);
destroyWindow(win_name);
return 0;
}
上一段代码的输出图像如下:
将图像保存到文件
图像也可以从 OpenCV 程序保存到磁盘。当我们想要将处理后的图像存储到计算机上的磁盘时,这很有用。OpenCV 提供了 imwrite 函数来完成此操作。此函数的语法如下:
bool flag = imwrite("images/save_image.jpg", img);
imwrite 函数接受两个参数。第一个参数是你想要保存的文件名及其路径。第二个参数是你想要保存的 img 变量。此函数返回一个布尔值,表示文件是否成功保存到磁盘上。
在本节中,我们使用 OpenCV 处理了图像。在下一节中,我们将使用 OpenCV 处理视频,视频不过是图像的序列。
在 OpenCV 中处理视频
本节将展示使用 OpenCV 从文件和摄像头读取视频的过程。它还将描述将视频保存到文件的过程。这也可以与连接到计算机的 USB 摄像头一起工作。视频不过是图像的序列。尽管 OpenCV 不是针对视频处理应用程序优化的,但它在这方面做得相当不错。OpenCV 无法捕获音频,因此我们必须使用其他一些与 OpenCV 一起使用的工具来捕获音频和视频。
在计算机上处理视频
本节描述了读取存储在计算机上的视频文件的过程。在所有使用 OpenCV 的视频处理应用程序中,视频的所有帧将逐个读取、处理并在屏幕上显示。
以下代码用于读取和显示视频——随后将逐行解释:
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main(int argc, char* argv[])
{
//open the video file from PC
VideoCapture cap("images/rhinos.avi");
// if not success, exit program
if (cap.isOpened() == false)
{
cout << "Cannot open the video file" << endl;
return -1;
}
cout<<"Press Q to Quit" << endl;
String win_name = "First Video";
namedWindow(win_name);
while (true)
{
Mat frame;
// read a frame
bool flag = cap.read(frame);
//Breaking the while loop at the end of the video
if (flag == false)
{
break;
}
//display the frame
imshow(win_name, frame);
//Wait for 100 ms and key 'q' for exit
if (waitKey(100) == 'q')
{
break;
}
}
destroyWindow(win_name);
return 0;
}
在包含库之后,在主函数中处理视频的第一件事是创建一个VideoCapture对象。VideoCapture类提供了许多构造函数,可用于处理视频。当我们想要处理存储在计算机上的视频文件时,我们需要在创建VideoCapture对象时将视频的名称及其路径作为参数传递给构造函数。
此对象提供了许多方法和属性,可以提供与视频相关的信息。我们将根据需要查看它们。它提供了isopened属性,该属性指示对象创建是否成功以及视频是否可用。它返回一个布尔值。如果cap.isopened为false,则视频不可用,因此无需在程序中进一步操作。这通过一个if循环处理,当视频不可用时,会通知用户并退出程序。
VideoCapture类提供了一个读取方法,可以逐个捕获帧。为了处理整个视频,我们必须启动一个连续的循环,该循环一直运行到视频的末尾。无限while循环可以完成这项工作。在while循环内部,使用读取方法读取第一帧。此方法有一个参数。它是一个 Mat 对象,我们希望在其中存储帧。它返回一个布尔值,指示帧是否已成功读取。当循环到达视频的末尾时,此布尔值将返回false,表示没有可用的帧。这个标志在循环中持续检查视频的结束;如果检测到,我们将使用break语句退出while循环。
帧是单个图像,因此显示该帧的过程与之前看到的是相同的。在上面的代码中,waitKey函数在if语句中使用。它会在每个帧之后等待 100 毫秒以获取按键。if语句正在检查按键是否为q。如果是q,则表示用户想要退出视频,因此在if中包含了 break 语句。
此代码将在整个视频播放完毕或用户在键盘上按下q键时终止视频显示。在整个本书中,我们将使用这种编码实践来处理视频。前一个程序的输出如下。截图是视频的一帧:
我们在每帧之间使用了 100 毫秒的延迟。当你将这个值减少到,比如,10 毫秒时,你认为会发生什么?答案是,每帧将显示得更快。这并不意味着视频的帧率改变了。这只是意味着帧之间的延迟减少了。如果你想看到视频的实际帧率,可以使用cap对象的CAP_PROP_FPS属性。它可以使用以下代码显示:
double frames_per_second = cap.get(CAP_PROP_FPS);
cout << "Frames per seconds of the video is : " << frames_per_second ;
cap对象还具有其他属性,例如CAP_PROP_FRAME_WIDTH和CAP_PROP_FRAME_HEIGHT,它们表示帧的宽度和高度。这些属性也可以通过get方法获取。这些属性可以通过使用 cap 对象的set方法来设置。set方法有两个参数。第一个参数是属性的name,第二个参数是我们想要设置的value。
本节描述了从文件中读取视频的方法。下一节将展示从网络摄像头或 USB 摄像头处理视频的过程。
处理网络摄像头视频
本节描述了从连接到计算机的网络摄像头或 USB 摄像头捕获视频的过程。OpenCV 的好处是,相同的代码将适用于笔记本电脑和任何可以运行 C/C++的嵌入式系统。这有助于在任意硬件平台上部署计算机视觉应用。捕获视频并显示的代码如下:
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main(int argc, char* argv[])
{
//open the Webcam
VideoCapture cap(0);
// if not success, exit program
if (cap.isOpened() == false)
{
cout << "Cannot open Webcam" << endl;
return -1;
}
//get the frames rate of the video from webcam
double frames_per_second = cap.get(CAP_PROP_FPS);
cout << "Frames per seconds : " << frames_per_second << endl;
cout<<"Press Q to Quit" <<endl;
String win_name = "Webcam Video";
namedWindow(win_name); //create a window
while (true)
{
Mat frame;
bool flag = cap.read(frame); // read a new frame from video
//show the frame in the created window
imshow(win_name, frame);
if (waitKey(1) == 'q')
{
break;
}
}
return 0;
}
在从网络摄像头或 USB 摄像头捕获视频时,需要将那个摄像头的设备 ID 作为VideoCapture对象构造函数的参数提供。连接的主摄像头将具有设备 ID 零。笔记本电脑的网络摄像头或 USB 摄像头(当没有网络摄像头时)将具有设备 ID 零。如果有多个摄像头连接到设备,它们的设备 ID 将是(0,1),依此类推。在前面的代码中,零表示代码将使用主摄像头来捕获视频。
另一段代码与从文件读取视频的代码大致相似。在这里,视频的帧率也被获取并显示。帧将以每 1 毫秒的间隔逐个读取,并在创建的窗口中显示。您必须按q键来终止操作。使用网络摄像头捕获的视频输出如下:
将视频保存到磁盘
要从 OpenCV 程序保存视频,我们需要创建VideoWriter类的对象。将视频保存到文件的代码如下:
Size frame_size(640, 640);
int frames_per_second = 30;
VideoWriter v_writer("images/video.avi", VideoWriter::fourcc('M', 'J', 'P', 'G'), frames_per_second, frame_size, true);
//Inside while loop
v_writer.write(frame);
//After finishing video write
v_writer.release();
在创建VideoWriter类的对象时,构造函数接受五个参数。第一个参数是你想要保存的视频文件名及其绝对或相对路径。第二个参数是用于视频编解码器的四个字符代码。它是通过VideoWriter::fourcc函数创建的。在这里,我们使用运动 JPEG 编解码器,因此它的四个字符代码是'M'、'J'、'P'和'G'。根据你的需求和操作系统,可以使用其他编解码器。第三个参数是每秒帧数。它可以指定为一个之前定义的整数变量或直接在函数中的整数值。在前面的代码中,使用了每秒30帧。第四个参数是帧的大小。它使用size关键字和两个参数frame_width和frame_height定义。在前面的代码中,它被定义为 640 x 640。第五个参数指定要存储的帧是彩色还是灰度。如果是真的,帧将以彩色帧保存。
要开始使用VideoWriter对象写入帧,OpenCV 提供了一个write方法。此方法用于逐个将帧写入视频,因此它包含在一个无限while循环中。此方法仅接受一个参数,即帧变量的名称。帧的大小应与创建VideoWriter对象时指定的相同。在写入完成后,重要的是刷新并关闭创建的视频文件。这可以通过使用release方法释放创建的VideoWriter对象来完成。
总结来说,在本节中,我们探讨了从设备上的文件或摄像头读取视频的过程。我们还看到了将视频写入文件的代码。从下一节开始,我们将看到如何使用 CUDA 加速的 OpenCV 在图像或视频上操作。
使用 OpenCV CUDA 模块的基本计算机视觉应用
在前面的章节中,我们了解到 CUDA 提供了一个优秀的接口,可以充分利用 GPU 的并行计算能力来加速复杂的计算应用。在本节中,我们将看到如何利用 CUDA 的能力与 OpenCV 一起用于计算机视觉应用。
OpenCV CUDA 模块简介
OpenCV 有一个 CUDA 模块,它包含数百个可以利用 GPU 能力的函数。它仅支持 Nvidia GPU,因为它在后台使用 Nvidia CUDA 运行时。为了使用 CUDA 模块,OpenCV 必须编译时设置WITH_CUDA标志为 ON。
使用 OpenCV CUDA 模块的亮点之一是它提供了一个与常规 OpenCV API 相似的 API。它也不需要详细了解 CUDA 编程,尽管了解 CUDA 和 GPU 架构不会有害。研究人员已经表明,使用具有 CUDA 加速的函数可以比类似的 CPU 函数提供 5x-100x 的速度提升。
在下一节中,我们将看到如何使用 CUDA 模块与 OpenCV 结合,在各种计算机视觉和图像处理应用中使用,这些应用在图像的各个像素上操作。
图像上的算术和逻辑运算
在本节中,我们将看到如何在图像上执行各种算术和逻辑运算。我们将使用 OpenCV CUDA 模块中定义的函数来执行这些操作。
两个图像的相加
当两个图像大小相同时,可以执行两个图像的相加。OpenCV 在cv::cuda命名空间内提供了一个add函数用于加法操作。它执行两个图像的逐像素相加。假设在两个图像中,(0,0)处的像素强度值分别为 100 和 150。结果图像中的强度值将是 250,这是两个强度值的相加。OpenCV 的加法是一个饱和操作,这意味着如果加法的结果超过 255,它将被饱和到 255。执行加法的代码如下:
#include <iostream>
#include "opencv2/opencv.hpp"
int main (int argc, char* argv[])
{
//Read Two Images
cv::Mat h_img1 = cv::imread("images/cameraman.tif");
cv::Mat h_img2 = cv::imread("images/circles.png");
//Create Memory for storing Images on device
cv::cuda::GpuMat d_result1,d_img1, d_img2;
cv::Mat h_result1;
//Upload Images to device
d_img1.upload(h_img1);
d_img2.upload(h_img2);
cv::cuda::add(d_img1,d_img2, d_result1);
//Download Result back to host
d_result1.download(h_result1);
cv::imshow("Image1 ", h_img1);
cv::imshow("Image2 ", h_img2);
cv::imshow("Result addition ", h_result1);
cv::imwrite("images/result_add.png", h_result1);
cv::waitKey();
return 0;
}
当任何计算机视觉操作需要在 GPU 上执行时,图像必须存储在设备内存中。为此内存的分配可以使用gpumat关键字,它与用于主机内存的 Mat 类型相似。图像的读取方式与之前相同。读取两个图像用于相加,并存储在主机内存中。这些图像使用设备memory变量的upload方法复制到设备内存。主机图像变量作为参数传递给此方法。
GPU CUDA 模块中的函数定义在cv::cuda命名空间中。它需要设备内存中的图像作为其参数。CUDA 模块中的 add 函数用于图像相加。它需要三个参数。前两个参数是要相加的两个图像,最后一个参数是结果将存储的目标。所有三个变量都应该使用gpumat定义。
使用设备变量的download方法将结果图像复制回主机。将结果复制的img主机变量作为参数传递给download方法。然后使用上一节中解释的相同函数显示和存储此图像。程序的输出如下:
从两个图像中减去
使用 OpenCV 和 CUDA 可以在图像上执行其他算术运算。OpenCV 提供了subtract函数来减去两个图像。它也是一个饱和操作,这意味着当减法的结果低于零时,它将被饱和到零。subtract命令的语法如下:
//d_result1 = d_img1 - d_img2
cv::cuda::subtract(d_img1, d_img2,d_result1);
再次,两个要减去的图像作为前两个参数提供,结果图像作为第三个参数提供。两个图像之间的减法结果如下:
图像混合
有时需要以不同的比例混合两个图像,而不是直接将两个图像相加。图像混合可以用以下方程式表示:
result = α * img1 + β * img2 + γ
这可以通过 OpenCV 中的addWeighted函数轻松实现。该函数的语法如下:
cv::cuda::addWeighted(d_img1,0.7,d_img2,0.3,0,d_result1)
该函数有六个参数。第一个参数是第一个源图像,第二个参数是第一个图像的混合权重,第三个参数是第二个源图像,第四个参数是第二个图像的混合权重,第五个参数是在混合时需要添加的常数伽玛,最后一个参数指定结果需要存储的目标位置。该函数将img1的 70%和img2的 30%用于混合。该函数的输出如下:
图像反转
除了算术运算外,OpenCV 还提供了对单个位进行操作的布尔运算。它包括 AND、OR、NOT 等。AND 和 OR 在掩码操作中非常有用,我们将在后面看到。NOT 操作用于反转图像,其中黑色转换为白色,白色转换为黑色。它可以表示为以下方程式:
result_image = 255 - input_image
在方程式中,255表示 8 位图像的最大强度值。进行图像反转的程序如下:
#include <iostream>
#include "opencv2/opencv.hpp"
int main (int argc, char* argv[])
{
cv::Mat h_img1 = cv::imread("images/circles.png");
//Create Device variables
cv::cuda::GpuMat d_result1,d_img1;
cv::Mat h_result1;
//Upload Image to device
d_img1.upload(h_img1);
cv::cuda::bitwise_not(d_img1,d_result1);
//Download result back to host
d_result1.download(h_result1);
cv::imshow("Result inversion ", h_result1);
cv::imwrite("images/result_inversion.png", h_result1);
cv::waitKey();
return 0;
}
该程序与算术运算程序类似。使用bitwise_not函数进行图像反转。图像应该是灰度图像。它接受两个参数。第一个参数指示要反转的源图像,第二个参数指示反转图像要存储的目标位置。bitwise_not操作的输出如下:
如所示,通过反转,白色转换为黑色,黑色转换为白色。
总结来说,在本节中,我们看到了使用 OpenCV 和 CUDA 的各种算术和逻辑操作。在下一节中,我们将看到一些在计算机视觉应用中广泛使用的计算机视觉操作。
改变图像的色彩空间
如前所述,OpenCV 可以以灰度图像或彩色图像的形式读取图像,彩色图像具有三个通道:绿色、蓝色和红色,这种格式称为 BGR 格式。其他图像处理软件和算法在 RGB 图像上工作,其中红色通道后面是绿色和蓝色。还有许多其他颜色格式可以用于特定应用。这些包括 HSV 颜色空间,其中三个通道是色调、饱和度和亮度。色调代表颜色值,饱和度表示颜色的灰度级别,亮度代表颜色的亮度。另一个颜色空间是 YCrCb,它也非常有用。这个系统用图像中的一个亮度分量:亮度(Y),和两个色度分量:色度(Cb 和 Cr)来表示颜色。
OpenCV 支持许多其他颜色空间,例如 XYZ、HLS、Lab 等。OpenCV 支持超过 150 种颜色转换方法。使用 OpenCV 中的cvtColor函数可以将一种颜色空间转换为另一种颜色空间。以下是一个使用此函数在不同颜色空间之间转换的示例:
#include <iostream>
#include "opencv2/opencv.hpp"
int main (int argc, char* argv[])
{
cv::Mat h_img1 = cv::imread("images/autumn.tif");
//Define device variables
cv::cuda::GpuMat d_result1,d_result2,d_result3,d_result4,d_img1;
//Upload Image to device
d_img1.upload(h_img1);
//Convert image to different color spaces
cv::cuda::cvtColor(d_img1, d_result1,cv::COLOR_BGR2GRAY);
cv::cuda::cvtColor(d_img1, d_result2,cv::COLOR_BGR2RGB);
cv::cuda::cvtColor(d_img1, d_result3,cv::COLOR_BGR2HSV);
cv::cuda::cvtColor(d_img1, d_result4,cv::COLOR_BGR2YCrCb);
cv::Mat h_result1,h_result2,h_result3,h_result4;
//Download results back to host
d_result1.download(h_result1);
d_result2.download(h_result2);
d_result3.download(h_result3);
d_result4.download(h_result4);
cv::imshow("Result in Gray ", h_result1);
cv::imshow("Result in RGB", h_result2);
cv::imshow("Result in HSV ", h_result3);
cv::imshow("Result in YCrCb ", h_result4);
cv::waitKey();
return 0;
}
imshow函数期望以 BGR 颜色格式传递彩色图像,因此使用imshow显示其他颜色格式的输出可能不会很吸引人。以下是在不同颜色格式下使用相同图像的前一个程序的输出:
图像阈值化
图像阈值化是一种非常简单的图像分割技术,用于根据某些强度值从灰度图像中提取重要区域。在这种技术中,如果像素值大于某个阈值值,则分配一个值,否则分配另一个值。
OpenCV 和 CUDA 中用于图像阈值化的函数是cv::cuda::threshold。此函数有许多参数。第一个参数是源图像,它应该是一个灰度图像。第二个参数是结果要存储的目标。第三个参数是阈值值,它用于分割像素值。第四个参数是maxVal常量,它表示如果像素值超过阈值值时赋予的值。OpenCV 提供不同类型的阈值化技术,由函数的最后一个参数决定。以下是一些阈值化类型:
-
cv:.THRESH_BINARY:如果像素的强度大于阈值,则将该像素强度设置为maxVal常量。否则将该像素强度设置为零。 -
cv::THRESH_BINARY_INV:如果像素的强度大于阈值,则将该像素强度设置为零。否则将该像素强度设置为maxVal常量。 -
cv::THRESH_TRUNC:这基本上是一个截断操作。如果像素的强度大于阈值,则将该像素强度设置为阈值。否则,保持强度值不变。 -
cv::THRESH_TOZERO:如果像素的强度大于阈值,则保持像素强度不变。否则将该像素强度设置为零。 -
cv::THRESH_TOZERO_INV:如果像素的强度大于阈值,则将该像素强度设置为零。否则保持像素强度不变。
实现所有这些阈值技术使用 OpenCV 和 CUDA 的程序如下:
#include <iostream>
#include "opencv2/opencv.hpp"
int main (int argc, char* argv[])
{
cv::Mat h_img1 = cv::imread("images/cameraman.tif", 0);
//Define device variables
cv::cuda::GpuMat d_result1,d_result2,d_result3,d_result4,d_result5, d_img1;
//Upload image on device
d_img1.upload(h_img1);
//Perform different thresholding techniques on device
cv::cuda::threshold(d_img1, d_result1, 128.0, 255.0, cv::THRESH_BINARY);
cv::cuda::threshold(d_img1, d_result2, 128.0, 255.0, cv::THRESH_BINARY_INV);
cv::cuda::threshold(d_img1, d_result3, 128.0, 255.0, cv::THRESH_TRUNC);
cv::cuda::threshold(d_img1, d_result4, 128.0, 255.0, cv::THRESH_TOZERO);
cv::cuda::threshold(d_img1, d_result5, 128.0, 255.0, cv::THRESH_TOZERO_INV);
cv::Mat h_result1,h_result2,h_result3,h_result4,h_result5;
//Copy results back to host
d_result1.download(h_result1);
d_result2.download(h_result2);
d_result3.download(h_result3);
d_result4.download(h_result4);
d_result5.download(h_result5);
cv::imshow("Result Threshhold binary ", h_result1);
cv::imshow("Result Threshhold binary inverse ", h_result2);
cv::imshow("Result Threshhold truncated ", h_result3);
cv::imshow("Result Threshhold truncated to zero ", h_result4);
cv::imshow("Result Threshhold truncated to zero inverse ", h_result5);
cv::waitKey();
return 0;
}
在所有阈值技术的cv::cuda::threshold函数中,128 被用作像素强度的阈值,这是黑色(0)和白色(255)之间的中点。maxVal常量被设置为 255,当像素强度超过阈值时将用于更新像素强度。其他程序与之前看到的其他 OpenCV 程序类似。
程序的输出如下,显示了输入图像以及所有五种阈值技术的输出:
带有和没有 CUDA 支持的 OpenCV 应用程序的性能比较
可以通过处理单个图像所需的时间来衡量图像处理算法的性能。当算法在视频上工作时,性能是通过每秒帧数来衡量的,这表示它在一秒内可以处理的帧数。当算法每秒可以处理超过 30 帧时,它可以被认为是实时工作的。我们还可以衡量在 OpenCV 中实现的算法的性能,这将在本节中讨论。
如我们之前讨论的,当 OpenCV 与 CUDA 兼容性构建时,它可以显著提高算法的性能。OpenCV 在 CUDA 模块中的函数被优化以利用 GPU 并行处理能力。OpenCV 还提供了仅在 CPU 上运行的类似函数。在本节中,我们将比较上一节中构建的带有和不使用 GPU 的阈值操作的性能。我们将从处理一张图像所需的时间和每秒帧数来比较阈值操作的性能。以下是在 CPU 上实现阈值并测量性能的代码:
#include <iostream>
#include "opencv2/opencv.hpp"
using namespace cv;
using namespace std;
int main (int argc, char* argv[])
{
cv::Mat src = cv::imread("images/cameraman.tif", 0);
cv::Mat result_host1,result_host2,result_host3,result_host4,result_host5;
//Get initial time in miliseconds
int64 work_begin = getTickCount();
cv::threshold(src, result_host1, 128.0, 255.0, cv::THRESH_BINARY);
cv::threshold(src, result_host2, 128.0, 255.0, cv::THRESH_BINARY_INV);
cv::threshold(src, result_host3, 128.0, 255.0, cv::THRESH_TRUNC);
cv::threshold(src, result_host4, 128.0, 255.0, cv::THRESH_TOZERO);
cv::threshold(src, result_host5, 128.0, 255.0, cv::THRESH_TOZERO_INV);
//Get time after work has finished
int64 delta = getTickCount() - work_begin;
//Frequency of timer
double freq = getTickFrequency();
double work_fps = freq / delta;
std::cout<<"Performance of Thresholding on CPU: " <<std::endl;
std::cout <<"Time: " << (1/work_fps) <<std::endl;
std::cout <<"FPS: " <<work_fps <<std::endl;
return 0;
}
在前面的代码中,使用了来自 cv 命名空间的阈值函数,该函数仅使用 CPU 进行执行,而不是 cv::cuda 模块。算法的性能使用 gettickcount 和 gettickfrequency 函数进行测量。gettickcount 函数返回自系统启动后经过的毫秒数。我们测量了在图像操作代码执行前后的时间滴答。时间滴答之间的差异表示算法执行过程中处理图像所经过的滴答数。这个时间被测量在 delta 变量中。gettickfrequncy 函数返回计时器的频率。处理图像所需的总时间可以通过将时间滴答除以计时器频率来测量。这个时间的倒数表示 每秒帧数 (FPS)。这两个性能指标都打印在控制台上,用于 CPU 上的阈值应用。控制台输出如下:
如输出所示,CPU 处理一个图像需要 0.169766 秒,相当于 5.89046 FPS。现在我们将同样的算法实现到 GPU 上,并尝试测量代码的性能。根据之前的讨论,这应该会极大地提高算法的性能。GPU 实现的代码如下:
#include <iostream>
#include "opencv2/opencv.hpp"
int main (int argc, char* argv[])
{
cv::Mat h_img1 = cv::imread("images/cameraman.tif", 0);
cv::cuda::GpuMat d_result1,d_result2,d_result3,d_result4,d_result5, d_img1;
//Measure initial time ticks
int64 work_begin = getTickCount();
d_img1.upload(h_img1);
cv::cuda::threshold(d_img1, d_result1, 128.0, 255.0, cv::THRESH_BINARY);
cv::cuda::threshold(d_img1, d_result2, 128.0, 255.0, cv::THRESH_BINARY_INV);
cv::cuda::threshold(d_img1, d_result3, 128.0, 255.0, cv::THRESH_TRUNC);
cv::cuda::threshold(d_img1, d_result4, 128.0, 255.0, cv::THRESH_TOZERO);
cv::cuda::threshold(d_img1, d_result5, 128.0, 255.0, cv::THRESH_TOZERO_INV);
cv::Mat h_result1,h_result2,h_result3,h_result4,h_result5;
d_result1.download(h_result1);
d_result2.download(h_result2);
d_result3.download(h_result3);
d_result4.download(h_result4);
d_result5.download(h_result5);
//Measure difference in time ticks
int64 delta = getTickCount() - work_begin;
double freq = getTickFrequency();
//Measure frames per second
double work_fps = freq / delta;
std::cout <<"Performance of Thresholding on GPU: " <<std::endl;
std::cout <<"Time: " << (1/work_fps) <<std::endl;
std::cout <<"FPS: " <<work_fps <<std::endl;
return 0;
}
在代码中,使用了来自 cv::cuda 模块的函数,该模块针对 GPU 并行处理能力进行了优化。图像被复制到设备内存,在 GPU 上进行操作,然后复制回主机。性能指标以类似的方式计算,并在控制台上打印。程序输出如下:
如所示,GPU 实现仅需要 0.55 毫秒来处理单个图像,相当于 1816 FPS。这比 CPU 实现有显著改进,尽管必须记住这是一个非常简单的应用,并不适合于 CPU 和 GPU 之间的性能比较。这个应用仅展示如何测量 OpenCV 中任何代码的性能。
通过运行 OpenCV 安装在 samples/gpu 目录中的示例代码,可以更真实地比较 CPU 和 GPU 的性能。其中一个代码,hog.cpp,从图像中计算 方向直方图 (HoG)特征,并使用 支持向量机 (SVM)进行分类。尽管算法的细节超出了本书的范围,但它给你一个关于使用 GPU 实现时性能改进的想法。在摄像头视频上的性能比较如下:
如所见,当我们仅使用 CPU 时,代码的性能大约为 13 FPS,如果我们使用 GPU,则性能提升至 24 FPS,这几乎是 CPU 性能的两倍。这会让你对使用 CUDA 与 OpenCV 结合的重要性有所了解。
总结来说,在本节中,我们比较了使用 CUDA(GPU)和不使用 CUDA(CPU)时 OpenCV 的性能。这再次强调了使用 CUDA 将极大地提高计算机视觉应用性能的观点。
摘要
在本章中,我们首先介绍了计算机视觉和图像处理。我们描述了 OpenCV 库,它专门为计算机视觉应用而设计,以及它与其他计算机视觉软件的不同之处。OpenCV 可以通过使用 CUDA 利用 GPU 的并行处理能力。我们查看在所有操作系统上安装具有 CUDA 的 OpenCV 的安装过程。我们描述了从磁盘读取图像、在屏幕上显示它以及将其保存回磁盘的过程。视频不过是图像的序列。我们学习了如何处理来自磁盘的视频以及从摄像头捕获的视频。我们开发了几个图像处理应用程序,对图像执行不同的操作,例如算术运算、逻辑运算、颜色空间转换和阈值处理。在最后一节中,我们比较了相同算法在 CPU 和 GPU 上的性能,包括处理图像所需的时间和 FPS。因此,在本章结束时,你对 OpenCV 与 CUDA 在计算机视觉应用中的有用性以及如何使用它编写简单代码有了了解。在下一章中,我们将在此基础上尝试开发更多有用的计算机视觉应用,例如使用 OpenCV 进行滤波、边缘检测和形态学操作。
问题
-
说明计算机视觉和图像处理这两个术语之间的区别
-
为什么 OpenCV 非常适合在嵌入式系统上部署计算机视觉应用
-
编写一个 OpenCV 命令,以红色初始化 1960 x 1960 彩色图像
-
编写一个程序,从网络摄像头捕获帧并将其保存到磁盘
-
OpenCV 用于读取和显示彩色图像的颜色格式是什么
-
编写一个程序,从网络摄像头捕获视频,将其转换为灰度并显示在屏幕上
-
编写一个程序来测量 GPU 上加法和减法操作的性能
-
编写一个程序进行图像的位与和或操作,并解释它如何用于遮罩
第六章:使用 OpenCV 和 CUDA 的基本计算机视觉操作
上一章描述了使用 OpenCV 和 CUDA 处理图像和视频的过程。我们查看了一些基本的图像和视频处理应用的代码,并比较了带有和没有 CUDA 加速的 OpenCV 代码的性能。在这一章中,我们将在此基础上构建知识,并尝试使用 OpenCV 和 CUDA 开发一些更多的计算机视觉和图像处理应用。这一章描述了在彩色和灰度图像中访问单个像素强度的方法。直方图是图像处理中的一个非常有用的概念。这一章描述了计算直方图的方法以及直方图均衡化如何提高图像的视觉效果。这一章还将描述如何使用 OpenCV 和 CUDA 执行不同的几何变换。图像滤波是一个非常重要的概念,它在图像预处理和特征提取中非常有用。这一章将详细描述这一点。本章的最后部分将描述不同的形态学操作,如腐蚀、膨胀、开运算和闭运算。
本章将涵盖以下主题:
-
在 OpenCV 中访问单个像素强度
-
直方图计算和直方图均衡化
-
图像变换
-
图像的滤波操作
-
图像的形态学操作
技术要求
本章需要具备图像处理和计算机视觉的基本理解。它需要熟悉基本的 C 或 C++ 编程语言、CUDA 以及前几章中解释的所有示例代码。本章中使用的所有代码都可以从以下 GitHub 链接下载:github.com/PacktPublishing/Hands-On-GPU-Accelerated-Computer-Vision-with-OpenCV-and-CUDA。代码可以在任何操作系统上执行,尽管它只在 Ubuntu 16.04 上进行了测试。
查看以下视频以查看代码的实际效果:
访问图像的单个像素强度
当我们处理图像时,有时需要访问特定位置的像素强度值。当我们想要改变一组像素的亮度或对比度,或者想要执行其他像素级操作时,这非常有用。对于一个 8 位灰度图像,该点的强度值将在 0 到 255 的范围内,而对于彩色图像,将会有三个不同的强度值,分别对应蓝色、绿色和红色通道,所有这些通道的值都在 0 到 255 之间。
OpenCV 提供了一个cv::Mat::at<>方法来访问任何通道图像在特定位置的强度值。它需要一个参数,即要访问强度值的位置。该点通过Point类传递,行和列值作为参数。对于灰度图像,该方法将返回一个标量对象,而对于彩色图像,它将返回一个包含三个强度的向量。访问灰度图像以及彩色图像在特定位置的像素强度的代码如下:
#include <iostream>
#include "opencv2/opencv.hpp"
int main ()
{
//Gray Scale Image
cv::Mat h_img1 = cv::imread("images/cameraman.tif",0);
cv::Scalar intensity = h_img1.at<uchar>(cv::Point(100, 50));
std::cout<<"Pixel Intensity of gray scale Image at (100,50) is:" <<intensity.val[0]<<std::endl;
//Color Image
cv::Mat h_img2 = cv::imread("images/autumn.tif",1);
cv::Vec3b intensity1 = h_img1.at<cv::Vec3b>(cv::Point(100, 50));
std::cout<<"Pixel Intensity of color Image at (100,50) is:"<<intensity1<<std::endl;
return 0;
}
灰度图像首先被读取,然后在这个图像对象上调用at方法。强度值在(100,50)点被测量,这表示第 100 行和第 50 列的像素。它返回一个标量,存储在强度变量中。该值被打印在控制台上。对于彩色图像,遵循相同的程序,但返回值将是一个包含三个强度的向量,存储在Vec3b对象中。强度值被打印在控制台上。上述程序的输出如下:
如所示,灰度图像在(100,50)处的像素强度为9,而对于彩色图像则是[175,179,177],这表示蓝色强度为175,绿色强度为179,红色强度为177。同样的方法用于修改特定位置的像素强度。假设你想将(100,50)位置的像素强度改为128,则可以编写如下代码:
h_img1.at<uchar>(100, 50) = 128;
总结来说,在本节中我们看到了一种访问和改变特定位置强度值的方法。在下一节中,我们将看到在 OpenCV 中计算直方图的方法。
OpenCV 中的直方图计算和平滑
直方图是图像的一个重要属性,因为它提供了该图像外观的全局描述。可以从直方图中获得大量信息。它代表了图像中灰度级别的相对出现频率。它基本上是在 X 轴上灰度级别和 Y 轴上每个灰度级别的像素数的图表。如果直方图集中在左侧,则图像会非常暗;如果集中在右侧,则图像会非常亮。为了获得良好的图像视觉质量,它应该均匀分布。
以下图像展示了暗、亮和正常图像的直方图:
OpenCV 提供了一个函数来计算图像的直方图。该函数的语法如下:
void cv::cuda::calcHist ( InputArray src, OutputArray hist)
函数需要两个数组作为参数。第一个数组是需要计算直方图的输入图像。第二个参数是输出数组,其中将存储直方图。输出可以绘制成直方图,如图像前面的截图所示。如前所述,平坦的直方图可以提高图像的视觉质量。OpenCV 和 CUDA 提供了一个函数来平坦直方图,这在下一节中将有描述。
直方图均衡化
完美的图像在其所有灰度级别中像素数量相等。因此,直方图应该具有大的动态范围和整个范围内的像素数量相等。这可以通过一种称为直方图均衡化的技术来实现。它是任何计算机视觉应用中非常重要的预处理步骤。在本节中,我们将看到如何使用 OpenCV 和 CUDA 对灰度图像和彩色图像进行直方图均衡化。
灰度图像
灰度图像通常是 8 位单通道图像,具有 256 个不同的灰度级别。如果直方图分布不均匀,图像可能太暗或太亮,此时应进行直方图均衡化以改善图像的视觉质量。以下代码描述了在灰度图像上进行直方图均衡化的过程:
#include <iostream>
#include "opencv2/opencv.hpp"
int main ()
{
cv::Mat h_img1 = cv::imread("images/cameraman.tif",0);
cv::cuda::GpuMat d_img1,d_result1;
d_img1.upload(h_img1);
cv::cuda::equalizeHist(d_img1, d_result1);
cv::Mat h_result1;
d_result1.download(h_result1);
cv::imshow("Original Image ", h_img1);
cv::imshow("Histogram Equalized Image", h_result1);
cv::waitKey();
return 0;
}
读取的图像被上传到设备内存以进行直方图均衡化。这是一个计算密集型的步骤,因此 CUDA 加速将有助于提高程序的性能。OpenCV 提供了equalizeHist函数用于直方图均衡化。它需要两个参数。第一个参数是源图像,第二个参数是目标图像。目标图像被下载回主机并在控制台上显示。直方图均衡化后的输出如下:
如所见,直方图均衡化后的图像在视觉质量上优于原始图像。接下来将描述对彩色图像进行相同操作的过程。
彩色图像
直方图均衡化也可以应用于彩色图像。它必须在单独的通道上执行。因此,彩色图像必须分成三个通道。每个通道的直方图独立均衡,然后合并通道以重建图像。以下是对彩色图像进行直方图均衡化的代码:
#include <iostream>
#include "opencv2/opencv.hpp"
int main ()
{
cv::Mat h_img1 = cv::imread("images/autumn.tif");
cv::Mat h_img2,h_result1;
cvtColor(h_img1, h_img2, cv::COLOR_BGR2HSV);
//Split the image into 3 channels; H, S and V channels respectively and store it in a std::vector
std::vector< cv::Mat > vec_channels;
cv::split(h_img2, vec_channels);
//Equalize the histogram of only the V channel
cv::equalizeHist(vec_channels[2], vec_channels[2]);
//Merge 3 channels in the vector to form the color image in HSV color space.
cv::merge(vec_channels, h_img2);
//Convert the histogram equalized image from HSV to BGR color space again
cv::cvtColor(h_img2,h_result1, cv::COLOR_HSV2BGR);
cv::imshow("Original Image ", h_img1);
cv::imshow("Histogram Equalized Image", h_result1);
cv::waitKey();
return 0;
}
在 BGR 颜色空间中,直方图通常不进行均衡化;使用 HSV 和 YCrCb 颜色空间进行均衡化。因此,在代码中,将 BGR 颜色空间转换为 HSV 颜色空间。然后,使用split函数将其拆分为三个独立的通道。现在,色调和饱和度通道包含颜色信息,因此没有必要均衡这些通道。直方图均衡化仅在值通道上执行。使用merge函数将三个通道合并回重建彩色图像。使用imshow将 HSV 彩色图像转换回 BGR 颜色空间进行显示。程序的输出如下:
总结来说,直方图均衡化可以提升图像的视觉效果,因此它是任何计算机视觉应用中非常重要的预处理步骤。下一节将描述图像的几何变换。
图像的几何变换
有时,在更大的计算机视觉应用中,需要缩放图像、平移图像和旋转图像。本节将解释这类几何变换。
图像缩放
在某些计算机视觉应用中,图像需要具有特定的尺寸。因此,需要将任意大小的图像转换为特定尺寸。OpenCV 提供了一个用于缩放图像的函数。图像缩放的代码如下:
#include <iostream>
#include "opencv2/opencv.hpp"
int main ()
{
cv::Mat h_img1 = cv::imread("images/cameraman.tif",0);
cv::cuda::GpuMat d_img1,d_result1,d_result2;
d_img1.upload(h_img1);
int width= d_img1.cols;
int height = d_img1.size().height;
cv::cuda::resize(d_img1,d_result1,cv::Size(200, 200), cv::INTER_CUBIC);
cv::cuda::resize(d_img1,d_result2,cv::Size(0.5*width, 0.5*height), cv::INTER_LINEAR);
cv::Mat h_result1,h_result2;
d_result1.download(h_result1);
d_result2.download(h_result2);
cv::imshow("Original Image ", h_img1);
cv::imshow("Resized Image", h_result1);
cv::imshow("Resized Image 2", h_result2);
cv::waitKey();
return 0;
}
可以使用两个不同的函数获取图像的高度和宽度,如代码所示。Mat对象的rows和cols属性分别描述图像的height和width。Mat对象还有一个size()方法,它具有height和width属性,用于查找图像的大小。图像以两种方式缩放。在第一种方式中,图像被缩放为特定的(200,200)大小,在第二种方式中,它被缩放为其原始尺寸的一半。OpenCV 提供了resize函数来执行此操作。它有四个参数。
前两个参数分别是源图像和目标图像。第三个参数是目标图像的大小。它使用Size对象定义。当图像缩放时,必须从源图像对目标图像上的像素值进行插值。有各种插值方法可供使用,例如双线性插值、双三次插值和面积插值。这些插值方法作为resize函数的第四个参数提供。它可以设置为cv::INTER_LINEAR (双线性)、cv::INTER_CUBIC (双三次)或cv::INTER_AREA (面积)。图像缩放的代码输出如下:
图像平移和旋转
图像平移和旋转是某些计算机视觉应用中需要的重要几何变换。OpenCV 提供了一个简单的 API 来在图像上执行这些变换。执行平移和旋转的代码如下:
#include <iostream>
#include "opencv2/opencv.hpp"
int main ()
{
cv::Mat h_img1 = cv::imread("images/cameraman.tif",0);
cv::cuda::GpuMat d_img1,d_result1,d_result2;
d_img1.upload(h_img1);
int cols= d_img1.cols;
int rows = d_img1.size().height;
//Translation
cv::Mat trans_mat = (cv::Mat_<double>(2,3) << 1, 0, 70, 0, 1, 50);
cv::cuda::warpAffine(d_img1,d_result1,trans_mat,d_img1.size());
//Rotation
cv::Point2f pt(d_img1.cols/2., d_img1.rows/2.);
cv::Mat rot_mat = cv::getRotationMatrix2D(pt, 45, 1.0);
cv::cuda::warpAffine(d_img1, d_result2, rot_mat, cv::Size(d_img1.cols, d_img1.rows));
cv::Mat h_result1,h_result2;
d_result1.download(h_result1);
d_result2.download(h_result2);
cv::imshow("Original Image ", h_img1);
cv::imshow("Translated Image", h_result1);
cv::imshow("Rotated Image", h_result2);
cv::waitKey();
return 0;
}
需要创建一个平移矩阵,该矩阵指定了图像在水平和垂直方向上的平移。它是一个 2 x 3 的矩阵,如下所示:
tx 和 ty 是沿 x 和 y 方向的平移偏移。在代码中,这个矩阵使用 Mat 对象创建,其中 X-方向的偏移量为 70,Y-方向的偏移量为 50。这个矩阵作为参数传递给 warpAffine 函数以实现图像平移。warpAffine 函数的其他参数分别是源图像、目标图像和输出图像的大小。
应该创建一个旋转矩阵,用于在特定点以特定角度旋转图像。OpenCV 提供了 cv::getRotationMatrix2D 函数来构建这个旋转矩阵。它需要三个参数。第一个参数是旋转点;在这种情况下使用图像的中心。第二个参数是旋转角度,指定为 45 度。最后一个参数是缩放比例,指定为 1。构建的旋转矩阵再次作为参数传递给 warpAffine 函数以实现图像旋转。
图像平移和图像旋转代码的输出如下:
总结来说,本节描述了使用 OpenCV 和 CUDA 实现的各种几何变换,如图像缩放、图像平移和图像旋转。
图像上的滤波操作
到目前为止描述的方法都是针对单个像素强度进行的,被称为点处理方法。有时查看像素的邻域而不是仅查看单个像素强度是有帮助的。这些被称为邻域处理技术。邻域可以是 3 x 3、5 x 5、7 x 7 等等,并且以特定像素为中心。图像滤波是邻域处理中的一个重要技术。
过滤是信号处理中的一个重要概念,其中我们拒绝一定频率范围的信号,并允许一定频率范围的信号通过。图像中是如何测量频率的?如果一个区域的灰度值变化缓慢,那么它是一个低频区域。如果灰度值变化剧烈,那么它是一个高频区域。通常,图像的背景被认为是低频区域,而边缘是高频区域。卷积是邻域处理和图像滤波中的一个非常重要的数学概念。它将在下一节中解释。
图像上的卷积操作
卷积的基本思想源于生物学中类似的概念,称为感受野,其中对图像中的某些部分敏感,而对其他部分不敏感。它可以用以下方式数学表示:
*g(x,y)=f(x,y)h(x,y)= ∑∑f(n,m)h(x-n,y-m)
简化形式下,此方程是滤波器h与图像的子图像f(以*(x,y)点为中心)的点积。此乘积的答案是图像中的(x,y)点g*。为了说明卷积操作在图像上的工作原理,以下图显示了将 3 x 3 滤波器应用于 6 x 6 大小图像的示例:
通过将最左侧的红色窗口与滤波器进行点积,以找到目标图像中的一个点。点积的答案将是 2 ((11 + 11 + 10 + 15 + 11 +17 +10 +11 + 11)/9)*。将此窗口向右移动 1 个像素后,重复相同的操作,答案将是 3。这将对图像中的所有窗口重复进行,以构建目标图像。通过改变 3 x 3 滤波器矩阵的值,可以构建不同的低通和高通滤波器。这将在下一两节中解释。
图像上的低通滤波
低通滤波器从图像中移除高频内容。通常,噪声被认为是高频内容,因此低通滤波器从图像中移除噪声。有许多类型的噪声,如高斯噪声、均匀噪声、指数噪声和盐和胡椒噪声,这些都会影响图像。低通滤波器用于消除这类噪声。有许多类型的低通滤波器可用:
-
平均或箱式滤波器
-
高斯滤波器
-
中值滤波器
本节解释了这些滤波器及其使用 OpenCV 的实现。
平均滤波器
平均滤波器,正如其名所示,对邻域像素执行平均操作。如果图像中存在高斯噪声,则可以使用低通平均滤波器来去除噪声。由于平均操作,它还会模糊图像的边缘。邻域可以是 3 x 3、5 x 5、7 x 7 等。滤波器窗口的尺寸越大,图像的模糊程度就越高。3 x 3 和 5 x 5 平均掩模如下:
OpenCV 提供了一个简单的接口,可以在图像上应用许多类型的滤波器。以下代码演示了如何使用不同掩模应用平均滤波器:
#include <iostream>
#include "opencv2/opencv.hpp"
int main ()
{
cv::Mat h_img1 = cv::imread("images/cameraman.tif",0);
cv::cuda::GpuMat d_img1,d_result3x3,d_result5x5,d_result7x7;
d_img1.upload(h_img1);
cv::Ptr<cv::cuda::Filter> filter3x3,filter5x5,filter7x7;
filter3x3 = cv::cuda::createBoxFilter(CV_8UC1,CV_8UC1,cv::Size(3,3));
filter3x3->apply(d_img1, d_result3x3);
filter5x5 = cv::cuda::createBoxFilter(CV_8UC1,CV_8UC1,cv::Size(5,5));
filter5x5->apply(d_img1, d_result5x5);
filter7x7 = cv::cuda::createBoxFilter(CV_8UC1,CV_8UC1,cv::Size(7,7));
filter7x7->apply(d_img1, d_result7x7);
cv::Mat h_result3x3,h_result5x5,h_result7x7;
d_result3x3.download(h_result3x3);
d_result5x5.download(h_result5x5);
d_result7x7.download(h_result7x7);
cv::imshow("Original Image ", h_img1);
cv::imshow("Blurred with kernel size 3x3", h_result3x3);
cv::imshow("Blurred with kernel size 5x5", h_result5x5);
cv::imshow("Blurred with kernel size 7x7", h_result7x7);
cv::waitKey();
return 0;
}
cv::Ptr是一个用于智能指针的模板类,用于存储cv::cuda::Filter类型的过滤器。然后,使用createBoxFilter函数创建不同窗口大小的平均滤波器。它需要三个必选参数和三个可选参数。第一个和第二个参数是源图像和目标图像的数据类型。它们被假定为CV_8UC1,表示 8 位无符号灰度图像。第三个参数定义了滤波器窗口的大小。它可以是一个 3 x 3、5 x 5、7 x 7 等。第四个参数是锚点,其默认值为(-1,-1),表示锚点位于核的中心点。最后两个可选参数与像素插值方法和边界值相关,这里省略。
创建的过滤器指针有一个应用方法,该方法用于将创建的过滤器应用于任何图像。它有三个参数。第一个参数是源图像,第二个参数是目标图像,第三个可选参数是 CUDA 流,它用于多任务处理,如本书前面所述。在代码中,对图像应用了不同大小的三个平均滤波器。结果如下:
从输出中可以看出,随着滤波器大小的增加,用于平均的像素更多,这会在图像上引入更多的模糊。尽管大滤波器可以消除更多的噪声。
高斯滤波器
高斯滤波器使用具有高斯分布的掩码来过滤图像,而不是简单的平均掩码。此滤波器还引入了图像上的平滑模糊,并且广泛用于从图像中消除噪声。一个 5 x 5 的高斯滤波器,其标准差约为 1,如下所示:
OpenCV 提供了一个实现高斯滤波器的函数。其代码如下:
#include <iostream>
#include "opencv2/opencv.hpp"
int main ()
{
cv::Mat h_img1 = cv::imread("images/cameraman.tif",0);
cv::cuda::GpuMat d_img1,d_result3x3,d_result5x5,d_result7x7;
d_img1.upload(h_img1);
cv::Ptr<cv::cuda::Filter> filter3x3,filter5x5,filter7x7;
filter3x3 = cv::cuda::createGaussianFilter(CV_8UC1,CV_8UC1,cv::Size(3,3),1);
filter3x3->apply(d_img1, d_result3x3);
filter5x5 = cv::cuda::createGaussianFilter(CV_8UC1,CV_8UC1,cv::Size(5,5),1);
filter5x5->apply(d_img1, d_result5x5);
filter7x7 = cv::cuda::createGaussianFilter(CV_8UC1,CV_8UC1,cv::Size(7,7),1);
filter7x7->apply(d_img1, d_result7x7);
cv::Mat h_result3x3,h_result5x5,h_result7x7;
d_result3x3.download(h_result3x3);
d_result5x5.download(h_result5x5);
d_result7x7.download(h_result7x7);
cv::imshow("Original Image ", h_img1);
cv::imshow("Blurred with kernel size 3x3", h_result3x3);
cv::imshow("Blurred with kernel size 5x5", h_result5x5);
cv::imshow("Blurred with kernel size 7x7", h_result7x7);
cv::waitKey();
return 0;
}
createGaussianFilter函数用于创建高斯滤波器的掩码。源图像和目标图像的数据类型、滤波器大小以及水平方向的标准差作为参数提供给函数。我们还可以提供一个垂直方向的标准差作为参数;如果没有提供,则其默认值等于水平方向的标准差。使用apply方法将不同大小的创建的高斯掩码应用于图像。程序输出如下:
同样,随着高斯滤波器大小的增加,图像中引入了更多的模糊。高斯滤波器用于消除噪声并在图像上引入平滑的模糊。
中值滤波
当图像受到椒盐噪声的影响时,它不会被平均或高斯滤波器消除。它需要一个非线性滤波器。在邻域中进行中值运算而不是平均可以帮助消除椒盐噪声。在这个滤波器中,邻域中 9 个像素值的中值放置在中心像素上。它将消除由椒盐噪声引入的极端高或低值。尽管 OpenCV 和 CUDA 提供了一个中值滤波函数,但它的速度比 OpenCV 中的常规函数慢,因此使用以下代码实现中值滤波:
#include <iostream>
#include "opencv2/opencv.hpp"
int main ()
{
cv::Mat h_img1 = cv::imread("images/saltpepper.png",0);
cv::Mat h_result;
cv::medianBlur(h_img1,h_result,3);
cv::imshow("Original Image ", h_img1);
cv::imshow("Median Blur Result", h_result);
cv::waitKey();
return 0;
}
OpenCV 中的medianBlur函数用于实现中值滤波。它需要三个参数。第一个参数是源图像,第二个参数是目标图像,第三个参数是中值操作的窗口大小。中值滤波的输出如下:
源图像受到椒盐噪声的影响,如图中的截图所示。这种噪声通过 3x3 大小的中值滤波器完全消除,而没有引入极端的模糊。因此,中值滤波是图像应用受到椒盐噪声影响时的一个非常重要的预处理步骤。
总结一下,我们看到了三种类型的低通滤波器,它们在各种计算机视觉应用中得到了广泛使用。平均滤波器和高斯滤波器用于消除高斯噪声,但它们也会模糊图像的边缘。中值滤波器用于去除椒盐噪声。
图像的高通滤波
高通滤波器从图像中移除低频分量并增强高频分量。因此,当高通滤波器应用于图像时,它会移除背景,因为它是低频区域,并增强边缘,这些是高频分量。因此,高通滤波器也可以称为边缘检测器。滤波器的系数将改变,否则它与上一节中看到的滤波器相似。有许多高通滤波器可用,如下所示:
-
Sobel 滤波器
-
Scharr 滤波器
-
拉普拉斯滤波器
在本节中,我们将分别看到它们中的每一个。
Sobel 滤波器
Sobel 算子或 Sobel 滤波器是一种广泛用于边缘检测应用的图像处理和计算机视觉算法。它是一个 3 x 3 的滤波器,用于近似图像强度函数的梯度。它提供了一个单独的滤波器来计算水平和垂直方向的梯度。该滤波器以与本章前面所述类似的方式与图像卷积。水平和垂直的 3 x 3 Sobel 滤波器如下:
实现此 Sobel 滤波器的代码如下:
#include <iostream>
#include "opencv2/opencv.hpp"
int main ()
{
cv::Mat h_img1 = cv::imread("images/blobs.png",0);
cv::cuda::GpuMat d_img1,d_resultx,d_resulty,d_resultxy;
d_img1.upload(h_img1);
cv::Ptr<cv::cuda::Filter> filterx,filtery,filterxy;
filterx = cv::cuda::createSobelFilter(CV_8UC1,CV_8UC1,1,0);
filterx->apply(d_img1, d_resultx);
filtery = cv::cuda::createSobelFilter(CV_8UC1,CV_8UC1,0,1);
filtery->apply(d_img1, d_resulty);
cv::cuda::add(d_resultx,d_resulty,d_resultxy);
cv::Mat h_resultx,h_resulty,h_resultxy;
d_resultx.download(h_resultx);
d_resulty.download(h_resulty);
d_resultxy.download(h_resultxy);
cv::imshow("Original Image ", h_img1);
cv::imshow("Sobel-x derivative", h_resultx);
cv::imshow("Sobel-y derivative", h_resulty);
cv::imshow("Sobel-xy derivative", h_resultxy);
cv::waitKey();
return 0;
}
OpenCV 提供了createSobelFilter函数来实现 Sobel 滤波器。它需要许多参数。前两个参数是源图像和目标图像的数据类型。第三个和第四个参数分别是x和y导数的阶数。对于计算x导数或垂直边缘,提供 1 和 0,而对于计算y导数或水平边缘,提供 0 和 1。第五个参数表示核的大小,是可选的。默认值是 3。也可以提供导数的比例。
要同时看到水平和垂直边缘,需要将x导数和y导数的结果相加。结果如下:
Sobel 算子提供了非常不精确的导数近似,但仍然在计算机视觉应用中的边缘检测方面非常有用。它没有旋转对称性;为了克服这一点,使用了 Scharr 算子。
Scharr 滤波器
由于 Sobel 没有提供旋转对称性,因此使用不同的滤波器掩码来克服这一点,如下所示:
从掩码中可以看出,Scharr 算子更重视中间行或中间列以找到边缘。实现 Scharr 滤波器的程序如下:
#include <iostream>
#include "opencv2/opencv.hpp"
int main ()
{
cv::Mat h_img1 = cv::imread("images/blobs.png",0);
cv::cuda::GpuMat d_img1,d_resultx,d_resulty,d_resultxy;
d_img1.upload(h_img1);
cv::Ptr<cv::cuda::Filter> filterx,filtery;
filterx = cv::cuda::createScharrFilter(CV_8UC1,CV_8UC1,1,0);
filterx->apply(d_img1, d_resultx);
filtery = cv::cuda::createScharrFilter(CV_8UC1,CV_8UC1,0,1);
filtery->apply(d_img1, d_resulty);
cv::cuda::add(d_resultx,d_resulty,d_resultxy);
cv::Mat h_resultx,h_resulty,h_resultxy;
d_resultx.download(h_resultx);
d_resulty.download(h_resulty);
d_resultxy.download(h_resultxy);
cv::imshow("Original Image ", h_img1);
cv::imshow("Scharr-x derivative", h_resultx);
cv::imshow("Scharr-y derivative", h_resulty);
cv::imshow("Scharr-xy derivative", h_resultxy);
cv::waitKey();
return 0;
}
OpenCV 提供了createScharrFilter函数来实现 Scharr 滤波器。它需要许多参数。前两个参数是源图像和目标图像的数据类型。第三个和第四个参数分别是x和y导数的阶数。对于计算x导数或垂直边缘,提供 1 和 0,而对于计算y导数或水平边缘,提供 0 和 1。第五个参数,表示核的大小,是可选的。默认值是 3。
要同时看到水平和垂直边缘,需要将x导数和y导数的结果相加。结果如下:
Laplacian 滤波器
Laplacian 滤波器也是一种导数算子,用于在图像中找到边缘。不同之处在于 Sobel 和 Scharr 是一阶导数算子,而 Laplacian 是二阶导数算子。它也同时找到水平和垂直方向的边缘,这与 Sobel 和 Scharr 算子不同。Laplacian 滤波器计算二阶导数,因此对图像中的噪声非常敏感,在应用 Laplacian 滤波器之前最好对图像进行模糊处理以去除噪声。实现 Laplacian 滤波器的代码如下:
#include <iostream>
#include "opencv2/opencv.hpp"
int main ()
{
cv::Mat h_img1 = cv::imread("images/blobs.png",0);
cv::cuda::GpuMat d_img1,d_result1,d_result3;
d_img1.upload(h_img1);
cv::Ptr<cv::cuda::Filter> filter1,filter3;
filter1 = cv::cuda::createLaplacianFilter(CV_8UC1,CV_8UC1,1);
filter1->apply(d_img1, d_result1);
filter3 = cv::cuda::createLaplacianFilter(CV_8UC1,CV_8UC1,3);
filter3->apply(d_img1, d_result3);
cv::Mat h_result1,h_result3;
d_result1.download(h_result1);
d_result3.download(h_result3);
cv::imshow("Original Image ", h_img1);
cv::imshow("Laplacian filter 1", h_result1);
cv::imshow("Laplacian filter 3", h_result3);
cv::waitKey();
return 0;
}
使用createLaplacianFilter函数在图像上应用了两个核大小为 1 和 3 的 Laplacian 滤波器。除了核的大小外,该函数还需要作为参数提供源图像和目标图像的数据类型。创建的 Laplacian 滤波器通过apply方法应用于图像。Laplacian 滤波器的输出如下:
总结来说,在本节中,我们描述了不同的高通滤波器,如 Sobel、Scharr 和 Laplacian 滤波器。Sobel 和 Scharr 是一阶导数算子,用于计算边缘,它们对噪声的敏感性较低。Laplacian 是一个二阶导数算子,用于计算边缘,它对噪声非常敏感。
图像的形态学操作
图像形态学处理图像的区域和形状。它用于提取对表示形状和区域有用的图像成分。图像形态学将图像视为集合的总和,这与之前看到的其他图像处理操作不同。图像与一个小模板相互作用,该模板称为结构元素,它定义了图像形态学中的感兴趣区域或邻域。本节中解释了可以在图像上执行的各种形态学操作,将逐一进行说明。
-
腐蚀:腐蚀将中心像素设置为邻域内所有像素的最小值。邻域由结构元素定义,它是一个由 1s 和 0s 组成的矩阵。腐蚀用于扩大物体中的孔洞,缩小边界,消除岛屿,并去除可能存在于图像边界上的狭窄半岛。
-
膨胀:膨胀将中心像素设置为邻域内所有像素的最大值。膨胀增加了白色块的大小,减少了黑色区域的大小。它用于填充物体中的孔洞并扩展物体的边界。
-
开运算:图像开运算基本上是腐蚀和膨胀的组合。图像开运算定义为腐蚀后跟膨胀。这两个操作都使用相同的结构元素执行。它用于平滑图像的轮廓,破坏狭窄的桥梁并隔离相互接触的物体。它在分析发动机油中的磨损颗粒、回收纸中的墨水颗粒等方面得到应用。
-
闭运算:图像闭运算定义为膨胀后跟腐蚀。这两个操作都使用相同的结构元素执行。它用于融合狭窄的裂缝并消除小孔。
形态学算子可以通过将它们应用于仅包含黑白两色的二值图像来轻松理解。OpenCV 和 CUDA 提供了一个简单的 API 来在图像上应用形态学变换。相应的代码如下:
#include <iostream>
#include "opencv2/opencv.hpp"
int main ()
{
cv::Mat h_img1 = cv::imread("images/blobs.png",0);
cv::cuda::GpuMat d_img1,d_resulte,d_resultd,d_resulto, d_resultc;
cv::Mat element = cv::getStructuringElement(cv::MORPH_RECT,cv::Size(5,5));
d_img1.upload(h_img1);
cv::Ptr<cv::cuda::Filter> filtere,filterd,filtero,filterc;
filtere = cv::cuda::createMorphologyFilter(cv::MORPH_ERODE,CV_8UC1,element);
filtere->apply(d_img1, d_resulte);
filterd = cv::cuda::createMorphologyFilter(cv::MORPH_DILATE,CV_8UC1,element);
filterd->apply(d_img1, d_resultd);
filtero = cv::cuda::createMorphologyFilter(cv::MORPH_OPEN,CV_8UC1,element);
filtero->apply(d_img1, d_resulto);
filterc = cv::cuda::createMorphologyFilter(cv::MORPH_CLOSE,CV_8UC1,element);
filterc->apply(d_img1, d_resultc);
cv::Mat h_resulte,h_resultd,h_resulto,h_resultc;
d_resulte.download(h_resulte);
d_resultd.download(h_resultd);
d_resulto.download(h_resulto);
d_resultc.download(h_resultc);
cv::imshow("Original Image ", h_img1);
cv::imshow("Erosion", h_resulte);
cv::imshow("Dilation", h_resultd);
cv::imshow("Opening", h_resulto);
cv::imshow("closing", h_resultc);
cv::waitKey();
return 0;
}
首先需要创建一个定义形态学操作邻域的结构元素。这可以通过使用 OpenCV 中的getStructuringElement函数来完成。需要将结构元素的形状和尺寸作为参数传递给此函数。在代码中,定义了一个 5 x 5 大小的矩形结构元素。
形态学操作的过滤器是通过使用createMorphologyFilter函数创建的。它需要三个必填参数。第一个参数定义要执行的操作。cv::MORPH_ERODE用于腐蚀,cv::MORPH_DILATE用于膨胀,cv::MORPH_OPEN用于开运算,cv::MORPH_CLOSE用于闭运算。第二个参数是图像的数据类型,第三个参数是之前创建的结构元素。使用apply方法将这些过滤器应用于图像。
图像上形态学操作的输出如下:
从输出结果可以看出,腐蚀操作会减小物体的边界,而膨胀操作则会使其变厚。我们将白色部分视为物体,黑色部分则是背景。开运算可以平滑图像的轮廓。闭运算可以消除图像中的小孔。如果将结构元素的尺寸从 5 x 5 增加到 7 x 7,那么在腐蚀操作中边界腐蚀会更加明显,而在膨胀操作中边界会变得更厚。在 5 x 5 腐蚀图像中可见的左侧的小圆圈,在用 7 x 7 尺寸腐蚀时会被移除。
使用 7 x 7 结构元素的形态学操作的输出如下:
总结来说,形态学操作对于找出定义图像形状和区域的组件非常重要。它可以用来填充图像中的孔洞并平滑图像的轮廓。
摘要
本章介绍了在图像中特定位置访问像素强度的方法。当我们对图像进行逐点操作时,这非常有用。直方图是描述图像的一个非常重要的全局特征。本章介绍了计算直方图和直方图均衡化的方法,这可以提高图像的视觉效果。详细解释了各种几何变换,如图像缩放、旋转和平移。图像滤波是一种有用的邻域处理技术,用于消除噪声和提取图像的边缘特征,并进行了详细描述。低通滤波器用于去除噪声,但它也会模糊图像的边缘。高通滤波器去除背景,这是一个低频区域,同时增强边缘,这些是高频区域。本章的最后部分介绍了不同的形态学操作,如腐蚀、膨胀、开运算和闭运算,这些可以用来描述图像的形状并填充图像中的空洞。在下一章中,我们将使用这些概念,结合 OpenCV 和 CUDA 构建一些有用的计算机视觉应用。
问题
-
编写一个 OpenCV 函数,在控制台打印任何彩色图像在位置(200,200)的像素强度。
-
编写一个 OpenCV 函数,将图像调整到(300,200)像素大小。使用双线性插值方法。
-
编写一个 OpenCV 函数,通过 2 倍上采样图像。使用面积插值方法。
-
判断对错:随着平均滤波器大小的增加,模糊程度降低。
-
判断对错:中值滤波器可以去除高斯噪声。
-
可以采取哪些步骤来降低拉普拉斯算子的噪声敏感性?
-
编写一个 OpenCV 函数来实现顶帽和黑帽形态学操作。
第七章:使用 OpenCV 和 CUDA 进行目标检测和跟踪
上一章描述了使用 OpenCV 和 CUDA 的基本计算机视觉操作。在这一章中,我们将看到如何使用这些基本操作以及 OpenCV 和 CUDA 来开发复杂的计算机视觉应用。我们将使用目标检测和跟踪的例子来展示这个概念。目标检测和跟踪是计算机视觉中一个非常活跃的研究领域。它涉及在图像中识别物体的位置并在一系列帧中跟踪它。基于颜色、形状和图像的其他显著特征,已经提出了许多用于此任务的方法。在这一章中,这些算法使用 OpenCV 和 CUDA 实现。我们首先解释基于颜色的物体检测,然后描述检测特定形状物体的方法。所有物体都有显著的特性,可以用来检测和跟踪物体。本章描述了不同特征检测算法的实现以及如何使用它们来检测物体。本章的最后部分将演示使用背景减除技术,该技术将前景与背景分离以进行目标检测和跟踪。
本章将涵盖以下主题:
-
目标检测和跟踪简介
-
基于颜色的目标检测和跟踪
-
基于形状的目标检测和跟踪
-
基于特征的物体检测
-
使用 Haar 级联的目标检测
-
背景减除方法
技术要求
本章需要具备良好的图像处理和计算机视觉理解。它还需要一些关于用于目标检测和跟踪的算法的基本知识。需要熟悉基本的 C 或 C++编程语言、CUDA 以及前几章中解释的所有代码。本章中使用的所有代码都可以从以下 GitHub 链接下载:github.com/PacktPublishing/Hands-On-GPU-Accelerated-Computer-Vision-with-OpenCV-and-CUDA。代码可以在任何操作系统上执行,尽管它只在 Ubuntu 16.04 上进行了测试。
查看以下视频以查看代码的实际应用:
目标检测和跟踪简介
目标检测和跟踪是计算机视觉领域的一个活跃的研究课题,它通过一系列帧努力检测、识别和跟踪物体。已经发现,视频序列中的目标检测和跟踪是一个具有挑战性的任务,并且是一个非常耗时的过程。目标检测是构建更大计算机视觉系统的第一步。可以从检测到的物体中推导出大量信息,如下所示:
-
检测到的目标可以被分类到特定的类别
-
可以在图像序列中进行跟踪
-
可以从检测到的对象中获取更多关于场景或其他对象推断的信息
目标跟踪被定义为在视频的每一帧中检测对象,并建立从一帧到另一帧检测到的对象的对应关系。
目标检测与跟踪的应用
目标检测与跟踪可用于开发视频监控系统以跟踪可疑活动、事件和人员。它可以用于开发智能交通系统以跟踪车辆和检测交通违规行为。在自动驾驶汽车中,目标检测对于提供周围环境信息和规划导航至关重要。它对于自动驾驶员辅助系统中的行人检测或车辆检测也非常有用。它可用于医疗领域的应用,如乳腺癌检测或脑瘤检测等。它可用于面部和手势识别。它在工业装配和生产线的质量控制中具有广泛的应用。对于搜索引擎中的图像检索和照片管理也非常重要。
目标检测的挑战
目标检测是一个具有挑战性的任务,因为现实生活中的图像会受到噪声、光照变化、动态背景、阴影效果、相机抖动和运动模糊的影响。当要检测的对象旋转、缩放或被遮挡时,目标检测变得困难。许多应用需要检测多个对象类别。如果检测的类别数量很大,那么处理速度就成为一个重要问题,同时系统在处理这些类别时,如何不损失准确性也是一个关键问题。
有许多算法可以克服这些挑战中的一些。这些算法在本章中进行了讨论。本章没有详细描述这些算法,但更侧重于如何使用 CUDA 和 OpenCV 来实现它们。
基于颜色的目标检测与跟踪
一个对象有许多全局特征,如颜色和形状,这些特征描述了对象的整体。这些特征可以用于在一系列帧中检测对象并跟踪它。在本节中,我们将使用颜色作为特征来检测具有特定颜色的对象。当要检测的对象是特定颜色且与背景颜色不同时,这种方法很有用。如果对象和背景颜色相同,则这种检测方法将失败。在本节中,我们将尝试使用 OpenCV 和 CUDA 从网络摄像头流中检测任何蓝色对象。
蓝色目标检测与跟踪
第一个问题可能是应该使用哪种颜色空间来分割蓝色。红绿蓝(RGB)颜色空间没有将颜色信息与强度信息分开。将颜色信息与强度信息分开的颜色空间,如色调饱和度值(HSV)和YCrCb(其中 Y′是亮度分量,CB 和 CR 是蓝差和红差色度分量),对于这类任务非常理想。每种颜色在色调通道中都有一个特定的范围,可以用来检测该颜色。以下是从开始摄像头、捕获帧到上传设备内存以进行 GPU 操作的样板代码:
#include <iostream>
#include "opencv2/opencv.hpp"
using namespace cv;
using namespace std;
int main()
{
VideoCapture cap(0); //capture the video from web cam
// if webcam is not available then exit the program
if ( !cap.isOpened() )
{
cout << "Cannot open the web cam" << endl;
return -1;
}
while (true)
{
Mat frame;
// read a new frame from webcam
bool flag = cap.read(frame);
if (!flag)
{
cout << "Cannot read a frame from webcam" << endl;
break;
}
cuda::GpuMat d_frame, d_frame_hsv,d_intermediate,d_result;
cuda::GpuMat d_frame_shsv[3];
cuda::GpuMat d_thresc[3];
Mat h_result;
d_frame.upload(frame);
d_result.download(h_result);
imshow("Thresholded Image", h_result);
imshow("Original", frame);
if (waitKey(1) == 'q')
{
break;
}
}
return 0;
}
}
要检测蓝色,我们需要在 HSV 颜色空间中找到蓝色颜色的范围。如果范围准确,则检测将准确。蓝色颜色在三个通道(色调、饱和度和值)中的范围如下:
lower_range = [110,50,50]
upper_range = [130,255,255]
这个范围将被用来在特定通道中对图像进行阈值处理,以创建蓝色颜色的掩码。如果这个掩码再次与原始帧进行 AND 操作,那么结果图像中就只会剩下蓝色对象。以下是这个操作的代码:
//Transform image to HSV
cuda::cvtColor(d_frame, d_frame_hsv, COLOR_BGR2HSV);
//Split HSV 3 channels
cuda::split(d_frame_hsv, d_frame_shsv);
//Threshold HSV channels for blue color according to range
cuda::threshold(d_frame_shsv[0], d_thresc[0], 110, 130, THRESH_BINARY);
cuda::threshold(d_frame_shsv[1], d_thresc[1], 50, 255, THRESH_BINARY);
cuda::threshold(d_frame_shsv[2], d_thresc[2], 50, 255, THRESH_BINARY);
//Bitwise AND the channels
cv::cuda::bitwise_and(d_thresc[0], d_thresc[1],d_intermediate);
cv::cuda::bitwise_and(d_intermediate, d_thresc[2], d_result);
摄像头捕获的帧被转换为 HSV 颜色空间。蓝色在这三个通道中具有不同的范围,因此每个通道都需要单独进行阈值处理。使用split方法将通道分割,并使用threshold函数进行阈值处理。每个通道的最小和最大范围用作下限和上限阈值。在此范围内的通道值将被转换为白色,其他则转换为黑色。这三个阈值通道通过逻辑 AND 操作得到一个用于蓝色颜色的最终掩码。这个掩码可以用来从视频中检测和跟踪具有蓝色颜色的对象。
两个帧的输出,一个没有蓝色对象,另一个有蓝色对象,如下所示:
从结果可以看出,当帧中不包含任何蓝色对象时,掩码几乎为黑色;而在下面的帧中,当蓝色对象进入画面时,该部分变为白色。这种方法仅在背景不包含对象颜色时才会有效。
基于形状的对象检测与跟踪
对象的形状也可以作为全局特征来检测具有独特形状的对象。这个形状可以是直线、多边形、圆形或任何其他不规则形状。对象边界、边缘和轮廓可以用来检测具有特定形状的对象。在本节中,我们将使用 Canny 边缘检测算法和 Hough 变换来检测两个规则形状,即直线和圆形。
Canny 边缘检测
在上一章中,我们看到了各种高通滤波器,这些滤波器可以用作边缘检测器。在本节中,我们使用 OpenCV 和 CUDA 实现了结合高斯滤波、梯度查找、非极大值抑制和阈值滞后的 Canny 边缘检测算法。正如上一章所解释的,高通滤波器对噪声非常敏感。在 Canny 边缘检测中,在检测边缘之前先进行高斯平滑,这使得它对噪声的敏感性降低。它还在检测边缘后有一个非极大值抑制阶段,以从结果中去除不必要的边缘。
Canny 边缘检测是一个计算密集型任务,难以用于实时应用。算法的 CUDA 版本可以用来加速它。实现 Canny 边缘检测算法的代码描述如下:
#include <cmath>
#include <iostream>
#include "opencv2/opencv.hpp"
using namespace std;
using namespace cv;
using namespace cv::cuda;
int main()
{
Mat h_image = imread("images/drawing.JPG",0);
if (h_image.empty())
{
cout << "can not open image"<< endl;
return -1;
}
GpuMat d_edge,d_image;
Mat h_edge;
d_image.upload(h_image);
cv::Ptr<cv::cuda::CannyEdgeDetector> Canny_edge = cv::cuda::createCannyEdgeDetector(2.0, 100.0, 3, false);
Canny_edge->detect(d_image, d_edge);
d_edge.download(h_edge);
imshow("source", h_image);
imshow("detected edges", h_edge);
waitKey(0);
return 0;
}
OpenCV 和 CUDA 提供了createCannyEdgeDetector类用于 Canny 边缘检测。创建此类的对象时,可以传递许多参数。前两个参数是阈值滞后的低阈值和高阈值。如果某一点的强度梯度大于最大阈值,则将其分类为边缘点。如果梯度小于低阈值,则该点不是边缘点。如果梯度在阈值之间,则根据连通性决定该点是否为边缘点。第三个参数是边缘检测器的孔径大小。最后一个参数是布尔参数,表示是否使用L2_norm或L1_norm进行梯度幅度计算。L2_norm计算成本较高,但更准确。真值表示使用L2_norm。代码的输出如下所示:
您可以调整下限和上限阈值,以更准确地检测给定图像的边缘。边缘检测是许多计算机视觉应用的重要预处理步骤,Canny 边缘检测被广泛用于此目的。
使用霍夫变换进行直线检测
在许多计算机视觉应用中,如车道检测,检测直线非常重要。它还可以用来检测其他规则形状的一部分直线。霍夫变换是计算机视觉中用于检测直线的流行特征提取技术。我们不会详细介绍霍夫变换如何检测直线,但我们将看到它如何在 OpenCV 和 CUDA 中实现。实现霍夫变换进行直线检测的代码如下:
#include <cmath>
#include <iostream>
#include "opencv2/opencv.hpp"
using namespace std;
using namespace cv;
using namespace cv::cuda;
int main()
{
Mat h_image = imread("images/drawing.JPG",0);
if (h_image.empty())
{
cout << "can not open image"<< endl;
return -1;
}
Mat h_edge;
cv::Canny(h_image, h_edge, 100, 200, 3);
Mat h_imagec;
cv::cvtColor(h_edge, h_imagec, COLOR_GRAY2BGR);
Mat h_imageg = h_imagec.clone();
GpuMat d_edge, d_lines;
d_edge.upload(h_edge);
{
const int64 start = getTickCount();
Ptr<cuda::HoughSegmentDetector> hough = cuda::createHoughSegmentDetector(1.0f, (float) (CV_PI / 180.0f), 50, 5);
hough->detect(d_edge, d_lines);
const double time_elapsed = (getTickCount() - start) / getTickFrequency();
cout << "GPU Time : " << time_elapsed * 1000 << " ms" << endl;
cout << "GPU FPS : " << (1/time_elapsed) << endl;
}
vector<Vec4i> lines_g;
if (!d_lines.empty())
{
lines_g.resize(d_lines.cols);
Mat h_lines(1, d_lines.cols, CV_32SC4, &lines_g[0]);
d_lines.download(h_lines);
}
for (size_t i = 0; i < lines_g.size(); ++i)
{
Vec4i line_point = lines_g[i];
line(h_imageg, Point(line_point[0], line_point[1]), Point(line_point[2], line_point[3]), Scalar(0, 0, 255), 2, LINE_AA);
}
imshow("source", h_image);
imshow("detected lines [GPU]", h_imageg);
waitKey(0);
return 0;
}
OpenCV 提供了createHoughSegmentDetector类用于实现霍夫变换。它需要一个图像的边缘图作为输入。因此,使用 Canny 边缘检测器从图像中检测边缘。Canny 边缘检测器的输出上传到设备内存以进行 GPU 计算。正如上一节所讨论的,边缘也可以在 GPU 上计算。
创建了createHoughSegmentDetector对象。它需要许多参数。第一个参数表示在霍夫变换中使用的参数r的分辨率,通常取 1 像素。第二个参数是参数 theta 的弧度分辨率,通常取 1 弧度或π/180。第三个参数是需要形成线条的最小点数,通常取 50 像素。最后一个参数是考虑为同一直线的两个点之间的最大间隔,通常取 5 像素。
创建的对象的检测方法用于检测直线。它需要两个参数。第一个参数是要检测边缘的图像,第二个参数是存储检测到的线条点的数组。该数组包含检测到的线条的起始和结束(x,y)点。使用 OpenCV 的线条函数通过for循环迭代该数组,在图像上绘制单个线条。最终图像使用imshow函数显示。
霍夫变换是一个数学密集型步骤。为了展示 CUDA 的优势,我们将实现 CPU 和 CUDA 的相同算法,并比较它们的性能。以下是对 CPU 霍夫变换的代码:
Mat h_imagec;
vector<Vec4i> h_lines;
{
const int64 start = getTickCount();
HoughLinesP(h_edge, h_lines, 1, CV_PI / 180, 50, 60, 5);
const double time_elapsed = (getTickCount() - start) / getTickFrequency();
cout << "CPU Time : " << time_elapsed * 1000 << " ms" << endl;
cout << "CPU FPS : " << (1/time_elapsed) << endl;
}
for (size_t i = 0; i < h_lines.size(); ++i)
{
Vec4i line_point = h_lines[i];
line(h_imagec, Point(line_point[0], line_point[1]), Point(line_point[2], line_point[3]), Scalar(0, 0, 255), 2, LINE_AA);
}
imshow("detected lines [CPU]", h_imagec);
使用HoughLinesP函数在 CPU 上通过概率霍夫变换检测线条。前两个参数是源图像和存储输出线条点的数组。第三个和第四个参数是r和 theta 的分辨率。第五个参数是表示线条的最小交点数的阈值。第六个参数表示形成线条所需的最小点数。最后一个参数表示考虑在相同线条上的点的最大间隔。
函数返回的数组使用for循环迭代,以在原始图像上显示检测到的线条。GPU 和 CPU 函数的输出如下:
以下截图显示了 GPU 和 CPU 代码在霍夫变换性能上的比较:
在 CPU 上处理单个图像大约需要 4 毫秒,在 GPU 上需要 1.5 毫秒,这在 CPU 上相当于 248 FPS,在 GPU 上相当于 632 FPS,几乎是 GPU 上 2.5 倍的提升。
圆检测
霍夫变换也可以用于圆检测。它可以用于许多应用,如球检测和跟踪以及硬币检测等,在这些应用中,对象是圆形的。OpenCV 和 CUDA 提供了一个类来实现这一点。以下是用霍夫变换进行硬币检测的代码:
#include "opencv2/opencv.hpp"
#include <iostream>
using namespace cv;
using namespace std;
int main(int argc, char** argv)
{
Mat h_image = imread("images/eight.tif", IMREAD_COLOR);
Mat h_gray;
cvtColor(h_image, h_gray, COLOR_BGR2GRAY);
cuda::GpuMat d_gray,d_result;
std::vector<cv::Vec3f> d_Circles;
cv::Ptr<cv::cuda::HoughCirclesDetector> detector = cv::cuda::createHoughCirclesDetector(1, 100, 122, 50, 1, max(h_image.size().width, h_image.size().height));
d_gray.upload(h_gray);
detector->detect(d_gray, d_result);
d_Circles.resize(d_result.size().width);
if (!d_Circles.empty())
d_result.row(0).download(cv::Mat(d_Circles).reshape(3, 1));
cout<<"No of circles: " <<d_Circles.size() <<endl;
for( size_t i = 0; i < d_Circles.size(); i++ )
{
Vec3i cir = d_Circles[i];
circle( h_image, Point(cir[0], cir[1]), cir[2], Scalar(255,0,0), 2, LINE_AA);
}
imshow("detected circles", h_image);
waitKey(0);
return 0;
}
有一个createHoughCirclesDetector类用于检测圆形物体。创建了该类的对象。在创建该类的对象时可以提供许多参数。第一个参数是dp,表示累加器分辨率与图像分辨率的倒数,通常取为 1。第二个参数是检测到的圆心之间的最小距离。第三个参数是 Canny 阈值,第四个参数是累加器阈值。第五和第六个参数是要检测的圆的最小和最大半径。
圆心之间的最小距离取为100像素。您可以尝试调整这个值。如果这个值减小,那么原始图像上会错误地检测到许多圆,而如果这个值增加,那么一些真正的圆可能会被错过。最后两个参数,即最小和最大半径,如果不知道确切的尺寸,可以取为0。在前面的代码中,它被设置为1和图像的最大尺寸,以检测图像中的所有圆。程序输出如下:
Hough 变换对高斯噪声和椒盐噪声非常敏感。因此,在应用 Hough 变换之前,有时最好先使用高斯和中值滤波器对图像进行预处理。这将给出更准确的结果。
总结来说,我们使用了 Hough 线变换和圆变换来检测具有规则形状的物体。轮廓和凸性也可以用于形状检测。这些功能在 OpenCV 中可用,但 CUDA 实现中不可用。您将不得不开发这些函数的自己的版本。
关键点检测器和描述符
到目前为止,我们使用了全局特征,如颜色和形状来检测物体。这些特征易于计算,快速,并且需要的内存量小,但它们只能在已有关于物体的某些信息的情况下使用。如果没有这些信息,则使用局部特征,这些特征需要更多的计算和内存,但它们更准确。在本节中,解释了寻找局部特征的多种算法。它们也被称为关键点检测器。关键点是表征图像的点,可以用来精确地定义一个物体。
来自加速区域测试(FAST)特征检测器的特征
FAST 算法用于从图像中检测角点作为关键点。它通过对每个像素应用段测试来检测角点。它考虑像素周围的 16 像素圆。如果在半径为 16 的圆中有连续的n个点,其强度大于Ip+t或小于Ip-t,则该像素被认为是角点。Ip是像素p的强度,t是选定的阈值。
有时,不是检查半径内的所有点,而是检查几个选定的点以确定强度值来决定角落点。这加速了 FAST 算法的性能。FAST 提供了可以作为关键点利用的角落点来检测对象。它是旋转不变的,因为即使对象旋转,对象的角落也会保持不变。FAST 不是尺度不变的,因为尺寸的增加可能会导致强度值的平滑过渡,而不是在角落处的尖锐过渡。
OpenCV 和 CUDA 提供了一个高效实现 FAST 算法的方法。以下是用 FAST 算法检测关键点的程序:
#include <iostream>
#include "opencv2/opencv.hpp"
using namespace cv;
using namespace std;
int main()
{
Mat h_image = imread( "images/drawing.JPG", 0 );
//Detect the key-points using FAST Detector
cv::Ptr<cv::cuda::FastFeatureDetector> detector = cv::cuda::FastFeatureDetector::create(100,true,2);
std::vector<cv::key point> key-points;
cv::cuda::GpuMat d_image;
d_image.upload(h_image);
detector->detect(d_image, key-points);
cv::drawkey-points(h_image,key-points,h_image);
//Show detected key-points
imshow("Final Result", h_image );
waitKey(0);
return 0;
}
OpenCV 和 CUDA 提供了一个FastFeatureDetector类来实现 FAST 算法。这个类的对象是通过类的 create 方法创建的。它需要三个参数。第一个参数是要用于 FAST 算法的强度阈值。第二个参数指定是否使用非最大抑制。它是一个布尔值,可以指定为true或false。第三个参数表示用于计算邻域的 FAST 方法。有三个方法,cv2.FAST_FEATURE_DETECTOR_TYPE_5_8、cv2.FAST_FEATURE_DETECTOR_TYPE_7_12和cv2.FAST_FEATURE_DETECTOR_TYPE_9_16,可以作为标志0、1或2指定。
创建的对象的 detect 方法用于检测关键点。它需要一个输入图像和一个存储关键点的向量作为参数。可以使用drawkey-points函数在原始图像上绘制计算出的关键点。它需要源图像、关键点的向量和目标图像作为参数。
可以改变强度阈值以检测不同数量的关键点。如果阈值低,则更多的关键点会通过分割测试并被归类为关键点。随着阈值的增加,检测到的关键点数量将逐渐减少。同样,如果非最大抑制是假的,则在单个角落点可能会检测到多个关键点。以下是代码的输出:
从输出结果可以看出,随着阈值从 10 增加到 50 和 100,关键点的数量减少。这些关键点可以用于检测查询图像中的对象。
定向 FAST 和旋转 BRIEF (ORB) 特征检测
ORB 是一个非常高效的特徵检测和描述算法。它是特征检测的 FAST 算法和特征描述的二进制鲁棒独立基本特征(BRIEF)算法的组合。它为广泛用于对象检测的 SURF 和 SIFT 算法提供了一个高效的替代方案。由于它们是专利的,使用它们需要付费。ORB 在无需付费的情况下匹配 SIFT 和 SURF 的性能。
OpenCV 和 CUDA 提供了一个易于实现的 ORB 算法的 API。实现 ORB 算法的代码如下:
#include <iostream>
#include "opencv2/opencv.hpp"
using namespace cv;
using namespace std;
int main()
{
Mat h_image = imread( "images/drawing.JPG", 0 );
cv::Ptr<cv::cuda::ORB> detector = cv::cuda::ORB::create();
std::vector<cv::key point> key-points;
cv::cuda::GpuMat d_image;
d_image.upload(h_image);
detector->detect(d_image, key-points);
cv::drawkey-points(h_image,key-points,h_image);
imshow("Final Result", h_image );
waitKey(0);
return 0;
}
ORB类的对象是通过create方法创建的。此方法的所有参数都是可选的,因此我们使用了它的默认值。创建的对象的detect方法用于从图像中检测关键点。它需要一个输入图像和关键点向量的向量,输出将存储在这些参数中。使用drawkey-points函数在图像上绘制检测到的关键点。前述代码的输出如下:
ORB类还提供了一个方法来计算所有关键点的描述符。这些描述符可以准确地描述对象,并可用于从图像中检测对象。这些描述符也可以用于对对象进行分类。
加速鲁棒特征检测和匹配
SURF 通过基于简单二维盒滤波器的计算来近似高斯拉普拉斯。使用积分图像可以轻松计算与盒滤波器的卷积,这提高了算法的性能。SURF 依赖于 Hessian 矩阵的行列式来处理尺度和位置。Hessian 矩阵的近似行列式可以表示为:
其中,w是滤波器响应的相对权重,用于平衡行列式的表达式。Dx、Dy是拉普拉斯算子在X和Y方向的结果。
SURF 在水平和垂直方向上使用小波响应,使用积分图像方法进行方向分配。还应用了适当的 Gaussian 权重。通过计算角度为 60 度的滑动方向窗口内所有响应的总和来估计主导方向。
对于特征描述,SURF 在水平和垂直方向上使用 Haar 小波响应。这是对图像中的所有子区域进行计算的结果,从而得到一个具有 64 个维度的 SURF 特征描述符。维度越低,计算和匹配的速度越快。为了提高精度,SURF 特征描述符还有一个扩展的 128 维版本。SURF 是旋转不变和尺度不变的。
与使用 128 维特征向量的 SIFT 相比,SURF 具有更高的处理速度,因为它使用 64 方向的特性向量。SURF 擅长处理模糊和旋转的图像,但不擅长处理视点和光照变化。
OpenCV 和 CUDA 提供了一个 API 来计算 SURF 关键点和描述符。我们还将看到如何使用这些 API 在查询图像中检测对象。SURF 特征检测和匹配的代码如下:
#include <stdio.h>
#include <iostream>
#include "opencv2/opencv.hpp"
#include "opencv2/features2d.hpp"
#include "opencv2/xfeatures2d.hpp"
#include "opencv2/xfeatures2d/nonfree.hpp"
#include "opencv2/xfeatures2d/cuda.hpp"
using namespace cv;
using namespace cv::xfeatures2d;
using namespace std;
int main( int argc, char** argv )
{
Mat h_object_image = imread( "images/object1.jpg", 0 );
Mat h_scene_image = imread( "images/scene1.jpg", 0 );
cuda::GpuMat d_object_image;
cuda::GpuMat d_scene_image;
cuda::GpuMat d_key-points_scene, d_key-points_object;
vector< key point > h_key-points_scene, h_key-points_object;
cuda::GpuMat d_descriptors_scene, d_descriptors_object;
d_object_image.upload(h_object_image);
d_scene_image.upload(h_scene_image);
cuda::SURF_CUDA surf(150);
surf( d_object_image, cuda::GpuMat(), d_key-points_object, d_descriptors_object );
surf( d_scene_image, cuda::GpuMat(), d_key-points_scene, d_descriptors_scene );
Ptr< cuda::DescriptorMatcher > matcher = cuda::DescriptorMatcher::createBFMatcher();
vector< vector< DMatch> > d_matches;
matcher->knnMatch(d_descriptors_object, d_descriptors_scene, d_matches, 3);
surf.downloadkey-points(d_key-points_scene, h_key-points_scene);
surf.downloadkey-points(d_key-points_object, h_key-points_object);
std::vector< DMatch > good_matches;
for (int k = 0; k < std::min(h_key-points_object.size()-1, d_matches.size()); k++)
{
if ( (d_matches[k][0].distance < 0.75*(d_matches[k][1].distance)) &&
((int)d_matches[k].size() <= 2 && (int)d_matches[k].size()>0) )
{
good_matches.push_back(d_matches[k][0]);
}
}
std::cout << "size:" <<good_matches.size();
Mat h_image_result;
drawMatches( h_object_image, h_key-points_object, h_scene_image, h_key-points_scene,
good_matches, h_image_result, Scalar::all(-1), Scalar::all(-1),
vector<char>(), DrawMatchesFlags::DEFAULT );
imshow("Good Matches & Object detection", h_image_result);
waitKey(0);
return 0;
}
从磁盘读取两张图像。第一张图像包含要检测的对象。第二张图像是要搜索对象的查询图像。我们将从这两张图像中计算 SURF 特征,然后匹配这些特征以从查询图像中检测对象。
OpenCV 提供了用于计算 SURF 特征的 SURF_CUDA 类。该类的对象被创建。它需要一个 Hessian 阈值作为参数。这里取值为 150。这个阈值决定了 Hessian 行列式计算输出的点必须有多大,才能被认为是关键点。更大的阈值值将导致更少但更显著的兴趣点,而较小的值将导致更多但不太显著的点。可以根据应用来选择。
这个 surf 对象用于从对象和查询图像中计算关键点和描述符。图像、图像的数据类型、存储关键点的向量以及描述符作为参数传递。为了匹配查询图像中的对象,需要匹配两张图像中的描述符。OpenCV 提供了不同的匹配算法来实现这个目的,如 Brute-Force 匹配器和 快速近似最近邻库(FLANN)匹配器。
程序中使用的是 Brute-Force 匹配器;这是一个简单的方法。它使用某种距离计算方法,将对象中与查询图像中所有其他特征匹配的特征描述符取出来。它返回最佳匹配关键点,或使用最近邻算法通过 matcher 类的 knnMatch 方法返回最佳 k 个匹配。knnMatch 方法需要两组描述符以及最近邻的数量。在代码中取值为 3。
从 knnMatch 方法返回的匹配点中提取出良好的匹配关键点。这些良好的匹配是通过使用原始论文中描述的比率测试方法找到的。这些良好的匹配用于从场景中检测对象。
使用 drawMatches 函数在两张图像的匹配良好点之间画线。它需要许多参数。第一个参数是源图像,第二个参数是源图像的关键点,第三个参数是第二张图像,第四个参数是第二张图像的关键点,第五个参数是输出图像。第六个参数是线条和关键点的颜色。这里取值为 Scalar::all(-1),表示将随机选择颜色。第七个参数是关键点的颜色,这些关键点没有匹配。它也取值为 Scalar::all(-1),表示将随机选择颜色。最后两个参数指定了绘制匹配的掩码和标志设置。使用空掩码,以便绘制所有匹配。
这些匹配可以用来在检测到的物体周围绘制边界框,这将定位场景中的物体。绘制边界框的代码如下:
std::vector<Point2f> object;
std::vector<Point2f> scene;
for (int i = 0; i < good_matches.size(); i++) {
object.push_back(h_key-points_object[good_matches[i].queryIdx].pt);
scene.push_back(h_key-points_scene[good_matches[i].trainIdx].pt);
}
Mat Homo = findHomography(object, scene, RANSAC);
std::vector<Point2f> corners(4);
std::vector<Point2f> scene_corners(4);
corners[0] = Point(0, 0);
corners[1] = Point(h_object_image.cols, 0);
corners[2] = Point(h_object_image.cols, h_object_image.rows);
corners[3] = Point(0, h_object_image.rows);
perspectiveTransform(corners, scene_corners, Homo);
line(h_image_result, scene_corners[0] + Point2f(h_object_image.cols, 0),scene_corners[1] + Point2f(h_object_image.cols, 0), Scalar(255, 0, 0), 4);
line(h_image_result, scene_corners[1] + Point2f(h_object_image.cols, 0),scene_corners[2] + Point2f(h_object_image.cols, 0),Scalar(255, 0, 0), 4);
line(h_image_result, scene_corners[2] + Point2f(h_object_image.cols, 0),scene_corners[3] + Point2f(h_object_image.cols, 0),Scalar(255, 0, 0), 4);
line(h_image_result, scene_corners[3] + Point2f(h_object_image.cols, 0),scene_corners[0] + Point2f(h_object_image.cols, 0),Scalar(255, 0, 0), 4);
OpenCV 提供了findHomography函数,用于根据良好的匹配搜索场景中物体的位置、方向和比例。前两个参数是从物体和场景图像中提取的良好匹配的关键点。随机样本一致性(RANSAC)方法作为参数之一传递,用于找到最佳平移矩阵。
找到这个平移矩阵后,使用perspectiveTransform函数来找到物体。它需要一个四角点和平移矩阵作为参数。这些变换点用于在检测到的物体周围绘制边界框。用于查找特征和匹配物体的 SURF 程序的输出如下:
该图包含物体图像、查询图像和检测图像。从前面的图像可以看出,SURF 可以准确地确定物体的位置,即使物体被旋转。尽管有时它可能会检测到错误特征。可以通过改变 Hessian 阈值和测试比率来找到最佳匹配。
因此,总结一下,在本节中,我们看到了 FAST、ORB 和 SURF 关键点检测算法。我们还看到了如何使用这些点通过使用 SURF 特征作为示例来匹配和定位图像中的物体。您也可以尝试使用 FAST 和 ORB 特征来完成相同的工作。在下一节中,我们将详细讨论用于从图像中检测面部和眼睛的 Haar 级联。
使用 Haar 级联进行物体检测
Haar 级联使用矩形特征来检测物体。它使用不同大小的矩形来计算不同的线和边缘特征。矩形包含一些黑白区域,如图所示,它们在图像中的不同位置居中:
Haar-like 特征选择算法背后的思想是计算矩形内部白色像素总和与黑色像素总和之间的差异。
这种方法的主要优势是使用积分图像快速进行求和计算。这使得 Haar 级联非常适合实时物体检测。它处理图像所需的时间比之前描述的 SURF 等算法少。由于计算量较小且内存占用较少,该算法也可以在嵌入式系统(如 Raspberry Pi)上实现。它被称为 Haar-like,因为它基于与 Haar 小波相同的原理。Haar 级联在人体检测中广泛使用,包括面部和眼部检测等部分。它还可以用于表情分析。Haar 级联可用于检测车辆等物体。
在本节中,描述了使用 Haar 级联从图像和摄像头中检测人脸和眼睛的方法。Haar 级联是一种机器学习算法,需要对其进行训练以执行特定任务。对于特定应用从头开始训练 Haar 级联是困难的,因此 OpenCV 提供了一些训练好的 XML 文件,可用于检测对象。这些 XML 文件位于 OpenCV 或 CUDA 安装的opencv\data\haarcascades_cuda文件夹中。
使用 Haar 级联进行人脸检测
在本节中,我们将使用 Haar 级联从图像和实时摄像头中检测人脸。使用 Haar 级联从图像中检测人脸的代码如下:
#include "opencv2/objdetect/objdetect.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/cudaobjdetect.hpp"
#include <iostream>
#include <stdio.h>
using namespace std;
using namespace cv;
int main( )
{
Mat h_image;
h_image = imread("images/lena_color_512.tif", 0);
Ptr<cuda::CascadeClassifier> cascade = cuda::CascadeClassifier::create("haarcascade_frontalface_alt2.xml");
cuda::GpuMat d_image;
cuda::GpuMat d_buf;
d_image.upload(h_image);
cascade->detectMultiScale(d_image, d_buf);
std::vector<Rect> detections;
cascade->convert(d_buf, detections);
if (detections.empty())
std::cout << "No detection." << std::endl;
cvtColor(h_image,h_image,COLOR_GRAY2BGR);
for(int i = 0; i < detections.size(); ++i)
{
rectangle(h_image, detections[i], Scalar(0,255,255), 5);
}
imshow("Result image", h_image);
waitKey(0);
return 0;
}
OpenCV 和 CUDA 提供了CascadeClassifier类,可用于实现 Haar 级联。使用 create 方法创建该类的对象。它需要加载训练好的 XML 文件的文件名。创建的对象有detectMultiScale方法,可以从图像中检测到多个尺度的对象。它需要一个图像文件和一个Gpumat数组作为参数来存储输出结果。使用CascadeClassifier对象的 convert 方法将此gpumat向量转换为标准矩形向量。此转换向量包含绘制检测到的对象矩形坐标。
detectMultiScale函数有许多参数可以在调用函数之前修改。这些包括用于指定每次图像缩放时图像大小将减少多少的scaleFactor,以及指定每个矩形应保留的最小邻居数minNeighbors;minSize指定最小对象大小,maxSize指定最大对象大小。所有这些参数都有默认值,所以在正常情况下通常不需要修改。如果我们想更改它们,那么在调用detectMultiscale函数之前可以使用以下代码:
cascade->setMinNeighbors(0);
cascade->setScaleFactor(1.01);
第一个函数将设置最小邻居数为0,第二个函数将在每次缩放后通过一个因子1.01减小图像大小。缩放因子对于检测不同大小物体的检测非常重要。如果它很大,则算法完成所需时间会更短,但可能有些面部无法检测到。如果它很小,则算法完成所需时间会更长,并且会更准确。前述代码的输出如下:
从视频中
Haar 级联的相同概念可以用于从视频中检测人脸。检测人脸的代码包含在while循环中,以便在视频的每一帧中检测到人脸。从摄像头进行人脸检测的代码如下:
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main()
{
VideoCapture cap(0);
if (!cap.isOpened()) {
cerr << "Can not open video source";
return -1;
}
std::vector<cv::Rect> h_found;
cv::Ptr<cv::cuda::CascadeClassifier> cascade = cv::cuda::CascadeClassifier::create("haarcascade_frontalface_alt2.xml");
cv::cuda::GpuMat d_frame, d_gray, d_found;
while(1)
{
Mat frame;
if ( !cap.read(frame) ) {
cerr << "Can not read frame from webcam";
return -1;
}
d_frame.upload(frame);
cv::cuda::cvtColor(d_frame, d_gray, cv::COLOR_BGR2GRAY);
cascade->detectMultiScale(d_gray, d_found);
cascade->convert(d_found, h_found);
for(int i = 0; i < h_found.size(); ++i)
{
rectangle(frame, h_found[i], Scalar(0,255,255), 5);
}
imshow("Result", frame);
if (waitKey(1) == 'q') {
break;
}
}
return 0;
}
初始化摄像头并逐个捕获摄像头帧。此帧上传到设备内存中进行 GPU 处理。通过使用类的create方法创建CascadeClassifier类的对象。创建对象时提供面部检测的 XML 文件作为参数。在while循环内部,对每个帧应用detectMultiscale方法,以便在每帧中检测不同大小的面部。使用convert方法将检测到的位置转换为矩形向量。然后使用for循环迭代此向量,以便使用rectangle函数在所有检测到的面部上绘制边界框。程序输出如下:
使用 Haar 级联进行眼睛检测
本节将描述在检测人类眼睛中使用 Haar 级联的方法。用于眼睛检测的已训练 Haar 级联的 XML 文件位于 OpenCV 安装目录中。此文件用于检测眼睛。其代码如下:
#include <iostream>
#include <stdio.h>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main( )
{
Mat h_image;
h_image = imread("images/lena_color_512.tif", 0);
Ptr<cuda::CascadeClassifier> cascade = cuda::CascadeClassifier::create("haarcascade_eye.xml");
cuda::GpuMat d_image;
cuda::GpuMat d_buf;
d_image.upload(h_image);
cascade->setScaleFactor(1.02);
cascade->detectMultiScale(d_image, d_buf);
std::vector<Rect> detections;
cascade->convert(d_buf, detections);
if (detections.empty())
std::cout << "No detection." << std::endl;
cvtColor(h_image,h_image,COLOR_GRAY2BGR);
for(int i = 0; i < detections.size(); ++i)
{
rectangle(h_image, detections[i], Scalar(0,255,255), 5);
}
imshow("Result image", h_image);
waitKey(0);
return 0;
}
}
代码与面部检测的代码类似。这是使用 Haar 级联的优势。如果给定对象的已训练 Haar 级联的 XML 文件可用,则相同的代码可以在所有应用中工作。只需在创建CascadeClassifier类的对象时更改 XML 文件的名称。在前面的代码中,使用了用于眼睛检测的已训练 XML 文件haarcascade_eye.xml。其他代码是自解释的。缩放因子设置为1.02,以便在每次缩放时图像大小将减少1.02。
眼睛检测程序的输出如下:
由于捕获图像时采用的角度不同,眼睛的大小各不相同,但 Haar 级联仍然能够有效地定位两只眼睛。代码的性能也可以进行测量,以查看其工作速度有多快。
总结来说,在本节中,我们展示了使用 Haar 级联进行面部和眼睛检测的应用。一旦有了训练文件,实现起来非常简单,它是一个非常强大的算法。它在内存和处理能力有限的嵌入式或移动环境中被广泛使用。
使用背景减法进行对象跟踪
背景减法是从一系列视频帧中分离前景对象与背景的过程。它在对象检测和跟踪应用中被广泛使用,以去除背景部分。背景减法分为四个步骤:
-
图像预处理
-
背景建模
-
前景检测
-
数据验证
图像预处理始终执行以去除图像中存在的任何类型的噪声。第二步是建模背景,以便它可以与前景分离。在某些应用中,视频的第一帧被用作背景,并且不进行更新。通过计算每一帧与第一帧之间的绝对差值来分离前景和背景。
在其他技术中,背景是通过取算法看到的所有帧的平均值或中值来建模的,并且该背景与前景分离。这种方法对于光照变化将更加稳健,并且会产生比第一种方法更动态的背景。还可以使用更统计密集的模型,如使用帧历史记录的高斯模型和支持向量模型来建模背景。
第三步是通过计算当前帧与背景之间的绝对差值来将前景从建模的背景中分离出来。这个绝对差值与设定的阈值进行比较,如果它大于阈值,则认为物体是移动的;如果它小于阈值,则认为物体是静止的。
高斯混合(MoG)方法
MoG 是一种广泛使用的背景减法方法,用于根据高斯混合将前景从背景中分离出来。背景从帧序列中持续更新。使用 K 个高斯分布的混合来分类像素为前景或背景。帧的时间序列也被加权以改进背景建模。持续变化的强度被分类为前景,而静态的强度被分类为背景。
OpenCV 和 CUDA 提供了一个简单的 API 来实现 MoG 背景减法。相应的代码如下:
#include <iostream>
#include <string>
#include "opencv2/opencv.hpp"
using namespace std;
using namespace cv;
using namespace cv::cuda;
int main()
{
VideoCapture cap("abc.avi");
if (!cap.isOpened())
{
cerr << "can not open camera or video file" << endl;
return -1;
}
Mat frame;
cap.read(frame);
GpuMat d_frame;
d_frame.upload(frame);
Ptr<BackgroundSubtractor> mog = cuda::createBackgroundSubtractorMOG();
GpuMat d_fgmask,d_fgimage,d_bgimage;
Mat h_fgmask,h_fgimage,h_bgimage;
mog->apply(d_frame, d_fgmask, 0.01);
while(1)
{
cap.read(frame);
if (frame.empty())
break;
d_frame.upload(frame);
int64 start = cv::getTickCount();
mog->apply(d_frame, d_fgmask, 0.01);
mog->getBackgroundImage(d_bgimage);
double fps = cv::getTickFrequency() / (cv::getTickCount() - start);
std::cout << "FPS : " << fps << std::endl;
d_fgimage.create(d_frame.size(), d_frame.type());
d_fgimage.setTo(Scalar::all(0));
d_frame.copyTo(d_fgimage, d_fgmask);
d_fgmask.download(h_fgmask);
d_fgimage.download(h_fgimage);
d_bgimage.download(h_bgimage);
imshow("image", frame);
imshow("foreground mask", h_fgmask);
imshow("foreground image", h_fgimage);
imshow("mean background image", h_bgimage);
if (waitKey(1) == 'q')
break;
}
return 0;
}
使用createBackgroundSubtractorMOG类来创建用于 MoG 实现的对象。在创建对象时可以提供一些可选参数。这些参数包括history、nmixtures、backgroundRatio和noiseSigma。history参数表示用于建模背景的先前帧的数量。其默认值是 200。nmixture参数指定用于分离像素的高斯混合的数量。其默认值是 5。您可以根据应用程序的需要调整这些值。
创建的对象的apply方法用于从第一帧创建前景掩码。它需要一个输入图像和一个图像数组作为输入来存储前景掩码和学习率。在while循环的每一帧之后,都会持续更新前景掩码和背景图像。getBackgroundImage函数用于获取当前的背景模型。
前景掩码用于创建一个前景图像,指示哪些对象目前正在移动。它基本上是逻辑的,在原始帧和前景掩码之间操作。前景掩码、前景图像和建模的背景在每帧之后下载到主机内存,以便在屏幕上显示。
MoG 模型应用于 PETS 2009 数据集的视频,该数据集广泛用于行人检测。它具有静态背景,视频中有人员在移动。视频的两个不同帧的输出如下:
如所示,MoG 非常有效地建模背景。只有移动的人存在于前景掩码和前景图像中。此前景图像可用于检测到的对象的进一步处理。如果一个人停止行走,那么他将成为背景的一部分,如第二帧的结果所示。因此,此算法只能用于检测移动对象。它不会考虑静态对象。MoG 在帧率方面的性能如下:
每帧更新帧率。可以看出,它大约是每秒 330 帧,这非常高,易于用于实时应用。OpenCV 和 CUDA 还提供了 MoG 的第二版本,可以通过createBackgroundSubtractorMOG2类调用。
GMG 用于背景减法
GMG 算法的名称 GMG 来源于提出该算法的发明者的首字母。该算法是背景估计和每像素贝叶斯分割的组合。它使用贝叶斯推理将背景与前景分开。它还使用帧的历史记录来建模背景。它再次根据帧的时间序列进行加权。新的观测值比旧的观测值加权更多。
OpenCV 和 CUDA 为 GMG 算法的实现提供了与 MoG 类似的 API。实现背景减法的 GMG 算法的代码如下:
#include <iostream>
#include <string>
#include "opencv2/opencv.hpp"
#include "opencv2/core.hpp"
#include "opencv2/core/utility.hpp"
#include "opencv2/cudabgsegm.hpp"
#include "opencv2/cudalegacy.hpp"
#include "opencv2/video.hpp"
#include "opencv2/highgui.hpp"
using namespace std;
using namespace cv;
using namespace cv::cuda;
int main()
{
VideoCapture cap("abc.avi");
if (!cap.isOpened())
{
cerr << "can not open video file" << endl;
return -1;
}
Mat frame;
cap.read(frame);
GpuMat d_frame;
d_frame.upload(frame);
Ptr<BackgroundSubtractor> gmg = cuda::createBackgroundSubtractorGMG(40);
GpuMat d_fgmask,d_fgimage,d_bgimage;
Mat h_fgmask,h_fgimage,h_bgimage;
gmg->apply(d_frame, d_fgmask);
while(1)
{
cap.read(frame);
if (frame.empty())
break;
d_frame.upload(frame);
int64 start = cv::getTickCount();
gmg->apply(d_frame, d_fgmask, 0.01);
double fps = cv::getTickFrequency() / (cv::getTickCount() - start);
std::cout << "FPS : " << fps << std::endl;
d_fgimage.create(d_frame.size(), d_frame.type());
d_fgimage.setTo(Scalar::all(0));
d_frame.copyTo(d_fgimage, d_fgmask);
d_fgmask.download(h_fgmask);
d_fgimage.download(h_fgimage);
imshow("image", frame);
imshow("foreground mask", h_fgmask);
imshow("foreground image", h_fgimage);
if (waitKey(30) == 'q')
break;
}
return 0;
}
使用createBackgroundSubtractorGMG类创建用于 GMG 实现的对象。在创建对象时可以提供两个参数。第一个参数是用于建模背景的先前帧数。在上述代码中取为40。第二个参数是决策阈值,用于将像素分类为前景。其默认值为 0.8。
创建的对象的apply方法用于第一帧以创建前景掩码。通过使用帧的历史记录,前景掩码和前景图像在while循环内部持续更新。前景掩码用于以类似于 MoG 所示的方式创建前景图像。GMG 算法在相同视频和两个帧上的输出如下:
与 MoG 相比,GMG 的输出噪声更大。可以对 GMG 的结果应用形态学开闭操作,以去除结果中存在的阴影噪声。GMG 算法在 FPS 方面的性能如下:
由于它比 MoG 计算量更大,帧率较低,但仍然达到 120 FPS,这比实时性能所需的 30 FPS 要高。
总结来说,在本节中我们看到了两种背景建模和背景减法的方法。与 GMG 算法相比,MoG 算法更快且噪声更少。GMG 算法需要形态学操作来去除结果中存在的噪声。
摘要
本章描述了 OpenCV 和 CUDA 在实时目标检测和跟踪应用中的作用。它从目标检测和跟踪的介绍开始,包括在这个过程中遇到的问题和它的应用。不同的特征,如颜色、形状、直方图和其他独特的关键点,如角点,可以用来检测和跟踪图像中的对象。基于颜色的目标检测更容易实现,但要求对象与背景有明显的颜色差异。对于基于形状的目标检测,已经描述了 Canny 边缘检测技术来检测边缘,以及 Hough 变换用于直线和圆的检测。它有许多应用,如土地检测、球跟踪等。颜色和形状是全局特征,更容易计算且需要的内存较少。它们更容易受到噪声的影响。其他算法如 FAST、ORB 和 SURF 已经详细描述,这些算法可以用来从图像中检测关键点,这些关键点可以用来准确描述图像,进而可以用来检测图像中的对象。ORB 是开源的,并且无需成本就能提供与 SURF 相当的结果。SURF 是专利的,但它更快,具有尺度不变性和旋转不变性。已经描述了 Haar 级联,这是一个简单的算法,用于从图像中检测对象,如人脸、眼睛和人体。它可以用于嵌入式系统中的实时应用。本章的最后部分详细描述了背景减法算法,如 MoG 和 GMG,这些算法可以将前景与背景分离。这些算法的输出可以用于目标检测和跟踪。下一章将描述如何将这些应用部署在嵌入式开发板上。
问题
-
编写一个 OpenCV 代码,用于从视频中检测黄色对象。
-
在哪些情况下,使用颜色进行的目标检测会失败?
-
为什么 Canny 边缘检测算法比上一章中看到的其他边缘检测算法更好?
-
可以采取什么措施来降低 Hough 变换的噪声敏感性?
-
在 FAST 关键点检测器中,阈值的重要性是什么?
-
在 SURF 检测器中,Hessian 阈值的重要性是什么?
-
如果 Haar 级联中的尺度因子从 1.01 变为 1.05,那么它将对输出产生什么影响?
-
比较 MoG 和 GMG 背景减法方法。如何从 GMG 输出中去除噪声?
第八章:Jetson TX1 开发板简介及在 Jetson TX1 上安装 OpenCV
上一章介绍了使用 OpenCV 和 CUDA 的各种计算机视觉应用。当这些应用需要在实际场景中部署时,就需要一个嵌入式开发板,能够通过利用 OpenCV 和 CUDA 高速处理图像。Nvidia 提供了多个基于 GPU 的开发板,如 Jetson TK1、TX1 和 TX2,它们非常适合高端计算任务,如计算机视觉。本章将介绍其中一个开发板,即 Jetson TX1。还将详细讨论该板可用的特性和应用。CUDA 和 OpenCV 对于计算机视觉应用至关重要,因此本章将详细讨论在 Jetson TX1 上安装它们的步骤。
本章将涵盖以下主题:
-
Jetson TX1 开发板简介
-
Jetson TX1 开发板的特性和应用
-
基本要求和在 Jetson TX1 开发板上安装 JetPack 的步骤
技术要求
本章要求对 Linux 操作系统(OS)和网络有良好的理解。它还需要任何 Nvidia GPU 开发板,例如 Jetson TK1、TX1 或 TX2。本章中使用的 JetPack 安装文件可以从以下链接下载:developer.nvidia.com/embedded/jetpack。
Jetson TX1 简介
当高端视觉计算和计算机视觉应用需要在实际场景中部署时,就需要嵌入式开发平台,这些平台能够高效地执行计算密集型任务。例如,Raspberry Pi 可以使用 OpenCV 进行计算机视觉应用和摄像头接口功能,但对于实时应用来说非常慢。专注于 GPU 制造的 Nvidia 开发了用于计算密集型任务的模块,这些模块可用于在嵌入式平台上部署计算机视觉应用,包括 Jetson TK1、Jetson TX1 和 Jetson TX2。
Jetson TK1 是一个初步的板,包含 192 个 CUDA 核心,配备 Nvidia Kepler GPU。它是三者中最便宜的。Jetson TX1 在处理速度方面处于中等水平,拥有 256 个 CUDA 核心,采用 Maxwell 架构,运行频率为 998 MHz,同时配备 ARM CPU。Jetson TX2 在处理速度和价格方面都是最高的。它包含 256 个 CUDA 核心,采用 Pascal 架构,运行频率为 1,300 MHz。本章将详细介绍 Jetson TX1。
Jetson TX1 是专为要求较高的嵌入式应用开发的小型模块化系统。它是基于 Linux 的,提供具有万亿次浮点运算性能的超高性能,可用于计算机视觉和深度学习应用。以下照片展示了 Jetson TX1 模块:
该模块的尺寸为 50 x 87 mm,这使得它很容易集成到任何系统中。Nvidia 还提供了 Jetson TX1 开发板,该板可以快速用于原型设计应用,并包含此 GPU。以下照片展示了整个开发套件:
如照片所示,除了 GPU 模块外,开发套件还包含摄像头模块、USB 端口、以太网端口、散热器、风扇和天线。它支持包括 JetPack、Linux for Tegra、CUDA Toolkit、cuDNN、OpenCV 和 VisionWorks 在内的软件生态系统。这使得它非常适合进行深度学习和计算机视觉研究的开发者进行快速原型设计。以下章节将详细介绍 Jetson TX1 开发套件的功能。
Jetson TX1 的重要特性
Jetson TX1 开发套件具有许多特性,使其非常适合超级计算任务:
-
它是采用 20 nm 技术构建的系统级芯片,包含一个 1.73 GHz 的 ARM Cortex A57 四核 CPU 和一个 998 MHz 的 256 核心 Maxwell GPU。
-
它配备了 4 GB 的 DDR4 内存,数据总线为 64 位,工作速度为 1,600 MHz,相当于 25.6 GB/s。
-
它包含一个 500 万像素的 MIPI CSI-2 摄像头模块。它支持高达六路双通道或三路四通道摄像头,速度为 1,220 MP/s。
-
开发套件还包含一个标准 USB 3.0 类型 A 端口和微型 USB 端口,用于将鼠标、键盘和 USB 摄像头连接到板上。
-
它还配备了以太网端口和 Wi-Fi 连接,用于网络连接。
-
它可以通过 HDMI 端口连接到 HDMI 显示设备。
-
该套件包含一个散热器和风扇,用于在 GPU 设备达到峰值性能时进行冷却。
-
在空闲状态下,它仅消耗 1 瓦特的电力,在正常负载下大约为 8-10 瓦,当模块完全使用时可达 15 瓦。在 5.7 瓦的功耗下,它可以每秒处理 258 张图像,相当于性能/瓦特值为 45。一个普通的 i7 CPU 处理器在 62.5 瓦的功耗下,每秒可以处理 242 张图像,相当于性能/瓦特值为 3.88。因此,Jetson TX1 比 i7 处理器好 11.5 倍。
Jetson TX1 的应用
Jetson TX1 可用于许多需要计算密集型任务的深度学习和计算机视觉应用。以下是一些 Jetson TX1 可以使用的领域和应用:
-
它可用于构建各种计算密集型任务的自主机器和自动驾驶汽车。
-
它可用于各种计算机视觉应用,如目标检测、分类和分割。它还可以用于医学成像,分析 MRI 图像和**计算机断层扫描(CT)**图像。
-
它可以用来构建智能视频监控系统,这些系统能够帮助进行犯罪监控或交通监控。
-
它可以用于生物信息学和计算化学,用于模拟 DNA 基因、测序、蛋白质对接等。
-
它可以用于需要快速计算的各种防御设备。
在 Jetson TX1 上安装 JetPack
当 TX1 首次启动时,应该安装预装的 Linux 操作系统。以下命令可以完成安装:
cd ${HOME}/NVIDIA-INSTALLER
sudo ./installer.sh
在执行这两个命令后重启 TX1,将启动带有用户界面的 Linux 操作系统。Nvidia 提供了一套软件开发工具包(SDK),其中包含构建计算机视觉和深度学习应用所需的所有软件,以及用于闪存开发板的目标操作系统。这个 SDK 被称为JetPack。最新的 JetPack 包含 Linux for Tegra (L4T)板支持包;TensorRT,用于计算机视觉应用中的深度学习推理;最新的 CUDA 工具包,cuDNN,这是一个 CUDA 深度神经网络库;VisionWorks,它也用于计算机视觉和深度学习应用;以及 OpenCV。
当你安装 JetPack 时,所有这些包都将默认安装。本节描述了在板上安装 JetPack 的步骤。这个过程很长、繁琐,对于一个 Linux 新手来说有点复杂。所以,请仔细遵循以下章节中给出的步骤和截图。
安装的基本要求
在 TX1 上安装 JetPack 有一些基本要求。JetPack 不能直接在板上安装,因此需要一个运行 Ubuntu 14.04 的 PC 或虚拟机作为宿主机。安装过程不使用最新的 Ubuntu 版本,但你可以在它上面自由尝试。Jetson TX1 板需要一些外围设备,如鼠标、键盘和显示器,这些可以通过 USB 和 HDMI 端口连接。Jetson TX1 板应通过以太网线连接到与宿主机相同的路由器。安装还需要一根 micro USB 到 USB 线,用于通过串行传输将板与 PC 连接,以便在板上传输软件包。通过检查路由器配置来记录板的 IP 地址。如果所有要求都得到满足,则转到以下章节进行 JetPack 的安装。
安装步骤
本节描述了安装最新 JetPack 版本的步骤,并附有截图。所有步骤都需要在运行 Ubuntu 14.04 的宿主机上执行:
- 通过以下链接从官方 Nvidia 网站下载最新的 JetPack 版本,
developer.nvidia.com/embedded/jetpack,并点击下载按钮,如图所示:
-
在本书撰写时,使用的最新版本是 JetPack 3.3。它用于演示安装过程。下载文件的名称是 JetPack-L4T-3.3-linux-x64_b39.run。
-
在桌面上创建一个名为 jetpack 的文件夹,并将此文件复制到该文件夹中,如图所示:
- 通过右键单击并选择“打开”选项在该文件夹中启动一个终端。该文件需要执行,因此它应该具有执行权限。如果不是这种情况,请更改权限,然后启动安装程序,如图所示:
- 系统将启动 JetPack 3.3 的安装向导,如图所示。只需在此窗口中点击“下一步”:
- 向导将询问要下载和安装软件包的目录。您可以选择当前目录进行安装,并在该目录中创建一个新文件夹以保存下载的软件包,如图所示。然后点击“下一步”:
- 安装向导将要求您选择要安装 JetPack 软件包的开发板。选择 Jetson TX1,如图所示,然后点击“下一步”:
- 组件管理器窗口将显示,显示哪些软件包将被下载和安装。它将显示 CUDA Toolkit、cuDNN、OpenCV 和 VisionWorks 等软件包,以及操作系统镜像,如图所示:
- 系统将要求您接受许可协议。因此,点击“接受所有”,如图所示,然后点击“下一步”:
- 系统将开始下载软件包,如图所示:
- 当所有软件包都下载并安装完成后,点击“下一步”以在主机上完成安装。它将显示以下窗口:
- 系统将要求您选择网络布局,即如何将板连接到主机 PC。板和主机 PC 连接到同一路由器,因此选择第一个选项,该选项指示设备通过同一路由器或交换机访问互联网,如图所示,然后点击“下一步”:
- 系统将询问用于将板连接到网络的接口。我们必须使用以太网线将路由器连接到板,因此我们将选择 eth0 接口,如图所示:
- 这将完成主机上的安装,并显示将要传输和安装到板上的包的摘要。当您在窗口中点击“下一步”时,它将显示通过微型 USB 到 USB 线将板连接到 PC 以及以强制 USB 恢复模式启动板的步骤。以下窗口显示了这些步骤:
-
要进入强制恢复模式,在按下电源按钮后,按下强制恢复按钮,同时按下并释放重置按钮。然后释放强制恢复按钮。设备将以强制恢复模式启动。
-
在窗口中输入lsusb命令;如果连接正确,它将开始将包传输到设备。如果您使用的是虚拟机,那么您必须从虚拟机的 USB 设置中启用设备。如果尚未选择,请选择 USB 3.0 控制器。在输入lsusb命令后启动的过程如下所示:
- 该过程将在设备上刷新操作系统。这个过程可能需要很长时间,最长可达一小时才能完成。刷新完成后,会要求重置设备以获取ssh的 IP 地址。写下之前记录的 IP 地址,以及默认的用户名和密码,即
ubuntu,然后点击“下一步”。之后的窗口如下所示:
- 点击“下一步”,它将所有包,如 CUDA Toolkit、VisionWorks、OpenCV 和多媒体,推送到设备。以下窗口将显示:
- 在过程完成后,会询问是否要删除在过程中下载的所有包。如果您想删除,则勾选复选框或保持原样,如下面的截图所示:
-
点击“下一步”,安装过程将完成。
-
重启 Jetson TX1 开发板,它将以正常的 Ubuntu OS 启动。您还将观察到已安装的所有包的示例。我们将在下一章中看到如何在板上使用 CUDA 和 OpenCV。
摘要
本章介绍了用于在嵌入式平台上部署计算机视觉和深度学习应用的 Jetson TX1 开发板。它是一个小型信用卡大小的模块,可用于计算密集型应用。它的每瓦性能比最新的 i7 处理器更好。它可用于许多领域,在这些领域中,计算机视觉和深度学习被用于性能提升和嵌入式部署。Nvidia 提供了一套开发套件,其中包含此模块以及其他外围设备,可用于所有应用的快速原型设计。Nvidia 还提供了一套名为 JetPack 的 SDK,它是一系列软件包的集合,例如OpenCV、CUDA和Visionworks。本章详细描述了在 Jetson TX1 上安装 JetPack 的过程。下一章将描述使用 OpenCV 和 CUDA 在 Jetson TX1 上部署计算机视觉应用的过程。
问题
-
使用 Jetson TX1 而不是 Raspberry Pi 的优势是什么?
-
可以与 Jetson TX1 接口连接多少个摄像头?
-
如何连接超过两个 USB 设备到 Jetson TX1?
-
对或错:Jetson TX1 在功耗方面比最新的 i7 处理器有更好的性能。
-
对或错:Jetson TX1 不包含 CPU。
-
当 Jetson TX1 预装了 Ubuntu 操作系统时,JetPack 的安装要求是什么?
795

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



