OpenCV 4.12.0
开源计算机视觉
加载中...
搜索中...
无匹配项
内核API

G-API Kernel API

G-API 背后的核心思想是可移植性 – 使用 G-API 构建的 pipeline 必须是可移植的(或者至少能够被移植)。这意味着,当为新平台编译时,它可以直接运行,或者 G-API 提供必要的工具来使其在那里运行,而算法本身几乎不需要进行任何更改。

这个想法可以通过将内核接口与其实现分离来实现。一旦使用内核接口构建了 pipeline,它就变成了与实现无关的 – 实现细节(即使用哪个内核)在单独的阶段传递(图编译)。

内核实现层次结构可能如下所示

内核 API/实现层次结构示例

那么 pipeline 本身只能用 AB 等来表达,并且在执行中选择使用哪个实现就变成了一个外部参数。

定义内核

G-API 提供了一个宏来定义一个新的内核接口 – G_TYPED_KERNEL()

#include <opencv2/gapi.hpp>
G_TYPED_KERNEL(GFilter2D,
"org.opencv.imgproc.filters.filter2D")
{
static cv::GMatDesc // outMeta 的返回值类型
outMeta(cv::GMatDesc in , // 输入 GMat 的描述符
int ddepth , // depth 参数
cv::Mat /* coeffs */, // (未使用)
cv::Point /* anchor */, // (未使用)
double /* scale */, // (未使用)
int /* border */, // (未使用)
cv::Scalar /* bvalue */ ) // (未使用)
{
return in.withDepth(ddepth);
}
};

这个宏是定义新类型的快捷方式。它接受三个参数来注册一个新类型,并且需要类型主体存在(参见下文)。宏参数是

  1. 内核接口名称 – 也用作使用此宏定义的新类型的名称;
  2. 内核签名 – 一个 std::function<>-like 签名,它定义了内核的 API;
  3. 内核的唯一名称 – 用于在系统内剥离其类型信息时识别内核。

内核声明可以被视为函数声明 – 在这两种情况下,都必须按照定义的方式使用新实体。

内核签名定义了内核的使用语法 – 在图构建期间它需要哪些参数。实现还可以使用此签名将其派生到后端特定的回调签名中(参见下一章)。

内核可以接受任何类型的值,并且 G-API 动态类型以特殊方式处理。所有其他类型对 G-API 都是不透明的,并且按原样传递给内核的 outMeta() 或执行回调中。

内核的返回值只能是 G-API 动态类型 – cv::GMatcv::GScalarcv::GArray<T>。如果一个操作有多个输出,它应该被包装到一个 std::tuple<> 中(它只能包含提到的 G-API 类型)。不支持任意输出数的操作。

一旦定义了一个内核,它就可以在带有特殊的、G-API 提供的 "::on()" 方法的 pipeline 中使用。此方法具有与内核中定义的相同的签名,因此以下代码

cv::GMat out = GFilter2D::on(/* GMat */ in,
/* int */ -1,
/* Mat */ conv_kernel_mat,
/* Point */ cv::Point(-1,-1),
/* double */ 0.,
/* int */ cv::BORDER_DEFAULT,
/* Scalar */ cv::Scalar(0));

是一个完全合法的构造。不过,这个例子有些冗长,所以通常内核声明带有一个 C++ 函数包装器(“工厂方法”),它可以启用可选参数、更紧凑的语法、Doxygen 注释等。

int ddepth,
cv::Point anchor = cv::Point(-1,-1),
double scale = 0.,
int border = cv::BORDER_DEFAULT,
{
return GFilter2D::on(in, ddepth, k, anchor, scale, border, bval);
}

所以现在它可以像这样使用

cv::GMat out = filter2D(in, -1, conv_kernel_mat);

额外信息

在当前版本中,内核声明主体(大括号内的所有内容)必须包含一个静态函数 outMeta()。此函数建立操作的输入和输出元数据之间的函数依赖关系。

元数据是内核操作的数据的信息。由于非 G-API 类型对 G-API 是不透明的,因此 G-API 仅关心 G* 数据描述符(即 cv::GMat 等的维度和格式)。

outMeta() 也是如何将内核签名转换为派生回调的一个示例 – 请注意,在此示例中,outMeta() 签名完全遵循内核签名(在宏中定义),但不同 – 内核期望 cv::GMat,而 outMeta() 接受并返回 cv::GMatDesccv::GMat 的 G-API 结构元数据)。

outMeta() 的目的是在计算中传播元数据信息,从输入到输出,并推断内部(中间的、临时的)数据对象的元数据。此信息是进一步 pipeline 优化、内存分配以及 G-API 框架在图编译期间执行的其他操作所必需的。

实现内核

一旦声明了一个内核,它的接口就可以用于在不同的后端实现此内核的版本。这个概念自然是从面向对象编程的“接口/实现”习语中投影出来的:一个接口可以被多次实现,并且内核的不同实现应该可以相互替换,而不会破坏算法(pipeline)逻辑(里氏替换原则)。

每个后端都定义了自己实现内核接口的方式。尽管如此,这种方式是常规的 – 无论插件是什么,它的内核实现都必须从内核接口类型“派生”。

然后,内核实现被组织成内核包。内核包作为编译参数传递给 cv::GComputation::compile(),并提供一些提示给 G-API,说明如何选择合适的内核(有关这方面的更多信息,请参见“异构性”[待定])。

例如,前面提到的 Filter2D 在“参考”CPU(OpenCV)插件中以这种方式实现(注意 – 这是一个简化的形式,带有不正确的边界处理)

#include <opencv2/gapi/cpu/gcpukernel.hpp> // GAPI_OCV_KERNEL()
#include <opencv2/imgproc.hpp> // cv::filter2D()
GAPI_OCV_KERNEL(GCPUFilter2D, GFilter2D)
{
static void
run(const cv::Mat &in, // in - 从 GMat 派生
const int ddepth, // 不透明(按原样传递)
const cv::Mat &k, // 不透明(按原样传递)
const cv::Point &anchor, // 不透明(按原样传递)
const double delta, // 不透明(按原样传递)
const int border, // 不透明(按原样传递)
const cv::Scalar &, // 不透明(按原样传递)
cv::Mat &out) // out - 从 GMat 派生(返回值)
{
cv::filter2D(in, out, ddepth, k, anchor, delta, border);
}
};

请注意 CPU(OpenCV)插件是如何转换原始内核签名的

  • 输入 cv::GMat 已被替换为 cv::Mat,它保存了底层 OpenCV 函数调用的实际输入数据;
  • 输出 cv::GMat 已被转换为额外的输出参数,因此 GCPUFilter2D::run() 比原始内核签名多接受一个参数。

内核开发人员的基本直觉是不要关心 cv::Mat 对象来自哪里,而不是原始的 cv::GMat – 只需遵循插件定义的签名约定即可。G-API 将在执行期间调用此方法并提供所有必要的信息(并按原样转发原始不透明数据)。

复合内核

有时,内核只是 API 级别上的一个单独的东西。这对于用户来说很方便,但在特定的实现方面,最好有多个内核(一个子图)来完成这件事。一个例子是 goodFeaturesToTrack() – 虽然在 OpenCV 后端它可能仍然是一个单独的内核,但在 Fluid 中它变成了复合的 – Fluid 可以处理 Harris 响应计算,但不能对 STL 向量进行稀疏非极大值抑制和点提取

可以使用通用宏 GAPI_COMPOUND_KERNEL() 定义复合内核实现

#include <opencv2/gapi/gcompoundkernel.hpp> // GAPI_COMPOUND_KERNEL()
using PointArray2f = cv::GArray<cv::Point2f>;
G_TYPED_KERNEL(HarrisCorners,
<PointArray2f(cv::GMat,int,double,double,int,double)>,
"org.opencv.imgproc.harris_corner")
{
static cv::GArrayDesc outMeta(const cv::GMatDesc &,
int,
double,
double,
int,
double)
{
// G-API 中数组没有特殊的元数据(尚未)
}
};
// 定义形成 GoodFeatures 的 Fluid-backend-local 内核
G_TYPED_KERNEL(HarrisResponse,
<cv::GMat(cv::GMat,double,int,double)>,
"org.opencv.fluid.harris_response")
{
static cv::GMatDesc outMeta(const cv::GMatDesc &in,
double,
int,
double)
{
return in.withType(CV_32F, 1);
}
};
G_TYPED_KERNEL(ArrayNMS,
<PointArray2f(cv::GMat,int,double)>,
"org.opencv.cpu.nms_array")
{
static cv::GArrayDesc outMeta(const cv::GMatDesc &,
int,
double)
{
}
};
GAPI_COMPOUND_KERNEL(GFluidHarrisCorners, HarrisCorners)
{
static PointArray2f
expand(cv::GMat in,
int maxCorners,
double quality,
double minDist,
int blockSize,
double k)
{
cv::GMat response = HarrisResponse::on(in, quality, blockSize, k);
return ArrayNMS::on(response, maxCorners, minDist);
}
};
// 然后将 HarrisResponse 实现为 Fluid 内核,将 NMSresponse 实现为通用(OpenCV)内核
// 作为通用 (OpenCV) 内核

区分复合内核与 G-API 高阶函数很重要,G-API 高阶函数是一个看起来像内核的 C++ 函数,但实际上生成了一个子图。核心区别在于,复合内核是一个实现细节,内核实现可以是复合的也可以不是(取决于后端能力),而高阶函数是 G-API 术语中的“宏”,因此不能充当需要由后端实现的接口。