AI使用 G-API 的人脸分析管道

简介:为什么选择G-API

许多计算机视觉算法在视频流上运行,而不是在单个图像上运行。流处理通常由多个步骤组成,如解码、预处理、检测、跟踪、分类(对检测到的对象)和可视化,形成视频处理管道。此外,此类流水线的许多步骤可以并行运行——现代平台在同一芯片上具有不同的硬件模块,如解码器和 GPU,并且可以插入额外的加速器作为扩展,例如用于深度学习卸载的英特尔® Movidius™ 神经计算模块。

鉴于所有这些选项和视频分析算法的多样性,有效地管理此类管道很快就会成为一个问题。当然,它可以手动完成,但这种方法无法扩展:如果需要对算法进行更改(例如,添加新的流水线步骤),或者将其移植到具有不同功能的新平台上,则需要重新优化整个流水线。

从版本 4.2 开始,OpenCV 为此问题提供了解决方案。OpenCV G-API 现在可以通过传统的计算机视觉以及视频捕获/解码在单个管道中管理深度学习推理(任何现代分析管道的基石)。G-API 负责流水线本身,因此如果算法或平台发生变化,执行模型会自动适应它。

管道概述

我们的示例应用程序基于 OpenVINO™ 工具套件 Open Model Zoo 中的“交互式人脸检测”演示。简化的管道包括以下步骤:

  1. 图像采集与解码;
  2. 预处理检测;
  3. 使用两个网络对每个检测到的物体进行分类和预处理;
  4. 可视化。

构建管道

为视频流案例构建 G-API 图与常规使用 G-API 没有太大区别——它仍然是关于定义图数据使用 cv::GMat、cv::GScalar 和 cv::GArray)及其操作。推理也成为图中的操作,但定义方式略有不同。

声明深度学习拓扑

与传统的 CV 函数(参见 core 和 imgproc)相比,G-API 为每个函数声明不同的操作,而 G-API 中的推理是单个通用操作 cv::gapi::infer<>。像往常一样,它只是一个接口,可以在后台以多种方式实现。在 OpenCV 4.2 中,只有基于 OpenVINO™ 推理引擎的后端可用,OpenCV 自己的基于 DNN 模块的后端即将推出。

cv::gapi::infer<> 由我们将要执行的拓扑的详细信息进行参数化。与操作一样,G-API 中的拓扑是强类型的,并使用特殊的宏 G_API_NET() 进行定义:

人脸检测器:取一个垫子,返回另一个垫子
G_API_NET(Faces, <cv::GMat(cv::GMat)>, “人脸检测器”);
年龄/性别识别 - 取一个垫子,返回两个垫子:
一个用于年龄,一个用于性别。在 G-API 中,多返回值操作
使用 std::tuple<> 定义。
AGInfo = std::tuple<cv::GMat, cv::GMat>;
G_API_NET(AgeGender, <AGInfo(cv::GMat)>, “age-gender-recoginition”);
情绪识别 - 取一个垫子,返回另一个垫子。
G_API_NET(情绪,<cv::GMatcv::GMat)>,“情绪识别”);

与使用 G_API_OP() 定义操作的方式类似,网络描述需要三个参数:

  1. 类型名称。每个定义的拓扑都被声明为一个不同的 C++ 类型,在程序中进一步使用 - 见下文;
  2. 类似 - 的 API 签名。G-API 特征网络作为常规“函数”,用于获取和返回数据。这里 network(检测器)接受一个 cv::GMat 并返回一个 cv::GMat,而已知 network 提供两个输出(分别是年龄和性别 blob)——因此它有一个作为返回类型。std::function<>FacesAgeGenderstd::tuple<>
  3. 拓扑名称 – 可以是任何非空字符串,G-API 使用这些名称来区分内部网络。名称在单个图形的作用域中应该是唯一的。

构建 GComputation

现在上面的流水线用 G-API 表示,如下所示:

cv::GComputation pp([]() {
声明一个空的 GMat - 管道的开头。
cv::GMat 输入;
在输入帧上运行人脸检测。结果是单个 GMat,
内部表示 1x1x200x7 SSD 输出。
这是 infer 的单补丁版本:
- 推理正在对整个输入图像运行;
- 图像被转换并调整为网络的预期格式
自然而然。
cv::GMat 检测 = cv::gapi::infer<custom::Faces>(in);
使用
自定义内核。注意:解析 SSD 可能会成为“标准”内核。
cv::GArray<cv::Rect> faces = custom::P ostProc::on(detections, in);
现在,在每张检测到的人脸上运行年龄/性别模型。该模型有两个
产出(分别针对年龄和性别)。
这里使用了一种特殊的面向 ROI 列表的 infer<>() 形式:
- 第一个输入参数是要处理的矩形列表,
- 第二个是从哪里获取投资回报率的图像;
- 裁剪/调整大小/布局转换会自动为每个图像贴片进行
从列表中
- 推理结果也以列表的形式返回 (GArray<>)
- 由于有两个输出,因此推断<>返回两个数组(通过 std::tuple)。
std::tie(年龄,性别) = cv::gapi::infer<custom::AgeGender>(faces, in);
识别每张脸上的情绪。
这里也使用了面向 ROI 列表的 infer<>()。
由于 custom::Emotions 网络产生单个输出,因此只有一个
GArray<> 在此处返回。
cv::GArray<cv::GMat> emotions = cv::gapi::infer<custom::Emotions>(faces, in);
同时返回解码后的帧作为结果。
输入矩阵不能指定为输出矩阵,因此在此处使用 copy()
(此副本将在未来进行优化)。
现在指定计算的边界 - 我们的管道消耗
一个图像并产生五个输出。
返回 cv::GComputation(cv:GIn(in),
cv::GOut(框架、面孔、年龄、性别、情绪));
});

每个管道都从声明空数据对象开始,这些对象充当管道的输入。然后我们调用一个通用的 cv::gapi::infer<>专门用于检测网络。cv::gapi::infer<> 从其模板参数继承其签名 – 在本例中,它需要一个输入 cv::GMat 并生成一个输出 cv::GMatFaces

在此示例中,我们使用基于 SSD 的预训练网络,其输出需要解析为检测数组(感兴趣的对象区域,ROI)。它是由一个自定义操作完成的,该操作将一个矩形数组(类型)返回给管道。此操作还会通过置信度阈值过滤掉结果,而这些细节隐藏在内核本身中。尽管如此,在图构建的那一刻,我们只使用接口进行操作,不需要实际的内核来表达管道——所以这个后处理的实现将在后面列出。custom::PostProccv::GArray<cv::Rect>

在检测结果输出被解析为对象数组后,我们可以对其中任何一个对象运行分类。G-API 目前还不支持图内循环的语法,但 cv::gapi::infer<> 带有一个特殊的面向列表的重载。for_each()

用户可以调用 cv::gapi::infer<> 将 cv::GArray 作为第一个参数,因此 G-API 假设它需要在给定帧的给定列表(第二个参数)的每个矩形上运行关联的网络。这种操作的结果也是一个列表——cv::GMat 的 cv::GArray

由于网络本身产生两个输出,因此基于列表的 cv::gapi::infer 版本的输出类型是数组的元组。我们习惯于将此输入分解为两个不同的对象。AgeGenderstd::tie()

Emotionsnetwork 生成单个输出,因此其基于列表的推理的返回类型为 。cv::GArray<cv::GMat>

配置管道

G-API 将构造与配置严格分开,其理念是保持算法代码本身与平台无关。在上面的清单中,我们只声明了我们的操作并表达了整体数据流,但甚至没有提到我们使用 OpenVINO™。我们只描述了我们的工作,但没有描述我们如何做。将这两个方面明确分开是 G-API 的设计目标。

编译管道时会出现特定于平台的细节,即从声明形式转变为可执行形式。如何运行东西的方式是通过编译参数指定的,新的推理/流式处理功能也不例外。

G-API 建立在实现接口的后端之上(详见架构内核)——因此 cv::gapi::infer<> 是一个可以由不同后端实现的功能。在 OpenCV 4.2 中,只有用于推理的 OpenVINO™ 推理引擎后端可用。G-API 中的每个推理后端都必须提供一个特殊的可参数化结构来表达特定于后端的神经网络参数——在本例中,它是 cv::gapi::ie::P arams:

cmd.get<std::string>(“fdm”), // 读取 cmd args: 拓扑 IR 的路径
cmd.get<std::string>(“fdw”), // 读取 cmd args: 权重路径
cmd.get<std::string>(“fdd”), // 读取 cmd args: device specifier
};
cmd.get<std::string>(“agem”), // 读取 cmd args: 拓扑 IR 的路径
cmd.get<std::string>(“agew”), // 读取 cmd args: 权重路径
cmd.get<std::string>(“aged”), // 读取 cmd args: device specifier
}.cfgOutputLayers({ “age_conv3”, “prob” });
cmd.get<std::string>(“emom”), // 读取 cmd args: 拓扑 IR 的路径
cmd.get<std::string>(“emow”), // 读取 cmd args: 权重路径
cmd.get<std::string>(“emod”), // 读取 cmd args: device specifier
};

这里我们定义了三个参数对象:、 和 。每个对象都是我们使用的每个特定网络的 cv::gapi::ie::P arams 结构参数化。在编译阶段,G-API 会使用此信息自动将网络参数与其 cv::gapi::infer<> 调用在图形中匹配。det_netage_netemo_net

无论拓扑结构如何,每个参数结构都由三个字符串参数构建,特定于 OpenVINO™ 推理引擎:

  1. 拓扑的中间表示形式(.xml 文件)的路径;
  2. 拓扑模型权重的路径(.bin 文件);
  3. 运行位置的设备 – “CPU”、“GPU” 等 – 取决于您的 OpenVINO™ 工具套件安装。这些参数取自命令行分析器。

定义网络并实现自定义内核后,将编译管道以进行流式处理:

形成一个内核包(使用我们的
后处理)和一个网络包(包含我们的三个网络)。
自动内核 = cv::gapi::kernels<custom::OCVPostProc>();
汽车网络 = cv::gapi::networks(det_net, age_net, emo_net);
编译我们的管道,并将我们的内核和网络作为
参数。这是 G-API 学习的地方
我们实际使用的网络和内核(图
描述本身对此一无所知)。
auto cc = pp.compileStreaming(cv::compile_args(内核,网络));

cv::GComputation::compileStreaming() 触发了一种特殊的面向视频的图形编译形式,其中 G-API 试图优化吞吐量。此编译的结果是一个特殊类型的 cv::GStreamingCompiled 对象 – 与传统的可调用 cv::GCompiled 相比,这些对象在语义上更接近媒体播放器。

注意

无需在 cv::GComputation::compileStreaming() – G-API 中传递描述输入视频流格式的元数据参数,自动计算输入向量的格式,并动态调整管道以适应这些格式。用户仍然可以像使用常规 cv::GComputation::compile() 一样将元数据传递到那里,以便将管道修复为特定的输入格式。

运行管道

流水线优化基于同时处理多个输入视频帧,并行运行流水线的不同步骤。这就是为什么当框架完全控制视频流时,它效果最好。

流式处理 API 背后的想法是,用户为管道指定一个输入源,然后 G-API 自动管理其执行,直到结束或用户中断执行。G-API 从源中提取新的图像数据,并将其传递到管道进行处理。

流源由接口 cv::gapi::wip::IStreamSource 表示。实现此接口的对象可以通过帮助程序函数作为常规输入传递给。在 OpenCV 4.2 中,每个流水线只允许一个流源 - 此要求将在未来放宽。GStreamingCompiledcv::gin()

OpenCV 附带了一个很棒的类 cv::VideoCapture,默认情况下,G-API 附带了一个基于它的流源类 – cv::gapi::wip::GCaptureSource。用户可以实现自己的流媒体源,例如使用 VAAPI 或其他媒体或网络 API。

示例应用程序指定输入源,如下所示:

自动in_src = cv::gapi::wip::make_src<cv::gapi::wip::GCaptureSource>(输入);
cc.setSource(cv::gin(in_src));

请注意,GComputation 可能仍具有多个输入,例如 cv::GMat、cv::GScalar 或 cv::GArray 对象。用户也可以在输入向量中传递各自的主机端类型(cv::Mat、cv:Scalar、std::vector<>),但在流模式下,这些对象将创建“无穷无尽”的常量流。允许混合使用真实视频源流和常量数据流。

运行流水线很简单——只需调用 cv::GStreamingCompiled::start() 并使用阻塞 cv::GStreamingCompiled::p ull() 或非阻塞 cv::GStreamingCompiled::try_pull() 获取数据;重复上述步骤,直到直播结束:

指定数据源后,开始执行
cc.start();
声明我们将从管道接收的数据对象。
cv::Mat框架;捕获的帧本身
std::vector<cv::Rect>面;检测到的人脸数组
标准::矢量<cv::Mat> out_ages;推断年龄数组(每张脸一个斑点)
std::vector<cv::Mat> out_genders;推断的性别数组(每张脸一个斑点)
标准::矢量<cv::Mat> out_emotions;分类情绪数组(每张脸一个斑点)
根据显示选项实施不同的执行策略
以获得最佳性能。
(cc.running()) {
自动out_vector = cv::gout(frame, faces, out_ages, out_genders, out_emotions);
如果 (no_show) {
这纯粹是一种视频处理。无需平衡
具有 UI 渲染功能。使用阻塞 pull() 获取
数据。如果流结束,则中断循环。
if (!cc.pull(std::move(out_vector)))
;
} 否则 if (!cc.try_pull(std::move(out_vector))) {
使用非阻塞 try_pull() 获取数据。
如果没有数据,请让 UI 刷新(并处理按键)
ifcv::waitKey(1) >= 0) 中断;
否则继续;
}
在这一点上,我们肯定有数据(在
阻塞或非阻塞方式)。
帧++;
标签::D rawResults(frame, faces, out_ages, out_genders, out_emotions);
标签::D rawFPS(frame, frames, avg.fps(frames));
if (!no_show) cv::imshow“Out”, frame);
}

上面的代码可能看起来很复杂,但实际上它处理两种模式——带和不带图形用户界面 (GUI):

  • 当示例以“无头”模式运行(设置了选项)时,此代码只是使用阻塞从管道中提取数据,直到它结束。这是性能最高的执行模式。--purepull()
  • 当结果也显示在屏幕上时,窗口系统需要花费一些时间来刷新窗口内容并处理 GUI 事件。在这种情况下,演示会以非阻塞方式拉取数据,直到没有更多可用数据(但它不会标记流的结束——只是意味着新数据尚未准备好),然后才显示最新获得的结果并刷新屏幕。使用此技巧减少在 GUI 中花费的时间可以稍微提高整体性能。try_pull()

与串行模式的比较

该示例还可以在串行模式下运行,以用于参考和基准测试。在本例中,使用常规的 cv::GComputation::compile() 并生成常规的单帧 cv::GCompiled 对象;G-API 中未应用流水线优化;用户有责任从 cv::VideoCapture 对象获取图像帧并将其传递给 G-API。

cv::VideoCapture cap(输入);
cv::Mat in_frame,框架;捕获的帧本身
std::vector<cv::Rect>面;检测到的人脸数组
标准::矢量<cv::Mat> out_ages;推断年龄数组(每张脸一个斑点)
std::vector<cv::Mat> out_genders;推断的性别数组(每张脸一个斑点)
标准::矢量<cv::Mat> out_emotions;分类情绪数组(每张脸一个斑点)
(cap.read(in_frame)) {
pp.apply(cv::gin(in_frame),
cv::gout(框架、面部、out_ages、out_genders、out_emotions),
cv::compile_args(内核、网络));
标签::D rawResults(frame, faces, out_ages, out_genders, out_emotions);
帧++;
如果(帧 == 1u) {
仅在处理第一帧后启动计时器 -- 编译
在这里即时发生
avg.start();
} {
测量所有其他帧的 Measurfe 和绘制 FPS
标签::D rawFPS(frame, frames, avg.fps(frames-1));
}
如果 (!no_show) {
cv::imshow“输出”, 帧);
ifcv::waitKey(1) >= 0) 中断;
}
}

在测试机器(英特尔®酷睿™ i5-6600)上,OpenCV 支持 [Intel® TBB] 支持,检测器网络分配给 CPU,分类器分配给 iGPU,流水线样本的性能比串行样本高出 1.36 倍(因此整体吞吐量增加了 +36%)。

结论

G-API 引入了一种构建和优化混合管道的技术方法。切换到新的执行模型不需要更改使用 G-API 表示的算法代码,只是触发图形的方式不同。

列表:后处理内核

G-API 提供了一种将自定义代码插入管道的简单方法,即使它以流模式运行并处理张量数据。推理结果由多维 cv::Mat 对象表示,因此访问这些对象就像使用常规 DNN 模块一样简单。

本示例中定义和实现基于 OpenCV 的 SSD 后处理内核,如下所示:

SSD 后处理功能——这不是一个网络,而是一个内核。
内核主体是单独声明的,这只是一个接口。
此操作需要两个 Mat(检测和源图像),
并返回 ROI 向量(按默认阈值过滤)。
阈值(或要选择的类)可能成为参数,但由于
这个内核是自定义的,没有多大意义。
G_API_OPPostProc, <cv::GArray<cv::Rect>(cv::GMat, cv::GMat)>, “custom.fd_postproc” ) {
静态 cv::GArrayDesc outMeta(const cv::GMatDesc &, const cv::GMatDesc &) {
G-API 引擎需要此功能才能弄清楚
给定输入参数,输出格式是什么。
由于输出是一个数组(具有特定类型),
没有什么可描述的。
}
};
上述内核的基于 OpenCV 的实现。
GAPI_OCV_KERNEL(OCVPostProc、PostProc) {
静态无效运行(const cv::Mat &in_ssd_result,
const cv::Mat &in_frame,
std::vector<cv::Rect> &out_faces) {
const int MAX_PROPOSALS = 200;
const int OBJECT_SIZE = 7;
const cv::Size upscale = in_frame。大小();
const cv::Rect surface({0,0}, upscale);
out_faces.clear();
const float *data = in_ssd_result。PTR<浮点>();
forint i = 0; i < MAX_PROPOSALS; i++) {
常量浮点数 image_id = data[i * OBJECT_SIZE + 0];批次 ID
const 浮点置信度 = data[i * OBJECT_SIZE + 2];
const float rc_left = data[i * OBJECT_SIZE + 3];
常量浮点数 rc_top = data[i * OBJECT_SIZE + 4];
常量浮点数 rc_right = data[i * OBJECT_SIZE + 5];
常量浮点数 rc_bottom = data[i * OBJECT_SIZE + 6];
if (image_id < 0.f) { // 表示检测结束
;
}
if (confidence < 0.5f) { // 硬编码快照
继续;
}
将浮点坐标转换为绝对图像
帧坐标;按源图像边界进行裁剪。
钢筋混凝土。x = static_cast<int>(rc_left * 高档。宽度);
钢筋混凝土。y = static_cast<int>(rc_top * 高档。高度);
钢筋混凝土。宽度 = static_cast<int>(rc_right * 高档。width) - rc。;
钢筋混凝土。高度 = static_cast<int>(rc_bottom * 高档。高度) - RC。y;
out_faces.push_back(rc & surface);
}
}
};

   在线教程

有需要的小伙伴,可以点击下方链接免费领取或者V扫描下方二维码免费领取🆓

请添加图片描述

人工智能书籍

第一阶段:零基础入门(3-6个月)

新手应首先通过少而精的学习,看到全景图,建立大局观。 通过完成小实验,建立信心,才能避免“从入门到放弃”的尴尬。因此,第一阶段只推荐4本最必要的书(而且这些书到了第二、三阶段也能继续用),入门以后,在后续学习中再“哪里不会补哪里”即可。

第二阶段:基础进阶(3-6个月)

熟读《机器学习算法的数学解析与Python实现》并动手实践后,你已经对机器学习有了基本的了解,不再是小白了。这时可以开始触类旁通,学习热门技术,加强实践水平。在深入学习的同时,也可以探索自己感兴趣的方向,为求职面试打好基础。

第三阶段:工作应用

这一阶段你已经不再需要引导,只需要一些推荐书目。如果你从入门时就确认了未来的工作方向,可以在第二阶段就提前阅读相关入门书籍(对应“商业落地五大方向”中的前两本),然后再“哪里不会补哪里”。

 有需要的小伙伴,可以点击下方链接免费领取或者V扫描下方二维码免费领取🆓

在这里插入图片描述

  • 17
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值