OpenCV 4.12.0
开源计算机视觉
加载中...
搜索中...
无匹配项
在 G-API 上移植各向异性图像分割

上一个教程: 使用 G-API 进行人脸分析流水线
下一个教程: 使用 G-API 实现人脸美化算法

介绍

在本教程中,您将学习

  • 如何将现有算法转换为 G-API 计算(图);
  • 如何检查和分析 G-API 图;
  • 如何在不改变代码的情况下自定义图执行。

本教程基于基于梯度结构张量的各向异性图像分割

快速入门:使用 OpenCV 后端

在开始之前,我们先回顾一下原始算法实现

#include <iostream>
using namespace cv;
using namespace std;
void calcGST(const Mat& inputImg, Mat& imgCoherencyOut, Mat& imgOrientationOut, int w);
int main()
{
int W = 52; // 窗口大小为 WxW
double C_Thr = 0.43; // 相关性阈值
int LowThr = 35; // 方向阈值1,范围从 0 到 180
int HighThr = 57; // 方向阈值2,范围从 0 到 180
samples::addSamplesDataSearchSubDirectory("doc/tutorials/imgproc/anisotropic_image_segmentation/images");
Mat imgIn = imread(samples::findFile("gst_input.jpg"), IMREAD_GRAYSCALE);
if (imgIn.empty()) // 检查图像是否加载
{
cout << "ERROR : Image cannot be loaded..!!" << endl;
return -1;
}
Mat imgCoherency, imgOrientation;
calcGST(imgIn, imgCoherency, imgOrientation, W);
Mat imgCoherencyBin;
imgCoherencyBin = imgCoherency > C_Thr;
Mat imgOrientationBin;
inRange(imgOrientation, Scalar(LowThr), Scalar(HighThr), imgOrientationBin);
Mat imgBin;
imgBin = imgCoherencyBin & imgOrientationBin;
normalize(imgCoherency, imgCoherency, 0, 255, NORM_MINMAX, CV_8U);
normalize(imgOrientation, imgOrientation, 0, 255, NORM_MINMAX, CV_8U);
imshow("Original", imgIn);
imshow("Result", 0.5 * (imgIn + imgBin));
imshow("Coherency", imgCoherency);
imshow("Orientation", imgOrientation);
imwrite("result.jpg", 0.5*(imgIn + imgBin));
imwrite("Coherency.jpg", imgCoherency);
imwrite("Orientation.jpg", imgOrientation);
waitKey(0);
return 0;
}
void calcGST(const Mat& inputImg, Mat& imgCoherencyOut, Mat& imgOrientationOut, int w)
{
Mat img;
inputImg.convertTo(img, CV_32F);
// GST分量计算(开始)
// J = (J11 J12; J12 J22) - GST
Mat imgDiffX, imgDiffY, imgDiffXY;
Sobel(img, imgDiffX, CV_32F, 1, 0, 3);
Sobel(img, imgDiffY, CV_32F, 0, 1, 3);
multiply(imgDiffX, imgDiffY, imgDiffXY);
Mat imgDiffXX, imgDiffYY;
multiply(imgDiffX, imgDiffX, imgDiffXX);
multiply(imgDiffY, imgDiffY, imgDiffYY);
Mat J11, J22, J12; // J11, J22 和 J12 是 GST 分量
boxFilter(imgDiffXX, J11, CV_32F, Size(w, w));
boxFilter(imgDiffYY, J22, CV_32F, Size(w, w));
boxFilter(imgDiffXY, J12, CV_32F, Size(w, w));
// GST分量计算(停止)
// 特征值计算(开始)
// lambda1 = 0.5*(J11 + J22 + sqrt((J11-J22)^2 + 4*J12^2))
// lambda2 = 0.5*(J11 + J22 - sqrt((J11-J22)^2 + 4*J12^2))
Mat tmp1, tmp2, tmp3, tmp4;
tmp1 = J11 + J22;
tmp2 = J11 - J22;
multiply(tmp2, tmp2, tmp2);
multiply(J12, J12, tmp3);
sqrt(tmp2 + 4.0 * tmp3, tmp4);
Mat lambda1, lambda2;
lambda1 = tmp1 + tmp4;
lambda1 = 0.5*lambda1; // 最大特征值
lambda2 = tmp1 - tmp4;
lambda2 = 0.5*lambda2; // 最小特征值
// 特征值计算(停止)
// 相干性计算(开始)
// 相关性 = (lambda1 - lambda2)/(lambda1 + lambda2)) - 各向异性度量
// 相关性是各向异性程度(局部方向的一致性)
divide(lambda1 - lambda2, lambda1 + lambda2, imgCoherencyOut);
// 相干性计算(停止)
// 方向角计算(开始)
// tan(2*Alpha) = 2*J12/(J22 - J11)
// Alpha = 0.5 atan2(2*J12/(J22 - J11))
phase(J22 - J11, 2.0*J12, imgOrientationOut, true);
imgOrientationOut = 0.5*imgOrientationOut;
// 方向角计算(停止)
}
n 维密集数组类
定义 mat.hpp:830
cv::getTickFrequency
double getTickFrequency()
void convertTo(OutputArray m, int rtype, double alpha=1, double beta=0) const
使用可选缩放将数组转换为另一种数据类型。
用于指定图像或矩形大小的模板类。
Definition types.hpp:335
CV_8U
#define CV_8U
#define CV_32F
Definition interface.h:78
int main(int argc, char *argv[])
定义 highgui_qt.cpp:3
定义 core.hpp:107
STL 命名空间。

检查 calcGST()

函数 calcGST() 显然是一个图像处理流水线

考虑到以上几点,calcGST() 是一个很好的起点。在原始代码中,它的原型定义如下

void calcGST(const Mat& inputImg, Mat& imgCoherencyOut, Mat& imgOrientationOut, int w);

使用 G-API,我们可以将其定义如下

void calcGST(const cv::GMat& inputImg, cv::GMat& imgCoherencyOut, cv::GMat& imgOrientationOut, int w);

重要的是要理解,新的基于 G-API 的 calcGST() 版本将只生成一个计算图,与它的原始版本(实际计算值)形成对比。这是一个主要区别——像这样的基于 G-API 的函数用于构建图,而不是处理实际数据。

让我们开始实现 calcGST(),从计算 \(J\) 矩阵开始。原始代码如下所示

void calcGST(const Mat& inputImg, Mat& imgCoherencyOut, Mat& imgOrientationOut, int w)
{
Mat img;
inputImg.convertTo(img, CV_32F);
// GST分量计算(开始)
// J = (J11 J12; J12 J22) - GST
Mat imgDiffX, imgDiffY, imgDiffXY;
Sobel(img, imgDiffX, CV_32F, 1, 0, 3);
Sobel(img, imgDiffY, CV_32F, 0, 1, 3);
multiply(imgDiffX, imgDiffY, imgDiffXY);

这里我们需要为每个新操作声明输出对象(例如,img 作为 cv::Mat::convertTo 的结果,imgDiffX 等作为 cv::Sobelcv::multiply 的结果)。

G-API 对应代码如下所示

void calcGST(const cv::GMat& inputImg, cv::GMat& imgCoherencyOut, cv::GMat& imgOrientationOut, int w)
{
auto img = cv::gapi::convertTo(inputImg, CV_32F);
auto imgDiffX = cv::gapi::Sobel(img, CV_32F, 1, 0, 3);
auto imgDiffY = cv::gapi::Sobel(img, CV_32F, 0, 1, 3);
auto imgDiffXY = cv::gapi::mul(imgDiffX, imgDiffY);

这段代码片段展示了 G-API 和传统 OpenCV 之间以下语法差异

  • 所有标准 G-API 函数默认位于 "cv::gapi" 命名空间;
  • G-API 操作返回其结果——无需向函数传递额外的“输出”参数。

注意——这段代码也使用了 auto——像 imgimgDiffX 等中间对象的类型由 C++ 编译器自动推断。在这个例子中,类型由 G-API 操作返回值确定,它们都是 cv::GMat

G-API 标准内核尽量遵循 OpenCV API 约定——因此 cv::gapi::sobel 接受与 cv::Sobel 相同的参数,cv::gapi::mul 遵循 cv::multiply,等等(除了有返回值)。

calcGST() 函数的其余部分可以以相同的方式轻松实现。以下是其完整的源代码

void calcGST(const cv::GMat& inputImg, cv::GMat& imgCoherencyOut, cv::GMat& imgOrientationOut, int w)
{
auto img = cv::gapi::convertTo(inputImg, CV_32F);
auto imgDiffX = cv::gapi::Sobel(img, CV_32F, 1, 0, 3);
auto imgDiffY = cv::gapi::Sobel(img, CV_32F, 0, 1, 3);
auto imgDiffXY = cv::gapi::mul(imgDiffX, imgDiffY);
auto imgDiffXX = cv::gapi::mul(imgDiffX, imgDiffX);
auto imgDiffYY = cv::gapi::mul(imgDiffY, imgDiffY);
auto J11 = cv::gapi::boxFilter(imgDiffXX, CV_32F, cv::Size(w, w));
auto J22 = cv::gapi::boxFilter(imgDiffYY, CV_32F, cv::Size(w, w));
auto J12 = cv::gapi::boxFilter(imgDiffXY, CV_32F, cv::Size(w, w));
auto tmp1 = J11 + J22;
auto tmp2 = J11 - J22;
auto tmp22 = cv::gapi::mul(tmp2, tmp2);
auto tmp3 = cv::gapi::mul(J12, J12);
auto tmp4 = cv::gapi::sqrt(tmp22 + 4.0*tmp3);
auto lambda1 = tmp1 + tmp4;
auto lambda2 = tmp1 - tmp4;
imgCoherencyOut = (lambda1 - lambda2) / (lambda1 + lambda2);
imgOrientationOut = 0.5*cv::gapi::phase(J22 - J11, 2.0*J12, true);
}

运行 G-API 图

在用 G-API 语言定义 calcGST() 后,我们可以基于它构建一个图,并最终运行它——传入输入图像并获取结果。在此之前,我们先看看原始代码是怎样的

Mat imgCoherency, imgOrientation;
calcGST(imgIn, imgCoherency, imgOrientation, W);
Mat imgCoherencyBin;
imgCoherencyBin = imgCoherency > C_Thr;
Mat imgOrientationBin;
inRange(imgOrientation, Scalar(LowThr), Scalar(HighThr), imgOrientationBin);
Mat imgBin;
imgBin = imgCoherencyBin & imgOrientationBin;
normalize(imgCoherency, imgCoherency, 0, 255, NORM_MINMAX, CV_8U);
normalize(imgOrientation, imgOrientation, 0, 255, NORM_MINMAX, CV_8U);
imshow("Original", imgIn);
imshow("Result", 0.5 * (imgIn + imgBin));
imshow("Coherency", imgCoherency);
imshow("Orientation", imgOrientation);
imwrite("result.jpg", 0.5*(imgIn + imgBin));
imwrite("Coherency.jpg", imgCoherency);
imwrite("Orientation.jpg", imgOrientation);
waitKey(0);

基于 G-API 的函数,例如 calcGST(),不能直接应用于输入数据,因为它是一个构建代码,而不是处理代码。为了运行计算,需要创建一个特殊的 cv::GComputation 类对象。这个对象将我们的 G-API 代码(它是 G-API 数据和操作的组合)封装成一个可调用对象,类似于 C++11 的 std::function<>

cv::GComputation 类有许多构造函数可用于定义图。通常,用户需要传递图的边界——输入和输出对象,在这些对象上定义 GComputation。然后 G-API 分析从输出到输入的调用流,并重建指定边界之间的操作图。这听起来可能很复杂,但实际上代码是这样的

// 使用 G-API 计算梯度结构张量并进行后处理以输出
cv::GMat imgCoherency, imgOrientation;
calcGST(in, imgCoherency, imgOrientation, W);
cv::GMat imgCoherencyBin = imgCoherency > C_Thr;
cv::GMat imgOrientationBin = cv::gapi::inRange(imgOrientation, LowThr, HighThr);
cv::GMat imgBin = imgCoherencyBin & imgOrientationBin;
cv::GMat out = cv::gapi::addWeighted(in, 0.5, imgBin, 0.5, 0.0);
// 归一化额外输出
cv::GMat imgCoherencyNorm = cv::gapi::normalize(imgCoherency, 0, 255, cv::NORM_MINMAX);
cv::GMat imgOrientationNorm = cv::gapi::normalize(imgOrientation, 0, 255, cv::NORM_MINMAX);
// 将图捕获到对象 segm 中
cv::GComputation segm(cv::GIn(in), cv::GOut(out, imgCoherencyNorm, imgOrientationNorm));
// 定义输出数据的 cv::Mats
cv::Mat imgOut, imgOutCoherency, imgOutOrientation;
// 运行图
segm.apply(cv::gin(imgIn), cv::gout(imgOut, imgOutCoherency, imgOutOrientation));
cv::imwrite("result.jpg", imgOut);
cv::imwrite("Coherency.jpg", imgOutCoherency);
cv::imwrite("Orientation.jpg", imgOutOrientation);

请注意,这段代码与原始代码略有不同:形成结果图像也是流水线的一部分(通过 cv::gapi::addWeighted 完成)。

这个 G-API 流水线的结果与原始结果位精确匹配(给定相同的输入图像)

使用 G-API 的分割结果

G-API 初始版本:完整列表

以下是各向异性图像分割移植到 G-API 的初始版本的完整列表

#include <iostream>
#include <utility>
#include "opencv2/gapi.hpp"
void calcGST(const cv::GMat& inputImg, cv::GMat& imgCoherencyOut, cv::GMat& imgOrientationOut, int w);
int main()
{
int W = 52; // 窗口大小为 WxW
double C_Thr = 0.43; // 相关性阈值
int LowThr = 35; // 方向阈值1,范围从 0 到 180
int HighThr = 57; // 方向阈值2,范围从 0 到 180
cv::Mat imgIn = cv::imread("input.jpg", cv::IMREAD_GRAYSCALE);
if (imgIn.empty()) // 检查图像是否加载
{
std::cout << "ERROR : Image cannot be loaded..!!" << std::endl;
return -1;
}
// 使用 G-API 计算梯度结构张量并进行后处理以输出
cv::GMat imgCoherency, imgOrientation;
calcGST(in, imgCoherency, imgOrientation, W);
cv::GMat imgCoherencyBin = imgCoherency > C_Thr;
cv::GMat imgOrientationBin = cv::gapi::inRange(imgOrientation, LowThr, HighThr);
cv::GMat imgBin = imgCoherencyBin & imgOrientationBin;
cv::GMat out = cv::gapi::addWeighted(in, 0.5, imgBin, 0.5, 0.0);
// 归一化额外输出
cv::GMat imgCoherencyNorm = cv::gapi::normalize(imgCoherency, 0, 255, cv::NORM_MINMAX);
cv::GMat imgOrientationNorm = cv::gapi::normalize(imgOrientation, 0, 255, cv::NORM_MINMAX);
// 将图捕获到对象 segm 中
cv::GComputation segm(cv::GIn(in), cv::GOut(out, imgCoherencyNorm, imgOrientationNorm));
// 定义输出数据的 cv::Mats
cv::Mat imgOut, imgOutCoherency, imgOutOrientation;
// 运行图
segm.apply(cv::gin(imgIn), cv::gout(imgOut, imgOutCoherency, imgOutOrientation));
cv::imwrite("result.jpg", imgOut);
cv::imwrite("Coherency.jpg", imgOutCoherency);
cv::imwrite("Orientation.jpg", imgOutOrientation);
return 0;
}
void calcGST(const cv::GMat& inputImg, cv::GMat& imgCoherencyOut, cv::GMat& imgOrientationOut, int w)
{
auto img = cv::gapi::convertTo(inputImg, CV_32F);
auto imgDiffX = cv::gapi::Sobel(img, CV_32F, 1, 0, 3);
auto imgDiffY = cv::gapi::Sobel(img, CV_32F, 0, 1, 3);
auto imgDiffXY = cv::gapi::mul(imgDiffX, imgDiffY);
auto imgDiffXX = cv::gapi::mul(imgDiffX, imgDiffX);
auto imgDiffYY = cv::gapi::mul(imgDiffY, imgDiffY);
auto J11 = cv::gapi::boxFilter(imgDiffXX, CV_32F, cv::Size(w, w));
auto J22 = cv::gapi::boxFilter(imgDiffYY, CV_32F, cv::Size(w, w));
auto J12 = cv::gapi::boxFilter(imgDiffXY, CV_32F, cv::Size(w, w));
auto tmp1 = J11 + J22;
auto tmp2 = J11 - J22;
auto tmp22 = cv::gapi::mul(tmp2, tmp2);
auto tmp3 = cv::gapi::mul(J12, J12);
auto tmp4 = cv::gapi::sqrt(tmp22 + 4.0*tmp3);
auto lambda1 = tmp1 + tmp4;
auto lambda2 = tmp1 - tmp4;
imgCoherencyOut = (lambda1 - lambda2) / (lambda1 + lambda2);
imgOrientationOut = 0.5*cv::gapi::phase(J22 - J11, 2.0*J12, true);
}

检查初始版本

在我们的算法的初始工作版本与 G-API 协同工作后,我们可以使用它来检查和学习 G-API 的工作原理。本章涵盖两个方面:理解图结构和内存分析。

理解图结构

G-API 代表“图 API”,但您在上面的示例中提到任何图了吗?这是最初的设计目标之一——G-API 在设计时就考虑了表达式,以使采用和移植过程更直接。人们通常在编写普通代码时不会考虑节点,因此 G-API 虽然是一个图 API,但它不强迫用户这样做。

但是,当定义 cv::GComputation 对象时,图仍然是隐式构建的。检查结果图的外观可能很有用,以检查它是否正确生成以及是否真正代表了我们的算法。了解图的结构以查看其是否存在任何冗余也很有用。

G-API 允许将生成的图转储到 .dot 文件,然后可以使用流行的开源图可视化软件 Graphviz 进行可视化。

为了将我们的图转储到 .dot 文件,在运行应用程序之前将 GRAPH_DUMP_PATH 设置为文件名,例如

$ GRAPH_DUMP_PATH=segm.dot ./bin/example_tutorial_porting_anisotropic_image_segmentation_gapi

现在,可以使用如下 dot 命令可视化此文件

$ dot segm.dot -Tpng -o segm.png

或使用 xdot 交互式查看(有关如何安装这些软件包,请参阅您的发行版/操作系统文档)。

各向异性图像分割图

上图展示了 G-API 内部算法表示的一些有趣方面

  1. G-API 底层图是一个二分图:它由操作节点和数据节点组成,其中数据节点只能连接到操作节点,操作节点只能连接到数据节点,并且同类节点之间从不直接连接。
  2. 图是定向的 - 图中的每条边都有方向。
  3. 图以数据节点类型“开始”和“结束”。
  4. 一个数据节点只能有一个写入器和多个读取器。
  5. 一个操作节点可以有多个输入,但每个输入都必须有一个唯一的端口号(在输入中)。
  6. 一个操作节点可以有多个输出,并且每个输出都必须有一个唯一的端口号(在输出中)。

测量内存占用

让我们测量和比较该算法的两个版本(基于 G-API 和基于 OpenCV)的内存占用。目前,G-API 版本也是基于 OpenCV 的,因为它内部回退到 OpenCV 函数。

在 GNU/Linux 上,应用程序内存占用可以使用 Valgrind 进行分析。在 Debian/Ubuntu 系统上,可以这样安装(假设您有管理员权限)

$ sudo apt-get install valgrind massif-visualizer

安装完成后,我们可以轻松地为我们的两个算法版本收集内存配置文件

$ valgrind --tool=massif --massif-out-file=ocv.out ./bin/example_tutorial_anisotropic_image_segmentation
==6101== Massif, a heap profiler
==6101== Copyright (C) 2003-2015, and GNU GPL'd, by Nicholas Nethercote
==6101== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==6101== Command: ./bin/example_tutorial_anisotropic_image_segmentation
==6101==
==6101==
$ valgrind --tool=massif --massif-out-file=gapi.out ./bin/example_tutorial_porting_anisotropic_image_segmentation_gapi
==6117== Massif, a heap profiler
==6117== Copyright (C) 2003-2015, and GNU GPL'd, by Nicholas Nethercote
==6117== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==6117== Command: ./bin/example_tutorial_porting_anisotropic_image_segmentation_gapi
==6117==
==6117==

完成后,我们可以使用 Massif Visualizer(在上述步骤中安装)检查收集到的配置文件。

以下是原始 OpenCV 版本算法的可视化内存配置文件

内存配置文件:原始各向异性图像分割示例

我们看到,内存随着应用程序的执行而分配,在 calcGST() 函数中达到峰值;然后,随着 calcGST() 完成执行并且所有临时缓冲区被释放,内存占用量下降。Massif 报告我们的峰值内存消耗为 7.6 MiB。

现在我们来看看 G-API 版本的配置文件

内存配置文件:各向异性图像分割示例的 G-API 移植

一旦 G-API 计算创建并开始执行,G-API 会一次性分配所有所需的内存,然后内存配置文件保持平坦,直到程序终止。Massif 报告我们的峰值内存消耗为 11.4 MiB。

读者此时可能会提出一个正确的问题——G-API 那么糟糕吗?那么使用它的理由是什么呢?

希望不是。我们在这里看到内存消耗增加的原因是,执行此图使用的是默认的朴素 OpenCV 后端。此后端主要用于在卸载/进一步优化之前进行快速原型设计和调试算法。

此后端目前尚未利用任何复杂的内存管理策略,因为这不是它的重点。在下一章中,我们将学习 Fluid 后端,并看看相同的 G-API 代码如何在完全不同的模型下运行(并且内存占用量缩小到几千字节)。

后端和内核

本章介绍如何以特殊方式执行 G-API 计算——例如,卸载到另一个设备,或以特殊智能进行调度。G-API 旨在使其图可移植——这意味着一旦以 G-API 术语定义了图,如果我们想在 CPU、GPU 或同时在两个设备上运行它,则无需对其进行任何更改。G-API 高级概述G-API 内核 API 对实现此目标的具体技术细节提供了更多说明。在本章中,我们将利用 G-API Fluid 后端使我们的图在 CPU 上实现缓存高效。

G-API 将后端定义为知道如何运行内核的底层实体。后端可能有(实际上也有)不同的内核 API,用于为该后端编程和集成内核。在这种情况下,内核操作的实现,操作是在顶层 API 定义的(参见G_TYPED_KERNEL() 宏)。

后端是一种了解设备和平台特性并根据这些特性执行其内核的东西。例如,可能有一个 Halide 后端,它允许使用 Halide 语言编写(实现)G-API 操作,然后为 G-API 图中能很好地映射到 Halide 的部分生成功能性 Halide 代码。

使用 Fluid 后端运行图

OpenCV 4.0 捆绑了两个 G-API 后端——我们刚刚使用的默认“OpenCV”后端,以及一个特殊的“Fluid”后端。

Fluid 后端通过实现所谓的“流式”执行模型来重新组织执行,以节省内存并实现近乎完美的缓存局部性。

为了开始使用 Fluid 内核,我们首先需要包含适当的头文件(默认不包含)

#include "opencv2/gapi/fluid/core.hpp" // Fluid Core 内核库
#include "opencv2/gapi/fluid/imgproc.hpp" // Fluid ImgProc 内核库

包含这些头文件后,我们可以形成一个新的内核包并将其指定给 G-API

// 准备内核包并运行图
cv::GKernelPackage fluid_kernels = cv::gapi::combine // 定义自定义内核包:
(cv::gapi::core::fluid::kernels(), // ...使用 Fluid Core 内核
cv::gapi::imgproc::fluid::kernels()); // ...和 Fluid ImgProc 内核

在 G-API 中,内核(或操作实现)是对象。内核被组织成集合,或内核包,由 cv::GKernelPackage 类表示。内核包的主要目的是捕获我们希望在图中使用的内核,并将其作为图编译选项传递

segm.apply(cv::gin(imgIn), // 输入数据向量
cv::gout(imgOut, imgOutCoherency, imgOutOrientation), // 输出数据向量
cv::compile_args(fluid_kernels)); // 要使用的内核包

传统的 OpenCV 在逻辑上划分为模块,每个模块提供一组功能。在 G-API 中,也存在“模块”,它们由特定后端提供的内核包表示。在此示例中,我们将 Fluid 内核包传递给 G-API,以便在我们的图中使用适当的 Fluid 函数。

内核包是可组合的——在上面的示例中,我们获取“Core”和“ImgProc”Fluid 内核包并将它们组合成一个。参见 cv::gapi::combine 的文档参考。

如果在选项中未指定内核包,G-API 将使用包含默认 OpenCV 实现的默认包,因此 G-API 图默认通过 OpenCV 函数执行。OpenCV 后端比任何其他后端提供更广泛的功能覆盖。如果指定了内核包(如本例所示),则会将其与默认包组合。这意味着用户指定的实现将在冲突时替换默认实现。

故障排除和自定义

在上述修改后(在 OpenCV 4.0 中),应用程序应该会崩溃并显示如下消息

$ ./bin/example_tutorial_porting_anisotropic_image_segmentation_gapi_fluid
terminate called after throwing an instance of 'std::logic_error'
what(): .../modules/gapi/src/backends/fluid/gfluidimgproc.cpp:436: Assertion kernelSize.width == 3 && kernelSize.height == 3 in function run failed
Aborted (core dumped)

Fluid 后端在 OpenCV 4.0 中存在一些限制(有关最新状态,请参阅此 wiki 页面)。特别是,此示例中使用的 Box 滤波器仅支持静态 3x3 内核大小。

我们可以通过避免 G-API 在此示例中使用 Fluid 版本的 Box 滤波器内核来轻松解决此问题。可以通过从我们刚刚创建的内核包中删除相应的内核来完成此操作

fluid_kernels.remove<cv::gapi::imgproc::GBoxFilter>(); // 移除不适合的 Fluid Box 滤波器,
// G-API 将在此处回退到 OpenCV。

现在,这个内核包没有 Box 滤波器内核接口的任何实现(指定为模板参数)。如上所述,G-API 现在将回退到 OpenCV 来运行这个内核。更改后的结果代码如下所示

// 准备内核包并运行图
cv::GKernelPackage fluid_kernels = cv::gapi::combine // 定义自定义内核包:
(cv::gapi::core::fluid::kernels(), // ...使用 Fluid Core 内核
cv::gapi::imgproc::fluid::kernels()); // ...和 Fluid ImgProc 内核
fluid_kernels.remove<cv::gapi::imgproc::GBoxFilter>(); // 移除不适合的 Fluid Box 滤波器,
// G-API 将在此处回退到 OpenCV。
segm.apply(cv::gin(imgIn), // 输入数据向量
cv::gout(imgOut, imgOutCoherency, imgOutOrientation), // 输出数据向量
cv::compile_args(fluid_kernels)); // 要使用的内核包

让我们检查切换到 Fluid 后端后此示例的内存配置文件。现在它看起来像这样

内存配置文件:各向异性图像分割示例的 G-API/Fluid 移植

现在工具报告为 4.7MiB——我们只更改了几行代码,而没有修改图本身!这比之前的 G-API 结果提高了约 2.4 倍,比原始 OpenCV 版本提高了约 1.6 倍。

我们再来看看图的内部表示现在是什么样子。将图转储到 .dot 文件将得到如下可视化结果

带有 OpenCV 和 Fluid 内核的各向异性图像分割图

此图在结构上与其前一版本没有差异(在操作和数据对象方面),但布局上的变化(在转储的左侧)很容易注意到。

可视化反映了 G-API 如何处理混合图,也称为异构图。此图中的大多数操作都由 Fluid 后端实现,但 Box 滤波器由 OpenCV 后端执行。可以很容易地看出图是分区的(用矩形表示)。G-API 根据亲和性对连接的操作进行分组,形成子图(或 G-API 术语中的),我们的顶层图成为多个较小子图的组合。每个后端决定如何执行其子图(岛),因此 Fluid 后端尽可能优化内存,而 OpenCV Box 滤波器访问的六个中间缓冲区被完全分配且无法优化掉。

结论

本教程演示了 G-API 是什么及其关键设计概念,如何将算法移植到 G-API,以及之后如何利用图模型的优势。

在 OpenCV 4.0 中,G-API 仍处于起步阶段——它更多是未来所有工作的基础,尽管现在已经可以使用。

此外,本教程将扩展新章节,包括自定义内核编程、并行性等。