![]() |
OpenCV 4.12.0
开源计算机视觉
|
G-API 背后的核心思想是可移植性 – 使用 G-API 构建的 pipeline 必须是可移植的(或者至少能够被移植)。这意味着,当为新平台编译时,它可以直接运行,或者 G-API 提供必要的工具来使其在那里运行,而算法本身几乎不需要进行任何更改。
这个想法可以通过将内核接口与其实现分离来实现。一旦使用内核接口构建了 pipeline,它就变成了与实现无关的 – 实现细节(即使用哪个内核)在单独的阶段传递(图编译)。
内核实现层次结构可能如下所示
那么 pipeline 本身只能用 A、B 等来表达,并且在执行中选择使用哪个实现就变成了一个外部参数。
G-API 提供了一个宏来定义一个新的内核接口 – G_TYPED_KERNEL()
这个宏是定义新类型的快捷方式。它接受三个参数来注册一个新类型,并且需要类型主体存在(参见下文)。宏参数是
std::function<>-like 签名,它定义了内核的 API;内核声明可以被视为函数声明 – 在这两种情况下,都必须按照定义的方式使用新实体。
内核签名定义了内核的使用语法 – 在图构建期间它需要哪些参数。实现还可以使用此签名将其派生到后端特定的回调签名中(参见下一章)。
内核可以接受任何类型的值,并且 G-API 动态类型以特殊方式处理。所有其他类型对 G-API 都是不透明的,并且按原样传递给内核的 outMeta() 或执行回调中。
内核的返回值只能是 G-API 动态类型 – cv::GMat、cv::GScalar 或 cv::GArray<T>。如果一个操作有多个输出,它应该被包装到一个 std::tuple<> 中(它只能包含提到的 G-API 类型)。不支持任意输出数的操作。
一旦定义了一个内核,它就可以在带有特殊的、G-API 提供的 "::on()" 方法的 pipeline 中使用。此方法具有与内核中定义的相同的签名,因此以下代码
是一个完全合法的构造。不过,这个例子有些冗长,所以通常内核声明带有一个 C++ 函数包装器(“工厂方法”),它可以启用可选参数、更紧凑的语法、Doxygen 注释等。
所以现在它可以像这样使用
在当前版本中,内核声明主体(大括号内的所有内容)必须包含一个静态函数 outMeta()。此函数建立操作的输入和输出元数据之间的函数依赖关系。
元数据是内核操作的数据的信息。由于非 G-API 类型对 G-API 是不透明的,因此 G-API 仅关心 G* 数据描述符(即 cv::GMat 等的维度和格式)。
outMeta() 也是如何将内核签名转换为派生回调的一个示例 – 请注意,在此示例中,outMeta() 签名完全遵循内核签名(在宏中定义),但不同 – 内核期望 cv::GMat,而 outMeta() 接受并返回 cv::GMatDesc(cv::GMat 的 G-API 结构元数据)。
outMeta() 的目的是在计算中传播元数据信息,从输入到输出,并推断内部(中间的、临时的)数据对象的元数据。此信息是进一步 pipeline 优化、内存分配以及 G-API 框架在图编译期间执行的其他操作所必需的。
一旦声明了一个内核,它的接口就可以用于在不同的后端实现此内核的版本。这个概念自然是从面向对象编程的“接口/实现”习语中投影出来的:一个接口可以被多次实现,并且内核的不同实现应该可以相互替换,而不会破坏算法(pipeline)逻辑(里氏替换原则)。
每个后端都定义了自己实现内核接口的方式。尽管如此,这种方式是常规的 – 无论插件是什么,它的内核实现都必须从内核接口类型“派生”。
然后,内核实现被组织成内核包。内核包作为编译参数传递给 cv::GComputation::compile(),并提供一些提示给 G-API,说明如何选择合适的内核(有关这方面的更多信息,请参见“异构性”[待定])。
例如,前面提到的 Filter2D 在“参考”CPU(OpenCV)插件中以这种方式实现(注意 – 这是一个简化的形式,带有不正确的边界处理)
请注意 CPU(OpenCV)插件是如何转换原始内核签名的
GCPUFilter2D::run() 比原始内核签名多接受一个参数。内核开发人员的基本直觉是不要关心 cv::Mat 对象来自哪里,而不是原始的 cv::GMat – 只需遵循插件定义的签名约定即可。G-API 将在执行期间调用此方法并提供所有必要的信息(并按原样转发原始不透明数据)。
有时,内核只是 API 级别上的一个单独的东西。这对于用户来说很方便,但在特定的实现方面,最好有多个内核(一个子图)来完成这件事。一个例子是 goodFeaturesToTrack() – 虽然在 OpenCV 后端它可能仍然是一个单独的内核,但在 Fluid 中它变成了复合的 – Fluid 可以处理 Harris 响应计算,但不能对 STL 向量进行稀疏非极大值抑制和点提取
可以使用通用宏 GAPI_COMPOUND_KERNEL() 定义复合内核实现
区分复合内核与 G-API 高阶函数很重要,G-API 高阶函数是一个看起来像内核的 C++ 函数,但实际上生成了一个子图。核心区别在于,复合内核是一个实现细节,内核实现可以是复合的也可以不是(取决于后端能力),而高阶函数是 G-API 术语中的“宏”,因此不能充当需要由后端实现的接口。