You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

17 KiB

11码控算法如何控制视频的编码码率

你好,我是李江。

上一节课我们讲了带宽预测算法,学习了如何去预测无时无刻不在变化着的网络带宽。准确的预测带宽是实时视频通话技术里面的一个非常重要的环节。

如果不能够很好地预测出实际带宽,那有可能引起数据超发,导致发送数据量大于实际网络的承受能力,继而引起视频画面的延时和卡顿;也有可能预测的带宽太低,导致发送的数据量远低于实际网络的承受能力,不能很好地利用网络带宽,最终导致视频画面模糊和很明显的马赛克现象。因此,一个好的带宽预测算法是至关重要的。

当然,好的带宽预测算法还只是开始,如何在预测出带宽之后能够控制数据的发送码率,使其尽量符合当前的网络带宽也是非常重要的。如果你没有做好发送码率的控制,想发送多少数据就发送多少数据的话,那跟没有网络带宽预测是一样的效果。要不就画面卡顿,要不就很模糊。

因此这节课我们就接着来讲讲与网络带宽预估算法一样重要的另外一个算法——视频码率控制算法。我们会先简单看一下码控算法的原理和类型然后再重点讲解其中最难也是在RTC场景中最重要的CBR码控算法。我们会非常详细地剖析CBR算法的原理讲解它是如何一步步尽量做到恒定码率的。

通过下面的图你可以清楚地了解码控算法在整个发送端流程中的位置和重要性。

图片

好了,下面我们首先来了解一下码控的原理和基本类型。

码控的原理

码控,顾名思义,就是码率控制,它是编码器的一个重要模块,主要的作用就是用算法来控制编码器输出码流的大小。虽然它是编码器的一个非常重要的部分,但是它并不是编码标准的一部分,也就是说,标准并没有给码控设定规则。我们平时用的编码器的码控都是编码器程序自己实现的。

那码控的原理是什么呢?其实码控就是为每一帧编码图像选择一个合适的QP值的过程。

我们知道当一帧图像的画面确定了之后画面的复杂度和QP值几乎决定了它编码之后的大小。由于编码器无法决定画面的复杂度因此**码控的目标就是选择一个合适的QP值以此来控制编码后码流的大小。**当然有些码控算法是可以直接外部指定使用哪个QP值去编码的就不需要编码器的码控算法去做决策了。但是最后的原理是一样的。那接下来我们就来看一下都有哪些码控算法吧。

码控的类型

常用的码控算法主要有VBR动态码率、CQP恒定QP、CRF恒定码率因子和CBR恒定码率这几种。

VBR

VBR指的是编码器输出码率随着原始视频画面复杂度的变化不断的变化。通常当画面复杂或者说运动比较多的时候使用的码率会比较高而当画面比较简单的时候使用的码率会比较低。VBR主要的目标是保证视频画面质量因此比较适合视频点播和短视频场景使用。

CQP

CQP很简单就是从头到尾每一个画面都是用同一个QP值去编码。根据我们视频编码的课程可知

  • 在画面复杂的时候残差比较大相同QP值做量化之后的残差还是比较大的编码之后的图像大小就会比较大。
  • 而画面简单的时候残差很小同一个QP值量化之后残差可能很小甚至都为0了编码之后的大小就会很小。
    其实我个人觉得CQP是一种特殊的VBR。但要注意的是CQP一般用来衡量编码算法的性能在实际工程当中不会使用。

CRF

CRF是x264默认的码控算法。它与CQP不同的是它的QP是会变化的。在画面运动大的时候它会根据具体算法提高QP值在画面运动小的时候它会降低QP值。

它的思想是运动很大的时候人眼不太关注细节因此QP可以稍微大一点运动比较小的时候人眼会将注意力放在细节上面因此QP稍微小一点。所以相比CQPCRF能够更省码率一些。但是CRF码控总体上得到的编码后图像的大小还是随着图像的画面复杂度在变化的。因此我觉得CRF也算是一种特殊的VBR。

CBR

另外一种码控算法就是CBR了它是恒定码率的。**这种码控方式用户需要设置一个目标码率值给编码器。**编码器在编码的时候不管图像画面复杂或简单、运动多或运动少的时候,都尽量使得输出的码率接近设置的目标码率。

这种方式非常适合RTC场景因为RTC场景希望编码的码率跟实际预测的带宽值接近不能超出目标码率太多也希望能够尽量有效地利用可用带宽不能太低于目标码率从而尽量保证编码后图像画面清晰。

因此在RTC场景中我们会将预估带宽分出一定比例给视频数据并将这部分带宽值当作目标码率设置给编码器。需要编码器的码控算法能够在各种网络状况下和各种画面变化的情况下都能使得输出的码率尽量接近于当前预估带宽得到的目标码率。

相信你光是看到这个描述就知道非常困难了。所以我们前面说了CBR是很重要但也是非常难的一种码控算法。那CBR到底怎么做到的呢我们就来详细讨论一下。

CBR算法

其实为了实现恒定码率我们需要做很多个步骤一步步的将输出码率逼近目标码率而不是一步到位确定QP就可以实现恒定码率的目标的。所以我们会分很多级做调整分别是帧组级、帧级、宏块组GOMGroup of MB级。具体如下图所示

图片

具体的操作过程如下:

  • 先确定帧组级帧组就是将连续的几个帧组成一组一般选择8个帧一组的输出大小尽量接近目标码率。
  • 然后,确定组内的每一帧具体应该分配多少的大小(称之为目标帧大小),才能保证帧组最后输出的大小可以达到要求。
  • 接下来我们再根据这个目标帧大小确定一个帧级的QP值。
  • 之后我们再确定帧内的宏块组宏块组就是连续的几行宏块组成的一组宏块一般可以选4行宏块应该分配多少大小来保证当前帧最后的输出大小能接近于目标帧大小。
  • 最后我们再确定宏块的QP值。
    还有一个很重要的事情,就是我们需要能够保证在不同的画面复杂度和不同的运动程度的情况下,并且输出码率都要尽量接近目标码率的话,我们还需要先计算得到当前帧的复杂度。

简单来说这个复杂度是能够大概衡量当前帧在做完预测之后残差值的总体大小的。当然我们并不是真正去做预测得到残差的而是通过一些算法近似估算一下残差的大概大小的。因为残差的大小和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的目标大小就是当前的帧目标剩余大小了。

具体计算过程如下图所示:

图片

我们将帧的实际剩余大小除以帧的目标剩余大小:

  • 如果这个比例大于1说明我们剩余的大小多了之后的GOM可以将QP调低一些我们将后面的GOM中的宏块QP值减去1或者2即可
  • 如果这个比例小于1说明我们剩余的大小少了之后的GOM的QP需要调高一些我们将后面的GOM中的宏块QP值加1或者2即可。
    也就是说通过这个计算之后我们就得到了GOM中所有宏块的QP值了。然后我们再根据这个QP值去编码每一个宏块。

到这里我们还有一个步骤需要做,就是需要计算一下当前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帧。

你可以把你的答案和疑惑写下来,分享到留言区,与我一起讨论。我们下节课再见!