RV1126单目摄像头取流,实现双路输出(一路H.264编码推流,一路给算法)

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_scale0rkispp_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