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 \]\[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平面(色度数据)

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

  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值)

相关推荐
纤纡.2 小时前
OpenCV 实战:从视频处理到图像轮廓检测的全维度解析
人工智能·opencv·音视频
·中年程序渣·2 小时前
Spring AI Alibaba入门学习(六)
人工智能·学习·spring
Dxy12393102162 小时前
PyTorch的CyclicLR详细介绍:给模型训练装上“涡轮增压”
人工智能·pytorch·python
RuiBo_Qiu2 小时前
【LLM进阶-Agent】7. Basic Reflection Agent 介绍
人工智能·ai-native
AI浩2 小时前
Hybrid-SORT:弱线索对于在线多目标跟踪的重要性
人工智能·计算机视觉·目标跟踪
小程故事多_802 小时前
AI Agent接口之争,MCP黯然退场,终端为何成终局答案
人工智能·aigc·cli·mcp
yuhaiqiang2 小时前
央视 315 曝光的GEO——给 AI投毒是怎么一回事?
人工智能
战场小包2 小时前
向日葵MCP实践指南
人工智能·agent·mcp
ECH00O002 小时前
10-Fine-tuning/微调:给AI上"专业课"
人工智能