OpenCV 中的 Graph API 介绍

快速认知

例如 OpenCV 中的 cv::Canny(…),可以改用 G-API 写成 cv::gapi::Canny(…)

为了让图像处理过程更快,并且让代码有更好的可移植性,OpenCV引入了Graph API这个单独的模块。顾名思义,这是一种基于图的计算方式。这里图的概念类似于pytorch中图的概念。

例如,若一张图片的处理过程(pipeline)为:

resize -> 灰度化 -> 模糊处理 -> 边缘检测 -> 结果

没有图之前的做法是一步一步对某张图片进行处理,有了图之后就相当于将这些步骤单独封装成一个方法,需要处理的时候调用该方法即可得到最终结果。那么这和我直接将这些步骤封装成一个方法有什么区别呢?

直接封装成一个方法其本质还是对图像按整个过程一步一步处理

而将其封装成图之后,编译器会对整个pipeline做一系列优化,例如去除某些不必要的步骤或增加一些操作等等。

如何创建图?

# 注意这里引入的是gapi模块
#include <opencv2/highgui.hpp>
#include <opencv2/gapi.hpp>
#include <opencv2/gapi/core.hpp>
#include <opencv2/gapi/imgproc.hpp>

// 构建图,注意数据结构为 GMat
cv::GMat in;
cv::GMat vga      = cv::gapi::resize(in, cv::Size(), 0.5, 0.5);
cv::GMat gray     = cv::gapi::BGR2Gray(vga);
cv::GMat blurred  = cv::gapi::blur(gray, cv::Size(5,5));
cv::GMat out      = cv::gapi::Canny(blurred, 32, 128, 3);
cv::GComputation ac(in, out);

// 使用图,注意数据结构为 Mat
cv::Mat output, input = cv::imread("pic-loction");
ac.apply(input, output); // 编译图并运行
cv::imshow("output", output);

G-API架构

基础架构图:

上述图共分三层:接口层、编译层、实现层

接口层

接口层就是gapi提供的一些操作数据的方法,例如上面代码中的 resize(…)、BGR2Gray(…)、blur(…)、Canny(…),这些方法又被称为核(Kernel)。

被操作的数据可以是上面代码中的 cv::GMat 也可以是 cv::GScalarcv::GArray

G-API已经提供了很多Kernel,可以在 core 和 imgproc 找到。同时,对于一些尚未被实现的原始方法和一些自定义的操作,开发人员也可以自定义Kernel(下文)。

编译层

使用上述接口层的API定义完图之后,可以调用两个方法对图进行编译:

  1. cv::GComputation::apply():隐式调用,例如上面的示例代码。这种调用方式会在图编译完成后立刻运行,相当于:创建一个图 -> 处理一张图片 ->销毁图
  2. cv::GComputation::compile():显式调用,使用该方法会生成一个 cv::GCompiled 对象,该对象就好比一个图封装成了一个方法,后续想要对图像进行处理时可以直接调用该方法。是一个复用的过程。

就像上面说的,使用图进行编译的好处就是编译器能对整个图像处理过程做一些额外的优化,例如数据的验证、处理过程的优化、甚至注入一些额外的代码等等。

实现层

G-API的接口和实现是分开的(参考了面向接口的设计理念)。上述两层相当于定义了图和其中的操作,而这些具体的操作则是由实现层完成的。

这意味着操作的实现可以是多种多样的(这也契合了G-API追求更好移植性的愿景)。可以直接使用原始OpenCV的API去实现,也可以使用例如 Fluid 作为实现(Fluid某些情况下具有更好的性能和更少的内存占用)。

图的执行

要执行图有两种方法

  1. cv::GComputation::apply():上面 编译层 部分已经有了介绍,其实就是将编译和运行封装成了一个方法
  2. cv::GCompiled::operator()():上述 cv::GComputation::compile() 将图完成编译后返回一个 cv::GCompiled 对象,调用该对象的 operator() 方法即可复用该图。

经过上述三层结构,整个图已经完成了定义和实现。且通过上述两个方法就可以运行图,那么图的运行过程是怎样的呢?

事实上,因为图本质还是会调用实现层的方法,例如 若使用opencv作为实现层,则图还是按照opencv方法的调用顺序去执行。对于 fluid 也是一样。

自定义 Kernel

上面说 kernel 其实指的是G-API中对数据的一系列操作方法。若我们想将现有的项目移植到G-API上,则可能遇到很多OpenCV原始的api在G-API中找不到的情况。例如,不管在原始API还是GAPI中我们都能找到Canny这个方法,但却不能在GAPI中找到 HoughCircles 这个方法——一种用于圆形检测的方法。这时候我们就可以自定义一个 GAPI中的 HoughCircles 方法。

自定义Kernel简单分为两步:

  1. 定义接口层:即定义一个GAPI中的接口(例如 HoughCircles ),该步骤很类似定义一个普通的方法
  2. 定义实现层:例如可以直接使用 OpenCV中的 HoughCircles 方法作为实现

具体步骤参见官方文档:https://docs.opencv.org/4.x/d0/d25/gapi_kernel_api.html

总结

G-API 和原始 OpenCV API 的关系类似于 JPA 和 MySQL 的关系,JPA是一个框架,它的实现可以是MySQL或其他的一些数据库。JPA 提供统一的接口对数据库进行操作,好比G-API提供统一的接口对后端(OpenCV、Fluid等)进行操作

G-API的使用和原始OpenCV的API的使用很类似,很多API都是一一对应的,例如Canny()、resize(…)、BGR2Gray(…) 等等。这些API被称为Kernel,我们也可以自定义一些新的Kernel

G-API为了实现更好的性能,引入了图的概念,即将图像处理的pipeline封装成一个可复用的图进行编译和优化

G-API为了实现更好的移植性,使用了三层架构,允许使用不同的后端实现,例如OpenCV、Fluid等

参考

https://docs.opencv.org/4.x/d0/d1e/gapi.html

https://docs.opencv.org/4.x/de/d4d/gapi_hld.html

https://docs.opencv.org/4.x/d0/d25/gapi_kernel_api.html

Leave a Comment