Video Codec

原理

视频编码的原理是对视频信号进行冗余信息的消除和压缩,以减少数据量并方便存储和传输。 视频编码的实现通常依赖于具体的视频编码标准,如H.264、H.265等。这些标准规定了具体的编码算法和参数,以实现高效的视频压缩和传输。

Video编码原理

以H.265编码协议为例,视频编码涉及以下关键步骤:

VP5out_vencode
  1. 分块

H.265首先将视频划分为若干个序列,一个序列划分为若干个图像组(GOP,Group of Picture),每个GOP代表一组连续的视频帧。

  1. 空间预测

H.265使用空间预测来去除图像块之间的冗余信息。 如下图所示,从空间的角度看,单个视频帧内部的像素点之间的像素值相差很小。 从时间的角度看,两个连续的视频帧之间也有很多相同的像素点。

空间采样示意图像时间采样示意图像
imageimage

预测编码是基于图像统计特性进行数据压缩的一种方法,利用图像在时间和空间上的相关性,通过已经重建的像素数据预测当前正在编码的像素。

帧内预测是指用于预测的像素和当前正在编码的像素均处于同一个视频帧内,且一般都在邻近的区域内。 由于邻近的像素之间有很强的相关性,像素值一般都非常接近,发生突变的概率非常小,差值均为0或者非常小的数。 所以,帧内预测编码后传输的是预测值和真实值之间的差值,即0附近的值,称为预测误差或残差。 这样就可以用较少的比特传输,达到压缩的效果。

H.265帧内预测编码以块为单位,使用相邻已经重建的块的重建值对正在编码的块进行预测。 预测分量分为亮度和色度两个,对应的预测块分别是亮度预测块和色度预测块。 为了适应高清视频的内容特征,提高预测精度,H.265采用了更加丰富的预测块尺寸和预测模式。

VP5out_vencode_3

帧间预测是指用于预测的像素和当前正在编码的像素不在同一个视频帧内,但是一般在相邻或附近的位置。一般情况下,帧间预测编码的压缩效果要比帧内预测好,主要原因是视频帧之间的相关性非常强。 如果视频帧中的运动物体变化速度很慢,那么视频帧之间的像素差值也就很小,时间冗余度就非常大。

帧间预测评估运动物体运动状况的方法是运动估计,它的主要思想就是对预测块从参考帧的给定范围中搜索匹配块,计算匹配块和预测块之间的相对位移,该相对位移就是运动矢量。 得到运动矢量后,需要对预测修正,也就是运动补偿。将运动矢量输入到运动补偿模块,"补偿"参考帧,即可得到当前编码帧的预测帧。预测帧和当前帧的差,就是帧间预测误差。

如果帧间预测只用到了前一帧图像,就称为前向帧间预测或单向预测。该预测帧也就是P帧,P帧可以参考前面的I帧或者P帧。

如果帧间预测不仅用到了前一帧图像预测当前块,还用到了后一帧图像,那么就是双向预测。该预测帧也就是B帧,B帧可以参考前面的I帧或P帧和后面的P帧。 由于P帧需要参考前面的I帧或P帧,而B帧需要参考前面I帧或P帧和后面的P帧,如果在一个视频流中,先到了B帧,而依赖的I帧、P帧还没有到, 那么该B帧还不能立即解码,那么应该怎么保证播放顺序呢?

其实,在视频编码时,会生成PTS和DTS。通常情况下,编码器在生成一个I帧后,会向后跳过几个帧,用前面的I帧作为参考帧对P帧编码,I帧和P帧之间的帧被编码为B帧。 推流的视频帧顺序在编码的时候就已经按照I帧、P帧、B帧的依赖顺序编好了,收到数据后直接解码即可。所以,不可能先收到B帧,再收到依赖的I帧和P帧。

示意图像
image
  1. 变换和量化

在H.265中,变换和量化是用来进一步压缩数据的。通过将预测误差进行变换,可以将数据从时域转换到频域,从而更好地去除数据冗余。 然后,对变换后的数据进行量化,将数据映射到较低的精度,从而进一步压缩数据。其过程可参考JPEG编码过程。

4.熵编码

在编码过程的最后一步,H.265使用熵编码来对数据进行无损压缩。熵编码的主要目的是最小化编码后数据的冗余,以提高数据压缩效率。其过程可参考JPEG编码过程。

Video解码原理

视频解码指将压缩的视频数据转换回原始视频格式的过程,视频解码的过程主要分为熵解码、逆量化、逆变换、运动补偿和解卷积以及后处理,每个步骤都是为了从高度压缩的数据中恢复尽可能接近原始视频的画面。 由于视频编码的目的是尽可能减少文件大小,解码过程必须精确地逆向执行编码过程中的每一个步骤,以恢复视频内容:

  1. 熵解码(Entropy Decoding)

熵解码是将压缩数据转换回更易于处理的视频格式的过程。在视频编码的过程中,通常会使用霍夫曼编码或算术编码等熵编码技术来减少数据量, 而熵解码的目的就是为了恢复视频编码过程中使用的符号,为下一步的逆量化做准备。

  1. 逆量化(Inverse Quantization)

逆量化是将量化(编码过程中减少数据精度以实现节省空间的步骤)翻转以便恢复原始数据精度的过程,该步骤对于恢复图像质量至关重要。

  1. 逆变换(Inverse Transform)

逆变换是逆转编码时使用的变化(例如离散余弦变换,DCT)以将数据从变换域(例如频率域)恢复到空间域(即原始图像)的过程,该步骤是图像重建的关键步骤。

  1. 运动补偿和解卷积(Motion Compensation & Deblocking)

运行补偿是指,对于预测帧(即基于前/后一帧生成的帧),需要使用运行矢量数据对完整帧进行重建。解卷积则是去除编码过程中产生的块效应(block artifacts)的过程。 这些步骤对于恢复流畅且连贯的视频播放至关重要。

  1. 后处理(Post-Processing)

后处理作为最后一步,会涉及一些去噪、锐化等提升视频质量的技术,该步骤可选,是否进行取决于视频播放的要求和硬件能力能否支撑。

API接口

Video编码接口

// Get the default parameter for vedio encoding
int32_t hbVPGetDefaultVideoEncParam(hbVPVideoEncParam *param);

// Create the video encoding context
int32_t hbVPCreateVideoEncContext(hbVPVideoContext *context,
                                  hbVPVideoEncParam const *param);

//  Release the video encoding context
int32_t hbVPReleaseVideoEncContext(hbVPVideoContext context);

// Create video encoding task
int32_t hbVPVideoEncode(hbUCPTaskHandle_t *taskHandle, 
                        hbVPImage const *srcImg,
                        hbVPVideoContext context);

// Get the output buffer where the encoding data is stored
int32_t hbVPGetVideoEncOutputBuffer(hbVPArray *outBuf,
                                    hbUCPTaskHandle_t taskHandle);                             

Video解码接口

// Get the default parameter for vedio decoding
int32_t hbVPGetDefaultVideoDecParam(hbVPVideoDecParam *param);

// Create the video decoding context
int32_t hbVPCreateVideoDecContext(hbVPVideoContext *context,
                                  hbVPVideoDecParam const *param);

// Release the video decoding context
int32_t hbVPReleaseVideoDecContext(hbVPVideoContext context);

// Create video decoding tasks
int32_t hbVPVideoDecode(hbUCPTaskHandle_t *taskHandle, 
                        hbVPArray const *srcBuf,
                        hbVPVideoContext const context);

// Get the output buffer where the decoding data is stored
int32_t hbVPGetVideoDecOutputBuffer(hbVPImage *outImg,
                                    hbUCPTaskHandle_t taskHandle);                      

详细接口信息请查看 hbVPVideoEncodehbVPVideoDecode

使用方法

Video编码使用方法

// Include the header
#include "hobot/hb_ucp.h"
#include "hobot/vp/hb_vp.h"
#include "hobot/vp/hb_vp_video_codec.h"

// init encoding param
hbVPVideoEncParam venc_param;
venc_param.videoType = HB_VP_VIDEO_TYPE_H265;
hbVPGetDefaultVideoEncParam(&venc_param);
venc_param.height = height;
venc_param.width = width;
venc_param.pixelFormat = image_format;

// create video encoding context and the Video Processing Unit internally alloc the output buffer
hbVPVideoContext enc_context{nullptr};
hbVPCreateVideoEncContext(&enc_context, venc_param)

// init task handle and schedule param
hbUCPTaskHandle_t task_handle{nullptr};
hbUCPSchedParam sched_param;
HB_UCP_INITIALIZE_SCHED_PARAM(&sched_param);
sched_param.backend = HB_UCP_CORE_ANY;
sched_param.priority = 0;

{
  // init image_buf, alloc memory for image data
  hbUCPSysMem image_mem;
  hbUCPMalloc(&image_mem, yuv_size, 0);
  hbVPImage image_buf;
  image_buf.imageFormat = image_format;
  image_buf.imageType = image_type;
  image_buf.width = width;
  image_buf.height = height;
  image_buf.stride = stride;
  image_buf.uvStride = uv_stride;
  image_buf.dataVirAddr = image_mem.virAddr;
  image_buf.dataPhyAddr = image_mem.phyAddr;
  image_buf.uvVirAddr = reinterpret_cast<char *>(image_mem.virAddr) + luma_size;
  image_buf.uvPhyAddr = image_mem.phyAddr + luma_size;

  // create video encoding task
  hbVPVideoEncode(&task_handle, &image_buf, enc_context);

  //  submit video encoding task
  hbUCPSubmitTask(task_handle, &sched_param);

  // wait for video encoding task done
  hbUCPWaitTaskDone(task_handle, 10);

  // get output buffer from Video Processing Unit
  hbVPArray out_buf;
  hbVPGetVideoEncOutputBuffer(&out_buf,task_handle);

  // process h264 or h265 data

  // release task handle and dequeue output buffer from Video Processing Unit
  // since the dequeue output buffer process relies on the context,
  // you must call the hbUCPReleaseTask interface before calling the hbVPReleaseJPEGEncContext interface
  hbUCPReleaseTask(task_handle);
}

// release memory
hbUCPFree(&image_mem);

// release encoding context and the Video Processing Unit internally release the output buffer
hbVPReleaseVideoEncContext(enc_context);

Video解码使用方法

// Include the header
#include "hobot/hb_ucp.h"
#include "hobot/vp/hb_vp.h"
#include "hobot/vp/hb_vp_video_codec.h"

// init decoding param
hbVPVideoDecParam vdec_param;
vdec_param.videoType = HB_VP_VIDEO_TYPE_H265;
hbVPGetDefaultVideoDecParam(&vdec_param);
vdec_param.pixelFormat = HB_VP_IMAGE_FORMAT_YUV420;
vdec_param.inBufSize = height * width * 3 / 2;

// create video decoding context and the Video Processing Unit internally alloc the output buffer
hbVPVideoContext dec_context{nullptr};
hbVPCreateVideoDecContext(&dec_context, vdec_param)

// init task handle and schedule param
hbUCPTaskHandle_t task_handle{nullptr};
hbUCPSchedParam sched_param;
HB_UCP_INITIALIZE_SCHED_PARAM(&sched_param);
sched_param.backend = HB_UCP_CORE_ANY;
sched_param.priority = 0;

// alloc larger memory for input
hbUCPSysMem src_mem;
ret = hbUCPMalloc(&(src_mem), param.inBufSize, 0);

{
  // init src_buf, alloc memory for image data
  memset(src_mem.virAddr, src_mem.memSize, 1);
  fread(src_mem.virAddr, 1, feedSizes, inFile);
  
  hbVPArray src_buf;
  src_buf.phyAddr = src_mem.phyAddr;
  src_buf.virAddr = src_mem.virAddr;
  src_buf.memSize = feedSize;
  src_buf.size = feedSize;
  src_buf.capacity = feedSize;

  // create video decoding task
  hbVPVideoDecode(&task_handle, &src_buf, dec_context);

  //  submit video decoding task
  hbUCPSubmitTask(task_handle, &sched_param);

  // wait for video decoding task done
  hbUCPWaitTaskDone(task_handle, 10);

  // get output buffer from Video Processing Unit
  hbVPImage out_img;
  hbVPGetVideoDecOutputBuffer(&out_img,task_handle);

  // process yuv data

  // release task handle and dequeue output buffer from Video Processing Unit
  // since the dequeue output buffer process relies on the context,
  // you must call the hbUCPReleaseTask interface before calling the hbVPReleaseJPEGDecContext interface
  hbUCPReleaseTask(task_handle);
}

// release memory
hbUCPFree(&src_mem);

// release encoding context and the Video Processing Unit internally release the output buffer
hbVPReleaseVideoDecContext(dec_context);

补充说明

码率控制模式

编码器支持H.264和H.265协议的码率控制,并提供CBR、VBR、AVBR、FixQp和QpMap五种码率控制方式,分别适用于H.264和H.265编码通道。 CBR(恒定码率)能够保证整体编码码率的稳定性;VBR(可变码率)确保编码图像质量的稳定性;AVBR(自适应码率)在码率和图像质量之间找到平衡,使两者相对稳定; FixQp模式下,固定每个I帧和P帧的QP值;QpMap为每帧图像中的每个块指定QP值,其中H.265的块大小为32x32,H.264的块大小为16x16。以下将以H.265协议为例介绍码率参数。

  1. CBR说明

CBR(恒定码率)表示编码过程中保持码率的稳定,确保视频流的整体编码码率恒定。以下是CBR模式下各个参数的含义:

数据项描述取值范围推荐值
intraPeriodI帧间隔[0, 2047]28
bitRate目标平均比特率,单位是kbps[1, 700000]1000
frameRate目标帧率,单位是fps[1, 240]30
initialRcQp指定码率控制时的初始QP值。当该值不在[0,51]范围内,编码器内部会决定初始值[0, 51]63
  1. VBR说明

VBR(可变码率)表示编码过程中根据场景的复杂性动态调整码率。在简单场景下,分配较大的QP(量化参数)以实现更高的压缩率; 在复杂场景下,分配较小的QP以保证图像质量的稳定性。以下是VBR模式下各参数的含义:

数据项描述取值范围推荐值
intraPeriodI帧间隔[0, 2047]28
intraQpI帧的QP值[0, 51]30
frameRate目标帧率,单位是fps[1, 240]30
  1. AVBR说明

AVBR(恒定平均目标码率)是一种在编码过程中保持平均码率稳定的策略。它结合了CBR和VBR的优点:在简单场景下分配较低的码率,在复杂场景下分配足够的码率,从而使码率在不同场景下得到合理分配。 同时,AVBR确保在一定时间内,平均码率接近预设的目标码率,从而控制输出文件的大小。 AVBR可以被视为CBR和VBR之间的折中方案,生成码率和图像质量相对稳定的码流。以下是AVBR模式下各参数的含义:

数据项描述取值范围推荐值
intraPeriodI帧间隔[0, 2047]28
bitRate目标平均比特率,单位是kbps[1, 700000]1000
frameRate目标帧率,单位是fps[1, 240]30
initialRcQp指定码率控制时的初始QP值。当该值不在[0,51]范围内,编码器内部会决定初始值[0, 51]63
  1. FixQp说明

FixQp(固定QP)模式下,编码器为每个I帧、P帧和B帧分配固定的量化参数(QP)值,不进行动态调整。该模式通常用于需要严格控制图像质量和压缩比的场景。 以下是FixQp模式下各参数的含义:

数据项描述取值范围推荐值
intraPeriodI帧间隔[0, 2047]28
frameRate目标帧率,单位是fps[1, 240]30
qpI强制I帧的QP值[0, 51]0
qpP强制P帧的QP值[0, 51]0
qpB强制B帧的QP值[0, 51]0
  1. QPMAP说明

QpMap 模式允许为一帧图像中的每一个宏块指定不同的量化参数(QP)值,从而实现更精细的码率控制。 对于H.265编码,宏块的大小为32x32。以下是QpMap模式下各参数的含义:

数据项描述取值范围推荐值
intraPeriodI帧间隔[0, 2047]28
frameRate目标帧率,单位是fps[1, 240]30
qpMapArrayQPMap,每个宏块需要指定一个QP值,每个QP值占一个字节,并且按照光栅扫描方向排序。指针地址-
qpMapArrayCountQPMap表的大小,其数值与图像的宽度和高度有关,计算公式:(ALIGN64(width)>>5)*(ALIGN64(height)>>5)[1, 8192x4096/(32x32)]-

GOP结构

H.264和H.265编码支持GOP结构的设置,可从预置的GOP结构中选择。以下为GOP预置结构介绍:

GopPresetIdxGOP StructureLow DelayGOP SizeEncoding OrderGOP结构说明
1IYes1I0-I1-I2-I3,…只有I帧,没有互相参考
2PYes1I-P0-P1-P2,…只有I帧和P帧,并且P帧参考2个前向参考帧
3BYes1I-B0-B1-B2,…只有I帧和B帧,并且B帧参考2个前向参考帧
6PPPPYes4I-P0-P1-P2-P3,…只有I帧和P帧,并且P帧参考2个前向参考帧
7BBBBYes4I-B0-B1-B2-B3,…只有I帧和B帧,并且B帧参考2个前向参考帧
9PYes1I-P0,…只有I帧和P帧,并且P帧参考1个前向参考帧