文章目录
TIOVX
TIOVX(TI OpenVX)是TI针对OpenVX标准的实施,主要在TDA4上使用
TDA4是TI基于深度学习技术的 L2/L3 近场分析系统的下一代 SoC 系列
1. OpenVX
OpenVX是Khronos Group为实现计算机视觉应用程序的跨平台加速而提出的一种开放的、免版税的标准。
1.1 简介
OpenVX标准中提供了一系列的图像处理的API函数和运行框架,不同的芯片厂如TI,Intel,Nvida等针对这个标准,提供了适应于自己平台的底层驱动和硬件加速。
特点:
- 高度抽象的API
- 跨架构的可移植性
- 低功耗视觉加速
1.2 相关资源
2. TIOVX详解
2.1 Overview
TIOVX Framework包含了官方OpenVX的标准API和TI扩展的API,其中
- public: Context, Parameter, Kernel, Node, Graph Array, Image, Scalar, Pyramid, ObjectArray
- TI: Target, Target Kernel, Obj Desc
TIOVX Platform提供了特定硬件(如TDAx, AM65x)的操作系统(如TI-rtos, linux)调用API。
TIOVX Kernel Wrapper提供了由硬件模块VPAC(Vision Pre-processing Accelerator)和DMPAC(Depth and Motion Perception Accelerator)封装成的Kernel,用户也可用Wrapper将自定义的算法(如OpenCV算法, DSP算法)封装成Kernel。
2.2 Framework API
标准API
Framework Object(框架对象)
-
Context(上下文):OpenVX context是OpenVX所有对象运行的对象域。所有数据对象以及所有框架对象都在context中运行。
-
Kernel(核):OpenVX中的内核是计算机视觉功能的抽象表示。
-
Parameter(参数):传递给计算机视觉功能的抽象输入或输出数据对象。该对象包含内核描述中该参数使用的签名。
-
Node(节点):节点是内核的一个实例,它将与一组特定的引用(参数)配对。节点仅从单个图创建并与单个图相关联。
-
Graph(图):一组以有向(仅单向)非循环(不循环)方式连接的节点。图可能具有与同一图中的其他节点集未连接的节点集。
Data Object(数据对象):
数据对象是由节点中的图形处理的对象。
- Array(数组):一个不透明的数组对象,可以是原始数据类型数组或结构数组。
- Convolution(卷积):一个不透明的对象,包含米× Nvx_int16值矩阵。还包含用于归一化的缩放因子。专门与vxuConvolve和 一起使用vxConvolveNode。
- Delay(延迟):一个不透明的对象,包含手动控制的、时间延迟的对象列表。
- Distribution(分布):包含频率分布(例如,直方图)的不透明对象。
- Image(图像):一个不透明的图像对象,可能是vx_df_image_e.
- LUT(查找表):与vxTableLookupNode和 一起使用的不透明查找表对象vxuTableLookup。
- Matrix(矩阵):一个不透明的对象,包含米× N 一些标量值的矩阵。
- Pyramid(金字塔):包含多级缩放vx_image对象的不透明对象。
- Remap(重映射):一个不透明的对象,其中包含用于转换图像的源点到目标点的映射。
- Scalar(标量):包含单一原始数据类型的不透明对象。
- Threshold(阈值):包含阈值配置的不透明对象。
- ObjectArray(数组对象):一个不透明的数组对象,可以是 OpenVX 的任何数据对象(非数据类型)的数组,但 Delay 和 ObjectArray 对象除外。
扩展API
- Target:TI Soc上的CPU、DSP、HWA等
- Target Kernel:在Target上运行的Kernel
- Obj Desc(Object Descriptor)
2.3 Graph示例
/* create graph */
vx_context context = vxCreateContext();
vx_image input = vxCreateImage( context, 640, 480, VX_DF_IMAGE_U8 );
vx_image output = vxCreateImage( context, 640, 480, VX_DF_IMAGE_U8 );
vx_graph graph = vxCreateGraph( context );
vx_image intermediate = vxCreateVirtualImage( graph, 640, 480, VX_DF_IMAGE_U8 );
vx_node F1 = vxF1Node( graph, input, intermediate );
vx_node F2 = vxF2Node( graph, intermediate, output );
/* verify graph */
if ( vxVerifyGraph( graph ) == VX_SUCCESS)
{
while(...)
{
// … write to input image …
/* execute graph */
vxProcessGraph( graph ); // or vxScheduleGraph(graph);
// … read from output image …
}
}
2.4 Pipeline
如图所示,当一帧图像从ISP输出,经过DSP到CPU一共需要99ms,当使用Pipeline时,同一时间每个节点都能同时运行,通过增加硬件利用率的方式达到了增加Graph吞吐量的目的,实现了30FPS。
在Graph运行过程中,节点从共享内存中读取源数据(obj_desc)进行处理,处理完后将数据输出到目标(obj_desc)的共享内存地址。
// 创建capture节点,获取图像
status = app_create_graph_capture(obj->graph, &obj->captureObj);
// 创建ldc节点,进行摄像头校正和格式转换
status = app_create_graph_ldc(obj->graph, &obj->ldcObj, obj->captureObj.raw_image_arr[0]);
// 创建mosaic节点,将四个摄像头图像拼成田字格
status = app_create_graph_img_mosaic(obj->graph, &obj->imgMosaicObj);
// 创建display节点,显示输出
status = app_create_graph_display(obj->graph, &obj->displayObj, display_in_image);
// 设置Graph参数(输入和输出)
graph_parameter_index = 0;
// 设置Graph参数
add_graph_parameter_by_node_index(obj->graph, obj->captureObj.node, 1);// Graph中图像输入的节点,图像输出在node中的参数位置
obj->captureObj.graph_parameter_index = graph_parameter_index;
graph_parameters_queue_params_list[graph_parameter_index].graph_parameter_index = graph_parameter_index;
graph_parameters_queue_params_list[graph_parameter_index].refs_list_size = APP_BUFFER_Q_DEPTH; // 设置节点缓冲区个数
graph_parameters_queue_params_list[graph_parameter_index].refs_list = (vx_reference*)&obj->captureObj.raw_image_arr[0];// 输出的图像
graph_parameter_index++;
// 设置Graph运行模式
status = vxSetGraphScheduleConfig(obj->graph,
VX_GRAPH_SCHEDULE_MODE_QUEUE_AUTO,
graph_parameter_index,
graph_parameters_queue_params_list);
status = tivxSetGraphPipelineDepth(obj->graph, APP_PIPELINE_DEPTH);
// 图像入队 次数等于缓冲区个数
status = vxGraphParameterEnqueueReadyRef(obj->graph, captureObj->graph_parameter_index, (vx_reference*)&obj->captureObj.raw_image_arr[obj->enqueueCnt], 1);
for(;;)
{
// capture节点输出一帧图像
status = vxGraphParameterDequeueDoneRef(obj->graph, captureObj->graph_parameter_index, (vx_reference*)&capture_input_arr, 1, &num_refs);
status = vxGraphParameterEnqueueReadyRef(obj->graph, captureObj->graph_parameter_index, (vx_reference*)&capture_input_arr, 1);
}
2.5 IPC 通信
/**
* @brief 向目标target发送obj_id
*
* obj_desc_id: 要发送的数据ID号 例如图像或控制命令
* dst_target_id: 接收消息的target,例如DSP,MSC硬件模块等
*/
vx_status tivxObjDescSend(uint32_t dst_target_id, uint16_t obj_desc_id)
"判断dst_target_id所在的核"
self_cpu_id:
vx_status tivxTargetQueueObjDesc(vx_enum target_id, uint16_t obj_desc_id)
remote_cpu_id:
/**
* @brief 向远程核发送消息
*
* cpu_id: 目标cpu ID号,target_id所在的cpu_id
* payload: tivxIpcPayloadMake((int32_t)dst_target_id, obj_desc_id) 由dst_target_id和obj_desc_id组合而成
* host_cpu_id: obj_desc->host_cpu_id 创建obj_desc的核 openvx_host_cpu_id 指A72
* host_port_id: obj_desc->host_port_id[self_cpu_id] RTOS: 13 Linux: 1024 目标核接收的端口
*/
vx_status tivxIpcSendMsg( vx_enum cpu_id, uint32_t payload, uint32_t host_cpu_id, uint32_t host_port_id)
"判断目标核cpu_id"
host_cpu_id: A72 Linux
/**
* @brief 向核发送消息
*
* dest_cpu_id: 目标核
* payload: 传递的数据 由dst_target_id和obj_desc_id组合而成
* port_id: host_port_id: RTOS: 13 Linux: 1024
*/
int32_t appIpcSendNotifyPort(uint32_t dest_cpu_id, uint32_t payload, uint32_t port_id)
remote_cpu_id: rtos
/**
* @brief 向核发送消息
*
* dest_cpu_id: 目标核
* payload: 传递的数据 由dst_target_id和obj_desc_id组合而成
*/
int32_t appIpcSendNotify(uint32_t dest_cpu_id, uint32_t payload)
/**
* @brief 向核发送消息
*
* dest_cpu_id: 目标核
* payload: 传递的数据 由dst_target_id和obj_desc_id组合而成
* port_id: obj->prm.tiovx_rpmsg_port_id
*/
int32_t appIpcSendNotifyPort(uint32_t dest_cpu_id, uint32_t payload, uint32_t port_id)
"发送的核运行rtos"
/**
* @brief 向远程核发送消息
*
* handle: obj->rpmsg_rx_handle 初始化时创建的接收句柄 接收发送共用同一个
* procId: g_app_to_ipc_cpu_id[dest_cpu_id] 目标核id
* dstEndPt: port_id 目标核接收的端点
* srcEndPt: obj->prm.tiovx_rpmsg_port_id 当前核发送的端点
* data: &payload
* len: sizeof(payload)
*/
int32_t RPMessage_send(RPMessage_Handle handle, uint32_t procId, uint32_t dstEndPt, uint32_t srcEndPt, void* data, uint16_t len)
"发送的核运行linux"
/**
* @brief 向rpmsg设备写入数据
*
* __fd: obj->tx_fds[dest_cpu_id] 初始化时根据rpmsg设备创建的fd
* __buf: &payload
* __n: sizeof(payload)
*/
ssize_t write(int __fd, const void *__buf, size_t __n)
/* 接收线程 RTOS */
static void appIpcRpmsgRxTaskMain(void *arg0, void *arg1)
while(!done)
{
/**
* @brief 接收消息
*
* handle: obj->rpmsg_rx_handle 初始化时创建的句柄
* data: &obj->rpmsg_rx_msg_buf 接收数据的缓冲区
* len: &len 数据的长度
* rplyEndPt: &reply_endpt
* rplyProcId: &src_cpu_id 发送数据的核ID
* timeout: SemaphoreP_WAIT_FOREVER 永久等待接收消息
*/
int32_t RPMessage_recv(RPMessage_Handle handle, void* data, uint16_t *len, uint32_t *rplyEndPt, uint32_t *rplyProcId, uint32_t timeout)
/**
* @brief 数据处理
*
* rpmsg_handle: obj->rpmsg_rx_handle 初始化时创建的句柄
* arg: obj
* data: obj->rpmsg_rx_msg_buf 接收数据的缓冲区
* len: len 数据长度
* src_cpu_id: src_cpu_id 发送数据的核ID
* src_endpt: reply_endpt 发送数据的端口
* dst_endpt: obj->prm.tiovx_rpmsg_port_id
*/
static void appIpcRpmsgRxHandler(RPMessage_Handle rpmsg_handle, void *arg, void *data, uint16_t len, uint32_t src_cpu_id, uint16_t src_endpt, uint16_t dst_endpt)
(payload & 0xFFFF0000) == 0xDEAD0000:
/**
* @brief 回复消息 主要用于测试
*
* dest_cpu_id: 目标核
* payload: 数据回传
* port_id: src_endpt 发送数据的端口
*/
int32_t appIpcSendNotifyPort(uint32_t dest_cpu_id, uint32_t payload, uint32_t port_id)
else:
/**
* @brief 消息处理回调函数
*
* 已注册为 tivxIpcHandler
* appIpcRegisterNotifyHandler(tivxIpcHandler)
*/
obj->ipc_notify_handler(app_cpu_id, payload)
/**
* @brief 回调函数
*
*/
static void tivxIpcHandler(uint32_t src_cpu_id, uint32_t payload)
/**
* @brief 消息处理回调函数
*
* 已注册为 tivxObjDescIpcHandler
* tivxIpcRegisterHandler(tivxObjDescIpcHandler)
*/
g_ipc_handler(payload)
/**
* @brief 消息处理回调函数
*
* payload: 消息数据
*/
static void tivxObjDescIpcHandler(uint32_t payload)
/**
* @brief 从payload获取数据id并存入队列
*
* target_id: tivxIpcPayloadGetTargetId(payload)
* obj_desc_id: tivxIpcPayloadGetObjDescId(payload) 数据id
*/
vx_status tivxTargetQueueObjDesc(vx_enum target_id, uint16_t obj_desc_id)
}
/* 接收线程 linux */
void *appIpcRpmsgRxTaskMain(void *arg)
while(!done)
{
/* 初始化 fd集合 */
FD_ZERO(&rfds);
/* 找fd的最大值 */
maxfd = MAX(maxfd, obj->tx_fds[i]);
/* 在集合中添加 fd */
FD_SET(obj->tx_fds[i], &rfds);
/* fd最大值+1 */
nfds = MAX(maxfd, obj->unblockfd) + 1;
/* 监视fd集合 */
status = select(nfds, &rfds, NULL, NULL, NULL);
for(i = 0; i< APP_IPC_CPU_MAX; i++)
{
/* 判断哪个fd接收到了消息 */
FD_ISSET(obj->tx_fds[i], &rfds)
/**
* @brief 从fd中获取消息
*
* __fd: obj->tx_fds[i] 初始化时为每个核创建的fd
* __buf: &payload 接收的数据
* __nbytes: sizeof(uint32_t) 数据长度
*/
ssize_t read(int __fd, void *__buf, size_t __nbytes)
/**
* @brief 消息处理回调函数
*
* app_cpu_id: i 发送数据的核ID
* payload: 获取到的数据
*/
static void appIpcRpmsgRxHandler(uint32_t app_cpu_id, uint32_t payload)
"后续同上"
}
}
2.6 Memory
内存分配与获取
。。。
3. 示例
//C71 DEMO
/*
* Graph,
*
* input_img_1 -> app_node_img_add -----> output_img
* ^
* |
* input_img_2 -----+
*/
// 创建OpenVX Context
obj->context = vxCreateContext();
// 将要运行的Kernel注册到Target中
vx_status app_c7x_kernel_img_add_register(vx_context context)
{
vx_kernel kernel = NULL;
vx_status status;
uint32_t index;
// 动态分配一个kernel ID并存储在app_c7x_kernel_img_add_kernel_id中
status = vxAllocateUserKernelId(context, &app_c7x_kernel_img_add_kernel_id);
if(status!=VX_SUCCESS)
{
printf(" ERROR: vxAllocateUserKernelId failed (%d)!!!\n", status);
}
if(status==VX_SUCCESS)
{
// 将Kernel注册到Context中
// 注册后可以通过ID和Name来获取Kernel
kernel = vxAddUserKernel(
context,
APP_C7X_KERNEL_IMG_ADD_NAME,
app_c7x_kernel_img_add_kernel_id,
NULL,
3, // kernel的参数个数
app_c7x_kernel_img_add_kernel_validate,// Kernel的验证函数
NULL, // Kernel的初始化函数
NUL); // Kernel的取消初始化
// 检查Kernel是否成功添加到Context
status = vxGetStatus((vx_reference)kernel);
}
if ( status == VX_SUCCESS)
{
// 定义Kernel的输入,输出及配置参数
index = 0;
status = vxAddParameterToKernel(kernel,
index, // 参数目录
VX_INPUT, // 参数类型
VX_TYPE_IMAGE, // 类型
VX_PARAMETER_STATE_REQUIRED // 是否必须
);
index++;
if ( status == VX_SUCCESS)
{
status = vxAddParameterToKernel(kernel,
index,
VX_INPUT,
VX_TYPE_IMAGE,
VX_PARAMETER_STATE_REQUIRED
);
index++;
}
if ( status == VX_SUCCESS)
{
status = vxAddParameterToKernel(kernel,
index,
VX_OUTPUT,
VX_TYPE_IMAGE,
VX_PARAMETER_STATE_REQUIRED
);
index++;
}
if ( status == VX_SUCCESS)
{
// 添加可运行Kernel的target
tivxAddKernelTarget(kernel, TIVX_TARGET_DSP_C7_1);
}
if ( status == VX_SUCCESS)
{
// Kernel 已经可以使用
status = vxFinalizeKernel(kernel);
}
if( status != VX_SUCCESS)
{
printf(" ERROR: vxAddParameterToKernel, vxFinalizeKernel failed (%d)!!!\n", status);
// 释放对Kernel的引用
vxReleaseKernel(&kernel);
kernel = NULL;
}
}
else
{
kernel = NULL;
printf(" ERROR: vxAddUserKernel failed (%d)!!!\n", status);
}
// 设置全局kernel句柄便于后面释放Kernel
app_c7x_kernel_img_add_kernel = kernel;
return status;
}
static vx_status app_create_graph(AppObj *obj)
{
vx_status status = VX_SUCCESS;
// 在Context中创建Graph
obj->graph = vxCreateGraph(obj->context);
APP_ASSERT_VALID_REF(obj->graph);
// 根据图像大小和图像类型创建图像
obj->input_img1 = vxCreateImage(obj->context, obj->width, obj->height, VX_DF_IMAGE_U8);
APP_ASSERT_VALID_REF(obj->input_img1);
// 设置图像名
vxSetReferenceName((vx_reference)obj->input_img1, "Input1ImageU8");
obj->input_img2 = vxCreateImage(obj->context, obj->width, obj->height, VX_DF_IMAGE_U8);
APP_ASSERT_VALID_REF(obj->input_img2);
vxSetReferenceName((vx_reference)obj->input_img2, "Input2ImageU8");
obj->output_img = vxCreateImage(obj->context, obj->width, obj->height, VX_DF_IMAGE_U8);
APP_ASSERT_VALID_REF(obj->output_img);
vxSetReferenceName((vx_reference)obj->output_img, "OutputImageU8");
// 将节点添加到Graph中,并指定输入输出
obj->node = app_c7x_kernel_img_add_kernel_node(
obj->graph,
obj->input_img1,
obj->input_img2,
obj->output_img
);
APP_ASSERT_VALID_REF(obj->node);
// 设置节点运行的Core
status = vxSetNodeTarget(obj->node, VX_TARGET_STRING, TIVX_TARGET_DSP_C7_1);
APP_ASSERT(status==VX_SUCCESS);
// 设置节点名
vxSetReferenceName((vx_reference)obj->node, "C7xImageAddition");
// 验证图像,运行Kernel的验证函数app_c7x_kernel_img_add_kernel_validate
status = vxVerifyGraph(obj->graph);
APP_ASSERT(status==VX_SUCCESS);
// 导出Graph结构到文件
status = tivxExportGraphToDot(obj->graph,".", "vx_app_c7x_kernel");
/* APP_ASSERT(status==VX_SUCCESS); */ /* dont assert on this error since its not critical */
return status;
}
static vx_status app_run_graph(AppObj *obj)
{
vx_status status = VX_SUCCESS;
printf(" Loading [%s] ...\n", obj->input_file_1);
// 读取文件数据到图像中
tivx_utils_load_vximage_from_bmpfile(obj->input_img1, obj->input_file_1, vx_true_e);
printf(" Loading [%s] ...\n", obj->input_file_2);
tivx_utils_load_vximage_from_bmpfile(obj->input_img2, obj->input_file_2, vx_true_e);
printf(" Running graph ...\n");
#if 1
// 执行Graph
status = vxScheduleGraph(obj->graph);
APP_ASSERT(status==VX_SUCCESS);
// 等待Graph运行完
status = vxWaitGraph(obj->graph);
APP_ASSERT(status==VX_SUCCESS);
#endif
printf(" Saving [%s] ...\n", obj->output_file);
tivx_utils_save_vximage_to_bmpfile(obj->output_file, obj->output_img);
printf(" Done !!!\n");
return status;
}
static void app_delete_graph(AppObj *obj)
{
vxReleaseNode(&obj->node);
vxReleaseGraph(&obj->graph);
vxReleaseImage(&obj->input_img1);
vxReleaseImage(&obj->input_img2);
vxReleaseImage(&obj->output_img);
}
vx_status app_c7x_kernel_img_add_unregister(vx_context context)
{
vx_status status;
/**
* - Remove user kernel from context and set the global 'app_c7x_kernel_img_add_kernel' to NULL
*
*/
status = vxRemoveKernel(app_c7x_kernel_img_add_kernel);
app_c7x_kernel_img_add_kernel = NULL;
if(status!=VX_SUCCESS)
{
printf(" Unable to remove kernel (%d)!!!\n", status);
}
return status;
}
vxReleaseContext(&obj->context);
4. 后记
开发TDA4快一年了,TIOVX也接触有些时间了,写篇文章记录一下,后续待补充 2021.8.23
添加pipeline 2021.11.15
添加ipc通信相关 2022.1.7