# 04|编码原理:视频究竟是怎么编码压缩的? 你好,我是李江。今天我们一起来聊一聊视频编码。 说到视频,我们首先想到的可能就是占内存。我们知道一个视频是由一连串图像序列组成的,视频中图像一般是YUV格式。现在,我们假设有一个电影视频,分辨率是1080P,帧率是25fps,并且时长是2小时,如果不做视频压缩的话,它的大小是1920 x 1080 x 1.5 x 25 x 2 x 3600 = 521.4G。而我们的电脑一般是500G的硬盘,那就连2部电影都放不下了。如果是在视频通话场景下的话,按照这个大小去传输视频数据,对流量和带宽资源的消耗也是非常大的,并且如此大的数据发送对网络要求也非常高,很显然我们是接受不了的。因此,做视频编码压缩就非常有必要。 那么,接下来我就深入讲讲视频编码,带你从预测编码、变换编码、熵编码等方面,系统了解下视频编码的原理。相信这节课过后,你会对视频编码有一个全面的了解。 ## 视频编码的原理 视频编码是对一帧帧图像来进行的。一般我们所熟知的彩色图像的格式是RGB的,即用红绿蓝三个分量的组合来表示所有颜色。但是,RGB三个颜色是有相关性的,为了去掉这个相关性,减少需要编码的信息量,我们通常会把RGB转换成YUV,也就是 **1个亮度分量和2个色度分量**。 另外,人眼对于亮度信息更加敏感,而对于色度信息稍弱,所以视频编码是将Y分量和UV分量分开来编码的。 而对于每一帧图像,又是划分成一个个块来进行编码的,**这一个个块在H264中叫做宏块**,而在VP9、AV1中称之为超级块,其实概念是一样的。宏块大小一般是16x16(H264、VP8),32x32(H265、VP9),64x64(H265、VP9、AV1),128x128(AV1)这几种。这里提到的H264、H265、VP8、VP9和AV1都是市面上常见的编码标准,下面我会介绍,这里就不再详细讲述。 **图像一般都是有数据冗余的**,主要包括以下4种: * **空间冗余。**比如说将一帧图像划分成一个个16x16的块之后,相邻的块很多时候都有比较明显的相似性,这种就叫空间冗余。 * **时间冗余。**一个帧率为25fps的视频中前后两帧图像相差只有40ms,两张图像的变化是比较小的,相似性很高,这种叫做时间冗余。 * **视觉冗余。**我们的眼睛是有视觉灵敏度这个东西的。人的眼睛对于图像中高频信息的敏感度是小于低频信息的。有的时候去除图像中的一些高频信息,人眼看起来跟不去除高频信息差别不大,这种叫做视觉冗余。 * **信息熵冗余。**我们一般会使用Zip等压缩工具去压缩文件,将文件大小减小,这个对于图像来说也是可以做的,这种冗余叫做信息熵冗余。 视频编码就是通过减少上述4种冗余来达到压缩视频的目的。接下来我们就一起来慢慢剥开视频编码这个“洋葱”吧。 对于一个YUV图像,我们把它划分成一个个16x16的宏块(以H264为例),Y、U、V分量的大小分别是16x16、8x8、8x8。这里我们只对Y分量进行分析(U、V分量同理)。假设Y分量这16x16个像素就是一个个数字,我们从左上角开始之字形扫描每一个像素值,则可以得到一个“像素串”。如下图所示: ![](https://static001.geekbang.org/resource/image/6c/16/6c879e4971995d927e9de46616256b16.jpg?wh=1280x720) 如果你是程序员的话,你肯定做过一个压缩字符串的编程题目,就是将 “aaaabbbccccc” 压缩成 “4a3b5c”,字符串由13个字节压缩到7个字节,这个叫做**行程编码**。如果我们对图像宏块扫描出来的这个“像素串”做同样的行程编码操作,是不是也有可能减小图像块呢? 在对“像素串”行程编码之前,我们先回过头来看看另一个行程编码的例子。如果刚才编程题的字符串是 “abcdabcdabcd” 的话,那么编码之后就会是 “1a1b1c1d1a1b1c1d1a1b1c1d”。字符串的大小从13字节变成了25字节,还变大了。 我们发现如果想要达到压缩的目的,我们必须要使得编码前的字符串中出现比较多连续相同的字符。这对于图像块也是一样的。我们需要使得扫描出来的“像素串”,也尽量出现连续相同的像素值,最好是一连串数字很小(比如0)的“像素串”,因为0在二进制中只占1个位就可以了。 这个地方你可能会有疑惑,0也是至少要一个字节存储,需要8个位,怎么会是1个位呢?这个有的编码算法是可以做到的,比如指数哥伦布编码,它就可以做到0只占用一个位。事实上,算术编码可以做到一个符号只占用0点几个位,连一个位都不用,这里不详细展开了,感兴趣的话你可以去查阅下资料。 那我们**如何做到将这串像素值变成有很多0的“像素串”呢**? 首先第一步,**我们通过减少图像块的空间冗余和时间冗余来接近这个目标**。刚才我们也说到,图像内部相邻宏块之间有很多相似性,并且两张图像之间也有很多相似性。因此,根据图像的这个特点,我们可以在编码的时候进行**帧内预测和帧间预测**。 帧内预测就是在当前编码图像内部已经编码完成的块中找到与将要编码的块相邻的块。一般就是即将编码块的左边块、上边块、左上角块和右上角块,通过将这些块与编码块相邻的像素经过多种不同的算法得到多个不同的预测块。 然后我们再用编码块减去每一个预测块得到一个个残差块。最后,我们取这些算法得到的残差块中像素的绝对值加起来最小的块为预测块。而得到这个预测块的算法为帧内预测模式。 ![](https://static001.geekbang.org/resource/image/69/6a/691de4570939f2a5953c848b697e0c6a.jpg?wh=1326x743) 由于这个残差块中像素的绝对值之和最小,这个残差块的像素值经过扫描之后的“像素串”是不是就比直接扫描编码块的“像素串”中的像素值更接近0了? 同理,帧间预测也是一样的。我们在前面已经编码完成的图像中,循环遍历每一个块,将它作为预测块,用当前的编码块与这个块做差值,得到残差块,取残差块中像素值的绝对值加起来最小的块为预测块,预测块所在的已经编码的图像称为参考帧。预测块在参考帧中的坐标值 (x0, y0) 与编码块在编码帧中的坐标值 (x1, y1) 的差值 (x0 - x1, y0 - y1) 称之为运动矢量。而在参考帧中去寻找预测块的过程称之为运动搜索。事实上编码过程中真正的运动搜索不是一个个块去遍历寻找的,而是有快速的运动搜索算法的。之后我们在帧间预测的课中会详细介绍。 总之,通过预测得到的残差块的像素值相比编码块的像素值,去除了大部分空间冗余信息和时间冗余信息,这样得到的像素值更小。如果把这个残差块做扫描得到的像素串送去做行程编码,是不是相比直接拿编码块的像素串去做编码更有可能得到更大的压缩率? **但是我们的目标不只是将像素值变小,而是希望能出现连续的0像素,那怎么办呢?** 这就需要利用我们人眼的视觉敏感性的特点了。我们刚才说了人眼对高频信息不太敏感。因为人眼看到的效果可能差别不大,所以我们可以去除一些高频信息。这个就是接下来我们要讨论的 **DCT变换和量化**。 为了分离图像块的高频和低频信息,我们需要将图像块变换到频域。常用的变换是DCT变换。DCT变换又叫离散余弦变换。在H264里面,如果一个块大小是16x16的,我们一般会划分成16个4x4的块(当然也有划分成8x8做变换的,我们这里以4x4为例)。然后对每个4x4的块做DCT变换得到相应的4x4的变换块。 变换块的每一个“像素值”我们称为系数。变换块左上角的系数值就是图像的低频信息,其余的就是图像的高频信息,并且高频信息占大部分。低频信息表示的是一张图的总体样貌。一般低频系数的值也比较大。而高频信息主要表示的是图像中人物或物体的轮廓边缘等变化剧烈的地方。高频系数的数量多,但高频系数的值一般比较小(注意不是所有的高频系数都一定小于低频,只是大多数高频系数比较小)。如下图所示(黄色为低频,绿色为高频): ![](https://static001.geekbang.org/resource/image/b7/ed/b737f23d14a7980d305d16d4136e62ed.jpg?wh=1280x720) 这样做完了DCT变换之后,低频和高频信息就分离开来了。由于低频信息在左上角,其余的都是高频信息。那么如果我们对变换块的像素值进行“之字形”扫描,这样得到的像素串,前面的就是数值比较大的低频系数,后面就是数值比较小的高频部分。 由于人眼对高频信息不太敏感,如果我们通过一种手段去除掉大部分高频信息,也就是将大部分高频信息置为0,但又不太影响人的观感,是不是就可以达到我们最初的目标,即可以得到有一连串0的像素串?这就涉及到量化操作了。 我们让变换块的系数都同时除以一个值,这个值我们称之为**量化步长**,也就是QStep(QStep是编码器内部的概念,用户一般使用**量化参数** QP这个值,QP和QStep一一对应,你可以自行去网上查询一下转换表),得到的结果就是量化后的系数。**QStep越大,得到量化后的系数就会越小**。同时,相同的QStep值,高频系数值相比低频系数值更小,量化后就更容易变成0。这样一来,我们就可以将大部分高频系数变成0。如下图所示: ![](https://static001.geekbang.org/resource/image/41/4d/41b9be2b425a8925c32bfe008f382f4d.jpg?wh=1280x720) 解码的时候,我们会将QStep乘以量化后的系数得到变换系数,很明显这个**变换系数和原始没有量化的变换系数是不一样的**,这个就是我们常说的有损编码。而到底损失多少呢? 这由QStep来控制,QStep越大,损失就越大。QStep跟QP一一对应,也就是说确定了一个QP值,就确定了一个QStep。所以从编码器应用角度来看,**QP值越大,损失就越大,从而画面的清晰度就会越低**。同时,QP值越大系数被量化成0的概率就越大,这样编码之后码流大小就会越小,压缩就会越高。 以上就是视频编码的推理过程。总结一下就是,为了能够在最后熵编码的时候压缩率更高,我们希望送到熵编码(以行程编码为例)的“像素串”,是一串含有很多0,并且最好连续为0的“像素串”。 为了达到这个目标,我们先通过帧内预测或者帧间预测去除空间冗余和时间冗余,从而得到一个像素值相比编码块小很多的残差块。之后我们再通过DCT变换将低频和高频信息分离开来得到变换块,然后再对变换块的系数做量化。由于高频系数通常比较小,很容易量化为0,同时人眼对高频信息不太敏感,这样我们就得到了一串含有很多个0,大多数情况下是一串含有连续0的“像素串”,并且人的观感还不会太明显。这样,最后熵编码就能把图像压缩成比较小的数据,以此达到视频压缩的目的。这就是**视频编码的原理**。 ## **编码器的对比及选择** 说完了编码器的原理,那么常用的编码标准有哪些呢?它们的区别又是什么?我们怎么选择合适的编码器? 现在市面上常见的编码标准有H264、H265、VP8、VP9和AV1。目前H264和VP8是最常用的编码标准,且两者的标准非常相似。H265和VP9分别是他们的下一代编码标准,这两个标准也非常相似。AV1是VP9的下一代编码标准。 H264和H265是需要专利费的,而VP8和VP9是完全免费的。由于H265需要付高额的版权费,以谷歌为首的互联网和芯片巨头公司组织了AOM联盟,开发了新一代压缩编码算法AV1,并宣布完全免费,以此来对抗高额专利费的H265。 目前普通产品还是使用H264最多,而H265因为专利费使用得比较少。VP8是WebRTC默认的编码标准,且WebRTC使用VP8最多。同时,WebRTC也支持VP9和AV1。YouTube使用了VP9和AV1。Netflix也使用了AV1。我们下面来比较一下H264、H265、AV1这三代编码算法的区别。现在你不理解没有关系,等你学完后面的课程会很容易明白下面表格中所涉及的知识的。 ![](https://static001.geekbang.org/resource/image/b9/a9/b94991907d00bd54743cef34b70e01a9.jpg?wh=1085x467) 从上面表格中可以看到,标准越新,最大编码块就越大,块划分的方式也越多,编码模式也就越多。因此压缩效率也会越高,但是带来的编码耗时也越大。所以在选择编码器的时候需要根据自己的实际应用场景来选择,同时还需要考虑专利费的问题。还有一个就是考虑有没有硬件支持的问题。目前H264和H265的硬件支持已经很好了,AV1才刚开始,硬件支持较少,之后会有更多硬编硬件支持。 我做了个简单的编码清晰度和耗时对比,都是在软件编码下进行的。具体如下表所示。我们可以看到相同码率下,AV1清晰度稍好于H265,而H264最差,但是编码耗时则相反,AV1最高,H265次之,H264速度最快。 ![](https://static001.geekbang.org/resource/image/c4/22/c48b488cfbd982114a7742f9618c1322.jpg?wh=853x186) 所以,如果是在性能比较差的机器上编码,最好使用H264和VP8等速度快的编码器。如果是在比较新的机器上,可以考虑H265编码。中等机器如果支持H265硬编也是可以考虑的。但有一个问题就是H265需要考虑专利费的问题,同时浏览器原生不支持H265编码,所以有这方面需求的,最好不要使用H265,可以考虑使用VP9,甚至可以考虑AV1。另外,由于AV1原生标准就支持屏幕编码的优化,所以屏幕编码场景下可以考虑使用AV1编码。 ## 小结 总结一下,我们今天主要讲了视频编码的必要性,以及视频编码的原理。**视频编码主要分为熵编码、预测、DCT变换和量化这几个步骤。** 1. 熵编码(以行程编码为例):视频编码中真正实现“压缩”的步骤,主要去除信息熵冗余。在出现连续多个0像素的时候压缩率会更高。 2. 帧内预测:为了提高熵编码的压缩率,先将当前编码块的相邻块像素经过帧内预测算法得到帧内预测块,再用当前编码块减去帧内预测块得到残差块,从而去掉空间冗余。 3. 帧间预测:类似于帧内预测,在已经编码完成的帧中,先通过运动搜索得到帧间预测块,再与编码块相减得到残差块,从而去除时间冗余。 4. DCT变换和量化:将残差块变换到频域,分离高频和低频信息。由于高频信息数量多但大小相对较小,又人眼对高频信息相对不敏感,我们利用这个特点,使用QStep对DCT系数进行量化,将大部分高频信息量化为0,达到去除视觉冗余的目的。 这里你需要注意的是,视频编码实际的步骤是预测、DCT变换和量化,最后是熵编码。经过这几步操作之后,视频中的冗余信息大部分被去除,达到了编码压缩的效果。当然,如何做帧内预测和帧间预测?如何找到最优的预测块?DCT变换和量化又是怎么做的呢?敬请期待我们接下来的课程,我会和你细聊。 ## 思考题 现在请你思考一下,视频编码过程中,一帧图像能同时进行帧内预测和帧间预测吗? 你可以把你的答案和感受写下来,分享到留言区,与我一起讨论。下节课再见。