开发首个应用
开发场景分析
开发场景1
应用场景
图像分类应用,利用Resnet50对于ImageNet数据集中的JPEG图像进行分类。
场景分析
用户可以从以下几方面入手分析该应用场景需要包含哪些功能,然后根据具体功能从LXHPL的接口库中选择对应的功能接口组织应用。
功能列表 |
目标 |
涉及的工具和接口 |
---|---|---|
模型推理 |
对于图片分类网络,选取开源的Tensorflow或者Pytorch Resnet50分类网络,利用Lyngor神经网络编译工具将该分类网络转换成可以运行在KA200上的离线模型,并基于该离线模型完成模型推理 |
|
图像解码 |
Resnet50网络的输入要求是分辨率为224×224的RGB图像,但是ImageNet数据集中的图像均为JPEG格式,需要先将JPEG图像解码为YUV格式的图像。 |
|
图像预处理 |
此步骤是将图像解码步骤中输出的YUV图像进行裁剪,然后对图像进行缩放和类型转换,输出224×224的RGB图像 |
|
数据传输 |
涉及将Client侧的图像数据传输至Server侧,并在Server侧完成模型推理后,将推理结果传输至Client侧 |
|
开发场景2
应用场景
目标检测类,利用YoloV3对于视频中存在的目标进行检测并跟踪。
场景分析
用户可以从以下几方面入手分析该应用场景需要包含哪些功能,然后根据具体功能从LXHPL的接口库中选择对应的功能接口组织应用。
功能列表 |
目标 |
涉及的工具和接口 |
---|---|---|
视频解封装 |
此步骤是去掉在线媒体流的网络协议或者离线媒体流的封装格式,生成对应的视频流裸流 |
|
视频解码 |
目的是将解封装之后的压缩码流数据解成YUV图像 |
|
图像预处理 |
与开发场景1类似,将视频解码输出的YUV图像进行裁剪,然后对图像进行缩放和类型转换,输出416×416的RGB图像 |
– |
模型推理 |
与开发场景1中类似 |
– |
数据传输 |
与开发场景1中类似 |
– |
推理数据后处理 |
将模型推理输出的数据进行后处理,计算出目标在原图中的坐标 |
– |
图像编码 |
将存在特定目标的YUV图像编码成JPEG并从Server侧回传至Client侧 |
|
创建代码目录
在开发应用前,用户需要自行创建代码目录,存放源文件、编译脚本、测试数据源以及模型文件等,参考以下目录结构。
ImageClassification /应用名称
├── build.sh /编译脚本
├── CMakeLists.txt /Cmake文件
├── data /测试数据源
│ └── image11.jpg
├── include /API头文件
│ ├── lyn_context.h
│ ├── lyn_event.h
│ └── lyn_stream.h
├── output /输出路径
│ ├── App
│ └── offline_model
├── src /应用程序源码
│ ├── main.cpp
│ └── process.cpp
└── tf_model /开源神经网络模型
└── Resnet50.pb
开发应用
Runtime资源初始化
Runtime资源申请,单进程+单线程+单Stream场景。
基本原理
用户需要按顺序依次申请下列资源:Context、Stream,确保这些资源可以管理运行时异步任务。
关于Device和Context
Context基于指定设备ID,单个进程中如果只调度一个Device,那么该进程只存在一个Context,所有Runtime资源均基于该Context创建。
用户可以调用lynSetDevice初始化设备,该接口内部会隐式创建Context作为当前进程的默认Context,该接口创建的Context只适用于简单场景,例如本节描述的单进程+单线程+单Stream场景,对于复杂场景,建议用户调用lynCreateContext显式创建Context。
调用lynSetDevice隐式创建的Context,在进程结束前需要且只能调用lynResetDevice接口进行释放。
对于调用lynCreateContext创建的显式Context,只能调用lynDestroyContext接口进行释放。
关于Context和Stream
单个进程中基于一个特定Device只存在一个Context,在该Context下用户可以调用lynCreateStream显式创建Stream,LXHPL不支持隐式创建Stream。
关于单进程、单线程、单Stream场景
单进程:一个用户应用对应一个进程。
单线程:应用程序中默认的主线程,用户没有调用线程创建接口显式创建线程。
单Stream:应用中只调用过一次lynCreateStream创建了一个Stream,在该Stream中下发的异步任务会严格按照顺序执行。
示例代码
用户可以从LynSDK提供的示例代码文件中查看完整样例代码,在示例代码中,调用各接口后都添加了异常判断和处理,以下是关键步骤代码示例,仅供参考,不可以直接拷贝编译。
#include “lyn_context.h”
#include “lyn_stream.h”
//显式创建Context
CHECK_ERR(lynCreateContext(&ctx, iDeviceID));
//显式创建Stream
CHECK_ERR(lynCreateStream(&vdecSendStream));
媒体文件解封装
基本原理
对于网络视频流或者离线视频流文件,在解码之前需要将网络协议或者离线封装格式解封装,得到裸码流(H.264或者H.265流)。
示例代码
用户可以从LynSDK提供的示例代码文件中查看完整样例代码,在示例代码中,调用各接口后都添加了异常判断和处理,以下是关键步骤代码示例,仅供参考,不可以直接拷贝编译。
#include “lyn_context.h”
#include “lyn_stream.h”
#include “lyn_demux.h”
//定义demux句柄
lynDemuxHandle_t hDemux = nullptr;
//定义codec参数结构体
lynCodecPara_t tDemuxPara ;
//定义压缩数据包结构体
lynPacket_t *pDmxPacket = new lynPacket_t();
//初始化解封装模块
CHECK_ERR(lynDemuxOpen(&hDemux, strVideoPath.c_str(), nullptr));
//获取codec参数
CHECK_ERR(lynDemuxGetCodecPara(hDemux, &tDemuxPara));
while (lynDemuxReadPacket(hDemux, pDmxPacket ) == 0) {
//不断发送压缩数据包直至读到EOS
}
数据跨设备传输(C2S)
基本原理
对于解封装得到的压缩码流(H.264或者H.265流),需要将其传输至Server端在KA200提供的硬解码器上进行解码,这就需要调用LXHPL提供的内存管理相关接口实现跨侧数据传输(Client to Server)。
示例代码
以下为关键步骤代码示例,仅供参考,不可以直接拷贝编译。解码模块的编码包数据拷贝由模块内部完成实现,以下代码不能在Sample中看到。
#include “lyn_context.h”
#include “lyn_stream.h”
#include “lyn_demux.h”
#include “lyn_memory.h”
void *buf = nullptr;
if (packet->eos != true) {
//根据packet大小分配Server侧内存
lynMalloc(&buf, packet->size);
//调用内存拷贝接口将packet数据传输至Server侧内存
lynMemcpyAsync(stream, buf, packet->data, packet->size, ClientToServer);
}
视频或图像解码
基本原理
传输至Server侧的压缩码流或待解码图像,需要在硬解码器上进行解码,从压缩码流转换为YUV图像。
示例代码
用户可以从LynSDK提供的示例代码文件中查看完整样例代码,在示例代码中,调用各接口后都添加了异常判断和处理,以下是关键步骤代码示例,仅供参考,不可以直接拷贝编译。
#include “lyn_context.h”
#include “lyn_stream.h”
#include “lyn_demux.h”
#include “lyn_memory.h”
#include “lyn_vdec.h”
//定义视频解码发送Stream和接收Stream
lynStream_t vdecSendStream = nullptr;
lynStream_t vdecRecvStream = nullptr;
//定义解码器句柄和属性结构体
lynVdecHandle_t hVdecode;
lynVdecAttr_t tVdecAttr;
tVdecAttr.codecId = tDemuxPara.codecId;
tVdecAttr.outputFmt = LYN_PIX_FMT_NV12;
tVdecAttr.scale = SCALE_NONE;
CHECK_ERR(lynVdecOpen(&hVdecode, &tVdecAttr));
//分配解码器的输出内存
uint32_t nVdecOutSize = tDemuxPara.width * tDemuxPara.height * 3 / 2;
FramePool<lynFrame_t> framePool(nVdecOutSize, 20);
//创建视频解码发送Stream和接收Stream
lynCreateStream(&vdecSendStream);
lynCreateStream(&vdecRecvStream );
//进入循环发送packet和接收frame
bool bEos = false;
while (!bEos) {
lynPacket_t *pDmxPacket = new lynPacket_t();
if (lynDemuxReadPacket(hDemux, pDmxPacket) != 0) {
bEos = true;
}
pDmxPacket->eos = bEos;
// 向解码器发送
CHECK_ERR(lynVdecSendPacketAsync(vdecSendStream, hVdecode, pDmxPacket));
CHECK_ERR(lynStreamAddAsyncCallback(vdecSendStream, OnDemuxCallback, pDmxPacket));
DECODE_DATA_T *pUserData = new DECODE_DATA_T();
pUserData->pFrameQueue = &frameQueue;
pUserData->pFrame = framePool.Pop();
pUserData->pFrame->size = nVdecOutSize;
pUserData->pFrame->eos = bEos;
//在接收Stream里不断从解码器获取解码后图像
CHECK_ERR(lynVdecRecvFrameAsync(vdecRecvStream, hVdecode, pUserData->pFrame));
CHECK_ERR(lynStreamAddAsyncCallback(vdecRecvStream, OnVdecCallback, pUserData));
}
图像预处理
基本原理
对解码器输出的YUV图像进行裁剪、缩放和颜色空间转换,主要是为了将原图转换成模型推理所要求的图像大小和格式。
示例代码
用户可以从LynSDK提供的示例代码文件中查看完整样例代码,在示例代码中,调用各接口后都添加了异常判断和处理,以下是关键步骤代码示例,仅供参考,不可以直接拷贝编译。
#include “lyn_api/lyn_ipe.h”
lynIpePicDesc_t vIpeInput;
lynIpePicDesc_t vIpeOutput;
lynIpeConfigDesc_t vIpeConfig;
lynIpeCreatePicDesc(&vIpeInput);
CHECK_ERR(lynIpeResetPicDesc(vIpeInput));
lynIpeCreatePicDesc(&vIpeOutput);
CHECK_ERR(lynIpeResetPicDesc(vIpeOutput));
lynIpeCreateConfigDesc(&vIpeConfig);
CHECK_ERR(lynIpeResetConfigDesc(vIpeConfig));
// 设置ipe模块配置
CHECK_ERR(lynIpeSetCropConfig(vIpeConfig, iRoiX, iRoiY, iCropLen, iCropLen));
CHECK_ERR(lynIpeSetResizeConfig(vIpeConfig, iDstLen, iDstLen));
//CHECK_ERR(lynIpeSetC2CConfig(vIpeConfig, LYN_PIX_FMT_BGR24, 0));
// 设置ipe输出端口
uint8_t *nIpeOutdata = nullptr;
CHECK_ERR(lynMalloc((void**)&nIpeOutdata, iIpeOutDataSize));
CHECK_ERR(lynIpeSetOutputPicData(vIpeOutput, nIpeOutdata));
// ipe处理
CHECK_ERR(lynIpeSetInputPicDesc(vIpeInput, attr.outBuf.data, info.output.width, info.output.height, info.output.outputFmt));
CHECK_ERR(lynIpeCalOutputPicDesc(vIpeOutput, vIpeInput, vIpeConfig, 0));
CHECK_ERR(lynIpeProcessAsync(picDecStream, vIpeInput, vIpeOutput, vIpeConfig));
lynIpeDestroyPicDesc(vIpeInput);
lynIpeDestroyPicDesc(vIpeOutput);
lynIpeDestroyConfigDesc(vIpeConfig);
模型推理
基本原理
将用户提供的标准RGB数据或者经过IPE单元处理得到的RGB数据送入神经网络模型,并完成分类或者检测等推理过程。
示例代码
用户可以从LynSDK提供的示例代码文件中查看完整样例代码,在示例代码中,调用各接口后都添加了异常判断和处理,以下是关键步骤代码示例,仅供参考,不可以直接拷贝编译。
#include “lyn_infer.h”
//定义神经网络模型信息描述结构体和模型句柄
lynModelDesc_t *pModelDesc = nullptr;
lynModel_t hModel = 0;
CHECK_ERR(lynLoadModel(strModelPath.c_str(), &hModel));
CHECK_ERR(lynModelGetDesc(hModel, &pModelDesc));
int iModelWidth = pModelDesc->inputTensorAttrArray->dims[2];
int iModelHeight = pModelDesc->inputTensorAttrArray->dims[1];
//定义神经网络模型输入输出Tensor阵列(面向批处理)
lynModelTensorAttr_t inputTensorAttrArray;
lynModelTensorAttr_t outputTensorAttrArray;
//分配apu的输出内存
int iApuOutSize = pModelDesc->outputDataLen;
char *pDevApuOutData = nullptr;
CHECK_ERR(lynMalloc((void **)&pDevApuOutData, iApuOutSize));
//如果模型支持动态分辨率
CHECK_ERR(lynModelSetDynamicHWSizeAsync(hStream, hModel, height, width));
CHECK_ERR(lynModelSetDynamicBatchSizeAsync(hStream, hModel, batch));
CHECK_ERR(lynExecuteModelAsync(hStream, hModel, pIpeBufOut, pDevApuOutData, 1));
数据后处理
基本原理
用户输入的图像数据在Server侧推理结束后,用户需要对推理数据进行后处理。用户可以选择将推理数据拷贝到Client侧使用Client侧的CPU自定义后处理程序处理对应结果。用户也可以使用arm_plugin模块自定义插件程序,通过SDL提供的Plugin管理接口调用ARM资源进行数据的后处理。具体接口说明参见《LynARMplugin开发指南》。
示例代码
用户可以从LynSDK提供的示例代码文件中查看完整样例代码,以下是关键步骤代码示例,仅供参考,不可以直接拷贝编译。
#include "lyn_infer.h"
lynPrugin_t plugin;
std::string m_pluginPath = ArgsParse::argPluginPath;
CHECK_ERR(lynPluginRegister(&plugin,m_pluginPath.c_str()));
lynPostProcessPara para;
para.imgW = vdecOutInfo.width;
para.imgH = vdecOutInfo.height;
para.modelW = yolov5Info.width;
para.modelH = yolov5Info.height;
para.modelClassNum = yolov5Info.classNum;
CHECK_ERR(lynMalloc((void **)¶.boxesInfo,sizeof(lynBoxesInfo)));
...
//使用apu进行推理
CHECK_ERR(lynRecordEvent(ipeStream, ipeEvent));
CHECK_ERR(lynStreamWaitEvent(apuStream, ipeEvent));
CHECK_ERR( lynExecuteModelAsync(apuStream, yolov5Info.model, pIpeOutBuf->Buffer(), apuBuffer, yolov5Info.batchSize));
CHECK_ERR(lynRecordEvent(apuStream,apuEvent));
CHECK_ERR(lynStreamWaitEvent(postStream,apuEvent));
para.apuData = apuBuffer;
CHECK_ERR(lynPluginRunAsync(postStream,plugin,"lynPostProcess",¶,sizeof(para)));
视频或图像编码
基本原理
Server侧的frame图像数据,通过sendFrame发送至硬件编码器进行编码,发送recvPacket指令来接收编码输出的Packet包。对于Client侧的数据,需用户编码前拷贝至Server侧,编码调用后再将Packet包拷贝至Client侧。
示例代码
用户可以从LynSDK提供的示例代码文件中查看完整样例代码,以下是关键步骤代码示例,仅供参考,不可以直接拷贝编译。
#include “lyn_context.h”
#include “lyn_stream.h”
#include “lyn_venc.h”
bool bEos = false;
while(!bEos){
// 视频编码
lynFrame_t *pFrameSend;
blockQueue.take(pFrameSend);
if (pFrameSend->eos) {
bEos = true;
std::cout<<" the last frame "<<std::endl;
}
// 发送编码流
SEND_ENCODE_DATA_T *pSendEncData = new SEND_ENCODE_DATA_T();
pSendEncData->pFramePool = &vencSendFramePool;
pSendEncData->data = pFrameSend->data;
CHECK_ERR(lynVencSendFrameAsync(vencSendStream, hVencode, pFrameSend));
CHECK_ERR(lynStreamAddAsyncCallback(vencSendStream, OnSendEncodeCallBack, pSendEncData));
// 发送接收编码流
RECV_ENCODE_DATA_T *pEncodeData = new RECV_ENCODE_DATA_T();
pEncodeData->strDstFile = strVideoOutPath;
pEncodeData->pPacket = vencRecvFramePool.Pop();
pEncodeData->pPacket->eos = bEos;
pEncodeData->pPacketPool = &vencRecvFramePool;
CHECK_ERR(lynVencRecvPacketAsync(vencRecvStream, hVencode, pEncodeData->pPacket));
CHECK_ERR(lynStreamAddAsyncCallback(vencRecvStream, OnRecvEncodeCallback, pEncodeData));
}
数据跨设备传输(S2C)
基本原理
对于解码器输出的图像、IPE单元输出的图像、模型推理的输出或者编码器的输出图像和视频,需要在Client侧进行存储、展示或者后处理的,与 数据跨设备传输(C2S) 中描述的类似,需要调用LXHPL提供的内存管理相关接口将相关数据拷贝回Client侧。
示例代码
用户可以从LynSDK提供的示例代码中查看完整样例代码,在示例代码中,调用各接口后都添加了异常判断和处理,以下是关键步骤代码示例,仅供参考,不可以直接拷贝编译。
#include “lyn_context.h”
#include “lyn_stream.h”
#include “lyn_demux.h”
#include “lyn_memory.h”
//此示例展示在APU执行完模型推理后将推理结果拷贝回Client侧
lynExecuteModelAsync(apu_stream, model, ipeOut[swap]->Buffer(), apuOut, ipeOut[swap]->ElementCount()));
//在APU Stream中插入唤醒Event
lynRecordEvent(apu_stream, apu_event);
//分配Client侧内存
void *hostPtr = malloc(size);
//同步等待直至该Event被触发,该接口会阻塞当前线程执行
lynEventSynchronize(apu_event);
//同步等待返回,意味着APU推理真正结束
//调用内存拷贝接口将APU推理输出拷贝至Client侧,然后释放Server侧内存
lynMemcpy(hostPtr, apuOut, size, ServerToClient);
lynFree(devPtr);
Runtime资源释放
基本原理
Client侧和Server侧所有计算任务结束之后,需要释放Runtime资源,包括Stream、Context、Device(仅限于调用过lynSetDevice接口)。释放资源时,需要按顺序释放,先释放Stream,再释放Context,最后复位Device(仅限于调用过lynSetDevice,这种场景需要在最后调用lynResetDevice销毁默认Context)。
Stream必须显式释放,即调用lynDestroyStream来销毁。
显式创建的Context,必须通过lynDestroyContext来销毁
隐式创建的Context,必须通过lynResetDevice来销毁。
示例代码
用户可以从LynSDK提供的示例代码文件中查看完整样例代码,在示例代码中,调用各接口后都添加了异常判断和处理,以下是关键步骤代码示例,仅供参考,不可以直接拷贝编译。
#include “lyn_context.h”
#include “lyn_stream.h”
//此示例展示的都是显式销毁过程
CHECK_ERR(lynSynchronizeStream(vencSendStream));
CHECK_ERR(lynSynchronizeStream(vencRecvStream));
CHECK_ERR(lynDestroyStream(vencSendStream));
CHECK_ERR(lynDestroyStream(vencRecvStream));
CHECK_ERR(lynVencClose(hVencode));
CHECK_ERR(lynDestroyContext(ctx));