![]() |
OpenCV 4.13.0
开源计算机视觉库 (Open Source Computer Vision)
|
上一个教程: 使用 G-API 进行人脸分析管道
下一个教程: 使用 G-API 实现人脸美化算法
在本教程中你将学习
本教程基于 通过梯度结构张量进行各向异性图像分割。
在开始之前,让我们回顾一下原始算法实现
函数 calcGST() 显然是一个图像处理管道
考虑到以上情况,calcGST() 是一个很好的起点。在原始代码中,它的原型定义如下
使用 G-API,我们可以将其定义如下
重要的是要理解,基于 G-API 的新版本 calcGST() 将只生成一个计算图,与它的原始版本不同,后者实际上计算值。这是一个主要的区别——基于 G-API 的函数用于构建图,而不是处理实际数据。
让我们开始用矩阵 \(J\) 的计算来实现 calcGST()。原始代码如下所示
在这里,我们需要为每个新操作声明输出对象 (参见 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() 函数的其余部分可以以相同的方式简单实现。下面是其完整源代码
在 calcGST() 以 G-API 语言定义后,我们可以基于它构建一个图并最终运行它——传递输入图像并获得结果。在此之前,让我们看看原始代码是什么样子
基于 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 代表 "Graph API",但您在上面的示例中提到任何图了吗?这是最初的设计目标之一——G-API 在设计时考虑了表达式,以使采用和移植过程更直接。人们通常在编写普通代码时不会考虑节点和边,因此 G-API,虽然是 Graph 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 中)应用程序应该崩溃,并显示类似以下消息
OpenCV 4.0 中的 Fluid 后端存在一些限制(有关最新状态,请参阅此 wiki 页面)。特别是,此示例中使用的 Box 滤波器仅支持静态 3x3 内核大小。
我们可以通过避免 G-API 在此示例中使用 Box 滤波器的 Fluid 版本内核来轻松解决此问题。可以通过从我们刚刚创建的内核包中删除适当的内核来实现
现在,这个内核包没有任何 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 仍处于初始阶段——它更多的是未来所有工作的基础,尽管现在已经可以使用。
此外,本教程将扩展新章节,包括自定义内核编程、并行性等等。