02-视频编码H264基础知识

1,405 阅读16分钟

视频编解码在音视频开发中的基础,我们在了解了编解码后,才可以对视频进行编码、传输、播放、存储、视频的封装和解封装也是要依赖编解码的,今天让我们一起学习视频的编码。

视频的发展到今天,视频的编解码还是主要以H264为主,在一些超高清的视频可能使用H265.下面就以H264进行讲解。

基础知识

为什么要编码?

  • 因为视频数据太大,不进行编码传输和存储所在空间都会很大。
  • 视频数据中,有很多冗余数据,把它们去掉人眼也不会发现。

在编码中如何压缩数据呢?

  • 帧内预测压缩,解决的是空域数据冗余问题。
  • 帧间预测压缩(运动估计与补偿),解决的是时域数据冗余问题。
  • 整数离散余弦变换(DCT),将空间上的相关性变为频域上无关的数据然后进行量化。
  • CABAC压缩。

经过压缩后的帧:

  • I帧:关键帧,采用帧内压缩技术。
  • P帧:向前参考帧,在压缩时,只参考前面已经处理的帧。采用帧间压缩技术。
  • B帧:双向参考帧,在压缩时,它即参考前而的帧,又参考它后面的帧。采用帧间压缩技术。

GOP 两个I帧之间是一个图像序列,在一个图像序列中只有一个I帧。如下图所示:

image.png

H264压缩技术

帧内预测压缩

一帧就是对应一个图像,把这个图像划分为很多宏块,再把宏块划分为多个子块,最后对子块进行预测得到更少的数据。

H264的基本原理其实非常简单,下我们就简单的描述一下H264压缩数据的过程。通过摄像头采集到的视频帧(按每秒 30 帧算),被送到 H264 编码器的缓冲区中。编码器先要为每一幅图片划分宏块。 以下面这张图为例:

image.png 划分宏块:H264默认是使用 16X16 大小的区域作为一个宏块,也可以划分成 8X8 大小。

image.png 划分好宏块后,计算宏块的像素值。

image.png 以此类推,计算一幅图像中每个宏块的像素值,所有宏块都处理完后如下面的样子。

image.png 继续划分子块:H264对比较平坦的图像使用 16X16 大小的宏块。但为了更高的压缩率,还可以在 16X16 的宏块上更划分出更小的子块。子块的大小可以是 8X16、 16X8、 8X8、 4X8、 8X4、 4X4非常的灵活。

image.png 上幅图中,红框内的 16X16 宏块中大部分是蓝色背景,而三只鹰的部分图像被划在了该宏块内,为了更好的处理三只鹰的部分图像,H264就在 16X16 的宏块内又划分出了多个子块。

image.png 这样再经过帧内压缩,可以得到更高效的数据。下图是分别使用mpeg-2和H264对上面宏块进行压缩后的结果。其中左半部分为MPEG-2子块划分后压缩的结果,右半部分为H264的子块划压缩后的结果,可以看出H264的划分方法更具优势。

image.png

对于这些数据,还可以通过帧内预测,数据更小。 在帧内预测模式中,预测块P是基于已编码重建块和当前块形成的。对亮度像素而言,P块用于4×4子块或者16×16宏块的相关操作。4×4亮度子块有9种可选预测模式,独立预测每一个4×4亮度子块,适用于带有大量细节的图像编码;16×16亮度块有4种预测模式,预测整个16×16亮度块,适用于平坦区域图像编码;色度块也有4种预测模式,类似于16×16亮度块预测模式。编码器通常选择使P块和编码块之间差异最小的预测模式。

下面举个例子,4*4亮度预测模式 4×4亮度块的上方和左方像素A~M为已编码和重构像素,用作编解码器中的预测参考像素。a~p为待预测像素,利用A~M值和9种模式实现。其中模式2(DC预测)根据A~M中已编码像素预测,而其余模式只有在所需预测像素全部提供才能使用。图6.15箭头表明了每种模式预测方向。对模式3~8,预测像素由A~M加权平均而得。例如,模式4中,d=round(B/4+C/2+D/4)。

image.png

image.png

image.png

其他的 88 1616 的也差不多,就不在这里细讲了,感兴趣的小伙伴可以去网上搜素一下,有很多的文章。

帧间预测压缩

帧内预测压缩解决了空间上数据冗余的问题,帧间压缩就是解决时间上数据冗余的问题。其中时间上的数据冗余是最大的。

为什么说时间上的冗余是最大的呢?假设摄像头每秒抓取30帧,这30帧的数据大部分情况下都是相关联的。也有可能不止30帧的的数据,可能几十帧,上百帧的数据都是关联特别密切的。

对于这些关联特别密切的帧,其实我们只需要保存一帧的数据,其它帧都可以通过这一帧再按某种规则预测出来,所以说视频数据在时间上的冗余是最多的。

为了达到相关帧通过预测的方法来压缩数据,就需要将视频帧进行分组。那么如何判定某些帧关系密切,可以划为一组呢?我们来看一下例子,下面是捕获的一组运动的台球的视频帧,台球从右上角滚到了左下角。

image.png H264编码器会按顺序,每次取出两幅相邻的帧进行宏块比较,计算两帧的相似度。

image.png 通过宏块扫描与宏块搜索可以发现这两个帧的关联度是非常高的。进而发现这一组帧的关联度都是非常高的。因此,上面这几帧就可以划分为一组。其算法是:在相邻几幅图像画面中,一般有差别的像素只有10%以内的点,亮度差值变化不超过2%,而色度差值的变化只有1%以内,我们认为这样的图可以分到一组。

在这样一组帧中,经过编码后,我们只保留第一帖的完整数据,其它帧都通过参考上一帧计算出来。我们称第一帧为IDR/I帧,其它帧我们称为P/B帧,这样编码后的数据帧组我们称为GOP

在H264编码器中将帧分组后,就要计算帧组内物体的运动矢量了。还以上面运动的台球视频帧为例,我们来看一下它是如何计算运动矢量的。

H264编码器首先按顺序从缓冲区头部取出两帧视频数据,然后进行宏块扫描。当发现其中一幅图片中有物体时,就在另一幅图的邻近位置(搜索窗口中)进行搜索。如果此时在另一幅图中找到该物体,那么就可以计算出物体的运动矢量了。下面这幅图就是搜索后的台球移动的位置。

image.png

通过上图中台球位置相差,就可以计算出台图运行的方向和距离。H264依次把每一帧中球移动的距离和方向都记录下来就成了下面的样子。

image.png 运动矢量计算出来后,将相同部分(也就是绿色部分)减去,就得到了补偿数据。我们最终只需要将补偿数据进行压缩保存,以后在解码时就可以恢复原图了。压缩补偿后的数据只需要记录很少的一点数据。如下所示:

image.png

我们把运动矢量与补偿称为帧间压缩技术,它解决的是视频帧在时间上的数据冗余。除了帧间压缩,帧内也要进行数据压缩,帧内数据压缩解决的是空间上的数据冗余。下面我们就来介绍一下帧内压缩技术。

上面啰啰嗦嗦把帧内预测压缩和帧间预测压缩讲完了。接下来就进入到了用代码完成案例了。

一些名词

帧率: 帧率(Frame Rate)是用于测量显示帧数的量度。所谓的测量单位为每秒显示帧数(frames per second,简称fps)或“赫兹”(Hz)。 每秒显示帧数(fps)或者帧率表示图形处理器处理场时每秒能够更新的次数。高帧率可以得到更流畅、更逼真的动画。一般来说,30fps就是可以接受的,但是将性能提升至60fps则可以明显提升交互感和逼真感,但是超过75fps就不容易察觉有明显的流畅度提升了。如果帧率超过屏幕刷新率,则只会浪费图像处理能力,因为监视器不能以这么快的速度更新,这样超过刷新率的帧率就浪费掉了。

分辨率: 视频分辨率是指视频成像产品所形成的图像大小或尺寸。

刷新率 刷新率是指屏幕每秒画面被刷新的次数,刷新率分为垂直刷新率和水平刷新率,一般提到的刷新率通常指垂直刷新率。垂直刷新率表示屏幕上图像每秒重绘多少次,也就是每秒屏幕刷新的次数,以Hz(赫兹)为单位。刷新率越高,图像就越稳定,图像显示就越自然清晰,对眼睛的影响也越小。刷新率越低,图像闪烁和抖动得就越厉害,眼睛疲劳得就越快。一般来说,如能达到80Hz以上的刷新率,就可以完全消除图像闪烁和抖动感,眼睛也不太容易疲劳。

编码格式 编码的目的是压缩数据量,采用编码算法压缩冗余数据。常用的编码格式有如下这两种。

  • MPEG(MPEG-2、MPEG-4)
  • H.26X(H.263、H.264/AVC、H.265/HEVC)

封装格式 把编码后的音视频数据以一定格式封装到一个容器,封装格式有MP4、MKV、AVI、TS等。

码率 码率也就是比特率,比特率是单位时间播放连续的媒体(如压缩后的音频或视频)的比特数量。比特率越高,带宽消耗得越多。比特(bit)就是二进制里面最小的单位,要么是0,要么是1。 文件大小(b)=码率(b/s)×时长(s)

此处提出一个问题,是码率越大,画质越好,视频越流畅吗?这是错误的说法,实际上视频质量和码率、编码算法都有关系。

DTS: 即Decode Time Stamp,主要用于标示读入内存中的比特流在什么时候开始送入解码器中进行解码。

PTS: 即Presentation Time Stamp,主要用于度量解码后的视频帧什么时候被显示出来。

YUV: 也被称作YCrCb,是被欧洲电视系统所采用的一种颜色编码方法(属于PAL),是PAL和SECAM模拟彩色电视制式采用的颜色空间模型。其中的Y、U、V几个字母不是英文单词的首字母,其中Y代表亮度,UV代表色差,U和V是构成颜色的两个分量。在Android 里面一般是使用YUV420.

RGB: 是一种颜色空间模型,通过对红(R)、绿(G)、蓝(B)3个颜色通道的变化以及它们相互之间的叠加来得到各式各样的颜色,RGB即代表红、绿、蓝3个通道的颜色。

为什么视频的数据使用YUV 而不是 RGB?

  • 因为YUV 数据更小,拿YUV420举例子

image.png 可以看到使用YUV 数据量少了一半。

H264数据结构

H264编码分层
  • NAL层:(Network Abstraction Layer,视频数据网络抽象层) : 它的作用是H264只要在网络上传输,在传输的过程每个包以太网是1500字节,而H264的帧往往会大于1500字节,所以要进行拆包,将一个帧拆成多个包进行传输,所有的拆包或者组包都是通过NAL层去处理的。
  • VCL层:(Video Coding Layer,视频数据编码层) : 对视频原始数据进行压缩

H264是一种码流 类似于一种不见头,也不见尾的一条河流。如何从和流中取到自己想要的数据呢,在H264的标砖中有这样的一个封装格式叫做"Annex-B"的字节流格式。 它是H264编码的主要字节流格式。几乎市面上的编码器是以这种格式进行输出的。起始码0x 00 00 00 01 或者 0x 00 00 01 作为分隔符

两个 0x 00 00 00 01之间的字节数据 是表示一个NAL Unit

image.png

原始的NALU单元组成:[start code] + [NALU header] + [NALU payload];

image.png NAL Header 的组成为: forbidden_zero_bit(1bit) + nal_ref_idc(2bit) + nal_unit_type(5bit)

  • forbidden_zero_bit:禁止位,初始为0,当网络发现NAL单元有比特错误时可设置该比特为1,以便接收方纠错或丢掉该单元。
  • nal_ref_idc:nal重要性指示,标志该NAL单元的重要性,值越大,越重要,解码器在解码处理不过来的时候,可以丢掉重要性为0的NALU。
  • nal_unit_type:NALU类型取值如下表所示:

image.png

image.png

image.png 看看一个例子:

00 00 00 01 为起始符,67 即 nal_unit_type。

0x67的二进制是 0110 0111forbidden_zero_bit(1bit) = 0nal_ref_idc(2bit) = 3nal_unit_type(5bit) = 7;即 SPS 类型。

通常情况我们看到的NLAU类型就是SPS、PPS、SEI、IDR的slice、非IDR这几种。

SODB 和 RBSP 关系: SODB(String Of Data Bits):最原始的编码数据RBSP, 长度不一定是8的倍数,此时需要对齐.  RBSP: 在SODB的后面添加了结尾比特(RBSP trailing bits 一个bit“1”)若干比特“0”,以便字节对齐。

image.png

我们知道码流是由一个个的NAL Unit组成的,NALU是由NALU头和RBSP数据组成,而RBSP可能是SPS,PPS,Slice或SEI,目前我们这里SEI不会出现,而且SPS位于第一个NALU,PPS位于第二个NALU,其他就是Slice(严谨点区分的话可以把IDR等等再分出来)了。

这其中NALU的RBSP除了能承载真实的视频压缩数据,还能传输编码器的配置信息,其中能传输视频压缩数据的为slice。

我们就可以这样理解:

  • 一个h264 数据由多个NALU组成
  • 一个NALU 有 起始码0x 00 00 01 或0x 00 00 00 01 和 slice 组成
  • 一个slice 就是真实的压缩数据了,里面有很多宏块的数据
  • 一个宏块数据里面又有 宏块类型、宏块预测数据和残差数据组成 slice 也有类型:

image.png

H264编码

使用的是哥伦布指数编码。

为什么使用哥伦布编码?

  • 因为音视频的数据很多,但是每个数据量比较小,使用哥伦布编码可以用更少的位数就能表达完整的意思,所以h264采用哥伦布编码的方式。

指数哥伦布码(Exponential-Golomb code, 即Exp-Golomb code)是熵编码的一种编码方式,正常来说,可以拓展位k阶,但是在H264中使用的是0阶指数哥伦布编码,在H.264中使用ue(v)表示0阶无符号指数哥伦布编码的解码过程,用se(v)表示0阶有符号指数哥伦布编码过程。

0阶无符号指数哥伦布编码过程

0阶无符号指数哥伦布编码最后生成的比特串格式为"前缀1后缀",前缀和后缀的长度是相同的。

假如我们待编码的数字codeNum = 4,0阶无符号指数哥伦布编码的步骤如下:

(1)将数字以二进制写出,4的二进制为100,因为0阶指数哥伦布编码所有不用去掉低位

(2)将上面的二进制+1,100加1为101,留下的比特数为3,3-1=2,所有需要增加前导0的个数为2

(3)因为第一步没有去掉,所有这一步不进行任何操作,最终生成的比特串为00101

0阶指数哥伦布编码可以简化为如下步骤

(1)将codeNum+1,4+1=5

(2)将加1后的数字用二进制表示,5的二进制位101,1后缀=101,后缀位01,长度2

(3)前缀与后缀长度相同,在前面加上2个0

下面对不同codeNum进行编码结果

codeNumcodeNum+1codeNum+1的二进制需补前缀0的个数编码后的比特串(红色表示补的前缀0)
01101
12101(0)010
23111(0)011
341002(00)00100
451012(00)00101
561102(00)00110
671112(00)00111

0阶无符号指数哥伦布编码的解析过程如下

(1)找到第一个不为0的bit,并记录总共找到了0的个数(num),这个时候读到的这个bit肯定是1

(2)然后读num个后缀

(3)1后缀转变成十进制就是原来的codeNum,codeNum = (1 <<i) + 后缀(十进制) - 1;

比如比特串的二进制为:00111,首先找到第一个不为0的比特,前面0的个数为2,然后再读2个后缀11,11十进制为3,这个时候codeNum = (1 << 2) + 3 - 1 = 4 + 3 - 1 = 6

总结

  • H264 的压缩原理
  • 一些名词的解释
  • H264 的数据结构
  • H264 使用 0阶无符号指数哥伦布编码