Linux44:opencv在H264码流中添加LOGO

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. 每个像素占1字节(Y值)

// 4×4的Y平面:16个像素,每个像素1字节

Y0 Y1 Y2 Y3 ← 每个格子1字节

Y4 Y5 Y6 Y7 ← 每个格子1字节

Y8 Y9 Y10Y11 ← 每个格子1字节

Y12Y13Y14Y15 ← 每个格子1字节

总共:16字节

2.每对UV占2字节(U和V各1字节)

// UV平面:2×2的UV对,每对2字节

U0V0 U1V1 ← 每对(U,V)占2字节(U占1字节,V占1字节)

U2V2 U3V3 ← 每对(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_bufferWIDTH \* HEIGHT \* 3/2; // 就是一个大数组

// 用OpenCV表示就是单通道

Mat nv12_mat = Mat(HEIGHT * 3/2, WIDTH, CV_8UC1, nv12_buffer);

// 注意:CV_8UC1 中的 C1 = 1 Channel

2. 从图像内容看:是彩色

NV12内存布局(单通道):

Y0Y1Y2Y3...Yn ← 亮度数据(黑白信息)

U0V0U1V1...UnVn ← 色度数据(颜色信息)

解码时:

像素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平面(色度数据)

这在您叠加灰度图时是正确的,因为:

  1. 灰度图只需要 Y 平面(亮度信息)
  2. 您没有修改 UV 平面(色度信息),所以颜色保持不变
  3. 在 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);

  1. 只修改图像的亮度部分
  2. 保持原有的颜色信息(UV平面)不变
  3. 灰度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值)

相关推荐
七夜zippoe3 分钟前
为什么 2026 年每个 Java 团队都该懂 AI Agent
java·开发语言·人工智能
举个栗子。5 分钟前
SwarmForge:AI 智能体协同编程框架,让多个 Agent 在隔离工作区并行协作
人工智能·开源·ai编程
AIGC小尼16 分钟前
Windows 本地 AI 漫剧全自动生产线部署完整教程(零基础、全指令、带源码、模型配置、排错方案)
人工智能·windows·ai漫剧
合米AI SOP系统21 分钟前
传统产线如何快速上马落地 AI 防错?合米科技 AI SOP 7天即可上线。
大数据·人工智能·科技
思录Echo21 分钟前
什么决定具身智能的最终走向?多技术路线与落地现实辨析
大数据·人工智能
ShallWeL39 分钟前
Orin 上多模型常驻与显存预算
人工智能·嵌入式硬件·nvidia·orin
xiaohaiAIgeo41 分钟前
【2026年】AI监控加行为分析守护实验室安全
大数据·人工智能·科普知识
IT_陈寒42 分钟前
Python的多线程就是个假把式,我算是体验到了
前端·人工智能·后端
集和诚JHCTECH1 小时前
案例分享 | BRAV-7721助力印刷电路板(PCB)智能缺陷复判系统
人工智能·嵌入式硬件·边缘计算