Mediapipe 在RK3399PRO上的初探(二)(自定义Calculator)
PS:要转载请注明出处,本人版权所有。
PS: 这个只是基于《我自己》的理解,
如果和你的原则及想法相冲突,请谅解,勿喷。
前置说明
本文作为本人csdn blog的主站的备份。(BlogID=104)
环境说明
- Ubuntu 18.04
- gcc version 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04)
- RK3399PRO 板卡
前言
本文有一篇前置文章为《Mediapipe 在RK3399PRO上的初探(一)(编译、运行CPU和GPU Demo, RK OpenglES 填坑,编译bazel)》 https://blog.csdn.net/u011728480/article/details/115838306 ,本文是在前置文章上的一点点探索与发现。
在前置文章中,我们介绍了一些编译和使用Mediapipe的注意事项,也初步跑起来了一点点demo,算是对这个框架有了一个初步的认知。但是前置文章也说了,了解这个框架的意义是替换我们小组现有的框架,而且能够支撑我们的板卡产品。于是我们还需要一个最最最最最最重要的功能就是“Custormer Calculator”,就是自定义计算节点,因为这个框架的核心就是计算节点。下文我们将会讲到这个框架的一些基本概念,这些概念都是来至于官方文档的机翻+我自己的理解。同时,我会给出一个我定义的计算节点的实例,算是给大家一个感性认知。
Mediapipe的一些概念(本小节基本来至于官方文档的机翻+我自己的理解,不感兴趣,请直接看下一小节)
本小节内容,主要参考 https://github.com/google/mediapipe/tree/master/docs/framework_concepts 的几个介绍概念的md文件。
MediaPipe 感觉中文直译为“媒体管道”,为啥会有这个名字呢?因为它把数据+处理组合成一个计算节点,然后把一个一个节点连接起来,用数据来驱动整个核心逻辑。如果大家对Caffe、ncnn类似的框架源码有一点点了解的话,就会觉得他们非常像,但是又不像。像是说,都是通过配置文件定义计算节点逻辑图,然后通过运算,得到我们想得到的逻辑图中节点的数据。不像的话,就是说的是Mediapipe的调度机制了,极大的增加了节点计算的并行功能,而那些框架是按照图节点的上下顺序进行执行的。
上面这段话可能有点抽象,我想表达的就是 Mediapipe 就是把任何一个“操作”都可以变为一个Calculator,因为我们的每一个项目的逻辑抽象出来都是 Calculator0+Data0->Calculator1+Data1->Calculator2+Data2->... ...,然后,Mediapipe 做的是基于这种calculator的调度和执行。这里我举个栗子:
- 人脸图+检测算法=人脸检测结果,这是一个Calculator
- RTSP流+解码模块=解码之后的图片,这是一个Calculator
好了,其他的就不过分的解读了,下面就使用MediaPipe的helloworld example( https://github.com/google/mediapipe/blob/master/mediapipe/examples/desktop/hello_world/hello_world.cc )为例,简单的说说以下几个概念。
Packet
Packet,是mediapipe中的数据单元,它可以接收任意类型的数据。也是mediapipe中的数据流动单元。就是在mediapipe中,我们设计的Graph中,所有的逻辑流动都是通过packet流动来实现的。
实例代码片段:
//MakePacket<std::string>("Hello World!") 创建一个packet,顺带说一句,我不喜欢这里的宏,不利于维护
MP_RETURN_IF_ERROR(graph.AddPacketToInputStream("in", MakePacket<std::string>("Hello World!").At(Timestamp(i))));
//从packet中获取数据
mediapipe::Packet packet;
packet.Get<std::string>();
Graph
Graph是由各个Calculator组成的,可以直接把Calculator理解为数据结构中图的节点。而Graph直接把他当做图就行了。Graph是我们定义的逻辑流程的具体载体,也就是说我们的业务逻辑是什么样子的,那么Graph里面就会有相应的逻辑流程。可以具备多输入输出。
实例代码片段:
CalculatorGraph graph;
Caculator(Node)
上面不是介绍了Graph和Packet嘛,这里的Calculator就是Graph里面的节点,也是处理Packet的具体单元。可以具备多输入输出。
实例代码片段:
//比如mediapipe/calculators/core/pass_through_calculator.h里面的定义,这个Calculator被Helloworld这个例子使用,作用就是把输入的数据直接传递到输出,不做任何处理,类似NOP
class PassThroughCalculator : public CalculatorBase
Stream
Stream 就是 Caculator 之间的连接起来后,形成的一个数据流动路径。
Side packets
Side packets 可以直接理解为一些静态的数据packet,在graph创建之后就不会改变的数据。
... ...
自定义实现Calculator
Talk is cheap, show me the code.
/*
* @Description:
* @Author: Sky
* @Date:
* @LastEditors: Sky
* @LastEditTime:
* @Github:
*/
#include <cstdio>
#include "mediapipe/framework/calculator_graph.h"
#include "mediapipe/framework/port/logging.h"
#include "mediapipe/framework/port/parse_text_proto.h"
#include "mediapipe/framework/port/status.h"
//customer calculator
#include "mediapipe/framework/calculator_framework.h"
#include "mediapipe/framework/port/canonical_errors.h"
class CustomerDataType{
public:
CustomerDataType(int i, float f, bool b, const std::string & str):
val_i(i),
val_f(f),
val_b(b),
s_str(str)
{}
int val_i = 1;
float val_f = 11.f;
bool val_b = true;
std::string s_str = "customer str.";
};
namespace mediapipe {
class MyStringProcessCalculator : public CalculatorBase {
public:
/*
Calculator authors can specify the expected types of inputs and outputs of a calculator in GetContract().
When a graph is initialized, the framework calls a static method to verify if the packet types of the connected inputs and outputs match the information in this specification.
*/
static absl::Status GetContract(CalculatorContract* cc) {
/*
class InputStreamShard;
typedef internal::Collection<InputStreamShard> InputStreamShardSet;
class OutputStreamShard;
typedef internal::Collection<OutputStreamShard> OutputStreamShardSet;
*/
//cc->Inputs().NumEntries() returns the number of input streams
// if (!cc->Inputs().TagMap()->SameAs(*cc->Outputs().TagMap())) {
// return absl::InvalidArgumentError("Input and output streams's TagMap can't be same.");
// }
//set stream
// for (CollectionItemId id = cc->Inputs().BeginId(); id < cc->Inputs().EndId(); ++id) {
// cc->Inputs().Get(id).SetAny();
// cc->Outputs().Get(id).SetSameAs(&cc->Inputs().Get(id));
// }
cc->Inputs().Index(0).SetAny();
cc->Inputs().Index(1).Set<CustomerDataType>();
cc->Outputs().Index(0).SetSameAs(&cc->Inputs().Index(0));
//set stream package
// for (CollectionItemId id = cc->InputSidePackets().BeginId(); id < cc->InputSidePackets().EndId(); ++id) {
// cc->InputSidePackets().Get(id).SetAny();
// }
// cc->InputSidePackets().Index(0).SetAny();
// cc->InputSidePackets().Index(1).Set<CustomerDataType>();//set customer data-type
if (cc->OutputSidePackets().NumEntries() != 0) {
// if (!cc->InputSidePackets().TagMap()->SameAs(*cc->OutputSidePackets().TagMap())) {
// return absl::InvalidArgumentError("Input and output side packets's TagMap can't be same.");
// }
// for (CollectionItemId id = cc->InputSidePackets().BeginId(); id < cc->InputSidePackets().EndId(); ++id) {
// cc->OutputSidePackets().Get(id).SetSameAs(&cc->InputSidePackets().Get(id));
// }
cc->OutputSidePackets().Index(0).SetSameAs(&cc->InputSidePackets().Index(0));
}
return absl::OkStatus();
}
absl::Status Open(CalculatorContext* cc) final {
for (CollectionItemId id = cc->Inputs().BeginId();id < cc->Inputs().EndId(); ++id) {
if (!cc->Inputs().Get(id).Header().IsEmpty()) {
cc->Outputs().Get(id).SetHeader(cc->Inputs().Get(id).Header());
}
}
if (cc->OutputSidePackets().NumEntries() != 0) {
for (CollectionItemId id = cc->InputSidePackets().BeginId(); id < cc->InputSidePackets().EndId(); ++id) {
cc->OutputSidePackets().Get(id).Set(cc->InputSidePackets().Get(id));
}
}
// Sets this packet timestamp offset for Packets going to all outputs.
// If you only want to set the offset for a single output stream then
// use OutputStream::SetOffset() directly.
cc->SetOffset(TimestampDiff(0));
return absl::OkStatus();
}
absl::Status Process(CalculatorContext* cc) final {
if (cc->Inputs().NumEntries() == 0) {
return tool::StatusStop();
}
//get node input data
mediapipe::Packet _data0 = cc->Inputs().Index(0).Value();
mediapipe::Packet _data1 = cc->Inputs().Index(1).Value();
//not safety.
char _tmp_buf[1024];
::memset(_tmp_buf, 0, 1024);
snprintf(_tmp_buf, 1024, _data0.Get<std::string>().c_str(), _data1.Get<CustomerDataType>().val_i, _data1.Get<CustomerDataType>().val_f, _data1.Get<CustomerDataType>().val_b, _data1.Get<CustomerDataType>().s_str.c_str());
std::string _out_data = _tmp_buf;
cc->Outputs().Index(0).AddPacket(MakePacket<std::string>(_out_data).At(cc->InputTimestamp()));
return absl::OkStatus();
}
absl::Status Close(CalculatorContext* cc) final {
return absl::OkStatus();
}
};
REGISTER_CALCULATOR(MyStringProcessCalculator);
}
namespace mediapipe {
absl::Status RunMyGraph() {
// Configures a simple graph, which concatenates 2 PassThroughCalculators.
CalculatorGraphConfig config = ParseTextProtoOrDie<CalculatorGraphConfig>(R"(
input_stream: "in"
input_stream: "customer_in"
output_stream: "out"
node {
calculator: "PassThroughCalculator"
input_stream: "in"
output_stream: "out1"
}
node {
calculator: "MyStringProcessCalculator"
input_stream: "out1"
input_stream: "customer_in"
output_stream: "out2"
}
node {
calculator: "PassThroughCalculator"
input_stream: "out2"
output_stream: "out"
}
)");
LOG(INFO)<<"parse graph cfg-str done ... ...";
CalculatorGraph graph;
MP_RETURN_IF_ERROR(graph.Initialize(config));
LOG(INFO)<<"init graph done ... ...";
ASSIGN_OR_RETURN(OutputStreamPoller poller,
graph.AddOutputStreamPoller("out"));
LOG(INFO)<<"add out-node to output-streampoller done ... ...";
MP_RETURN_IF_ERROR(graph.StartRun({}));
LOG(INFO)<<"start run graph done ... ...";
// Give 10 input packets that contains the same std::string "Hello World!".
for (int i = 0; i < 10; ++i) {
MP_RETURN_IF_ERROR(graph.AddPacketToInputStream(
"in", MakePacket<std::string>("CustomerCalculator: val_i %d, val_f %f, val_b %d, val_str %s").At(Timestamp(i))));
MP_RETURN_IF_ERROR(graph.AddPacketToInputStream(
"customer_in", MakePacket<CustomerDataType>(i, i + 1.f, i%2==0, "s" + std::to_string(i)).At(Timestamp(i))));
}
// Close the input stream "in".
MP_RETURN_IF_ERROR(graph.CloseInputStream("in"));
MP_RETURN_IF_ERROR(graph.CloseInputStream("customer_in"));
mediapipe::Packet packet;
// Get the output packets std::string.
while (poller.Next(&packet)) {
LOG(INFO) << packet.Get<std::string>();
}
LOG(INFO)<<"RunGraph Done";
return graph.WaitUntilDone();
}
} // namespace mediapipe
int main(int argc, char** argv) {
gflags::ParseCommandLineFlags(&argc, &argv, true);
FLAGS_minloglevel = 0;
FLAGS_stderrthreshold = 0;
FLAGS_alsologtostderr = 1;
google::InitGoogleLogging(argv[0]);
LOG(INFO) << "glog init success ... ...";
absl::Status run_status = mediapipe::RunMyGraph();
if (!run_status.ok())
LOG(ERROR) << "Failed to run the graph: " << run_status.message();
google::ShutdownGoogleLogging();
return 0;
}
下面简单介绍这段代码。
自定义Calculator:MyStringProcessCalculator
这里自定义了一个Calculator,主要作用就是传入snprintf的fmt字符串和fmt字符串所需要的数据。所以可以看到有两个输入,一个是string,一个是我自定义的data-type。输出是一个格式化之后的字符串,所以输出是string。
自定义Calculator主要还是实现4个接口,分别是GetContract,Open,Process,Close。其中GetContract是Graph初始化的时候,检查Calculator用的。Open接口是在Graph开始后,对Calculator做一些初始化工作,例如设定一些Calculator初始状态等。Process是实际的Calculator功能。
namespace mediapipe {
class MyStringProcessCalculator : public CalculatorBase {
public:
/*
Calculator authors can specify the expected types of inputs and outputs of a calculator in GetContract().
When a graph is initialized, the framework calls a static method to verify if the packet types of the connected inputs and outputs match the information in this specification.
*/
static absl::Status GetContract(CalculatorContract* cc) {
/*
class InputStreamShard;
typedef internal::Collection<InputStreamShard> InputStreamShardSet;
class OutputStreamShard;
typedef internal::Collection<OutputStreamShard> OutputStreamShardSet;
*/
//cc->Inputs().NumEntries() returns the number of input streams
// if (!cc->Inputs().TagMap()->SameAs(*cc->Outputs().TagMap())) {
// return absl::InvalidArgumentError("Input and output streams's TagMap can't be same.");
// }
//set stream
// for (CollectionItemId id = cc->Inputs().BeginId(); id < cc->Inputs().EndId(); ++id) {
// cc->Inputs().Get(id).SetAny();
// cc->Outputs().Get(id).SetSameAs(&cc->Inputs().Get(id));
// }
cc->Inputs().Index(0).SetAny();
cc->Inputs().Index(1).Set<CustomerDataType>();
cc->Outputs().Index(0).SetSameAs(&cc->Inputs().Index(0));
//set stream package
// for (CollectionItemId id = cc->InputSidePackets().BeginId(); id < cc->InputSidePackets().EndId(); ++id) {
// cc->InputSidePackets().Get(id).SetAny();
// }
// cc->InputSidePackets().Index(0).SetAny();
// cc->InputSidePackets().Index(1).Set<CustomerDataType>();//set customer data-type
if (cc->OutputSidePackets().NumEntries() != 0) {
// if (!cc->InputSidePackets().TagMap()->SameAs(*cc->OutputSidePackets().TagMap())) {
// return absl::InvalidArgumentError("Input and output side packets's TagMap can't be same.");
// }
// for (CollectionItemId id = cc->InputSidePackets().BeginId(); id < cc->InputSidePackets().EndId(); ++id) {
// cc->OutputSidePackets().Get(id).SetSameAs(&cc->InputSidePackets().Get(id));
// }
cc->OutputSidePackets().Index(0).SetSameAs(&cc->InputSidePackets().Index(0));
}
return absl::OkStatus();
}
absl::Status Open(CalculatorContext* cc) final {
for (CollectionItemId id = cc->Inputs().BeginId();id < cc->Inputs().EndId(); ++id) {
if (!cc->Inputs().Get(id).Header().IsEmpty()) {
cc->Outputs().Get(id).SetHeader(cc->Inputs().Get(id).Header());
}
}
if (cc->OutputSidePackets().NumEntries() != 0) {
for (CollectionItemId id = cc->InputSidePackets().BeginId(); id < cc->InputSidePackets().EndId(); ++id) {
cc->OutputSidePackets().Get(id).Set(cc->InputSidePackets().Get(id));
}
}
// Sets this packet timestamp offset for Packets going to all outputs.
// If you only want to set the offset for a single output stream then
// use OutputStream::SetOffset() directly.
cc->SetOffset(TimestampDiff(0));
return absl::OkStatus();
}
//这里是整个Calculator的核心,就是调用snprintf
absl::Status Process(CalculatorContext* cc) final {
if (cc->Inputs().NumEntries() == 0) {
return tool::StatusStop();
}
//get node input data
mediapipe::Packet _data0 = cc->Inputs().Index(0).Value();
mediapipe::Packet _data1 = cc->Inputs().Index(1).Value();
//not safety.
char _tmp_buf[1024];
::memset(_tmp_buf, 0, 1024);
snprintf(_tmp_buf, 1024, _data0.Get<std::string>().c_str(), _data1.Get<CustomerDataType>().val_i, _data1.Get<CustomerDataType>().val_f, _data1.Get<CustomerDataType>().val_b, _data1.Get<CustomerDataType>().s_str.c_str());
std::string _out_data = _tmp_buf;
cc->Outputs().Index(0).AddPacket(MakePacket<std::string>(_out_data).At(cc->InputTimestamp()));
return absl::OkStatus();
}
absl::Status Close(CalculatorContext* cc) final {
return absl::OkStatus();
}
};
REGISTER_CALCULATOR(MyStringProcessCalculator);
}
然后开始编译运行得到结果
编译。
# 注意,这里的--check_visibility=false 为了关闭bazel关于target之间的可见性检查,因为我的Calculator自定义放在我自己的目录的,有一个target对这个目录不可见,编译会报错。
bazel build -c dbg --define MEDIAPIPE_DISABLE_GPU=1 --copt -DMESA_EGL_NO_X11_HEADERS --copt -DEGL_NO_X11 my_target --check_visibility=false --verbose_failures --local_cpu_resources=1
然后运行。得到如下图的结果。

后记
好了,一个超级简单的自定义calculator已经实现了,相信你已经明白了吧。本系列也就此终结吧,以后随缘更新。
打赏、订阅、收藏、丢香蕉、硬币,请关注公众号(攻城狮的搬砖之路)

PS: 请尊重原创,不喜勿喷。
PS: 要转载请注明出处,本人版权所有。
PS: 有问题请留言,看到后我会第一时间回复。
Mediapipe 在RK3399PRO上的初探(二)(自定义Calculator)的更多相关文章
- Mediapipe 在RK3399PRO上的初探(一)(编译、运行CPU和GPU Demo, RK OpenglES 填坑,编译bazel)
PS:要转载请注明出处,本人版权所有. PS: 这个只是基于<我自己>的理解, 如果和你的原则及想法相冲突,请谅解,勿喷. 前置说明 本文作为本人csdn blog的主站的备份.(Bl ...
- egg.js 通过 form 和 ajax 两种方式上传文件并自定义目录和文件名
egg.js 通过 form 和 ajax 两种方式上传文件并自定义目录和文件名 评论:10 · 阅读:8437· 喜欢:0 一.需求 二.CSRF 校验 三.通过 form 表单上传文件 四.通过 ...
- 关于我上传的activiti自定义流程demo的说明
最近又收到了一些询问activiti的问题,其中好几个都是向我索要我上传的这个activiti自定义流程demo的数据库设计. 索要的多了,而我早就把这个库给删掉了,所以我便觉得有必要做一个说明: 我 ...
- 兼容IE8的flash上传框架"uploadify"自定义上传按钮样式的办法
(uploadify版本:3.2.1 ) 因为公司业务的原因,所做的项目需要兼容IE8,因此做的上传插件无奈选择的是基于flash的uploadify. 由于是基于flash的,所以使用过程中,难以给 ...
- git学习------>在CenterOS系统上安装GitLab并自定义域名访问GitLab管理页面
目前就职的公司一直使用SVN作为版本管理,现在打算尝试从SVN迁移到Git.安排我来预言并搭建好相关的环境以及自己尝试使用Git.今天我就尝试在Center OS系统上安装GitLab,现在在此记录一 ...
- h5 录音 自动生成proto Js语句 UglifyJS-- 对你的js做了什么 【原码笔记】-- protobuf.js 与 Long.js 【微信开发】-- 发送模板消息 能编程与会编程 vue2入坑随记(二) -- 自定义动态组件 微信上传图片
得益于前辈的分享,做了一个h5录音的demo.效果图如下: 点击开始录音会先弹出确认框: 首次确认允许后,再次录音不需要再确认,但如果用户点击禁止,则无法录音: 点击发送 将录音内容发送到对话框中.点 ...
- JavaScript初探 二 (了解数据)
JavaScript初探 (二) JavaScript 事件 HTML事件 HTML事件是可以在浏览器或用户做的某些事情 HTML事件的例子: HTML网页完成加载 HTML输入字段被修改 HTML按 ...
- JavaScript-JQ初探实现自定义滚动条
这是一个基本实现思路,如果有新手和我一样没什么事,喜欢瞎研究话,可以参考下. 一.Html <div class="scroll_con"> <div class ...
- ASP.NET MVC之文件上传【二】(九)
前言 上一节我们讲了简单的上传以及需要注意的地方,查相关资料时,感觉上传里面涉及到的内容还是比较多,于是就将上传这一块分为几节来处理,同时后续也会讲到关于做上传时遗漏的C#应该注意的地方,及时进行查漏 ...
随机推荐
- window.onresize绑定事件以及解绑事件
问题描述 在Vue工程中,添加样式,部分需要做到自适应,需要添加resize事件,由于是单页面应用,如果组件初始化的时候绑定事件,在切换页面的时候不去注销事件,如果来回切换,会让resize事件执行多 ...
- 微信小程序:应用生命周期
小程序的生命周期分为应用生命周期和页面生命周期. 应用指的是一个文件,是小程序的入口文件app.js,入口文件最外层方法名称是App,页面的js文件最外层是page,组件的js文件的最外层是compo ...
- python爬虫模拟登录验证码解决方案
[前言]几天研究验证码解决方案有三种吧.第一.手工输入,即保存图片后然后我们手工输入:第二.使用cookie,必须输入密码一次,获取cookie:第三.图像处理+深度学习方案,研究生也做相关课题,就用 ...
- SpringCloud(一):微服务架构概述
1-1. 系统进化理论概述 在系统架构与设计的实践中,经历了两个阶段,一个阶段是早些年常见的集中式系统,一个阶段是近年来流行的分布式系统: 集中式系统: 集中式系统也叫单体应用,就是把所有的程序.功 ...
- 5G组网方案:NSA和SA
目录 5G组网的8个选项 独立组网(SA) 选项1 选项2 选项5 选项6 总结 非独立组网(NSA) 选项3系列 选项3 选项3a 选项3x 选项7系列 选项4系列 选项8 演进路线 5G组网的8个 ...
- 【HTB系列】靶机Querier的渗透测试
出品|MS08067实验室(www.ms08067.com) 本文作者:大方子(Ms08067实验室核心成员) 总结与反思: 1.收集信息要全面 2.用snmp-check检查snmp目标是否开启服务 ...
- PUToast - 使用PopupWindow在Presentation上模拟Toast
PUToast Android10 (API 29) 之前 Toast 组件默认只能展示在主 Display 上,PUToast 通过构造一个 PopupWindoww 在 Presentation ...
- java基础知识 + 常见面试题
准备校招面试之Java篇 一. Java SE 部分 1.1 Java基础 1. 请你解释Object若不重写hashCode()的话,hashCode()如何计算出来的? Object 的 hash ...
- 剑指 Offer 40. 最小的k个数 + 优先队列 + 堆 + 快速排序
剑指 Offer 40. 最小的k个数 Offer_40 题目描述 解法一:排序后取前k个数 /** * 题目描述:输入整数数组 arr ,找出其中最小的 k 个数.例如,输入4.5.1.6.2.7. ...
- Hi3559AV100外接UVC/MJPEG相机实时采图设计(四):VDEC_Send_Stream线程分析
下面随笔将对Hi3559AV100外接UVC/MJPEG相机实现实时采图设计的关键点-VDEC_Send_Stream线程进行分析,一两个星期前我写了有三篇系列随笔,已经实现了项目功能,大家可以参考下 ...