你好,我是李江。
上一节课我们讲了带宽预测算法,学习了如何去预测无时无刻不在变化着的网络带宽。准确的预测带宽是实时视频通话技术里面的一个非常重要的环节。
如果不能够很好地预测出实际带宽,那有可能引起数据超发,导致发送数据量大于实际网络的承受能力,继而引起视频画面的延时和卡顿;也有可能预测的带宽太低,导致发送的数据量远低于实际网络的承受能力,不能很好地利用网络带宽,最终导致视频画面模糊和很明显的马赛克现象。因此,一个好的带宽预测算法是至关重要的。
当然,好的带宽预测算法还只是开始,如何在预测出带宽之后能够控制数据的发送码率,使其尽量符合当前的网络带宽也是非常重要的。如果你没有做好发送码率的控制,想发送多少数据就发送多少数据的话,那跟没有网络带宽预测是一样的效果。要不就画面卡顿,要不就很模糊。
因此,这节课我们就接着来讲讲与网络带宽预估算法一样重要的另外一个算法——视频码率控制算法。我们会先简单看一下码控算法的原理和类型,然后再重点讲解其中最难也是在RTC场景中最重要的CBR码控算法。我们会非常详细地剖析CBR算法的原理,讲解它是如何一步步尽量做到恒定码率的。
通过下面的图你可以清楚地了解码控算法在整个发送端流程中的位置和重要性。
好了,下面我们首先来了解一下码控的原理和基本类型。
码控,顾名思义,就是码率控制,它是编码器的一个重要模块,主要的作用就是用算法来控制编码器输出码流的大小。虽然它是编码器的一个非常重要的部分,但是它并不是编码标准的一部分,也就是说,标准并没有给码控设定规则。我们平时用的编码器的码控都是编码器程序自己实现的。
那码控的原理是什么呢?其实码控就是为每一帧编码图像选择一个合适的QP值的过程。
我们知道当一帧图像的画面确定了之后,画面的复杂度和QP值几乎决定了它编码之后的大小。由于编码器无法决定画面的复杂度,因此,码控的目标就是选择一个合适的QP值,以此来控制编码后码流的大小。当然有些码控算法是可以直接外部指定使用哪个QP值去编码的,就不需要编码器的码控算法去做决策了。但是最后的原理是一样的。那接下来我们就来看一下都有哪些码控算法吧。
常用的码控算法主要有:VBR(动态码率)、CQP(恒定QP)、CRF(恒定码率因子)和CBR(恒定码率)这几种。
VBR指的是编码器输出码率随着原始视频画面复杂度的变化不断的变化。通常当画面复杂或者说运动比较多的时候使用的码率会比较高;而当画面比较简单的时候使用的码率会比较低。VBR主要的目标是保证视频画面质量,因此比较适合视频点播和短视频场景使用。
CQP很简单就是从头到尾每一个画面都是用同一个QP值去编码。根据我们视频编码的课程可知:
CRF是x264默认的码控算法。它与CQP不同的是它的QP是会变化的。在画面运动大的时候,它会根据具体算法提高QP值;在画面运动小的时候,它会降低QP值。
它的思想是:运动很大的时候,人眼不太关注细节,因此QP可以稍微大一点;运动比较小的时候,人眼会将注意力放在细节上面,因此QP稍微小一点。所以相比CQP,CRF能够更省码率一些。但是CRF码控总体上得到的编码后图像的大小,还是随着图像的画面复杂度在变化的。因此,我觉得CRF也算是一种特殊的VBR。
另外一种码控算法就是CBR了,它是恒定码率的。这种码控方式用户需要设置一个目标码率值给编码器。编码器在编码的时候不管图像画面复杂或简单、运动多或运动少的时候,都尽量使得输出的码率接近设置的目标码率。
这种方式非常适合RTC场景,因为RTC场景希望编码的码率跟实际预测的带宽值接近,不能超出目标码率太多,也希望能够尽量有效地利用可用带宽,不能太低于目标码率,从而尽量保证编码后图像画面清晰。
因此,在RTC场景中,我们会将预估带宽分出一定比例给视频数据,并将这部分带宽值当作目标码率设置给编码器。需要编码器的码控算法,能够在各种网络状况下和各种画面变化的情况下,都能使得输出的码率尽量接近于当前预估带宽得到的目标码率。
相信你光是看到这个描述就知道非常困难了。所以我们前面说了,CBR是很重要但也是非常难的一种码控算法。那CBR到底怎么做到的呢?我们就来详细讨论一下。
其实,为了实现恒定码率,我们需要做很多个步骤,一步步的将输出码率逼近目标码率,而不是一步到位确定QP就可以实现恒定码率的目标的。所以,我们会分很多级做调整,分别是帧组级、帧级、宏块组GOM(Group of MB)级。具体如下图所示:
具体的操作过程如下:
简单来说,这个复杂度是能够大概衡量当前帧在做完预测之后残差值的总体大小的。当然,我们并不是真正去做预测得到残差的,而是通过一些算法近似估算一下残差的大概大小的。因为残差的大小和QP值决定了最后图像编码后的大小。
同时,在这里说明一下,因为我们主要讲解RTC下的CBR码控,所以我们只考虑I帧和P帧,不考虑B帧。等你理解了这些知识之后呢,你再去学习更复杂的CBR码控算法就会更轻松一些。
好了,那我们接下来就先讲讲如何计算图像的复杂度,之后我们再依次讨论一下如何在帧组级、帧级、宏块组GOM级别做码控操作,最后得到宏块的QP值。
根据帧类型复杂度求解可以分为两种算法:第一种就是I帧的复杂度计算;第二种就是P帧的复杂度计算。
I帧只做帧内预测,而帧内预测是用编码块周围已编码的像素来预测当前编码块的像素值的。因此,方差是一个比较能够表示I帧复杂度的值。
因为方差越大,表示帧的内部变化程度越剧烈,而你用周围的像素去预测当前编码块的像素值的话,有很大的可能会产生较大的残差。而方差越小的话,说明帧内部变化比较小,因此周围像素有较大的概率能够比较好的预测出待编码块的像素值。因此,我们计算I帧的复杂度的时候,是求每一个宏块的方差,最后将帧的所有宏块的方差之和作为帧的复杂度。具体求解过程如下图所示:
而P帧,主要是做帧间预测。我们知道,帧间预测就是去参考帧中找一个块来作为当前帧编码块的预测块,因此,我们选择使用将当前帧的宏块减去参考帧对应位置的宏块,求SAD值,并将所有宏块的SAD值加起来作为P帧的复杂度。具体求解过程如下图所示:
当然,我们会保存记录下I帧和P帧内部每一个宏块的复杂度值,这是因为后面还有地方会使用到。
CBR虽然是恒定码率,但它的意思是保证一段时间内的输出码率接近目标码率,比如说1秒或者几百毫秒,而不是保证每一帧输出都严格接近目标码率的。
这是因为算法没办法做到每一帧都这么精确。算法是根据一段时间内前面已经编码的结果来调节还未编码帧的QP,从而来达到一组帧的输出大小尽量接近目标码率的。因此,我们在开始的时候,需要根据目标码率来确定帧组的目标大小,之后再确定帧组内每一帧的目标大小。
我们先根据设定的目标码率和帧率值将两者相除,就可以计算得到每一帧的平均大小。然后我们将帧组的帧数(一般8个帧作为一组)乘以帧的平均大小,就是帧组的目标大小了。
在编码器刚开始编码的时候,帧组的剩余大小就是帧组的目标大小。当编码帧组中第一帧的时候,我们将帧组的剩余大小除以帧组的帧数,就得到帧组中第一帧的目标帧大小。当帧组中的第一帧编码完成之后,我们需要用第一帧的实际编码后的大小来更新帧组的剩余大小。
很简单就是将帧组的剩余大小减去第一帧编码后的实际大小。然后,第二帧的目标帧大小就是等于更新后的帧组的剩余大小除以帧组的剩余帧数。随着帧组中的一帧帧不断编码,我们不断更新帧组的剩余大小,不断调整帧的目标大小。
具体计算过程可以参考下图:
你可以很清楚地看到,如果帧组中的前面帧编码后的大小超出平均帧大小的话,后面帧的目标帧大小就会小于平均帧大小,也就是说,前面帧用多了就从后面帧里面扣。同样地,如果前面帧用少了,就补给后面的帧。这样是不是就能保证帧组的最后编码输出码率尽量接近帧组的目标码率了?
举个例子,就像是你一个月有3000零花钱,平均每天100元。前面10天你已经用了2000了,那后面20天你每天平均只能用50,要省着点花。如果你前面10天只用了500,那后面20天平均每天你可以用125,可以大方点花。帧组分配帧目标大小也是这个道理。
有了帧组级别码控中计算得到的目标帧大小之后,我们就能够计算当前帧的SliceQP了(我们这里为了讲述原理尽量简单清晰,只考虑一帧一个Slice,多Slice原理是一致的,就不展开讲了)。那怎么求呢?
我们根据前面计算得到的当前编码帧的帧复杂度和目标帧大小,再加上前面已经编码完成了的帧的复杂度和编码使用的QStep(与QP一一对应,请参考视频08里面的表格)以及使用这个QStep编码之后实际的编码大小来计算。公式如下:
其中I帧和P帧使用不同的公式,因为复杂度的计算方式不一样。
上面的公式是什么意思呢?其实大体的思想就是:一帧编码后的大小应该是和帧的复杂度成正比的,并且跟帧使用的QStep是成反比的。但是具体成多少比例怎么知道呢?
其实呢我们不知道,但是我们可以根据前面已经编码好了的帧估算一下。我们先大体计算一下,它们这些帧的复杂度和QStep跟最终的编码大小大概成多少比例。然后再使用这个比例来估算在当前帧的复杂度下,我们大概需要使用多少的QStep能使得输出的大小尽量接近目标帧大小。
我们通过上面的公式就计算得到了当前编码帧的QStep了,再通过08那节课里面的表格就可以转换成相应的SliceQP了。
其实,到这里我们就可以用SliceQP值去编码每一个宏块了。比如像VP8编码,它没有宏块级别的QP值,到这里码控就确定了最终QP了。但是H264还可以在宏块级别调整宏块的QP,因此,为了更精细化地调节码率,我们还可以根据已经编码宏块的实际使用的大小来调整未编码宏块的QP。这里就是我们前面提到的宏块组概念了,也就是GOM。
首先,在开始编码一个GOM之前,我们需要计算一下帧的实际剩余大小和帧的目标剩余大小。帧的实际剩余大小是用帧的目标大小减去帧中已编码GOM的实际大小。我们再使用帧的实际剩余大小加上前一个GOM的实际编码大小,减去该GOM的目标大小,就是帧的目标剩余大小。
这个地方我解释一下,帧的实际剩余大小加上GOM的实际编码大小,就是去掉前一个GOM的目标大小,再减去前一个GOM的目标大小,就是当前的帧目标剩余大小了。
具体计算过程如下图所示:
我们将帧的实际剩余大小除以帧的目标剩余大小:
到这里我们还有一个步骤需要做,就是需要计算一下当前GOM的目标大小,以备下一个GOM编码的时候做GOM级码控计算的时候使用。
GOM的目标大小是通过当前GOM的复杂度、当前帧剩余GOM的复杂度之和以及帧的剩余大小来计算的。计算公式如下所示:
我们是看当前GOM的复杂度占剩余GOM总复杂度的比例来分配目标大小的。其中,GOM的复杂度的值用前面复杂度计算时记录保存的宏块复杂度的值来计算。
其实,我们还可以通过每一个宏块调整一下QP的方式来做进一步精细化的调节,但是这个内容有点复杂了,等你学好了这节课之后,我们之后有机会再来深入讲解一下。这里就不展开讲解了。
对于CBR码控算法的整体流程,我用下面的图帮你总结了一下,方便你理解和记忆。
这节课我们主要讨论了码控算法,带你了解了一下码控算法的原理和基本类型。码控主要是为每一帧图像确定QP值的过程。如果在图像画面确定的情况下,并且QP值确定了的话,那当前图像编码后的大小就大致确定了,从而编码后的码率大小也基本确定了。
同时,常用的码控算法主要有CQP、CRF、VBR和CBR。并且,我们还对CBR进行了深入地探讨。我们知道了CBR主要分为:帧组级、帧级和GOM级三个级别的调整,并通过一步步不断精细化的调整最后尽量达到恒定码率的目标。
你可以通过下面的图来对这节课加强理解和记忆。
第一个I帧和第一个P帧的QP值怎么确定呢?因为在它们前面没有已经编码好了的I帧和P帧。
你可以把你的答案和疑惑写下来,分享到留言区,与我一起讨论。我们下节课再见!