OpenCV 4.11.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 << "错误:无法加载图像!!" << endl;
return -1; -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("原始图像", imgIn);
imshow("结果", 0.5 * (imgIn + imgBin));
imshow("相干性", imgCoherency);
imshow("方向", imgOrientation);
imwrite("result.jpg", 0.5*(imgIn + imgBin));
imwrite("Coherency.jpg", imgCoherency);
imwrite("Orientation.jpg", imgOrientation);
waitKey(0);
return -1; 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:829
bool empty() const
如果数组没有元素,则返回true。
void convertTo(OutputArray m, int rtype, double alpha=1, double beta=0) const
将数组转换为另一种数据类型,并可以选择缩放。
用于指定图像或矩形大小的模板类。
定义 types.hpp:335
#define CV_8U
定义 interface.h:73
#define CV_32F
定义 interface.h:78
int main(int argc, char *argv[])
定义 highgui_qt.cpp:3
定义 core.hpp:107
STL命名空间。

检查calcGST()

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

  • 它只是一系列针对多个cv::Mat的操作;
  • 代码中不涉及逻辑(条件语句)和循环;
  • 所有函数都作用于二维图像(例如cv::Sobelcv::multiplycv::boxFilter,cv::sqrt等)。

考虑到以上几点,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的函数用于构建图,而不是处理实际数据。

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

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);

在这里,我们需要为每个新的操作声明输出对象(参见cv::Mat::convertTo的结果img,以及cv::Sobelcv::multiply的结果imgDiffX和其他结果)。

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`——中间对象的类型,如`img`、`imgDiffX`等,由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);

像 calcGST() 这样的基于 G-API 的函数不能直接应用于输入数据,因为它是一个构造代码,而不是处理代码。为了运行计算,需要创建一个 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 << "错误:图像无法加载!.." << std::endl;
return -1; -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 -1; 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代码。

使用Fluid后端运行图

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

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

为了开始使用Fluid内核,我们首先需要包含相应的头文件(默认情况下不包含):

#include "opencv2/gapi/fluid/core.hpp" // Fluid核心内核库
#include "opencv2/gapi/fluid/imgproc.hpp" // Fluid图像处理内核库

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

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

在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函数。

内核包是可组合的——在上面的例子中,我们获取“核心”和“图像处理”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)

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

我们可以通过避免在该示例中使用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核心内核
cv::gapi::imgproc::fluid::kernels()); // ...和Fluid图像处理内核
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仍处于起步阶段——它更多的是未来所有工作的基础,尽管现在已经可以使用了。

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