开发首个应用

开发场景分析

开发场景1

应用场景

图像分类应用,利用Resnet50对于ImageNet数据集中的JPEG图像进行分类。

场景分析

用户可以从以下几方面入手分析该应用场景需要包含哪些功能,然后根据具体功能从LXHPL的接口库中选择对应的功能接口组织应用。

功能列表

目标

涉及的工具和接口

模型推理

对于图片分类网络,选取开源的Tensorflow或者Pytorch Resnet50分类网络,利用Lyngor神经网络编译工具将该分类网络转换成可以运行在KA200上的离线模型,并基于该离线模型完成模型推理

  1. 使用Lyngor编译器,将Resnet50转换为适配KA200的离线模型,具体操作说明参见【Lyngor用户指南】

  2. 调用lynLoadModel接口完成模型加载

  3. 调用lynExecuteModelAsync完成模型推理

  4. 调用lynUnloadModel完成模型卸载

图像解码

Resnet50网络的输入要求是分辨率为224×224的RGB图像,但是ImageNet数据集中的图像均为JPEG格式,需要先将JPEG图像解码为YUV格式的图像。

  1. 调用lynImageGetInfo接口获取图像信息

  2. 调用lynJpegDecodeAsync接口进行图像解码

图像预处理

此步骤是将图像解码步骤中输出的YUV图像进行裁剪,然后对图像进行缩放和类型转换,输出224×224的RGB图像

  1. 调用lynIpeCreatePicDesc和lynIpeCreateConfigDesc创建图像和配置描述

  2. 调用lynIpeSetCropConfig设置裁剪范围

  3. 调用lynIpeSetC2CConfig设置转换后类型

  4. 调用lynIpeCalOutputPicDesc计算输出图像尺寸

  5. 调用lynMalloc为输出图像分配内存空间

  6. 调用lynIpeSetOutputPicData设置输出图像内存空间地址

  7. 调用lynIpeProcessAsync接口执行图像预处理操作

数据传输

涉及将Client侧的图像数据传输至Server侧,并在Server侧完成模型推理后,将推理结果传输至Client侧

  1. 调用lynMalloc接口分配Server侧内存

  2. 调用lynMemcpy或者lynMemcpyAsync接口完成数据传输

开发场景2

应用场景

目标检测类,利用YoloV3对于视频中存在的目标进行检测并跟踪。

场景分析

用户可以从以下几方面入手分析该应用场景需要包含哪些功能,然后根据具体功能从LXHPL的接口库中选择对应的功能接口组织应用。

功能列表

目标

涉及的工具和接口

视频解封装

此步骤是去掉在线媒体流的网络协议或者离线媒体流的封装格式,生成对应的视频流裸流

  1. 调用lynDemuxOpen接口完成解封装初始化

  2. 调用lynDemuxGetCodecPara接口获取视频信息

  3. 调用lynDemuxReadPacket从媒体流中读取待解码包

  4. 调用lynDemuxFreePacket接口释放包数据内存

视频解码

目的是将解封装之后的压缩码流数据解成YUV图像

  1. 调用lynVdecOpen接口初始化解码器

  2. 调用lynVdecSendPacketAsync发送解封装之后的数据包

  3. 调用lynVdecRecvFrameAsync接收解码后的图像帧

  4. 调用lynVdecClose关闭解码器

图像预处理

与开发场景1类似,将视频解码输出的YUV图像进行裁剪,然后对图像进行缩放和类型转换,输出416×416的RGB图像

模型推理

与开发场景1中类似

数据传输

与开发场景1中类似

推理数据后处理

将模型推理输出的数据进行后处理,计算出目标在原图中的坐标

图像编码

将存在特定目标的YUV图像编码成JPEG并从Server侧回传至Client侧

  1. 调用lynJpegEncodeAsync进行JPEG编码

  2. 调用lynEncGetRemotePacketValidSize获取编码后的JPEG图像的真实大小

创建代码目录

在开发应用前,用户需要自行创建代码目录,存放源文件、编译脚本、测试数据源以及模型文件等,参考以下目录结构。

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 **)&para.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",&para,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));