![]() |
OpenCV 4.12.0
开源计算机视觉
|
上一个教程: 使用 G-API 进行人脸分析流水线
下一个教程: 使用 G-API 实现人脸美化算法
在本教程中,您将学习
本教程基于基于梯度结构张量的各向异性图像分割。
在开始之前,我们先回顾一下原始算法实现
函数 calcGST() 显然是一个图像处理流水线
考虑到以上几点,calcGST() 是一个很好的起点。在原始代码中,它的原型定义如下
使用 G-API,我们可以将其定义如下
重要的是要理解,新的基于 G-API 的 calcGST() 版本将只生成一个计算图,与它的原始版本(实际计算值)形成对比。这是一个主要区别——像这样的基于 G-API 的函数用于构建图,而不是处理实际数据。
让我们开始实现 calcGST(),从计算 \(J\) 矩阵开始。原始代码如下所示
这里我们需要为每个新操作声明输出对象(例如,img 作为 cv::Mat::convertTo 的结果,imgDiffX 等作为 cv::Sobel 和 cv::multiply 的结果)。
G-API 对应代码如下所示
这段代码片段展示了 G-API 和传统 OpenCV 之间以下语法差异
注意——这段代码也使用了 auto——像 img、imgDiffX 等中间对象的类型由 C++ 编译器自动推断。在这个例子中,类型由 G-API 操作返回值确定,它们都是 cv::GMat。
G-API 标准内核尽量遵循 OpenCV API 约定——因此 cv::gapi::sobel 接受与 cv::Sobel 相同的参数,cv::gapi::mul 遵循 cv::multiply,等等(除了有返回值)。
calcGST() 函数的其余部分可以以相同的方式轻松实现。以下是其完整的源代码
在用 G-API 语言定义 calcGST() 后,我们可以基于它构建一个图,并最终运行它——传入输入图像并获取结果。在此之前,我们先看看原始代码是怎样的
基于 G-API 的函数,例如 calcGST(),不能直接应用于输入数据,因为它是一个构建代码,而不是处理代码。为了运行计算,需要创建一个特殊的 cv::GComputation 类对象。这个对象将我们的 G-API 代码(它是 G-API 数据和操作的组合)封装成一个可调用对象,类似于 C++11 的 std::function<>。
cv::GComputation 类有许多构造函数可用于定义图。通常,用户需要传递图的边界——输入和输出对象,在这些对象上定义 GComputation。然后 G-API 分析从输出到输入的调用流,并重建指定边界之间的操作图。这听起来可能很复杂,但实际上代码是这样的
请注意,这段代码与原始代码略有不同:形成结果图像也是流水线的一部分(通过 cv::gapi::addWeighted 完成)。
这个 G-API 流水线的结果与原始结果位精确匹配(给定相同的输入图像)
以下是各向异性图像分割移植到 G-API 的初始版本的完整列表
在我们的算法的初始工作版本与 G-API 协同工作后,我们可以使用它来检查和学习 G-API 的工作原理。本章涵盖两个方面:理解图结构和内存分析。
G-API 代表“图 API”,但您在上面的示例中提到任何图了吗?这是最初的设计目标之一——G-API 在设计时就考虑了表达式,以使采用和移植过程更直接。人们通常在编写普通代码时不会考虑节点和边,因此 G-API 虽然是一个图 API,但它不强迫用户这样做。
但是,当定义 cv::GComputation 对象时,图仍然是隐式构建的。检查结果图的外观可能很有用,以检查它是否正确生成以及是否真正代表了我们的算法。了解图的结构以查看其是否存在任何冗余也很有用。
G-API 允许将生成的图转储到 .dot 文件,然后可以使用流行的开源图可视化软件 Graphviz 进行可视化。
为了将我们的图转储到 .dot 文件,在运行应用程序之前将 GRAPH_DUMP_PATH 设置为文件名,例如
$ GRAPH_DUMP_PATH=segm.dot ./bin/example_tutorial_porting_anisotropic_image_segmentation_gapi
现在,可以使用如下 dot 命令可视化此文件
$ dot segm.dot -Tpng -o segm.png
或使用 xdot 交互式查看(有关如何安装这些软件包,请参阅您的发行版/操作系统文档)。
上图展示了 G-API 内部算法表示的一些有趣方面
让我们测量和比较该算法的两个版本(基于 G-API 和基于 OpenCV)的内存占用。目前,G-API 版本也是基于 OpenCV 的,因为它内部回退到 OpenCV 函数。
在 GNU/Linux 上,应用程序内存占用可以使用 Valgrind 进行分析。在 Debian/Ubuntu 系统上,可以这样安装(假设您有管理员权限)
$ sudo apt-get install valgrind massif-visualizer
安装完成后,我们可以轻松地为我们的两个算法版本收集内存配置文件
$ valgrind --tool=massif --massif-out-file=ocv.out ./bin/example_tutorial_anisotropic_image_segmentation ==6101== Massif, a heap profiler ==6101== Copyright (C) 2003-2015, and GNU GPL'd, by Nicholas Nethercote ==6101== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info ==6101== Command: ./bin/example_tutorial_anisotropic_image_segmentation ==6101== ==6101== $ valgrind --tool=massif --massif-out-file=gapi.out ./bin/example_tutorial_porting_anisotropic_image_segmentation_gapi ==6117== Massif, a heap profiler ==6117== Copyright (C) 2003-2015, and GNU GPL'd, by Nicholas Nethercote ==6117== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info ==6117== Command: ./bin/example_tutorial_porting_anisotropic_image_segmentation_gapi ==6117== ==6117==
完成后,我们可以使用 Massif Visualizer(在上述步骤中安装)检查收集到的配置文件。
以下是原始 OpenCV 版本算法的可视化内存配置文件
我们看到,内存随着应用程序的执行而分配,在 calcGST() 函数中达到峰值;然后,随着 calcGST() 完成执行并且所有临时缓冲区被释放,内存占用量下降。Massif 报告我们的峰值内存消耗为 7.6 MiB。
现在我们来看看 G-API 版本的配置文件
一旦 G-API 计算创建并开始执行,G-API 会一次性分配所有所需的内存,然后内存配置文件保持平坦,直到程序终止。Massif 报告我们的峰值内存消耗为 11.4 MiB。
读者此时可能会提出一个正确的问题——G-API 那么糟糕吗?那么使用它的理由是什么呢?
希望不是。我们在这里看到内存消耗增加的原因是,执行此图使用的是默认的朴素 OpenCV 后端。此后端主要用于在卸载/进一步优化之前进行快速原型设计和调试算法。
此后端目前尚未利用任何复杂的内存管理策略,因为这不是它的重点。在下一章中,我们将学习 Fluid 后端,并看看相同的 G-API 代码如何在完全不同的模型下运行(并且内存占用量缩小到几千字节)。
本章介绍如何以特殊方式执行 G-API 计算——例如,卸载到另一个设备,或以特殊智能进行调度。G-API 旨在使其图可移植——这意味着一旦以 G-API 术语定义了图,如果我们想在 CPU、GPU 或同时在两个设备上运行它,则无需对其进行任何更改。G-API 高级概述和G-API 内核 API 对实现此目标的具体技术细节提供了更多说明。在本章中,我们将利用 G-API Fluid 后端使我们的图在 CPU 上实现缓存高效。
G-API 将后端定义为知道如何运行内核的底层实体。后端可能有(实际上也有)不同的内核 API,用于为该后端编程和集成内核。在这种情况下,内核是操作的实现,操作是在顶层 API 定义的(参见G_TYPED_KERNEL() 宏)。
后端是一种了解设备和平台特性并根据这些特性执行其内核的东西。例如,可能有一个 Halide 后端,它允许使用 Halide 语言编写(实现)G-API 操作,然后为 G-API 图中能很好地映射到 Halide 的部分生成功能性 Halide 代码。
OpenCV 4.0 捆绑了两个 G-API 后端——我们刚刚使用的默认“OpenCV”后端,以及一个特殊的“Fluid”后端。
Fluid 后端通过实现所谓的“流式”执行模型来重新组织执行,以节省内存并实现近乎完美的缓存局部性。
为了开始使用 Fluid 内核,我们首先需要包含适当的头文件(默认不包含)
包含这些头文件后,我们可以形成一个新的内核包并将其指定给 G-API
在 G-API 中,内核(或操作实现)是对象。内核被组织成集合,或内核包,由 cv::GKernelPackage 类表示。内核包的主要目的是捕获我们希望在图中使用的内核,并将其作为图编译选项传递
传统的 OpenCV 在逻辑上划分为模块,每个模块提供一组功能。在 G-API 中,也存在“模块”,它们由特定后端提供的内核包表示。在此示例中,我们将 Fluid 内核包传递给 G-API,以便在我们的图中使用适当的 Fluid 函数。
内核包是可组合的——在上面的示例中,我们获取“Core”和“ImgProc”Fluid 内核包并将它们组合成一个。参见 cv::gapi::combine 的文档参考。
如果在选项中未指定内核包,G-API 将使用包含默认 OpenCV 实现的默认包,因此 G-API 图默认通过 OpenCV 函数执行。OpenCV 后端比任何其他后端提供更广泛的功能覆盖。如果指定了内核包(如本例所示),则会将其与默认包组合。这意味着用户指定的实现将在冲突时替换默认实现。
在上述修改后(在 OpenCV 4.0 中),应用程序应该会崩溃并显示如下消息
Fluid 后端在 OpenCV 4.0 中存在一些限制(有关最新状态,请参阅此 wiki 页面)。特别是,此示例中使用的 Box 滤波器仅支持静态 3x3 内核大小。
我们可以通过避免 G-API 在此示例中使用 Fluid 版本的 Box 滤波器内核来轻松解决此问题。可以通过从我们刚刚创建的内核包中删除相应的内核来完成此操作
现在,这个内核包没有 Box 滤波器内核接口的任何实现(指定为模板参数)。如上所述,G-API 现在将回退到 OpenCV 来运行这个内核。更改后的结果代码如下所示
让我们检查切换到 Fluid 后端后此示例的内存配置文件。现在它看起来像这样
现在工具报告为 4.7MiB——我们只更改了几行代码,而没有修改图本身!这比之前的 G-API 结果提高了约 2.4 倍,比原始 OpenCV 版本提高了约 1.6 倍。
我们再来看看图的内部表示现在是什么样子。将图转储到 .dot 文件将得到如下可视化结果
此图在结构上与其前一版本没有差异(在操作和数据对象方面),但布局上的变化(在转储的左侧)很容易注意到。
可视化反映了 G-API 如何处理混合图,也称为异构图。此图中的大多数操作都由 Fluid 后端实现,但 Box 滤波器由 OpenCV 后端执行。可以很容易地看出图是分区的(用矩形表示)。G-API 根据亲和性对连接的操作进行分组,形成子图(或 G-API 术语中的岛),我们的顶层图成为多个较小子图的组合。每个后端决定如何执行其子图(岛),因此 Fluid 后端尽可能优化内存,而 OpenCV Box 滤波器访问的六个中间缓冲区被完全分配且无法优化掉。
本教程演示了 G-API 是什么及其关键设计概念,如何将算法移植到 G-API,以及之后如何利用图模型的优势。
在 OpenCV 4.0 中,G-API 仍处于起步阶段——它更多是未来所有工作的基础,尽管现在已经可以使用。
此外,本教程将扩展新章节,包括自定义内核编程、并行性等。