自己动手写 H.264 解码器---指数哥伦布熵编码

2021-01-25 14:36:11 浏览数 (1)

引言

更多文章请访问 自己动手写 H.264 解码器

在上一章节,我们介绍了 NALU 层的相关细节,并且简单介绍了 SPS 和 PPS 的概念。我们知道,解码器在解码一路码流的时候,总是要首先读入 SPS 和 PPS。那么我们本章就来详细介绍 SPS 和 PPS。

SPS 和 PPS 里面存放了解码需要的参数,我们首先要做的就是把码流里的这些参数给读出来。在读取之前,我们先得知道几件事。

  • 第一,我们从码流中拿到 SPS 和 PPS 的原始数据,实际上是经过一次压缩的,是将数据按照一定的规则进行处理,去掉多余的冗余数据的。当然这个压缩过程是无损的,就是解压后的数据和压缩前的数据是完全相同的,压缩过程并不会损失数据。这个过程被称之为熵编码,熵编码是一类编码的总称,而在 SPS 和 PPS 中主要使用的是一种叫做指数哥伦布编码的熵编码算法。H.264 除了使用了指数哥伦布编码之外,还使用了 CAVLC,CABAC 等熵编码算法,我们在后续的课程中会介绍。
  • 第二,H.264 码流中的操作单位是 bit,而不是 byte。很多第一次接触视频编解码的同学都会习惯性的按照字节去解析数据,这是错误的。视频编解码十分在乎体积,所有对于每一个比特都是格外珍惜的。所以这里的操作单位是比特。
  • 第二,指数哥伦布编码是变长编码的,一个值是可以随着他的值的不同而有不同的容量的。这也是熵编码的主要特征之一。

指数哥伦布 (Exponential-Golomb) 熵编码

在 H.264 中,指数哥伦布编码又分成了 4 种:

  • 无符号指数哥伦布熵编码 ue(v)
  • 有符号指数哥伦布熵编码 se(v)
  • 映射指数哥伦布熵编码 me(v)
  • 截断指数哥伦布熵编码 te(v)

我们先来介绍前两种:

无符号指数哥伦布熵编码 ue(v)

我们先来看看如何使用无符号指数哥伦布进行编码:

  • 先把要编码的数字加 1,假设我们要编码的数字是 4,那么我们先对 4 加 1,就是 5。
  • 将加 1 后的数字 5 先转换成二进制,就是: 101。
  • 转化成二进制之后,我们看转化成的二进制有多少位,然后在前面补位数减一个 0 。例如,101 有 3 位,那么我们应该在前面补两个 0。

最后,4 进行无符号指数哥伦布编码之后得到的二进制码流就是 0 0 1 0 1。


其实前两步还是比较好理解的,但是第三步,为什么要补这个 0 呢?明明 1 0 1 就可以表示这个数字了,为什么还要多牺牲两个 bit 的空间来补 0 呢?我们来看一个例子:

假设我们有两个数字,4 和 5,我们想要将这两个数字编码成一路二进制数据。我们利用上面提到的步骤来进行一下编码。

首先是 4,编码后的码流是 1 0 1(未经过补零)。

然后是 5,编码后的码流是 1 1 0(未经过补零)。

然后我们把这两组二进制数据收尾相连,得到以下码流:

1 0 1 1 1 0

大家立刻就会发现,这个样子的码流,因为压根就没办法知道里面到底编码了几个数字,也没法知道每个数字编码之后的数据长度,所以也根本没有办法进行解码,那么一个不能解码的码流,是没有任何意义的。

那么这时候我们再来看补零的操作,其实就是指明了码流的长度,在解码的时候,先读零,读到的几个连续的 0,就可以推断出码流的长度,由此就可以成功解码。


我们再来看看上面的例子:

首先是 4,编码后的码流是 0 0 1 0 1(经过补零)。

然后是 5,编码后的码流是 0 0 1 1 0(经过补零)。

然后我们把这两组二进制数据收尾相连,得到以下码流:

0 0 1 0 1 0 0 1 1 0

接下来我们尝试解码:

  • 先读 0 ,我们这里读到了 2 个连续的 0 之后碰到了一个 1,说明两个 0 之后有效的位数是 3。
cc3016ea01645c2b644e9d5150c83b21.pngcc3016ea01645c2b644e9d5150c83b21.png
  • 之后,我们根据读到的 0 的个数,推断出后续有效位数是 3 bit,然后我们继续读取 3 个 bit。得到 1 0 1 的二进制码流。将 1 0 1 转换成十进制就是 5,然后 5 减 1,则我们就解码出了第一个数字,结果是 4。
3e20292874c22afbf59eae6c653fd457.png3e20292874c22afbf59eae6c653fd457.png
  • 然后,我们再按照相同的规则对第二个数字进行解码,先读 0 ,我么读到了连续的两个 0 ,然后再向后读 3 个 bit,得到 1 1 0 的二进制码流。将 1 1 0 转换成十进制就是 6,然后 6 减 1,我们就解码出了第二个数字,结果是 5。
7e40b8456456d5a2be3e6ac883f3aa59.png7e40b8456456d5a2be3e6ac883f3aa59.png

有符号指数哥伦布熵编码 se(v)

我们再来看看如何使用无符号指数哥伦布进行编码:

  • 先把要编码的数字取绝对值后转换成二进制,例如我们要编码的数字是 -5,取绝对值就是 5 ,转换成二进制就是 1 0 1。
  • 在二进制序列后增加一位符号位:0表示正,1表示负。二进制序列就变成了 1 0 1 1。
  • 查看现在的二进制序列的长度,在前面补长度减一个零。二进制序列就变成了 0 0 0 1 0 1 1。

同样的,有符号指数哥伦布熵编码的解码方式和无符号指数哥伦布熵编码是非常像的,我们以 0 0 0 1 0 1 1 为例来看看如何解码

  • 先读 0 ,我们这里读到了 3 个连续的 0 之后碰到了一个 1,说明 3 个 0 之后有效的位数是 4。
f5870e4f3ff2b25e692f9fe8006bb681.pngf5870e4f3ff2b25e692f9fe8006bb681.png
  • 继续向后读 4 bit 的数据。
1d9bc3ba2ef98f8da419db3daaead82b.png1d9bc3ba2ef98f8da419db3daaead82b.png
  • 拿出符号位,0表示正,1表示负。
343013b14983ced81fc6d9d8cd969648.png343013b14983ced81fc6d9d8cd969648.png
  • 将剩余的二进制序列 1 0 1 转换成十进制,得到 5。然后根据符号位,得出最后结果 -5。
ff2b615e4faf29c2063547e435a1f3fd.pngff2b615e4faf29c2063547e435a1f3fd.png

映射指数哥伦布熵编码 me(v)

在说明映射指数哥伦布熵编码 me(v) 之前,我们先来尝试用无符号指数哥伦布熵编码两个数字。

我们先来编码数字 5。得到的码流是 0 0 1 1 0,占用 5 个 bit 的空间。

我们再来编码数字 47。得到的码流是 0 0 0 0 0 1 1 0 0 0 0,占用了 11 个 bit 的空间。


我们发现,指数哥伦布熵编码有个特性,那就是数字越大,占用的空间将会越大。那么如果我们要编码的数字集中在某一个区间比较小的范围,而这个范围内的数字又比较大,有没有什么办法能够节省一些空间呢?

为了解决这个问题,就出现了映射指数哥伦布熵编码。其实映射指数哥伦布熵编码的原理特别简单。映射指数哥伦布熵编码提供了一个码表,当你遇到一段码流的时候,你要先用无符号指数哥伦布熵编码进行解码,然后得到的结果其实是一个码表的索引,例如,你解码出的数字是 2,那么你到码表中,找到角标是 2 的元素出来就是最后的结果。


对于不同的情况,映射指数哥伦布熵编码可能会选择不同的码表,我们后面遇到具体情况的时候,再介绍码表应该如何选择。

截断指数哥伦布熵编码 te(v)

截断指数哥伦布熵编码要解决的问题其实和映射指数哥伦布熵编码要解决的问题差不多。

当语法元素以截断指数哥伦布解码时,首先需要判断的是语法元素的取值范围,假定为0, x, x≥1。根据x的取值情况,语法元素根据不同情况进行解析。若 x>1,解析方法同无符号指数哥伦布熵编码相同。若 x=1 ,语法元素值等同于下一位bit值的取反。

0 人点赞