# 视频编解码器是如何工作的?
# 是什么?为什么?怎么做?
是什么? 编解码器就是用于压缩或解压数字视频的软件或硬件。
为什么? 人们需要在有限带宽或存储空间下提高视频的质量。还记得当我们计算每秒 30 帧,每像素 24 bit,分辨率是 480x240 的视频需要多少带宽吗?没有压缩时是 82.944 Mbps。电视或互联网提供 HD/FullHD/4K 只能靠视频编解码器。
怎么做? 我们将简单介绍一下主要的技术。
视频编解码 vs 容器
初学者一个常见的错误是混淆数字视频编解码器和数字视频容器 (opens new window)。 我们可以将容器视为包含视频(也很可能包含音频)元数据的包装格式, 压缩过的视频可以看成是它承载的内容。
通常,视频文件的格式定义其视频容器。例如,文件
video.mp4
可能是 MPEG-4 Part 14 (opens new window) 容器,一个叫video.mkv
的文件可能是 matroska (opens new window)。我们可以使用 ffmpeg 或 mediainfo 来完全确定编解码器和容器格式。
# 历史
在我们跳进通用编解码器内部工作之前,让我们回头了解一些旧的视频编解码器。
# H26X 系列的诞生
视频编解码器 H.261 (opens new window) 诞生在 1990(技术上是 1988),被设计为以 64 kbit/s 的数据速率工作。它已经使用如色度子采样、宏块,等等理念。在 1995 年,H.263 视频编解码器标准被发布,并继续延续到 2001 年。
在 2003 年 H.264/AVC 的第一版被完成。在同一年,一家叫做 TrueMotion 的公司发布了他们的免版税有损视频压缩的视频编解码器,称为 VP3。在 2008 年,Google 收购了这家公司,在同一年发布 VP8。在 2012 年 12 月,Google 发布了 VP9,市面上大约有 3/4 的浏览器(包括手机)支持。
AV1 (opens new window) 是由 Google, Mozilla, Microsoft, Amazon, Netflix, AMD, ARM, NVidia, Intel, Cisco 等公司组成的开放媒体联盟(AOMedia) (opens new window)设计的一种新的免版税和开源的视频编解码器。第一版 0.1.0 参考编解码器发布于 2016 年 4 月 7 号。
# AV1 的诞生
2015 年早期,Google 正在开发 VP10,Xiph (Mozilla) 正在开发 Daala,Cisco 开源了其称为 Thor 的免版税视频编解码器。
接着 MPEG LA 宣布了 HEVC (H.265) 每年版税的的上限,比 H.264 高 8 倍,但很快他们又再次改变了条款:
- 不设年度收费上限
- 收取内容费(收入的 0.5%)
- 每单位费用高于 h264 的 10 倍
开放媒体联盟 (opens new window)由硬件厂商(Intel, AMD, ARM , Nvidia, Cisco),内容分发商(Google, Netflix, Amazon),浏览器维护者(Google, Mozilla),等公司创建。
这些公司有一个共同目标,一个免版税的视频编解码器,所以 AV1 诞生时使用了一个更简单的专利许可证 (opens new window)。Timothy B. Terriberry 做了一个精彩的介绍,关于 AV1 的概念,许可证模式和它当前的状态 (opens new window),就是本节的来源。
前往 https://arewecompressedyet.com/analyzer/ (opens new window), 你会惊讶于使用你的浏览器就可以分析 AV1 编解码器。
附:如果你想了解更多编解码器的历史,你需要了解视频压缩专利 (opens new window)背后的基本知识。
# 通用编解码器
我们接下来要介绍通用视频编解码器背后的主要机制,大多数概念都很实用,并被现代编解码器如 VP9, AV1 和 HEVC 使用。需要注意:我们将简化许多内容。有时我们会使用真实的例子(主要是 H.264)来演示技术。
# 第一步 - 图片分区
第一步是将帧分成几个分区,子分区甚至更多。
但是为什么呢有许多原因,比如,当我们分割图片时,我们可以更精确的处理预测,在微小移动的部分使用较小的分区,而在静态背景上使用较大的分区。
通常,编解码器将这些分区组织成切片(或瓦片),宏(或编码树单元)和许多子分区。这些分区的最大大小有所不同,HEVC 设置成 64x64,而 AVC 使用 16x16,但子分区可以达到 4x4 的大小。
还记得我们学过的帧的分类吗?你也可以把这些概念应用到块,因此我们可以有 I 切片,B 切片,I 宏块等等。
# 自己动手:查看分区
TIP
TODO
# 第二步 - 预测
一旦我们有了分区,我们就可以在它们之上做出预测。对于帧间预测,我们需要发送运动向量和残差;至于帧内预测,我们需要发送预测方向和残差。
# 自己动手:预测
TIP
TODO
# 第三步 - 转换
在我们得到残差块(预测分区-真实分区
)之后,我们可以用一种方式变换它,这样我们就知道哪些像素我们应该丢弃,还依然能保持整体质量。这个确切的行为有几种变换方式。
尽管有其它的变换方式 (opens new window),但我们重点关注离散余弦变换(DCT)。DCT (opens new window) 的主要功能有:
- 将像素块转换为相同大小的频率系数块。
- 压缩能量,更容易消除空间冗余。
- 可逆的,也意味着你可以还原回像素。
2017 年 2 月 2 号,F. M. Bayer 和 R. J. Cintra 发表了他们的论文:图像压缩的 DCT 类变换只需要 14 个加法 (opens new window)。
如果你不理解每个要点的好处,不用担心,我们会尝试进行一些实验,以便从中看到真正的价值。
我们来看下面的像素块(8x8):
下面是其渲染的块图像(8x8):
当我们对这个像素块应用 DCT 时, 得到如下系数块(8x8):
接着如果我们渲染这个系数块,就会得到这张图片:
如你所见它看起来完全不像原图像,我们可能会注意到第一个系数与其它系数非常不同。第一个系数被称为直流分量,代表了输入数组中的所有样本,有点类似于平均值。
这个系数块有一个有趣的属性:高频部分和低频部分是分离的。
在一张图像中,大多数能量会集中在低频部分 (opens new window),所以如果我们将图像转换成频率系数,并丢掉高频系数,我们就能减少描述图像所需的数据量,而不会牺牲太多的图像质量。
频率是指信号变化的速度。
让我们通过实验学习这点,我们将使用 DCT 把原始图像转换为频率(系数块),然后丢掉最不重要的系数。
首先,我们将它转换为其频域。
然后我们丢弃部分(67%)系数,主要是它的右下角部分。
然后我们从丢弃的系数块重构图像(记住,这需要可逆),并与原始图像相比较。
如我们所见它酷似原始图像,但它引入了许多与原来的不同,我们丢弃了 67.1875%,但我们仍然得到至少类似于原来的东西。我们可以更加智能的丢弃系数去得到更好的图像质量,但这是下一个主题。
# 使用全部像素形成每个系数
需要注意的是,每个系数并不直接映射到单个像素,而是所有像素的加权和。这个神奇的图形展示了如何使用每个指数唯一的权重来计算第一个和第二个系数。
你也可以尝试通过查看在 DCT 基础上形成的简单图片来可视化 DCT。例如,这是使用每个系数权重形成的字符 A (opens new window)。
# 自己动手:丢弃不同的系数
# 第四步 - 量化
当我们丢弃一些系数时,在最后一步(变换),我们做了一些形式的量化。这一步,我们选择性地剔除信息(有损部分)或者简单来说,我们将量化系数以实现压缩。
我们如何量化一个系数块?一个简单的方法是均匀量化,我们取一个块并将其除以单个的值(10),并舍入值。
我们如何逆转(重新量化)这个系数块?我们可以通过乘以我们先前除以的相同的值(10)来做到。
这不是最好的方法,因为它没有考虑到每个系数的重要性,我们可以使用一个量化矩阵来代替单个值,这个矩阵可以利用 DCT 的属性,多量化右下部,而少(量化)左上部,JPEG 使用了类似的方法 (opens new window),你可以通过查看源码看看这个矩阵 (opens new window)。
# 自己动手:量化
# 第五步 - 熵编码
在我们量化数据(图像块/切片/帧)之后,我们仍然可以以无损的方式来压缩它。有许多方法(算法)可用来压缩数据。我们将简单体验其中几个,你可以阅读这本很棒的书去深入理解:Understanding Compression: Data Compression for Modern Developers (opens new window)。
# VLC 编码:
让我们假设我们有一个符号流:a, e, r 和 t,它们的概率(从 0 到 1)由下表所示。
a | e | r | t | |
---|---|---|---|---|
概率 | 0.3 | 0.3 | 0.2 | 0.2 |
我们可以分配不同的二进制码,(最好是)小的码给最可能(出现的字符),大些的码给最少可能(出现的字符)。
a | e | r | t | |
---|---|---|---|---|
概率 | 0.3 | 0.3 | 0.2 | 0.2 |
二进制码 | 0 | 10 | 110 | 1110 |
让我们压缩 eat 流,假设我们为每个字符花费 8 bit,在没有做任何压缩时我们将花费 24 bit。但是在这种情况下,我们使用各自的代码来替换每个字符,我们就能节省空间。
第一步是编码字符 e 为 10
,第二个字符是 a,追加(不是数学加法)后是 [10][0]
,最后是第三个字符 t,最终组成已压缩的比特流 [10][0][1110]
或 1001110
,这只需 7 bit(比原来的空间少 3.4 倍)。
请注意每个代码必须是唯一的前缀码,Huffman 能帮你找到这些数字 (opens new window)。虽然它有一些问题,但是视频编解码器仍然提供该方法 (opens new window),它也是很多应用程序的压缩算法。
编码器和解码器都必须知道这个(包含编码的)字符表,因此,你也需要传送这个表。
# 算术编码
让我们假设我们有一个符号流:a, e, r, s 和 t,它们的概率由下表所示。
a | e | r | s | t | |
---|---|---|---|---|---|
概率 | 0.3 | 0.3 | 0.15 | 0.05 | 0.2 |
考虑到这个表,我们可以构建一个区间,区间包含了所有可能的字符,字符按出现概率排序。
让我们编码 eat 流,我们选择第一个字符 e 位于 0.3 到 0.6 (但不包括 0.6)的子区间,我们选择这个子区间,按照之前同等的比例再次分割。
让我们继续编码我们的流 eat,现在使第二个 a 字符位于 0.3 到 0.39 的区间里,接着再次用同样的方法编码最后的字符 t,得到最后的子区间 0.354 到 0.372。
我们只需从最后的子区间 0.354 到 0.372 里选择一个数,让我们选择 0.36,不过我们可以选择这个子区间里的任何数。仅靠这个数,我们将可以恢复原始流 eat。就像我们在区间的区间里画了一根线来编码我们的流。
反向过程(又名解码)一样简单,用数字 0.36 和我们原始区间,我们可以进行同样的操作,不过现在是使用这个数字来还原被编码的流。
在第一个区间,我们发现数字落入了一个子区间,因此,这个子区间是我们的第一个字符,现在我们再次切分这个子区间,像之前一样做同样的过程。我们会注意到 0.36 落入了 a 的区间,然后我们重复这一过程直到得到最后一个字符 t(形成我们原始编码过的流 eat)。
编码器和解码器都必须知道字符概率表,因此,你也需要传送这个表。
非常巧妙,不是吗?人们能想出这样的解决方案实在是太聪明了,一些视频编解码器使用 (opens new window)这项技术(或至少提供这一选择)。
关于无损压缩量化比特流的办法,这篇文章无疑缺少了很多细节、原因、权衡等等。作为一个开发者你应该学习更多 (opens new window)。刚入门视频编码的人可以尝试使用不同的熵编码算法,如 ANS (opens new window)。
# 自己动手:CABAC vs CAVLC
你可以生成两个流,一个使用 CABAC,另一个使用 CAVLC (opens new window),并比较生成每一个的时间以及最终的大小。
# 第六步 - 比特流格式
完成所有这些步之后,我们需要将压缩过的帧和内容打包进去。需要明确告知解码器编码定义,如颜色深度,颜色空间,分辨率,预测信息(运动向量,帧内预测方向),档次*,级别*,帧率,帧类型,帧号等等更多信息。
* 译注:原文为 profile 和 level,没有通用的译名
我们将简单地学习 H.264 比特流。第一步是生成一个小的 H.264* 比特流 (opens new window),可以使用本 repo 和 ffmpeg (opens new window) 来做。
./s/ffmpeg \
-i /files/i/minimal.png \
-pix_fmt yuv420p \
/files/v/minimal_yuv420.h264
* ffmpeg 默认将所有参数添加为 SEI NAL,很快我们会定义什么是 NAL。
这个命令会使用下面的图片作为帧,生成一个具有单个帧,64x64 和颜色空间为 yuv420 的原始 h264 比特流。
# H.264 比特流
AVC (H.264) 标准规定信息将在宏帧(网络概念上的)内传输,称为 NAL (opens new window)(网络抽象层)。NAL 的主要目标是提供“网络友好”的视频呈现方式,该标准必须适用于电视(基于流),互联网(基于数据包)等。
同步标记 (opens new window)用来定义 NAL 单元的边界。每个同步标记的值固定为 0x00 0x00 0x01
,最开头的标记例外,它的值是 0x00 0x00 0x00 0x01
。如果我们在生成的 h264 比特流上运行 hexdump,我们可以在文件的开头识别至少三个 NAL。
我们之前说过,解码器需要知道不仅仅是图片数据,还有视频的详细信息,如:帧、颜色、使用的参数等。每个 NAL 的第一位定义了其分类和类型。
NAL type id | 描述 |
---|---|
0 | Undefined |
1 | Coded slice of a non-IDR picture |
2 | Coded slice data partition A |
3 | Coded slice data partition B |
4 | Coded slice data partition C |
5 | IDR Coded slice of an IDR picture |
6 | SEI Supplemental enhancement information |
7 | SPS Sequence parameter set |
8 | PPS Picture parameter set |
9 | Access unit delimiter |
10 | End of sequence |
11 | End of stream |
... | ... |
通常,比特流的第一个 NAL 是 SPS,这个类型的 NAL 负责传达通用编码参数,如档次,级别,分辨率等。
如果我们跳过第一个同步标记,就可以通过解码第一个字节来了解第一个 NAL 的类型。
例如同步标记之后的第一个字节是 01100111
,第一位(0
)是 forbidden_zero_bit 字段,接下来的两位(11
)告诉我们是 nal_ref_idc 字段,其表示该 NAL 是否是参考字段,其余 5 位(00111
)告诉我们是 nal_unit_type 字段,在这个例子里是 NAL 单元 SPS (7)。
SPS NAL 的第 2 位 (binary=01100100, hex=0x64, dec=100
) 是 profile_idc 字段,显示编码器使用的配置,在这个例子里,我们使用高阶档次 (opens new window),一种没有 B(双向预测) 切片支持的高阶档次。
当我们阅读 SPS NAL 的 H.264 比特流规范时,会为参数名称,分类和描述找到许多值,例如,看看字段 pic_width_in_mbs_minus_1
和 pic_height_in_map_units_minus_1
。
参数名称 | 分类 | 描述 |
---|---|---|
pic_width_in_mbs_minus_1 | 0 | ue(v) |
pic_height_in_map_units_minus_1 | 0 | ue(v) |
ue(v): 无符号整形 Exp-Golomb-coded (opens new window)
如果我们对这些字段的值进行一些计算,将最终得出分辨率。我们可以使用值为 119( (119 + 1) * macroblock_size = 120 * 16 = 1920)
的 pic_width_in_mbs_minus_1
表示 1920 x 1080
,再次为了减少空间,我们使用 119
来代替编码 1920
。
如果我们再次使用二进制视图检查我们创建的视频 (ex: xxd -b -c 11 v/minimal_yuv420.h264
),可以跳到帧自身上一个 NAL。
我们可以看到最开始的 6 个字节:01100101 10001000 10000100 00000000 00100001 11111111
。我们已经知道第一个字节告诉我们 NAL 的类型,在这个例子里, (00101
) 是 IDR 切片 (5),可以进一步检查它:
对照规范,我们能解码切片的类型(slice_type),帧号(frame_num)等重要字段。
为了获得一些字段(ue(v), me(v), se(v) 或 te(v)
)的值,我们需要称为 Exponential-Golomb (opens new window) 的特定解码器来解码它。当存在很多默认值时,这个方法编码变量值特别高效。
这个视频里 slice_type 和 frame_num 的值是 7(I 切片)和 0(第一帧)。
我们可以将比特流视为一个协议,如果你想学习更多关于比特流的内容,请参考 ITU H.264 规范 (opens new window)。这个宏观图展示了图片数据(压缩过的 YUV)所在的位置。
我们可以探究其它比特流,如 VP9 比特流 (opens new window),H.265(HEVC) (opens new window)或是我们的新朋友 AV1 比特流 (opens new window),他们很相似吗?不 (opens new window),但只要学习了其中之一,学习其他的就简单多了。
# 自己动手:检查 H.264 比特流
我们可以生成一个单帧视频 (opens new window),使用 mediainfo (opens new window) 检查它的 H.264 比特流。事实上,你甚至可以查看解析 h264(AVC) 视频流的源代码 (opens new window)。
我们也可使用 Intel® Video Pro Analyzer (opens new window),需要付费,但也有只能查看前 10 帧的免费试用版,这已经够达成学习目的了。
# 回顾
我们可以看到我们学了许多使用相同模型的现代编解码器。事实上,让我们看看 Thor 视频编解码器框图,它包含所有我们学过的步骤。你现在应该能更好地理解数字视频领域内的创新和论文。
之前我们计算过我们需要 139GB 来保存一个一小时,720p 分辨率和 30fps 的视频文件,如果我们使用在这里学过的技术,如帧间和帧内预测,转换,量化,熵编码和其它我们能实现——假设我们每像素花费 0.031 bit——同样观感质量的视频,对比 139GB 的存储,只需 367.82MB。
我们根据这里提供的示例视频选择每像素使用 0.031 bit。
# 拓展
# H.265 如何实现比 H.264 更好的压缩率
我们已经更多地了解了编解码器的工作原理,那么就容易理解新的编解码器如何使用更少的数据量传输更高分辨率的视频。
我们将比较 AVC 和 HEVC,要记住的是:我们几乎总是要在压缩率和更多的 CPU 周期(复杂度)之间作权衡。
HEVC 比 AVC 有更大和更多的分区(和子分区)选项,更多帧内预测方向,改进的熵编码等,所有这些改进使得 H.265 比 H.264 的压缩率提升 50%。