解析H264视频编码原理——从孙艺珍的电影说起(一)

3,563 阅读24分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第3天,点击查看活动详情

更多博文,请看音视频系统学习的浪漫马车之总目录

实践项目: 介绍一个自己刚出炉的安卓音视频播放录制开源项目

视频理论基础:
视频基础知识扫盲
音视频开发基础知识之YUV颜色编码
解析H264视频编码原理——从孙艺珍的电影说起(一)
解析H264视频编码原理——从孙艺珍的电影说起(二)
H264码流结构一探究竟

Android平台MediaCodec系列:
Android硬编解码利器MediaCodec解析——从猪肉餐馆的故事讲起(一)
Android硬编解码工具MediaCodec解析——从猪肉餐馆的故事讲起(二)
Android硬编解码工具MediaCodec解析——从猪肉餐馆的故事讲起(三)

轻松入门OpenGL系列
一看就懂的OpenGL ES教程——图形渲染管线的那些事
一看就懂的OpenGL ES教程——再谈OpenGL工作机制
一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(一)
一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(二)
一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(三)
一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(四)
一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(五)
一看就懂的OpenGL ES教程——缓冲对象优化程序(一)
一看就懂的OpenGL ES教程——缓冲对象优化程序(二)
一看就懂的OpenGL ES教程——临摹画手的浪漫之纹理映射(理论篇)

当天边那颗星出现
你可知我又开始想念
有多少爱恋只能遥遥相望
就像月光洒向海面

每次听到李健老师这首歌出神入化的歌声,我就马上浮起《电影假如爱有天意》的画面,而现在的电影绝大部分都是由经过编码压缩的,所以视频编码就成为了视频开发非常关键的一环节。

上一篇 音视频入门之YUV颜色编码,介绍了视频的基本颜色编码方式,有了颜色编码的铺垫,那么就可以很自然地进入视频开发的主题地带了~

从今天开始讲下视频编码技术,计划用两篇博文讲解,第一篇讲解编码基本原理,第二篇讲解具体的码流结构,博文内容很多在网上其他博客不一定能够搜索到,并且尽量用工具和图像提供可视化的呈现以帮助大家更好理解。这两篇博文可以说将是 音视频系统学习的浪漫马车之总目录 系列非常重要的2篇博文,它们是视频理论知识最重要的部分,为后面视频的开发奠定最重要的基础。只有对编码原理和码流理解得足够彻底,才能做好开发工作,因为视频的开发不再是简单地调用api的开发,如果理解不透彻,遇到问题根本无从下手,甚至连api都不知道要怎么使用

这里的编码可以理解为压缩,就是将原始的yuv数据压缩为体积更小的数据。今天只打算将视频编码技术的基本原理讲一下,不涉及具体数学知识,从一个宏观角度去看整个视频编码流程。

上一篇音视频开发基础知识之YUV颜色编码 我们已经知道在视频中的像素是如何表示的,那么我们假设有一个电影视频,分辨率是 1080P,帧率是 25fps,并且时长是 2 小时,如果是yuv420的格式的话,一个像素为1.5字节,那么如果不做视频压缩的话,它的大小是 1920 x 1080 x 1.5 x 25 x 2 x 3600 = 521.4G,一台电脑能存放几部电影呢?如果在网络传输,那么对流量和带宽的消耗可是非常大的(你要知道,在同个时间点,你们小区在看片的人就已经很多了),所以,为了满足广大群众的需求,对视频进行压缩就显示非常有必要,于是视频压缩技术就应运而生。

H.264简介

说的视频编码技术,就不得不提到大名鼎鼎的H.264编码。目前市面常见的编码标准有H264、H265、VP8、VP9 和 AV1,而其中用的最普遍的视频编码就是H.264。

H264和H265都是国际标准化组织(ISO)和国际电信联盟(ITU)开发的编码标准,而VP8、VP9 和 AV1是谷歌开发的编码标准,H264 和 H265 是需要专利费的,所以VP8、VP9 和 AV1(都是免费)也是谷歌为了对抗他们高昂专利费而开发出来的。

由于H.264是目前最常用的编码标准,所以主要就来介绍它。H.264是国际标准化组织(ISO)和国际电信联盟(ITU)共同提出的继MPEG4之后的新一代数字视频压缩格式,也可以看做是国际电信联盟(ITU)提出的h26x系列和国际电信联盟(ITU)提出的MPEG系列斗争多年最后决定化干戈为玉帛,为共同造福人类而努力的产物。

这是2家组织的一个斗争转合作的大致历史,各位了解以下即可。 在这里插入图片描述 接下来,就是介绍H264的正题部分了。

视频编码基本原理

编码总体思路

既然叫做压缩,那么肯定是要去除冗余信息的,一般来说冗余信息要么是有重复多余的,可以直接丢弃或者换成另一种更省空间的方式来表达,要么是人感知不敏感,即使去掉一些信息,人也很难感知到。对于我么 Android开发来说,最熟悉的压缩莫过于Bitmap的压缩了,常见2种,一是压缩分辨率,像Android常见的图片框架都是根据控件大小研所图片分辨率的,这个就是对应去除重复多余的信息,一种是质量压缩,对应去掉一些人感知不敏感的信息。那么视频也是有类似的冗余信息的:

  1. 空间冗余,即相邻的像素往往很相似。
  2. 时间冗余,即相邻的帧的内容往往很相似。
  3. 视觉冗余,即人眼感知不敏感的信息。

H264压缩技术正是针对以上冗余信息进行一一攻破,主要采用了以下几种方法对视频数据进行压缩。最主要的步骤包括:

  1. 帧内预测压缩:解决的是空域数据冗余问题。   
  2. 帧间预测压缩(运动估计与补偿):解决的是时域数据冗余问题。
  3. 整数离散余弦变换(DCT),将空间上的相关性变为频域上无关的数据然后进行量化,解决视觉冗余问题。   
  4. 熵编码:真正的编码环节,将前面3部处理得到的数据使用编码算法编码为最终的码流。

详细请继续阅读之后部分:

帧内预测

空间冗余

一幅图像中相邻像素的亮度和色度信息是比较接近的,并且亮度和色度信息也是逐渐变化的,不太会出现突变。也就是说,图像具有空间相关性。 利用这种相关性,视频压缩就可以去除空间冗余信息。

回到开头提及的电影《假如爱有天意》中很喜欢的开头那段孙艺珍的画面:

在这里插入图片描述 随手圈出几个区域都是亮度和色度信息是比较接近的,那么这些区域,是不是就可以不用完整数据记录,而只要使用一小部分数据记录就可以表达全部呢? 在这里插入图片描述

比如android开发中的渐变色,我们并不需要指定整个图像全部像素数据,而只是记录开头和结束以及中间变化的颜色,加上渐变位置以及渐变方向

在这里插入图片描述

而视频也是利用了类似的方法去除冗余信息,叫做帧内预测,即帧内预测通过利用已经编码的相邻像素的值来预测待编码的像素值,最后达到减少空间冗余的目的。

具体预测方法

整体思路是利用一帧图像中已经编码部分来预测尚未编码部分图像,实际值和预测值之间的差别叫做残差。实际上真正编码的是残差数据,因为残差一般比较小,所以对残差编码比对实际数据编码会小很多。

所谓物以类聚人以群分,分而治之的思想又发挥重要作用了。为了可以利用编码部分来预测尚未编码部分图像,所以需要根据具体情况对一帧图像划分为若干个部分, 每个部分叫做块,其中某些块可以预测另外一些块。H264中对一帧图像划分为宏块的方式来分别进行帧内预测,宏块可以预测相邻的宏块,那么同个宏块的像素就使用一种预测模式。那么何为宏块呢?

一张图片

比如一帧图像如下: 在这里插入图片描述 H264默认是使用 16X16像素大小的区域作为一个宏块,其中亮度块为 16 x 16,色度块为 8 x 8,一个亮度块y对应2个色度块(uv),帧内预测中亮度块和色度块是分开独立进行预测的,比如左上角区域:

在这里插入图片描述 H264对比较平坦的图像使用 16X16 大小的宏块。但为了更高的压缩率,在细节复杂的地方,还可以在 16X16 的宏块上更划分出更小的子块。子块的大小可以是 8X16、 16X8、 8X8、 4X8、 8X4、 4X4,非常的灵活。

还是孙艺珍,红色块就是16*16的宏块,绿色剪头指的就是进一步划分的子块:

在这里插入图片描述

对于4*4的宏块,帧内预测模式总共有 9 个。其中有 8 种方向模式和一种 DC 模式,接下来简单介绍下各种帧内预测模式:

1.Vertical 模式 Vertical 模式就是指,当前编码亮度块的每一列的像素值,都是复制上边已经编码块的最下面那一行的对应位置的像素值。
在这里插入图片描述

2.Horizontal 模式 Horizontal 模式就是指,当前编码亮度块的每一行的像素值,都是复制左边已经编码块的最右边那一列的对应位置的像素值
在这里插入图片描述

3.DC 模式 DC 模式就是指,当前编码亮度块的每一个像素值,是上边已经编码块的最下面那一行和左边已编码块右边最后一列的所有像素值的平均值,所以DC 模式预测得到的块中每一个像素值都是一样的。
在这里插入图片描述

4.Diagonal Down-Left 模式 Diagonal Down-Left 模式是上边块和右上块的像素通过插值得到。如果上边块和右上块不存在则该模式无效。
在这里插入图片描述

5.Diagonal Down-Right 模式 Diagonal Down-Right 模式需要通过上边块、左边块和左上角对角的像素通过插值得到。如果这三个有一个不存在则该模式无效。
在这里插入图片描述

6.Vertical-Right 模式 Vertical-Right 模式是需要通过上边块、左边块以及左上角对角的像素插值得到的。

在这里插入图片描述

7.Horizontal-Down 模式 Horizontal-Down 模式需要通过上边块、左边块以及左上角对角的像素插值得到。必须要这三个都有效才能使用,否则该模式无效。

在这里插入图片描述

8.Vertical-Left 模式 Vertical-Left 模式是需要通过上边块和右上块最下面一行的像素通过插值得到。

在这里插入图片描述

9.Horizontal-Up 模式 Horizontal-Up 模式是需要通过左边块的像素通过插值得到的。

在这里插入图片描述

总结一下就是:
在这里插入图片描述

16 * 16和8 * 8的宏块预测模式一样,都是有4种帧内预测模式:

在这里插入图片描述 前面三种在4 * 4宏块预测模式中已经存在,只有最后一种plane是4 * 4宏块预测模式没有的。

plane预测块的每一个像素值,都是将上边已编码块的最下面那一行,和左边已编码块右边最后一列的像素值经过一定算法计算得到。

每一个宏块只能用一种预测模式,那如何选择呢?具体算法很复杂,大概思路就是对于每一个块或者子块,我们可以得到预测块,再用实际待编码的块减去预测块就可以得到残差块。然后在不同场景下根据不同的算法对残差块进行计算得到最优的预测模式

所谓工欲善其事必先利其器,可能上面基本都是理论的东西,看起来还是有点雾里看花,那么可以使用H264Visa这种优秀的视频查看工具来查看一个H264文件具体编码的数据,真真切切感受编码是如何进行的。

这里有一段抖音某个红人的视频:

在这里插入图片描述 用FFmpeg转为H264文件(具体怎么转后面讲FFmpeg再讲),用H264Visa打开:

暂时只看第一帧(因为第一帧必定是I帧,即只有帧内预测的帧,关于I帧后面会详细讲解) 在这里插入图片描述

上面一个个数字对应每个宏块的具体图像数值,可以表示该宏块的量化参数QP(关于量化,后面也会讲到),也可以切换为表示宏块码流大小。

选中其中一个宏块,可以查看宏块基本信息:

在这里插入图片描述 图中右边红框部分可以看到宏块的坐标、宏块大小、宏块类型以及QP等信息。可以看到该宏块是4*4、I宏块(仅使用帧内预测)、坐标为(16,48)。

可以直接看到该宏块的所用到的预测模式,可以看到亮度(Y)和色度(UV)是分别预测的,所用的宏块大小也是不一样的。

在这里插入图片描述

这是预测帧,可以看到和原始帧还是有差别的,看起来更模糊了些:

在这里插入图片描述原始帧和预测帧相减得到的就是残差帧了:

在这里插入图片描述 这可以说已经是面目全非了,只剩一点点轮廓,而这些残差数据将和相关预测信息全部一起送到后面步骤,即变换和量化、编码处理后打入码流。

用一种更为量化的方式查看该宏块,直接查看该宏块的原始帧yuv数据: 在这里插入图片描述 残差帧的yuv数据: 在这里插入图片描述 可以看出经过参数帧大部分像素数值变成0了,这部分数据将传入下一个阶段,而正是利用残差数据拥有大量0的特点(后面的变换量化还会增加0的个数),这一部分数据通过编码后放入码流将大大减少码流大小。

帧间预测

时间冗余

在一个视频中,一般前后两帧图像往往变化比较小,这就是视频的时间相关性。 而视频一般往往一秒会播放20-30帧,所以存在大量重复的图像数据,所以会有巨大的压缩空间。

还是孙艺珍在这部电影中的连续5帧画面: 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

在这里插入图片描述 很明显,5帧画面差别非常小,那么很容易想到,能不能只记录第一帧画面数据,然后后面几帧只记录差别数据呢?

视频编码中,就是通过在已经编码的帧里面找到一个块来预测待编码块的像素,从而达到减少时间冗余的目的,官方名称为:帧间预测。

具体来说,就是在前面某一帧找到一个内容很接近的块,那么只要再加上运动矢量,就可以表示当前的块。

比如这是前后两帧(图来源于:帧间预测:如何减少时间冗余),背景的树木都是静止的,只有汽车是移动的,对整个图片建立坐标系,那么汽车的移动就可以用运动矢量(-163,0)(16是因为每个宏块为1616)表示:

在这里插入图片描述

回到现实视频例子,比如前面一帧孙艺珍的左手是一个块:
在这里插入图片描述

到了后面若干帧之后,这个块的位置发生了移动,但是内容基本一致(存在较小的残差数据)
在这里插入图片描述

所以后面的这一帧就不必记录左手这一个块的图像信息了,只要记录运动矢量、所参考的宏块标识和残差数据即可,这样比直接把后面一帧的左手的图像数据编码进入码流要小很多。前面一帧也就是后面一帧的参考帧。

类似加上下图的绿色剪头:

在这里插入图片描述 用专业分析软件打开看看运动矢量分析图:

在这里插入图片描述

可以看到大量密密麻麻让人犯密集恐惧症的细线,表示运动矢量,而我们关注的孙艺珍左手的块(绿色框),上面的红色线方向大体和移动方向一致。

运动估计

帧间预测一个重要概念就是运动估计,就是寻找当前编码的块在已编码图像中的最佳对应块

如图,假设P为当前编码帧,Pr为参考帧,当前编码块为B,则运动估计要做的就是在Pr中寻找与B相减残差最小的块Br,Br就叫做B的最佳匹配块。 在这里插入图片描述

放到孙艺珍视频例子中,运动估计就是寻找上面第二幅图中孙艺珍左手在第一张图中对应的最佳图像块。当然我们人眼可以很快找到后面一帧左手和前面一帧左手是最佳匹配,但是计算机可就不好找了,所以帧间预测这里的难点在于如何找到最佳的参考块来预测当前块,这里涉及很多复杂的运动搜索算法,主要有这两种算法:

(1)全局搜索算法。该方法是把搜索区域内所有的像素块逐个与当前宏块进行比较,查找具有最小匹配误差的一个像素块为匹配块。这一方法的好处是可以找到最佳的匹配块,坏处是速度太慢。目前全局搜索算法极少使用。
(2)快速搜索算法。该方法按照一定的数学规则进行匹配块的搜索。这一方法的好处是速度快,坏处是可能只能得到次最佳的匹配块。

具体可以看下帧间预测:如何减少时间冗余,这里重点是要知道I帧,P帧和B帧这几个概念。

GOP

前面已经介绍了帧内预测和帧间预测2种压缩技术,结合具体视频内容,为了得到更好的压缩率,我们可以对不同的帧使用不同的预测压缩方式。我们知道一个视频会有若干个场景,即在一个内容为相似的背景或者空间内有着相似的人物或者物体,而每个场景会由若干帧组成,而这些帧往往是强相关的,最适合进行帧间预测,这里一个场景的帧的组合,就叫做GOP(group of pictures)。

比如刚才孙艺珍是在屋子的窗前,坐着打开收藏盒:在这里插入图片描述

这个场景持续了几秒,播放了好多好多帧,然后切到孙艺珍接电话的场景: 在这里插入图片描述
这里就是2个视频场景GOP,根据场景内帧的强相关性和帧预测理论,所以可以对一个场景的首帧进行帧内预测去除空间冗余,然后后面的帧来参考首帧,再后面的帧再来参考前面已经预测出来的帧,为了更好地降低压缩率,还可以不止一个参考帧,比如一帧可以参考前后2帧,于是就出现了I帧,P帧和B帧。

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

由于P、B帧同时包含帧间帧内预测,所以宏块同样也有划分I、P、B宏块,I宏块用来做帧内预测,宏块用来做向前参考帧的预测,B宏块用来做双向参考帧预测。I帧仅包含I宏块,P帧包含P宏块和I宏块,B帧包含B宏块和I宏块。

在这里插入图片描述 那么GOP究竟是什么呢?

GOP是一个图像序列,一般可以理解为一个场景的若干个帧,比如一段电影片段在主角在公园里,因为整体画面差别不大,所以可以放入一个gop中,接下来切到主角在室内了,那么此时就重新开始另一个gop了。在一个图像序列中只有一个I帧。如下图所示: 在这里插入图片描述 每个GOP首帧就是I帧,它采用帧内预测,是一个全帧压缩编码帧,描述了图像背景和运动主体的详情,不需要考虑运动矢量,解码时仅用I 帧的数据就可重构完整图像,是P帧和B帧的参考帧

后面的帧会使用帧间预测技术参考I帧或之后编码出来的P帧,P帧表示的是这一帧跟之前的一个关键帧(或P帧)的差别,P帧没有完整画面数据,只有与前一帧的画面差别的数据。P帧是以 I 帧或前面的P帧为参考帧,在 参考帧中找出P帧“某点”的预测值和运动矢量,取预测差值和运动矢量一起传送。在接收端根据运行矢量从 I 帧找出P帧“某点”的预测值并与差值相加以得到P帧“某点”样值,从而可得到完整的P帧。

B帧是双向差别帧,和P帧的主要区别在于它是参考前后2帧,也就是B帧记录的是本帧与前后帧的差别。 B帧以前面的 I 或P帧和后面的P帧为参考帧,“找出”B帧“某点”的预测值和两个运动矢量,并取预测差值和运动矢量传送。不过正是由于需要参考后面的P帧,所以B帧虽然提高了压缩率,但是也带来了编码延迟问题(需要等后面一帧编码好才能编码)。

B帧的双向预测图: 在这里插入图片描述

由于帧间预测需要参考编码好的帧,所以需要缓存队列缓存编码好的再解码重建的帧来给后续编码的帧作为参考帧。那为什么不直接拿原始宏块而要专门重新解码编码好的宏块做为参考呢?关键点在于为了和解码流程保持一致的参考宏块,因为编码出来的宏块和解码重建的宏块并非完全一致的,所以如果帧间预测在编码端和解码器端参考帧不一致,就会出错。

比如B帧需要前后参考帧,所以需要2个缓存队列: 在这里插入图片描述

这里由于B帧的引入,会导致一个现象,就是编码的帧顺序和播放的帧顺序会不一致在这里插入图片描述 所以也衍生了pts和dts2个时间戳,这在后面的代码开发是一种很需要注意的点,不然用错出问题了都不知道什么原因。

打个比方,就比如有一排人,每人说一句话,他们每个人要说的东西比较接近,但是要求说话字数尽量少。为了使得总说话的数量尽量少,第一个人说了完整的一句话(尽量概括),后面的人只说了相对第一个人说的话有区别的部分,而还有的人说了和前后2个人区别的部分,使得总的需要说的话更少。这样子只要推导一下就能推导出他们每个人完整的话的内容。

显然I帧压缩率最低,其次是P帧,B帧压缩率最高。

这里还要提到一种特殊的I帧,叫做IDR帧,因为帧间预测技术总是不断参考前面已经编码成功的帧,那如果其中一帧编码出错,那可能造成后面帧的错误传递,比如第一个人说话说错了一个字,那么后面参考他的人基于他的话说了区别部分,那么推导出的原话必然会出错。所以IDR帧的作用就是用来阻断错误传递的,它就限制后面同个GOP的帧不能参考前面的GOP的帧,这样一旦某一帧出错,错误也仅限于一个GOP内,不会传入下一个GOP。

Gop越长,编码的I帧就越少,压缩率越高,但是视频质量往往越差。那么Gop设置多长合适呢? 本地视频文件一般根据具体场景选择压缩率和质量的折中方案。直播流一般是帧率的倍数,一般不会设置过长,因为如果Gop太长,则进入直播间的时候等待I帧的时间会越长,导致出现黑屏卡顿的用户体验问题(因为必须有I帧才能解码整个Gop)。另外Gop太长,点播场景时进行视频的 seek 操作就会不方便。

继续用H264Visa分析前面抖音那段视频,现在看第三帧: 在这里插入图片描述 可以看到现在选中的是一个B帧的16*16的B宏块,根据前面说的,它支持帧内和帧间预测。

在这里插入图片描述 打开预测信息选项卡,可以看到具体的运动矢量数据(图右侧的MV(motion vector))是非常丰富的,因为此时的宏块是在主播的手的影像,视频此时手正处于运动中。

作为对比,看另一块静止的宏块:

在这里插入图片描述 可以看出运动矢量都是0。

注意到该宏块是属于skip类型,即不会放入码流的,为什么呢?因为它是不运动的宏块,和所参考的帧的对应参考宏块是完全一样的,所以当前帧是没有必要存放这一宏块数据的。

在这里插入图片描述

总结

由于篇幅关系,视频编码原理的前半部分就到这,本文主要是简单介绍了H264历史背景,并着重介绍了H264中的帧内帧间预测理论基础和具体方法,并通过工具查看了实际相关数据,下篇文章将继续讲解H264编码剩余部分——变换量化、熵编码:解析视频编码原理——从孙艺珍的电影说起(二)

由于视频编码技术复杂,本人水平有限,有错误的地方也请各位指正哈~

参考文章:
编码原理:视频究竟是怎么编码压缩的?
帧内预测:如何减少空间冗余?
帧间预测:如何减少时间冗余?
变换量化:如何减少视觉冗余?
H264 I帧 P帧 B帧
《深入理解视频编解码技术》
《H.264和MPEG-4视频压缩 新一代多媒体的视频编码技术》

原创不易,如果觉得本文对自己有帮助,别忘了随手点赞和关注,这也是我创作的最大动力~