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

G-API 内核 API

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

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

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

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

管道本身可以用 `A`、`B` 等来表示,选择在执行中使用哪个实现成为外部参数。

定义内核

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<>` 的签名,定义了内核的 API;
  3. 内核的唯一名称——在系统中剥离其类型信息时用于识别内核。

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

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

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

内核的返回值*只能*是 G-API 动态类型——cv::GMatcv::GScalar 或 ` cv::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));

是一个完全合法的构造。但是,此示例有一些冗长之处,因此内核声明通常带有 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()的目的是在计算中将元数据信息从输入传播到输出,并推断内部(中间、临时)数据对象的元数据。此信息对于G-API框架在图形编译期间进行的进一步管道优化、内存分配和其他操作是必需的。

实现内核

声明内核后,其接口可用于在不同的后端实现此内核的版本。这个概念自然地从面向对象的编程“接口/实现”习惯用法中推导出来:一个接口可以被多次实现,并且内核的不同实现应该可以互换,而不会破坏算法(管道)逻辑(Liskov替换原则)。

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

然后将内核实现组织到内核包中。内核包作为编译参数传递给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 后端本地内核
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)
{
静态 PointArray2f
expand(cv::GMat 输入图像,
int 最大角点数,
double 质量等级,
double 最小距离,
int 块大小,
double k)
{
cv::GMat 响应 = HarrisResponse::on(输入图像, 质量等级, 块大小, k);
返回 ArrayNMS::on(响应, 最大角点数, 最小距离);
}
};
// 然后将HarrisResponse实现为Fluid内核,将NMSresponse实现为通用(OpenCV)内核
// 作为通用(OpenCV)内核

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