整体码流流向:
因此代码也分为这几部分:
VI:采集视频 配置视频采集信息
模型推理线程:获取VI码流、载入模型、进行模型推理、保存推理结果
画框线程:获取VI码流、获取推理结果、显示推理结果、输出码流到VENC
VENC:编码
RTSP:获取VENC编码码流、传输
代码分析:
main函数:
从总体来看 main函数还是很简单的,首先创建了一个rknn队列,然后配置摄像头节点、初始化rtsp(默认端口554)(rtsp在主函数进行创建 rtsp 实例、创建 rtsp 接口、设置 rtsp 传输属性、同步 rtsp 时间戳然后在真正推流时候即线程中更新视频流设置并驱动事件即可)
后面就是对VI以及VENC的一些配置,然后创建了三个线程,这三个线程的创建是很重要的
int main(int argc, char *argv[])
{
rknn_queue = new RKNN_QUEUE();
RK_MPI_SYS_Init();
RK_U32 u32Width = 1920;
RK_U32 u32Height = 1080;
RK_CHAR *pDeviceName_01 = "rkispp_scale0";
RK_CHAR *pDeviceName_02 = "rkispp_scale1";
RK_CHAR *pDeviceName_03 = "rkispp_m_bypass";
RK_CHAR *pOutPath = "test_output.h264";
RK_CHAR *pIqfilesPath = NULL;
CODEC_TYPE_E enCodecType = RK_CODEC_TYPE_H264;
RK_CHAR *pCodecName = "H264";
RK_S32 s32CamId = 0;
RK_U32 u32BufCnt = 3;
int ret;
printf("#Device: %s\n", pDeviceName_01);
printf("#CodecName:%s\n", pCodecName);
printf("#Resolution: %dx%d\n", u32Width, u32Height);
printf("#Frame Count to save: %d\n", g_s32FrameCnt);
printf("#Output Path: %s\n", pOutPath);
printf("#CameraIdx: %d\n\n", s32CamId);
g_rtsplive = create_rtsp_demo(554);
g_rtsp_session = rtsp_new_session(g_rtsplive, "/live/main_stream");
rtsp_set_video(g_rtsp_session, RTSP_CODEC_ID_VIDEO_H264, NULL, 0);
rtsp_sync_video_ts(g_rtsp_session, rtsp_get_reltime(), rtsp_get_ntptime());
#if 1
VI_CHN_ATTR_S vi_chn_attr;
vi_chn_attr.pcVideoNode = pDeviceName_01;
vi_chn_attr.u32BufCnt = u32BufCnt;
vi_chn_attr.u32Width = u32Width;
vi_chn_attr.u32Height = u32Height;
vi_chn_attr.enPixFmt = IMAGE_TYPE_NV12;
vi_chn_attr.enBufType = VI_CHN_BUF_TYPE_MMAP;
vi_chn_attr.enWorkMode = VI_WORK_MODE_NORMAL;
ret = RK_MPI_VI_SetChnAttr(s32CamId, 0, &vi_chn_attr);
ret |= RK_MPI_VI_EnableChn(s32CamId, 0);
if (ret)
{
printf("ERROR: create rkisp0 VI[0] error! ret=%d\n", ret);
return 0;
}
#endif
VENC_CHN_ATTR_S venc_chn_attr;
memset(&venc_chn_attr, 0, sizeof(VENC_CHN_ATTR_S));
venc_chn_attr.stVencAttr.u32PicWidth = 1920;
venc_chn_attr.stVencAttr.u32PicHeight = 1080;
venc_chn_attr.stVencAttr.u32VirWidth = 1920;
venc_chn_attr.stVencAttr.u32VirHeight = 1080;
venc_chn_attr.stVencAttr.imageType = IMAGE_TYPE_NV12;
venc_chn_attr.stVencAttr.enType = RK_CODEC_TYPE_H264;
venc_chn_attr.stVencAttr.u32Profile = 66;
venc_chn_attr.stRcAttr.enRcMode = VENC_RC_MODE_H264CBR;
venc_chn_attr.stRcAttr.stH264Cbr.u32Gop = 30;
venc_chn_attr.stRcAttr.stH264Cbr.u32BitRate = 1920 * 1080 * 3;
venc_chn_attr.stRcAttr.stH264Cbr.fr32DstFrameRateDen = 1;
venc_chn_attr.stRcAttr.stH264Cbr.fr32DstFrameRateNum = 25;
venc_chn_attr.stRcAttr.stH264Cbr.u32SrcFrameRateDen = 1;
venc_chn_attr.stRcAttr.stH264Cbr.u32SrcFrameRateNum = 25;
ret = RK_MPI_VENC_CreateChn(0, &venc_chn_attr);
if (ret)
{
printf("ERROR: Create venc failed!\n");
exit(0);
}
ret = RK_MPI_VI_StartStream(s32CamId, 0);
if (ret)
{
printf("Start VI[0] failed! ret=%d\n", ret);
return -1;
}
//pthread_t vi_pid;
pthread_t rknn_pid;
pthread_t process_pid;
pthread_t rtsp_pid;
pthread_create(&rknn_pid, NULL, rknn_vi_thread, NULL);
pthread_create(&process_pid, NULL, rknn_vi_rknn_thread, NULL);
pthread_create(&rtsp_pid, NULL, rknn_venc_rtsp_thread, NULL);
printf("%s initial finish\n", __func__);
signal(SIGINT, sigterm_handler);
while (!quit)
{
usleep(500000);
}
if (g_output_file)
fclose(g_output_file);
printf("%s exit!\n", __func__);
// destroy venc before vi
ret = RK_MPI_VENC_DestroyChn(0);
if (ret)
{
printf("ERROR: Destroy VENC[0] error! ret=%d\n", ret);
return 0;
}
// destroy vi
ret = RK_MPI_VI_DisableChn(s32CamId, 0);
if (ret)
{
printf("ERROR: Destroy VI[0] error! ret=%d\n", ret);
return 0;
}
ret = RK_MPI_RGA_DestroyChn(0);
if (ret)
{
printf("ERROR: Destroy RGA[0] error! ret=%d\n", ret);
return 0;
}
if (pIqfilesPath)
{
#if 0
SAMPLE_COMM_ISP_Stop(s32CamId);
#endif
}
}
线程1:
首先pthread_detach使得子线程与主线程分离且结束时候自动回收资源
然后计算出比率比如vi采集的宽为1000,而模型推理输入的图片宽为100那么比例就是1000/10=10 那么model10的位置就是vi100的位置了
下面进入循环,首先从VI通道0阻塞的方式获取码流,然后把该码流拷贝到&Media_Buffer[0]所指向的位置即该数组的首地址
下面使用主函数创建的rknn队列(rknn_queue查看识别结果),并使用opencv创建了一个方框其高为1080宽为1920 CV_8UC1
: 8位无符号单通道图像(灰度图像),显示在VI图像上
从获取的结果中根据识别到的物体数量循环,如果置信度小于0.7则退出,如果大于0.7,分别转换得到1920*1080图片的坐标
在原图mb上进行画框,并在框框对应位置载入文字(识别结果)
最后将处理后的数据传输给VENC并销毁资源
总结一下画框和文字显示,画框其实是直接在图片上画框了,但是文字显示其实是先在图像上创建了一个和图片一样大的框框(矩阵),然后对这个框框的对应位置进行了文字显示,由于框框在图片上因此文字就显示在了图片上
//这个线程用来绘制框框
void *rknn_vi_thread(void *args)
{
pthread_detach(pthread_self());
int frame_id = 0;
MEDIA_BUFFER mb = NULL;
//我个人认为这里MODEL_INPUT_SIZE是模型训练时用的输入尺寸
float x_rate = (float)1920.0 / MODEL_INPUT_SIZE;
float y_rate = (float)1080.0 / MODEL_INPUT_SIZE;
printf("x_rate is %f, y_rate is %f\n", x_rate, y_rate);
while (!quit)
{
mb = RK_MPI_SYS_GetMediaBuffer(RK_ID_VI, 0, -1);
if (!mb)
{
printf("RK_MPI_SYS_GetMediaBuffer get null buffer!\n");
break;
}
memcpy(&Media_Buffer[0], (uint8_t *)RK_MPI_MB_GetPtr(mb), RK_MPI_MB_GetSize(mb));
Media_Buffer_size = RK_MPI_MB_GetSize(mb);
detect_result_group_t detect_result_group = rknn_queue->getDetectResult();
Mat orig_img = Mat(1080, 1920, CV_8UC1, RK_MPI_MB_GetPtr(mb));
for (int j = 0; j < detect_result_group.count; j++)
{
/*if (strcmp(detect_result_group.results[j].name, "person"))
continue;*/
if (detect_result_group.results[j].prop < 0.7)
continue;
int x = detect_result_group.results[j].box.left * x_rate;
int y = detect_result_group.results[j].box.top * y_rate;
int w = (detect_result_group.results[j].box.right -
detect_result_group.results[j].box.left) *
x_rate;
int h = (detect_result_group.results[j].box.bottom -
detect_result_group.results[j].box.top) *
y_rate;
if (x < 0)
x = 0;
if (y < 0)
y = 0;
while ((uint32_t)(x + w) >= 1920)
{
w -= 16;
}
while ((uint32_t)(y + h) >= 1080)
{
h -= 16;
}
printf("border=(%d %d %d %d)\n", x, y, w, h);
nv12_border((char *)RK_MPI_MB_GetPtr(mb), 1920, 1080, x, y, w, h, 0, 0, 255);
putText(orig_img, detect_result_group.results[j].name, Point(x + 32, y + 64), 2, 3, Scalar(255, 0, 0, 255));
}
RK_MPI_SYS_SendMediaBuffer(RK_ID_VENC, 0, mb);
RK_MPI_MB_ReleaseBuffer(mb);
}
}
线程2:
首先创建rknn模型连接符 ctx(类似于文件的fd) 然后使用load_model载入模型,这个函数内部也就是一个fopen函数 model_len是该函数执行后得到的模型长度
rknn载入模型,把ctx与该模型连接起来
下面这一段其实是判断了输入数与输出数其实可以不要 rknn_query 可以得到 输入数与输出数也可以得到其具体属性等等,具体可以参考瑞芯微 rknn手册
一直到下面计算x,y比例(和上一个线程一样),然后创建了一个数据结构体
进入while()循环,首先根据上述结构体创建了图像数据缓冲区mb,然后把上一线程中Media_Buffer(即vi获得的图像)拷贝到mb中
设置原始图像字节数1920*1080*3通道与模型图片字节300*300*3,先将采集的NV12图像转化为rgb888,然后再将输入图像转化为300*300*3
下面对模型输入输出进行配置:输入:图像数据 输出:名称与置信度
运行模型rknn_run 进行一次推理
最后对ssd预测做了一个后处理并把处理结果存在了detect_result_group,并把detect_result_group存入队列(这里是put,线程1 是get获取)
释放资源
void *rknn_vi_rknn_thread(void *args)
{
pthread_detach(pthread_self());
//rknn_context感觉类似于fd文件描述符一样,有点像模型描述符
rknn_context ctx;
int ret;
int model_len = 0;
unsigned char *model;
printf("Loading model ...\n");
model = load_model(g_ssd_path, &model_len);
ret = rknn_init(&ctx, model, model_len, 0);
if (ret < 0)
{
printf("rknn_init fail! ret=%d\n", ret);
return NULL;
}
// Get Model Input Output Info
rknn_input_output_num io_num;
ret = rknn_query(ctx, RKNN_QUERY_IN_OUT_NUM, &io_num, sizeof(io_num));
if (ret != RKNN_SUCC)
{
printf("rknn_query fail! ret=%d\n", ret);
return NULL;
}
printf("model input num: %d, output num: %d\n", io_num.n_input,
io_num.n_output);
printf("input tensors:\n");
rknn_tensor_attr input_attrs[io_num.n_input];
memset(input_attrs, 0, sizeof(input_attrs));
for (unsigned int i = 0; i < io_num.n_input; i++)
{
input_attrs[i].index = i;
ret = rknn_query(ctx, RKNN_QUERY_INPUT_ATTR, &(input_attrs[i]),
sizeof(rknn_tensor_attr));
if (ret != RKNN_SUCC)
{
printf("rknn_query fail! ret=%d\n", ret);
return NULL;
}
printRKNNTensor(&(input_attrs[i]));
}
printf("output tensors:\n");
rknn_tensor_attr output_attrs[io_num.n_output];
memset(output_attrs, 0, sizeof(output_attrs));
for (unsigned int i = 0; i < io_num.n_output; i++)
{
output_attrs[i].index = i;
ret = rknn_query(ctx, RKNN_QUERY_OUTPUT_ATTR, &(output_attrs[i]),
sizeof(rknn_tensor_attr));
if (ret != RKNN_SUCC)
{
printf("rknn_query fail! ret=%d\n", ret);
return NULL;
}
printRKNNTensor(&(output_attrs[i]));
}
rga_buffer_t src;
float x_rate = (float)1920 / MODEL_INPUT_SIZE;
float y_rate = (float)1080 / MODEL_INPUT_SIZE;
MB_IMAGE_INFO_S stImageInfo = {1920, 1080, 1920, 1080, IMAGE_TYPE_NV12};
while (!quit)
{
MEDIA_BUFFER mb = RK_MPI_MB_CreateImageBuffer(&stImageInfo, RK_TRUE, MB_FLAG_NOCACHED);
if (!mb)
{
printf("ERROR: no space left!\n");
break;
}
//Media_Buffer在上一线程是获取的vi视频流
memcpy(RK_MPI_MB_GetPtr(mb), Media_Buffer, Media_Buffer_size);
RK_MPI_MB_SetSize(mb, Media_Buffer_size);
int rga_buffer_size = 1920 * 1080 * 3;
int rga_buffer_model_input_size = MODEL_INPUT_SIZE * MODEL_INPUT_SIZE * 3;
unsigned char *rga_buffer = (unsigned char *)malloc(rga_buffer_size);
unsigned char *rga_buffer_model_input =
(unsigned char *)malloc(rga_buffer_model_input_size);
nv12_to_rgb24((unsigned char *)RK_MPI_MB_GetPtr(mb), rga_buffer, 1920,
1080);
//这几步 把vi码流数据转换为模型推理格式rgb 888 300x300
rgb24_resize(rga_buffer, rga_buffer_model_input, 1920, 1080, MODEL_INPUT_SIZE, MODEL_INPUT_SIZE);
rknn_input inputs[1];
memset(inputs, 0, sizeof(inputs));
inputs[0].buf = rga_buffer_model_input;
inputs[0].index = 0;
inputs[0].type = RKNN_TENSOR_UINT8;
inputs[0].size = rga_buffer_model_input_size;
inputs[0].fmt = RKNN_TENSOR_NHWC;
//设置模型推理输入
ret = rknn_inputs_set(ctx, io_num.n_input, inputs);
if (ret < 0)
{
printf("rknn_input_set fail! ret=%d\n", ret);
return NULL;
}
// Run
printf("rknn_run\n");
//执行一次模型推理。
ret = rknn_run(ctx, NULL);
if (ret < 0)
{
printf("rknn_run fail! ret=%d\n", ret);
return NULL;
}
rknn_output outputs[2];
memset(outputs, 0, sizeof(outputs));
outputs[0].want_float = 1;
outputs[1].want_float = 1;
ret = rknn_outputs_get(ctx, io_num.n_output, outputs, NULL);
if (ret < 0)
{
printf("rknn_outputs_get fail! ret=%d\n", ret);
return NULL;
}
//推理结果detect_result_group/
detect_result_group_t detect_result_group;
postProcessSSD((float *)(outputs[0].buf), (float *)(outputs[1].buf), MODEL_INPUT_SIZE,
MODEL_INPUT_SIZE, &detect_result_group);
//存放推理结果
rknn_queue->putDetectResult(detect_result_group);
RK_MPI_MB_ReleaseBuffer(mb);
mb = NULL;
if (rga_buffer != NULL)
{
free(rga_buffer);
rga_buffer = NULL;
}
if (rga_buffer_model_input != NULL)
{
free(rga_buffer_model_input);
rga_buffer_model_input = NULL;
}
}
return NULL;
}
线程3:
分离线程
循环中不断进行推流即:rtsp_tx_video
释放资源
删除rtsp实例:rtsp_do_event
void *rknn_venc_rtsp_thread(void *args)
{
pthread_detach(pthread_self());
MEDIA_BUFFER mb = NULL;
while (!quit)
{
mb = RK_MPI_SYS_GetMediaBuffer(RK_ID_VENC, 0, -1);
if (!mb)
{
printf("RK_MPI_SYS_GetMediaBuffer venc get null buffer!\n");
break;
}
rtsp_tx_video(g_rtsp_session, (unsigned char *)RK_MPI_MB_GetPtr(mb), RK_MPI_MB_GetSize(mb), RK_MPI_MB_GetTimestamp(mb));
RK_MPI_MB_ReleaseBuffer(mb);
rtsp_do_event(g_rtsplive);
}
return NULL;
}
最后在Ubuntu端进行rtsp拉流处理
ffplay –x 400 –y 400 rtsp://192.168.1.22:554/live/main_stream
-x -y不必多说是窗口大小
192.168.1.22是开发板即推流端IP
554是前面设置的端口号
/live/main_stream也是前面设置的流的路径
效果如下: