1.RV1126+OPENCV在视频中添加LOGO图像大体流程图

本章节属于实战课程,主要是利用RV1126的视频流结合OPENCV的API在视频流里面添加LOGO图像,换言之就是在RV1126的视频流里面叠加图片。大体流程我们来看上图,要完成这个功能我们需要创建两个线程(实际上还有初始化过程,这里先忽略了),第一个线程是opencv_vi_logo_handle_thread它主要是获取VI原始数据并有OPENCV转换成Mat矩阵然后添加LOGO图像,并把VI数据发送到VENC编码器。
第二个线程是get_venc_stream_thread它主要是获取H264的VENC码流数据,并且保存到H264文件。
2.具体代码实现
上图我们已经说了大概的流程图,这部分我们重点讲解代码的实现
2.1. RV1126 模块初始化并启动 VI 工作



上面代码是RV1126模块的初始化,包括VI模块的初始化(RK_MPI_VI_SetChnAttr )、使能VI模块(RK_MPI_VI_EnableChn )、VENC模块的初始化(RK_MPI_VENC_CreateChn )、启动VI工作(RK_MPI_VI_StartStream)。关于这方面的参数设置,我们就不详细说了,因为这方面的内容之前的课程已经详细说过。
2.2. opencv_vi_handle_thread 线程的讲解

上面是opencv_vi_logo_handle_thread 的具体实现。首先我们要通过imread读取图片,然后把图片cvtColor转换成灰度图片(由于VI模块的图像格式是NV12,所以我们的图片必须要以灰度图的方式进行添加)。
然后调用RK_MPI_SYS_GetMediaBuffer 获取每一帧的VI视频原始数据,然后使用OPENCV的API把每一个视频数据转换成Mat矩阵,具体的操作是:Mat rv1126_img_mat = Mat(HEIGHT, WIDTH, CV_8UC1, RK_MPI_MB_GetPtr(mb)) , 这是一个Mat构造器,第一个参数是HEIGHT:1080,第二个参数WIDTH:1920,第三个参数:图像格式CV_8UC1,第四个参数:具体的图像数据RK_MPI_MB_GetPtr(mb) **。**通过Mat的构造器,就可以把RV1126的VI视频数据转换成Mat,转换成Mat之后,我们就需要对Mat进行图层叠加操作。
Mat叠加操作,需要分两步,第一步:先创建一个感兴趣区域,Mat rv1126_img_mat_roi = rv1126_img_mat(Rect(100, 100, logo_img.cols, logo_img.rows)) , 这里的感兴趣区域以矩形为背景Rect(100,100,logo_img.cols ,logo_img.rows ),x:100,y: 100,width: logo_img.cols ,height:logo_img.rows 。第二步:利用copyTo函数把读取的图片拷贝到感兴趣区域rv1126_img_mat_roi , 具体代码是logo_img.copyTo(rv1126_img_mat_roi)。
进行上述所有的操作后,就需要把RV1126叠加过后的视频VI数据发送到H264的VENC编码器,调用的API是RK_MPI_SYS_SendMediaBuffer 。
2.3. get_venc_stream_thread 线程的讲解

上面是get_venc_stream_thread 的具体实现,在这个线程里面要通过RK_MPI_SYS_GetMediaBuffer获取每一帧H264的编码数据,然后用fwrite写入。
2.4. 输出结果:
经过上面的编码后,我们来看看输出的H264文件。可以看到这个H264文件,嵌入了周董的JPG图片。这个效果就实现了用OPENCV图片叠加的功能对RV1126的视频流进行图片LOGO的添加
cs
// Copyright 2020 Fuzhou Rockchip Electronics Co., Ltd. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <assert.h>
//#include <bits/types/FILE.h>
#include <fcntl.h>
#include <getopt.h>
#include <opencv2/imgproc.hpp>
#include <pthread.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
// #include "common/sample_common.h"
#include "rkmedia_api.h"
#include <opencv2/core.hpp>
// #include <opencv2/imgoroc.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
#define CAMERA_PATH "rkispp_scale0"
#define CAMERA_ID 0
#define CAMERA_CHN 0
#define VENC_CHN 0
#define WIDTH 1920
#define HEIGHT 1080
void *get_vi_pthread(void * args)
{
pthread_detach(pthread_self());
MEDIA_BUFFER mb = NULL;
Mat logo_img = imread("car.jpg");
cvtColor(logo_img,logo_img,COLOR_BGR2GRAY);
while (1)
{
mb = RK_MPI_SYS_GetMediaBuffer(RK_ID_VI,CAMERA_CHN,-1);
if(!mb)
{
printf("RK_MPI_SYS_GetMediaBuffer Failed\n");
break;
}
printf("RK_MPI_SYS_GetMediaBuffer Success\n");
Mat vi_img = Mat(HEIGHT,WIDTH,CV_8UC1,RK_MPI_MB_GetPtr(mb));
Mat vi_img_roi = vi_img(Rect(100,100,logo_img.cols,logo_img.rows));
logo_img.copyTo(vi_img_roi);
RK_MPI_SYS_SendMediaBuffer(RK_ID_VENC,VENC_CHN,mb);
RK_MPI_MB_ReleaseBuffer(mb);
}
return NULL;
}
void *get_venc_pthread(void * args)
{
pthread_detach(pthread_self());
MEDIA_BUFFER mb = NULL;
FILE *rv1126_H264_logo_file = fopen("opencv_logo.h264","w+");
while (1)
{
mb = RK_MPI_SYS_GetMediaBuffer(RK_ID_VENC,VENC_CHN,-1);
if(!mb)
{
printf("RK_MPI_SYS_GetMediaBuffer Failed\n");
break;
}
printf("RK_MPI_SYS_GetMediaBuffer Success\n");
fwrite(RK_MPI_MB_GetPtr(mb),RK_MPI_MB_GetSize(mb),1,rv1126_H264_logo_file);
RK_MPI_MB_ReleaseBuffer(mb);
}
return NULL;
}
int main()
{
int ret;
RK_MPI_SYS_Init();
VI_CHN_ATTR_S vi_chn_attr;
vi_chn_attr.pcVideoNode = CAMERA_PATH;
vi_chn_attr.u32Width = WIDTH;
vi_chn_attr.u32Height = HEIGHT;
vi_chn_attr.enPixFmt = IMAGE_TYPE_NV12;
vi_chn_attr.enWorkMode = VI_WORK_MODE_NORMAL;
vi_chn_attr.u32BufCnt = 3;
vi_chn_attr.enBufType = VI_CHN_BUF_TYPE_MMAP;
ret = RK_MPI_VI_SetChnAttr(CAMERA_ID,CAMERA_CHN,&vi_chn_attr);
if(ret)
{
printf("RK_MPI_VI_SetChnAttr Failed.\n");
return -1;
}
ret = RK_MPI_VI_EnableChn(CAMERA_ID,CAMERA_CHN);
if(ret)
{
printf("RK_MPI_VI_EnableChn Failed.\n");
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.stVencAttr.imageType = IMAGE_TYPE_NV12;
venc_chn_attr.stVencAttr.enRotation = VENC_ROTATION_0;
venc_chn_attr.stVencAttr.bByFrame = RK_FALSE;
venc_chn_attr.stVencAttr.u32PicWidth = WIDTH;
venc_chn_attr.stVencAttr.u32PicHeight = HEIGHT;
venc_chn_attr.stVencAttr.u32VirWidth = WIDTH;
venc_chn_attr.stVencAttr.u32VirHeight = HEIGHT;
venc_chn_attr.stVencAttr.u32Profile = 66;
venc_chn_attr.stRcAttr.enRcMode = VENC_RC_MODE_H264CBR;
venc_chn_attr.stRcAttr.stH264Cbr.u32SrcFrameRateNum = 25;
venc_chn_attr.stRcAttr.stH264Cbr.u32SrcFrameRateDen = 1;
venc_chn_attr.stRcAttr.stH264Cbr.fr32DstFrameRateNum = 25;
venc_chn_attr.stRcAttr.stH264Cbr.fr32DstFrameRateDen = 1;
venc_chn_attr.stRcAttr.stH264Cbr.u32Gop = 25;
venc_chn_attr.stRcAttr.stH264Cbr.u32BitRate = WIDTH * HEIGHT * 3;
ret = RK_MPI_VENC_CreateChn(VENC_CHN,&venc_chn_attr);
if(ret)
{
printf("RK_MPI_VENC_CreateChn Failed.\n");
return -1;
}
ret = RK_MPI_VI_StartStream(CAMERA_ID,CAMERA_CHN);
if(ret)
{
printf("RK_MPI_VI_StartStream Failed.\n");
return -1;
}
pthread_t get_vi_pid;
pthread_t get_venc_pid;
pthread_create(&get_vi_pid,NULL,get_vi_pthread,NULL);
pthread_create(&get_venc_pid,NULL,get_venc_pthread,NULL);
while (1)
{
sleep(2);
}
RK_MPI_VI_DisableChn(CAMERA_CHN,CAMERA_ID);
RK_MPI_VENC_DestroyChn(VENC_CHN);
return 0;
}
注:
Mat vi_img = Mat(HEIGHT,WIDTH,CV_8UC1,RK_MPI_MB_GetPtr(mb));
Mat vi_img_roi = vi_img(Rect(100,100,logo_img.cols,logo_img.rows));
logo_img.copyTo(vi_img_roi);
NV12采集是单通道色彩,在copyto的时候,必须logo的通道和图像的通道数一样
1.copyTo() 的规则
// copyTo() 的原型
Mat& copyTo(OutputArray m) const; // 深拷贝
void copyTo(OutputArray m, InputArray mask) const; // 带掩码的拷贝
// 重要限制:源和目标必须具有相同的类型(通道数、数据类型)
ddWeighted() 同样有严格的通道限制! 和 copyTo() 一样,要求输入的两个图像必须具有相同的通道数和数据类型。
2.NV12 格式的内存布局
NV12 = Y平面(全尺寸) + UV交错平面(半尺寸)
NV12 是 YUV 420 格式的一种,它的内存布局如下:

以4×4图像为例
- 每个像素占1字节(Y值)
// 4×4的Y平面:16个像素,每个像素1字节
Y0 \]\[Y1 \]\[Y2 \]\[Y3 \] ← 每个格子1字节 \[Y4 \]\[Y5 \]\[Y6 \]\[Y7 \] ← 每个格子1字节 \[Y8 \]\[Y9 \]\[Y10\]\[Y11\] ← 每个格子1字节 \[Y12\]\[Y13\]\[Y14\]\[Y15\] ← 每个格子1字节 总共:16字节
2.每对UV占2字节(U和V各1字节)
// UV平面:2×2的UV对,每对2字节
U0\]\[V0\] \[U1\]\[V1\] ← 每对(U,V)占2字节(U占1字节,V占1字节) \[U2\]\[V2\] \[U3\]\[V3\] ← 每对(U,V)占2字节(U占1字节,V占1字节) 总共:4对 × 2字节 = 8字节
1对UV服务4个Y像素
对应关系:
Y0, Y1, Y4, Y5 ← 共用 (U0,V0) 这对UV(2字节)
Y2, Y3, Y6, Y7 ← 共用 (U1,V1) 这对UV(2字节)
Y8, Y9, Y12, Y13 ← 共用 (U2,V2) 这对UV(2字节)
Y10,Y11,Y14,Y15 ← 共用 (U3,V3) 这对UV(2字节)
也就是说:
4个像素的彩色信息 = Y0+Y1+Y4+Y5(4字节)+ (U0,V0)(2字节)= 6字节
平均每个像素 = 6/4 = 1.5字节
1. 从存储角度看:是单通道
// NV12在内存中是连续的单通道数据
unsigned char nv12_buffer[WIDTH * HEIGHT * 3/2]; // 就是一个大数组
// 用OpenCV表示就是单通道
Mat nv12_mat = Mat(HEIGHT * 3/2, WIDTH, CV_8UC1, nv12_buffer);
// 注意:CV_8UC1 中的 C1 = 1 Channel
2. 从图像内容看:是彩色
NV12内存布局(单通道):
Y0\]\[Y1\]\[Y2\]\[Y3\]...\[Yn\] ← 亮度数据(黑白信息) \[U0\]\[V0\]\[U1\]\[V1\]...\[Un\]\[Vn\] ← 色度数据(颜色信息) 解码时: 像素0 = Y0 + (U0,V0) → 彩色 像素1 = Y1 + (U0,V0) → 彩色(和像素0共享颜色) 像素2 = Y2 + (U1,V1) → 彩色 像素3 = Y3 + (U1,V1) → 彩色
内存计算
总字节数 = Y平面大小 + UV平面大小
= (width × height) + (width × height/2)
= width × height × (1 + 1/2)
= width × height × 3/2
代码中
Mat vi_img = Mat(HEIGHT, WIDTH, CV_8UC1, RK_MPI_MB_GetPtr(mb));
这里您将 NV12 数据当作单通道灰度图处理,所以只使用了:
- Y平面 (亮度数据)- 前
HEIGHT × WIDTH字节 - 忽略了UV平面(色度数据)
这在您叠加灰度图时是正确的,因为:
- 灰度图只需要 Y 平面(亮度信息)
- 您没有修改 UV 平面(色度信息),所以颜色保持不变
- 在 NV12 中修改 Y 平面会影响亮度,但不会直接改变颜色
Mat vi_img = Mat(HEIGHT, WIDTH, CV_8UC1, RK_MPI_MB_GetPtr(mb)); // 只映射Y平面
Mat vi_img_roi = vi_img(Rect(100,100,logo_img.cols,logo_img.rows));
logo_img.copyTo(vi_img_roi);
- 只修改图像的亮度部分
- 保持原有的颜色信息(UV平面)不变
- 灰度logo会以亮度变化的形式叠加到彩色图像上
灰度logo以亮度变化形式叠加,logo区域颜色可能失真
3.YUV444
每个Y像素都有自己独立的U和V,UV分量和Y大小相同
色彩保留能力
// NV12 (你原来的) - 每4个像素共享颜色
Y0 Y1 Y2 Y3 → 共用 U0 V0
Y4 Y5 Y6 Y7 → 共用 U1 V1
// 修改Y平面只能改变亮度,无法单独控制每个像素的颜色
// YUV444 - 每个像素独立颜色
Y0 U0 V0 → 像素0的完整颜色
Y1 U1 V1 → 像素1的完整颜色
Y2 U2 V2 → 像素2的完整颜色
// 可以精确控制每个像素的颜色

4.YUV420

当摄像头以 YUV420(包括 NV12、NV21、YV12 等)格式采集图像时,采集到的是单通道数据,但通过特殊的 YUV 格式编码了彩色信息。
// YUV420 数据在内存中确实是单通道的
Mat yuv420_data = Mat(HEIGHT * 3/2, WIDTH, CV_8UC1); // 单通道!
// 注意:这是 1620行 × 1920列 的 单通道矩阵
// CV_8UC1 中的 C1 表示 1 Channel(单通道)
// 相比之下,真正的彩色图像是 3 通道的
Mat bgr_color = Mat(HEIGHT, WIDTH, CV_8UC3); // 3通道!
// CV_8UC3 中的 C3 表示 3 Channel(三通道)
5.内存对比
1. YUV420(NV12)- 本质是单通道
总数据量 = W × H × 1.5 字节
内存布局 = [Y数据] [UV交错数据] ← 但所有数据都在同一个通道里
可视化:
第1行: Y Y Y Y ... (1920个亮度值) ← 同一通道
第2行: Y Y Y Y ... (1920个亮度值)
...
第1080行: Y Y Y Y ... (1920个亮度值)
第1081行: U V U V U V ... (1920个UV值) ← 还是同一通道
...
2. RGB/BGR - 真正的三通道
总数据量 = W × H × 3 字节
内存布局 = 三个独立的通道
可视化:
通道0: B B B B ... (1920个B值)
通道1: G G G G ... (1920个G值)
通道2: R R R R ... (1920个R值)