RV1126单目摄像头取流,实现双路输出
-
- [0 前言](#0 前言)
- [1 环境搭建与准备](#1 环境搭建与准备)
-
- [1.1 硬件环境](#1.1 硬件环境)
- [1.2 软件环境](#1.2 软件环境)
- [1.2.1 设备树配置](#1.2.1 设备树配置)
- [1.2.2 SDK完整编译](#1.2.2 SDK完整编译)
- [2 实现思路与关键代码](#2 实现思路与关键代码)
-
- [2.1 实现思路](#2.1 实现思路)
-
- [2.1.1 视频流节点](#2.1.1 视频流节点)
- [2.1.2 视频编码](#2.1.2 视频编码)
- [2.2 关键代码](#2.2 关键代码)
-
- [2.2.1 Cmake配置](#2.2.1 Cmake配置)
- [2.2.2 初始化配置](#2.2.2 初始化配置)
- [2.2.3 推流回调](#2.2.3 推流回调)
- [2.2.4 取流推送给算法](#2.2.4 取流推送给算法)
- [3 运行效果](#3 运行效果)
-
- [3.1 ffmpeg拉流实时播放](#3.1 ffmpeg拉流实时播放)
- [3.2 算法端实时处理](#3.2 算法端实时处理)
- [4 参考资料](#4 参考资料)
0 前言
在机器人项目开发过程中,经常会需要从单目摄像头(算法爱叫它sensor)获取视频流,并将其进行双路输出,一路进行H.264
编码后实时推流用于远程监控等场景,另一路直接输出给算法进行图像分析、目标检测等处理。本文将详细介绍如何在RV1126上实现这一功能。
1 环境搭建与准备
1.1 硬件环境
首先,确保我们的硬件平台是基于RV1126的(这里我使用AIO-1126-JD4
开发板),并且正确连接好单目摄像头OS04A10
(通常是MIPI接口
)。RV1126支持两组MIPI CSI
,也就是可以两路摄像头同时输入(后期可扩展为双目)。AIO-1126-JD4
开发板集成了一个百兆以太网口、一个千兆以太网口和WiFi蓝牙一体的SIP模组AP6236
,方便后续的H.264编码推流。
详细可官网了解:AIO-1126-JD4
1.2 软件环境
1.2.1 设备树配置
使能csi_dphy0
节点,并配置好os04a10
摄像头的支持。
1.2.2 SDK完整编译
参考《Ubuntu20.04/22.04下Docker方案实现多平台SDK编译》第2章将SDK完整的编译一遍,这样,我们就可得到交叉编译工具链及依赖环境rv1126_rv1109_linux_release_20211022/buildroot/output/firefly_rv1126_rv1109/host
。
2 实现思路与关键代码
2.1 实现思路
2.1.1 视频流节点
RV1126平台的ISPP可同时提供4种分辨率视频流,用户层可以看到ISPP驱动提供的4个视频节点。rkispp_m_bypass不支持缩放,分辨率仅能保持sensor最大分辨率。rkispp_scale0分辨率超过2K之后,需要使用NV16格式。
我们接入摄像头后,在板子上可以通过以下指令看到rkispp_scale0
、rkispp_scale1
视频节点,可以用这两个视频节点实现双路取流。
grep '' /sys/class/video4linux/video*/name
rkispp_scale0
节点的视频流用来传送给算法,rkispp_scale1
节点的视频流用来编码成H.264
后推流出去。有小伙伴有疑问,为什么我不用ROS
直接发布出去?我只能说视频流和图片帧发布不是同一个概念,也不是一个量级的,RV1126
集成了专门的视频硬件编解码器,结合ISP
图像信号处理器和NPU
加以辅助,编解码速度直接拉满,综合来讲硬件编码后再推流的传输效率比直接ROS
图像发布的高很多。
2.1.2 视频编码
RV1126的视频编码模块(VENC
)支持多路实时编码,且每路编码独立,编码协议和编码profile
可以不同。支持视频编码同时,调度Region
模块对编码图像内容进行叠加和遮挡。支持H264/H265/MJPEG/JPEG
编码。
2.2 关键代码
2.2.1 Cmake配置
cmake
指定系统根目录,并配置查找路径和交叉编译工具gcc、g++
等。
set(TARGET_SYSROOT "/home/wzl/workspace/firefly/rv1126_rv1109_linux_release_20211022/buildroot/output/firefly_rv1126_rv1109/host/arm-buildroot-linux-gnueabihf/sysroot")
set(CMAKE_SYSROOT "${TARGET_SYSROOT}")
set(CMAKE_FIND_ROOT_PATH ${TARGET_SYSROOT} ${NATIVE_SYSROOT})
...
set(TOOLCHAIN_ROOT "/home/wzl/workspace/firefly/rv1126_rv1109_linux_release_20211022/buildroot/output/firefly_rv1126_rv1109/host")
set(CMAKE_C_COMPILER "${TOOLCHAIN_ROOT}/bin/arm-linux-gnueabihf-gcc")
set(CMAKE_CXX_COMPILER "${TOOLCHAIN_ROOT}/bin/arm-linux-gnueabihf-g++")
set(CMAKE_ASM_COMPILER "${TOOLCHAIN_ROOT}/bin/arm-linux-gnueabihf-gcc")
set(CMAKE_AR "${TOOLCHAIN_ROOT}/bin/arm-linux-gnueabihf-ar" CACHE FILEPATH "Archiver")
set(CMAKE_RANLIB "${TOOLCHAIN_ROOT}/bin/arm-linux-gnueabihf-ranlib")
另外,还需链接RK媒体库easymedia
和推流库rtsp
。
target_link_libraries(${PROJECT_NAME}
easymedia
rtsp
...
)
2.2.2 初始化配置
VI[0]
用来取流传给算法,VI[1]
用来编码后推流。
int Camera::Init() {
RK_U32 u32Width = 640;
RK_U32 u32Height = 480;
int frameCnt = 30;
RK_CHAR *pDeviceName = "rkispp_scale0";
RK_CHAR *pDeviceName_rtsp = "rkispp_scale1";
// 初始化rtsp推流
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());
RK_MPI_SYS_Init();
VI_CHN_ATTR_S vi_chn_attr;
vi_chn_attr.pcVideoNode = pDeviceName;
vi_chn_attr.u32BufCnt = 3;
vi_chn_attr.u32Width = u32Width;
vi_chn_attr.u32Height = u32Height;
vi_chn_attr.enPixFmt = IMAGE_TYPE_RGB888;
vi_chn_attr.enWorkMode = VI_WORK_MODE_NORMAL;
vi_chn_attr.enBufType = VI_CHN_BUF_TYPE_MMAP;
int ret = RK_MPI_VI_SetChnAttr(s32CamId, 0, &vi_chn_attr);
ret |= RK_MPI_VI_EnableChn(s32CamId, 0);
if (ret) {
printf("ERROR: create VI[0] error! ret=%d\n", ret);
return -1;
}
vi_chn_attr.pcVideoNode = pDeviceName_rtsp;
vi_chn_attr.enPixFmt = IMAGE_TYPE_NV12;
ret = RK_MPI_VI_SetChnAttr(s32CamId, 1, &vi_chn_attr);
ret |= RK_MPI_VI_EnableChn(s32CamId, 1);
if (ret) {
printf("ERROR: create VI[1] error! ret=%d\n", ret);
return -1;
}
VENC_CHN_ATTR_S venc_chn_attr;
memset(&venc_chn_attr, 0, sizeof(venc_chn_attr));
venc_chn_attr.stVencAttr.enType = RK_CODEC_TYPE_H264;
venc_chn_attr.stRcAttr.enRcMode = VENC_RC_MODE_H264CBR;
venc_chn_attr.stRcAttr.stH264Cbr.u32Gop = 30;
venc_chn_attr.stRcAttr.stH264Cbr.u32BitRate = u32Width * u32Height;
// frame rate: in 30/1, out 30/1.
venc_chn_attr.stRcAttr.stH264Cbr.fr32DstFrameRateDen = 1;
venc_chn_attr.stRcAttr.stH264Cbr.fr32DstFrameRateNum = 30;
venc_chn_attr.stRcAttr.stH264Cbr.u32SrcFrameRateDen = 1;
venc_chn_attr.stRcAttr.stH264Cbr.u32SrcFrameRateNum = 30;
venc_chn_attr.stVencAttr.imageType = IMAGE_TYPE_NV12;
venc_chn_attr.stVencAttr.u32PicWidth = u32Width;
venc_chn_attr.stVencAttr.u32PicHeight = u32Height;
venc_chn_attr.stVencAttr.u32VirWidth = u32Width;
venc_chn_attr.stVencAttr.u32VirHeight = u32Height;
venc_chn_attr.stVencAttr.u32Profile = 77;
ret = RK_MPI_VENC_CreateChn(1, &venc_chn_attr);
if (ret) {
printf("ERROR: create VENC[1] error! ret=%d\n", ret);
return -1;
}
//注册编码通道的回调函数
MPP_CHN_S pstChn;
pstChn.enModId = RK_ID_VENC;//模块号
pstChn.s32DevId = 0;//设备号 第一个摄像头
pstChn.s32ChnId = 1;//通道号 第一个通道
ret = RK_MPI_SYS_RegisterOutCb(&pstChn, video_packet_cb);
if(ret != 0){
printf("注册编码通道的回调函数失败\n");
return -1;
}
//绑定venc通道
pstSrcChn.enModId = RK_ID_VI;
pstSrcChn.s32DevId = 0;
pstSrcChn.s32ChnId = 1;
pstDestChn.enModId = RK_ID_VENC;
pstDestChn.s32DevId = 0;
pstDestChn.s32ChnId = 1;
ret = RK_MPI_SYS_Bind(&pstSrcChn,&pstDestChn);
if(ret != 0){
printf("绑定venc通道失败\n");
return -1;
}
p_get_image_thread = std::make_shared<std::thread>(&Camera::getImageWork, this);
p_get_image_thread->detach();
ret = RK_MPI_VI_StartStream(s32CamId, 0);
if (ret) {
printf("Start VI[0] failed! ret=%d\n", ret);
return -1;
}
return 0;
}
2.2.3 推流回调
void video_packet_cb(MEDIA_BUFFER mb) {
static RK_S32 packet_cnt = 0;
printf("#Get packet-%d, size %zu\n", packet_cnt, RK_MPI_MB_GetSize(mb));
//推流
if (g_rtsplive && g_rtsp_session) {
rtsp_tx_video(g_rtsp_session, (const uint8_t *)RK_MPI_MB_GetPtr(mb), RK_MPI_MB_GetSize(mb),
RK_MPI_MB_GetTimestamp(mb));
rtsp_do_event(g_rtsplive);
}
// fwrite(RK_MPI_MB_GetPtr(mb), RK_MPI_MB_GetSize(mb), 1, fp);
RK_MPI_MB_ReleaseBuffer(mb); //释放帧数据
packet_cnt++;
}
2.2.4 取流推送给算法
void Camera::getImageWork() {
int frame_id = 0;
MEDIA_BUFFER mb = NULL;
while (!quit) {
mb = RK_MPI_SYS_GetMediaBuffer(RK_ID_VI, s32CamId, -1);
if (!mb) {
printf("RK_MPI_SYS_GetMediaBuffer get null buffer!\n");
break;
}
MB_IMAGE_INFO_S stImageInfo = {0};
int ret = RK_MPI_MB_GetImageInfo(mb, &stImageInfo);
if (ret) {
printf("Warn: Get image info failed! ret = %d\n", ret);
printf("Get Frame:ptr:%p, fd:%d, size:%zu, mode:%d, channel:%d, "
"timestamp:%lld, ImgInfo:<wxh %dx%d, fmt 0x%x>\n",
RK_MPI_MB_GetPtr(mb), RK_MPI_MB_GetFD(mb), RK_MPI_MB_GetSize(mb),
RK_MPI_MB_GetModeID(mb), RK_MPI_MB_GetChannelID(mb),
RK_MPI_MB_GetTimestamp(mb), stImageInfo.u32Width,
stImageInfo.u32Height, stImageInfo.enImgType);
}
double timestamp = api::GetSteadyClockS();
cv::Mat image(480, 640, CV_8UC3, RK_MPI_MB_GetPtr(mb));
this->trackImageSig(image, timestamp);
RK_MPI_MB_ReleaseBuffer(mb);
}
}
3 运行效果
3.1 ffmpeg拉流实时播放
192.168.0.4
是RV1126板子的IP
地址。
ffplay "rtsp://192.168.0.4/live/main_stream"

3.2 算法端实时处理
这里移植了ORB_SLAM3
,感兴趣的小伙伴可以关注我后期的博客更新。
(缺一张SLAM配图,后期补上)
4 参考资料
1\] Rockchip_Developer_Guide_Linux_RKMedia_CN.pdf