视频编码原理(一)
1.视频为什么要编码?
在我们做音视频产品的时候,经常会把音视频数据进行网络传输,而此时音视频数据就需要进行编码(所谓编码就是指压缩)。因为在网络传输的时候,网络带宽有限,若此时网络传输的时候还用原始数据进行传输的时候,则会对网络带宽造成极大的负担。
比方说一个分辨率为1280 * 720 帧率为30帧的视频,按照YUV420格式的计算,它每秒传输的数据量就是1280* 720* 30 * 3/2 ~= 39.5M,这个数据量是极其惊人的。所以此时,我们就要引入视频编码技术来压缩视频,让其体积大小能够大幅度缩小,这样在网络传输的时候就会大大降低网络负担。在音视频开发中,一般分为H264、H265这两种最常见的编码格式。H264的压缩比能够达到1:100,H265的压缩比能够达到1:200,但是目前业内由于HEVC技术还没完全成熟,所以绝大部分设备都是用H264进行压缩处理。
2.VCL层和NAL层讲解
H264编码格式是目前业内最流行的视频编码格式,它是MPEG-4的第十个部分。H264有高压缩率、高图像质量、网络适应性很强等特点。在同等的图像质量下,H264的压缩比 远超绝大部分编码格式(HEVC除外)。在H264(HEVC)编码框架中分为两大层,一层是VCL(Video Coding Layer)、另一层是NAL层(Network Abstraction Layer)。VCL层主要负责内容的表示,NAL层主要负责对H264数据进行打包和传输。
3.VCL层知识点
VCL层包含了四个重要的知识点:帧内预测压缩、帧间预测压缩、变换量化、熵编码。
-
帧内压缩:也称之为空间压缩,当压缩一张图片的时候,若仅仅考虑帧的数据也不考虑相邻帧和帧之间的冗余数据,这样的方式就叫帧内压缩。在H264中1帧生成的原理的就是帧内压缩,帧内压缩可以独立解码出一帧完整的图像而不需要参考任何帧,帧内压缩表现出来的数据是最大16×16的宏块。帧内压缩本质上是空间的XY轴上进行压缩,它的压缩率比较小。
- 帧内压缩特点:不需要参考任何帧,通过预测的方式就可以把数据表示出来
-
帧间压缩:帧间压缩是通过对比相邻两帧之间的数据进行压缩,并且进一步提高压缩量。用这种方法可以先编码出一个完整的图像1帧,紧接着2帧的数据就不编码出一张完整的图像,而是只写入和1帧的不同的数据,这样2帧数据的大小则会大大降低。以此类推,3帧的数据参考2帧数据,并且也只写入2帧不同的数据,帧间压缩表现出来的数据同样也是最大16 x 16的宏块(它也包括:4x4,8x8)。按照这样的方法,不停地循环下去。这样的方式,就是P帧、B帧的实现方法。
-
变换量化:为了要让压缩的H264图像在网络传输中节省更多的码率,需要采用变换编码以及量化技术来消除图像信号中的相关性,以及减小图像编码中的动态变换范围。变换编码的原理就是把图像时域信号变换成频域信号,在频域范围内,绝大部分信号集中在低频区域,相对时域信号,码率能够大幅度下降。
- 而在H264中通常用下面方法进行处理:H264数据经过帧内压缩(16 * 16亮度、4 * 4亮度、8 * 8亮度)、帧间预测(4 * 4 ~ 16 * 16亮度)得到了残差值。这些残差值需要经过冗余的统计,并进行数据的变换和量化操作。对于H264的16 x 16的亮度块,通过4x4的前向DCT变换,然后对16个DC系数再进行4 x4 的Hadmard变换,最后把这16个DC系数进行量化。
-
熵编码:熵编码压缩是一种无损压缩模式,其实现原理是使用的编码模式来表示输入的数据,并达到压缩效果。常用的熵编码方式分别是变长编码(CAVLC)和算术编码(CABAC)。
-
**变长编码(CAVLC):**对出现概率大的符号,对出现概率小的符号提供长字节二进制码。
**算术编码(CABAC):**采用一个浮点数代替一串输入符号,范围是[0,1)
CABAC的编码压缩率远远大于CAVLC,CABAC具有更高的编码效果。
-
综上:首先通过帧内压缩和帧间压缩,再变换量化时域变频域得到低频视频数据,最后通过熵编码 二进制形式表示视频数据
4.NAL层
NAL负责以网络所要求的方式进行打包和传送。NAL层的组成部分(这里主要介绍H264码流的NAL,H265的后面会讲):StartCode|NALU Header|NALU Payload|StartCode|NALU Header|NALU Payload
NAL层一般由三部分组成,分别是StartCode(起始码:必须是00 00 00 01)、NALU Header(NALU头部)、NALU Payload(具体传输的视频内容)。
-
比方说一串H264码流的NALU:00 00 00 01 68 43 A0 25 56 2E…
- [00 00 00 01]是StartCode也就是起始码
- [68] 是NAL头部
- [43 A0 25 56 2E…]就是NALU Payload的数据
-
StartCode起始码:起始码是所有H264码流开始时候的分隔符,一般分为0x000001和0x00000001两种。
-
NALU头部:长度为一个字节,它的语法是NALU类型(5bit)、重要性位(2bit)、禁止位(1bit)
-
NALU Payload:有效负载,实际的视频数据。
视频编码原理(二)
H264重点帧和GOP的讲解
1.帧的概念
在视频中,帧是视频传输的最小单位。一帧实际上静态图片,视频本质上是由连续的静态图片组成,我们经常耳熟能详的30帧、60帧实际上指的就是静态图片的数量。比方说30帧:它指的是1秒内有30张静态图片;而60帧,它指的是1秒内有60张静态图片。换言之,帧率越高,它所生成的静态图片就越多,视频质量就越好、视频的流畅度也更好。
2.H264重点帧类型
H264中,最重要的NALU是SPS、PPS、SEI、I
帧。这就是一个经典的NALU,一般而言H264开头都是以SPS、PPS为开头,若SEI有数据则添加SEI数据(SEI帧可有可无),紧接着就是I帧。
-
SPS:全称是序列参数集(它的NALU是
00 00 00 01 67
),它保存了一组编码视频序列的全局参数。也就是原始视频经过压缩编码后组成的序列,而每一帧编码数据它所依赖的全局参数就保存到图像参数集里面。通常来说,SPS和PPS都是整个NALU的起始位置。下面来看看SPS里面比较重要的数据:- profile_idc:在H264中,通常定义了三种档次的profile
基准档次:baseline profile,profile_idc =66
主要档次:main profile, profile_idc = 77
扩展档次:extended profile , profile_idc = 88
最高档次:high profile, profile_idc = 100 - level_idc:标识当前的码流Level,编码的Level定义了某些特殊环境下最大的视频分辨率、帧率等参数,码流的等级由level_idc所决定。比方说在当前码流中,level_idc = 40,则说明该码流的等级是4.0
- seq_parameter_set_id:表示当前的序列参数集id,通过该id号,pps图像参数集合可以引用其表示的sps参数。
- log2_max_frame_num_minus4:用于计算MaxFrameNum的数值
- pic_order_cnt_type:
- profile_idc:在H264中,通常定义了三种档次的profile
-
PPS:除了序列参数集SPS之外,H264还有另外一个重要的参数集合Picture Parameter Set图像参数集合(PPS,它的NALU是
00 00 00 01 68
)。一般情况PPS经常和SPS联合在一起保存到视频文件的头部里面。PPS具体的参数。 -
SEI帧:这是一种用于视频流传输中的额外附加信息,SEI是H264标准的一部分。SEI可以传输多种类型的信息。SEI信息通过H264码流传输到解码器,解码器通过解析SEI对视频进行后处理操作。SEI的NALU格式是:
00 00 00 01 06 05 +payload_size + uuid + payload_content
。 -
I帧:这是我们常说的关键帧,NALU:
00 00 00 01 65
,它不需要参考任何帧就可以拥有一副完整的画面。 -
P帧:P帧是前向预测帧,它需要根据本帧和相邻的前一帧(I帧或P帧)的不同点来压缩本帧数据。P帧需要不停地参考前面帧的数据才能够压缩本帧数据。
-
B帧:B帧属于双向预测的帧,它需要根据相邻的前一帧和后一帧来压缩本帧,换言之B帧只记录本帧和前后帧的差值。B帧的压缩比是三种帧里面最高的,压缩比能达到200:1。B帧常用在高清电影的和蓝光影响的录制。
3.GOP的概念
Group of Picture图像组,它是把一个图像序列中连续几个图像组成一个小组,它本质上是两个I帧的距离。通常,GOP长度越长P帧/B帧的数量就会越多,压缩比更高,画面质量越好,所以在音视频中经常会用GOP的长度来改善画面质量。
GOP分为两种:一种是闭合GOP,另一种是开放GOP。
闭合GOP是指不对外开放的GOP结构,它的特点是GOP内的帧不可以参考前后其他的帧,闭合GOP一般都是以I帧开头。
开放GOP的特点是:允许其内的帧参考其他GOP内的帧,一般在有B帧的情况下才会出现open-gop。例如:末尾的两个B帧需要依赖下一个GOP中的I帧进行解码。
**GOP的长度设置:一般是帧率的整数倍。**比如视频的帧率是30帧,则GOP的长度最好设置为30、60等。则把GOP设置为60、120等。
视频编码原理(三)
视频码率和视频码率控制模式
1.码率的概念
视频码率是数据传输时单位时间内传输的数据位数,对于视频来说视频码率的单位是kbps(千比特率)每秒。简单说码率就是采样率,码率越高画面质量精度越高,处理的文件或者码流就越接近原始文件。视频码率传输的大小跟分辨率是息息相关的。
分辨率 | 视频码率 |
---|---|
640*360 | >=800kbps |
1280*720 | >=1500kbps |
1920*1080 | >=2500kbps |
3840*2160 | >=25000kbps |
分辨率越高,它所使用的码率就越高。如果推流的分辨率是4K分辨率,则需要25M以上的码率才能够保证画面质量。
设置码率也不是越高越好,因为码率收到网络带宽的限制,假如网络带宽只有10M,那么码率也不能超过10M。除了兼顾画面质量还要看网络带宽的情况。
2.码率控制模式
码率控制指的是 利用编码中的一些特殊手段控制图像的压缩比例,使其画面质量在不同场景中保持一个最优的状态。RV1126的码率控制方式分三种:CBR固定码率控制模式、VBR可变码率控制模式、AVBR码率控制模式。
-
CBR指的是固定比特率,它指的是在统计时间内编码码率处于平稳状态。这种方式的特点是码率调节会相对比较缓慢,它不会跟着图像的质量的波动去改变码率。CBR控制模式一般用于网络流媒体视频编码。CBR的优点在于压缩速度非常快,并且码率很平稳不会出现码率跳变的情况。但缺点也很明显,就是它不会根据画面的波动对码率进行节省,这样的话会导致每秒空间的浪费。在RV1126的API里面,CBR一般用平均比特率去表示。
-
VBR指的是动态码率,它的作用是在统计时间内允许编码编率出现波动,这种波动可以使得编码图像质量变高。VBR的特点是它是随着图像的复杂程度的不同而变化。假设在编码到简单图像的时候它会节省码率,而如果编码到一些相对复杂的图像它的码率将会提升,VBR模式一般使用在DVD、蓝光、HD录播上面。VBR同样也有优缺点,VBR的优点是它的尽可能保证整个图像编码质量,利用VBR编码的图像很少会出现马赛克、画面丢失的情况。但缺点同样也很明显,使用VBR编码出来的图像它的体积是不固定的。所以,它会根据画面的复杂程度去决定视频文件的大小,另外还有的是使用VBR进行编码的时候它的编码算法复杂度会变高,这就会导致解码端的复杂度升高。
VBR内部有三个参数组成分别是MAX、ARG、MIN,这三个参数共同调节VBR码率的画面。比方说,现在的码率是8Mbps/s来计算,那么它一个小时的视频文件大小就是(8Mbps/8)KB * 3600 = 3600MB约等于3.6G,这是一般情况下的计算。假设画面里面出现大量的复杂画面,则VBR将会把8Mbps码率提高到MAX,若此时画面有一些静止画面,则会把码率降低到ARG水平。
-
AVBR全称叫适配式可变码率控制方式,它的很多思想跟VBR基本上是一致的。但它比VBR强大的一点是这种控制方式能够自动检测当前编码场景是静止画面还是运动画面。若当前画面是静止画面则会主动降低码率、若当前画面是运动画面则会把码率主动提升。AVBR适用于当前编码视频静止画面和运动画面频繁出现的场景,所以AVBR码率控制方式经常用在大型体育比赛上面。
RV1126视频编码模块
VENC模块
VENC模块是RV1126专门用于视频编码的模块,VENC模块提供了多种编码方式,分别是:H264/H265/MJPEG/JPEG。
//VENC通道属性结构体。
typedef struct rkVENC_CHN_ATTR_S {
VENC_ATTR_S stVencAttr; //编码器属性
VENC_RC_ATTR_S stRcAttr; //码率控制器属性
VENC_GOP_ATTR_S stGopAttr; //GOP属性
} VENC_CHN_ATTR_S;
-
VENC_CHN_ATTRS是管理整个VENC模块的管理结构体,这里面它包含三个VENC子类结构体 VENC_ATTR_S、VENC_RC_ATTR_S、VENC_GOP_ATTR_S
-
VENC_ATTR_S:编码器基础属性结构体,分辨率、编码器类型、Profile等等
typedef struct rkVENC_ATTR_S {
CODEC_TYPE_E enType; //编码协议类型
IMAGE_TYPE_E imageType; //输入图像类型
RK_U32 u32VirWidth; //stride宽度
RK_U32 u32VirHeight; //stride高度
RK_U32 u32Profile; //编码的等级
// H.264: 66: baseline; 77:MP; 100:HP;
// H.265: default:Main;
// Jpege/MJpege: default:Baseline
RK_BOOL bByFrame; // reserve
RK_U32 u32PicWidth; //编码图像宽度。以像素为单位
RK_U32 u32PicHeight; //编码图像高度。以像素为单位
VENC_ROTATION_E enRotation;//图像旋转角度
union {
VENC_ATTR_H264_S stAttrH264e; // attributes of H264e
VENC_ATTR_H265_S stAttrH265e; // attributes of H265e
VENC_ATTR_MJPEG_S stAttrMjpege; // attributes of Mjpeg
VENC_ATTR_JPEG_S stAttrJpege; // attributes of jpeg
};
} VENC_ATTR_S;
/*
enType:编码器协议类型,支持的视频编码类型包括RK_CODEC_TYPE_H264、RK_CODEC_TYPE_H265、RK_CODEC_TYPE_JPEG、RK_CODEC_TYPE_MJPEG
imageType: 输入图像类型,venc的imageType和VI的imageType必须保持一致
*/
- VENC_RC_ATTR_S结构体:编码通道码率控制器属性结构体。主要设置码率控制属性
typedef struct rkVENC_RC_ATTR_S {
VENC_RC_MODE_E enRcMode;//码率控制类型
union {
VENC_H264_CBR_S stH264Cbr;
VENC_H264_VBR_S stH264Vbr;
VENC_H264_AVBR_S stH264Avbr;
VENC_MJPEG_CBR_S stMjpegCbr;
VENC_MJPEG_VBR_S stMjpegVbr;
VENC_H265_CBR_S stH265Cbr;
VENC_H265_VBR_S stH265Vbr;
VENC_H265_AVBR_S stH265Avbr;
};
} VENC_RC_ATTR_S;
- VENC_GOP_ATTR_S结构体(编码器GOP属性结构体),主要是设置编码器的GOP属性
typedef struct rkVENC_GOP_ATTR_S {
VENC_GOP_MODE_E enGopMode; //编码 GOP 类型。
RK_U32 u32GopSize; //编码 GOP 大小
RK_S32 s32IPQpDelta; //I 帧相对 P 帧的 QP 差值
RK_U32 u32BgInterval; //长期参考帧的间隔
RK_S32 s32ViQpDelta; //虚拟 I 帧相对于普通 P 帧的 QP 差值
} VENC_GOP_ATTR_S;
多线程获取VENC的H264码流数据
描述:对RV1126进行CMOS摄像头采集,并进行VENC的视频编码处理,最终获取到H264码流。
分为四个步骤:分别是初始化VI模块、初始化VENC模块、绑定VI和VENC模块、多线程获取每一帧H264码流数据
#include <assert.h>
#include <fcntl.h>
#include <getopt.h>
#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"
#define CAMERA_PATH "rkispp_scale0"
#define CAMERA_ID 0
#define CAMERA_CHN 0
#define VENC_CHN 0
void *get_h264_venc_thread(void *args)
{
pthread_detach(pthread_self());//线程分离自动回收
FILE *h264_file = fopen("test_camera.h264","w+");
MEDIA_BUFFER mb;
while(1)
{
mb = RK_MPI_SYS_GetMediaBuffer(RK_ID_VENC,VENC_CHN,-1);
if(!mb){
printf("get venc mb failed\n");
break;
}
printf("get h264 frame...\n");
fwrite(RK_MPI_MB_GetPtr(mb),RK_MPI_MB_GetSize(mb),1,h264_file);
RK_MPI_MB_ReleaseBuffer(mb);
}
return NULL;
}
int