OpenCV  4.10.0
开放源代码计算机视觉
正在加载...
正在搜索...
无匹配项
内核 API

G-API 内核 API

G-API 背后的核心思想是可移植性——使用 G-API 构建的管道必须是可移植的(或至少具备可移植性)。这意味着,它在为新平台编译后即可开箱即用, G-API 提供了必要的工具使其在该平台上运行,而算法本身几乎或无需变更。

这一思想可通过将内核接口与其实现分离来实现。使用内核接口构建管道后,它便与实现无关——具体实现细节(即要使用哪些内核)将传递到单独的阶段(图形编译)。

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

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

然后,管道本身仅可按 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 , // 深度参数
cv::Mat /* 系数 */, // (未使用)
cv::Point /* 锚点 */, // (未使用)
double /* 缩放 */, // (未使用)
int /* border */, // (unused)
cv::Scalar /* bvalue */ ) // (unused)
{
return in.withDepth(ddepth);
}
};
GMat 类表示图形中的图像或张量数据。
定义 gmat.hpp:68
n 维密集数组类
定义 mat.hpp:812
#define G_TYPED_KERNEL(Class,...)
定义 gkernel.hpp:347
定义 gmat.hpp:118
GAPI_WRAP GMatDesc withDepth(int ddepth) const
定义 gmat.hpp:188

此宏是新类型定义的快捷方式。它需要三个参数来注册一个新类型,并且需要类型体(请参阅下方)。宏参数包括:

  1. 内核接口名称——也作为使用此宏定义的新类型名称;
  2. 内核签名——一个std::function<>-like 签名,用于定义内核的 API;
  3. 内核的唯一名称——用于在系统中删除内核的类型信息时标识内核。

内核声明可以看作函数声明——在两种情况下,都必须根据其定义方式使用一个新实体。

内核签名定义内核的使用语法——绘制构造期间需要哪些参数。实现还可以使用此签名将其派生为后端特定回调签名(请参阅下一章)。

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

内核的返回值只能是 G-API 动态类型——cv::GMatcv::GScalarcv::GArray<T>。如果某个操作有多个输出,则应将其包装到一个std::tuple<>中(它仅可以包含上述 G-API 类型)。不支持任意输出数量的操作。

定义内核后,可以使用特殊 G-API 提供的方法 "::on()" 在管道中使用它。此方法具有按内核定义的相同签名,因此此代码

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));
@ BORDER_DEFAULT
与 BORDER_REFLECT_101 相同
Definition base.hpp:277

是一种完全合法的构造。此示例尽管有一些 verbosity,但 kernel 声明通常会带有 C++ 函数包装器(“factory 方法”),此包装器支持可选参数、更简洁的语法、Doxygen 注释等

cv::GMat filter2D(cv::GMat in,
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);

更多信息

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

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

outMeta() 也是 kernel 签名如何转换为派生回调的示例 - 请注意,在此示例中,outMeta() 签名完全遵循在宏中定义的 kernel 签名,但有所不同 - kernel 预期 cv::GMatoutMeta() 取用并返回 cv::GMatDesccv::GMat 的 G-API 结构元数据)。

outMeta() 的目的是在计算中将元数据信息从输入传播到输出,并推断内部(中间、临时)数据对象的元数据。G-API 框架在图编译过程中进行进一步的管道优化、内存分配和其他运算时需要这些信息。

实现内核

声明内核后,便可以使用其接口在不同的后台中实现此内核的版本。此概念自然而然地映射自面向对象编程中的“Interface/Implementation”惯用语:接口可以被多次实现,并且一个内核的不同实现应该可以相互替代而不破坏算法(管道)逻辑(里斯科夫替换原则)。

每个后端都定义了自己的实现内核接口的方式。不过这种方式是规律性的——无论插件是什么,其内核实现都必须“派生”自一个内核接口类型。

然后,内核实现被组织成内核包中。通过将内核包传递给 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);
}
};
#define GAPI_OCV_KERNEL(Name, API)
定义 gcpukernel.hpp:488
void filter2D(InputArray src, OutputArray dst, int ddepth, InputArray kernel, Point anchor=Point(-1,-1), double delta=0, int borderType=BORDER_DEFAULT)
使用内核对图像进行卷积。

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

  • 已用持有所述 OpenCV 底层函数调用的实际输入数据的 cv::Mat 来替代输入 cv::GMat
  • 输出 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 后端本地内核
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)核
cv::GArray<T> 模板类表示图中类 T 的对象列表。
定义 garray.hpp:366
#define GAPI_COMPOUND_KERNEL(Name, API)
定义 gcompoundkernel.hpp:134
#define CV_32F
定义 interface.h:78
GArrayDesc empty_array_desc()
定义 garray.hpp:45
定义 garray.hpp:39
GAPI_WRAP GMatDesc withType(int ddepth, int dchan) const
定义 gmat.hpp:199

将复合核同 G-API 高阶函数区分开来非常重要,后者是一个 C++ 函数,从外观上看像一个核,但实际上会生成一个子图。核心区别在于,复合核是一个实现细节,而核实现可能为复合核或非复合核(取决于后端功能),而高阶函数是一个 G-API 意义上的“宏”,因此无法作为需要后端实现的界面。