我需要知道:H.264

我在今年年初离开 YOLO 加入了一家在流媒体领域具有极深积累的小公司,负责视频群聊 SDK 的开发工作,YOLO 是一款直播 APP,我常戏称这是从技术下游(SDK 使用方)跑到了技术上游(SDK 提供方)。不过事情当然不是这么简单,经过长期的思考和探讨,我最终确认:实时多媒体领域,更宽泛一点来讲,实时视觉、感知的展现,在未来极长一段时间内都存在很大的需求,也存在很大的挑战,所以这将是我长期技术积累的大方向。

明确了大方向之后,就需要不懈地积累了。我一直强调基础知识的重要性,最近我就花时间学习了 H.264 的基础(《新一代视频压缩编码标准:H.264/AVC(第2版)》),力求搞清楚两个问题:H.264 编解码的过程是怎样的?H.264 码流的结构是怎样的?

_以前看书后分享的只是零碎的笔记,没敢发布博客,这一篇我力求根据自己的理解,把上述两个核心问题描述清楚,细节内容篇幅有限,就不做展开了。感兴趣的朋友可以阅读原书,当然,最正宗的资料莫过于 H.264 SPEC 了。_

视频编解码基础

视频为什么需要编码?

因为原始视频数据量太大

以分辨率为 640x480,帧率为 30 fps 的视频为例,如果直接传输/存储原始 RGB 数据,那码率将高达 210.94 Mbps(在专业领域码率单位通常使用 bit 而非 byte),1280x720 30 fps 码率更将高达 632.81 Mbps。

  • 640*480*3*8*30 = 210.94 Mbps(_宽*高*像素字节数*字节比特数*帧数_)
  • 1280*720*3*8*30 = 632.81 Mbps

这么高的码率显然不能直接使用。即便换成更节省空间的 YUV 格式,无论是通过网络传输还是磁盘存储,码率依然高得不可接受,所以必须进行编码压缩。

视频为什么可以编码?

因为视频数据存在冗余

首先是数据冗余,图像的各个像素之间、视频的各帧之间存在着很强的相关性。比如图片中的一堵白色墙面,各个区域的像素值很接近,比如日常拍摄的视频,内容基本上都是相同的物体在不同的位置移动。

其次是视觉冗余,根据人眼的一些特性比如亮度辨别阈值、视觉阈值、对亮度和色度的敏感度不同,即便引入适量的误差,也不会被察觉出来。

视频编码有哪些主要技术?

视频编码的目标就是在尽可能压缩数据的同时,保证视频的质量。因此,视频编码的主要技术都是围绕消除冗余、提高压缩比的。当然,考虑到基于分组交换的网络环境,以及实时多媒体应用场景,视频编码也要考虑网络自适应、容错等问题。

_注:下面这几段话,涉及了不少关键技术名词,没有相关背景的朋友可能会毫无概念,大家可以查阅维基百科词条了解其具体含义,关键词:预测编码,帧内预测,帧间预测,运动补偿,运动估计,运动矢量,变换编码,离散余弦变换,量化参数,熵编码,哈夫曼编码、算数编码_。

预测编码与运动补偿:预测编码旨在消除视频的数据冗余,经过编码压缩后,传输的不是图像中每个像素点的实际取样值,而是预测值与实际值之差。预测编码分为帧内预测和帧间预测,分别用来消除帧内冗余和帧间冗余。为了提高效率和效果,预测编码都是针对像素块完成,而不是像素点。帧内预测就是用邻近像素块预测该像素块,帧间预测则会先在邻近帧寻找该像素块的相似块,得到两者空间位置偏移量,再进行预测。我们把寻找偏移量(即相似块)的过程叫做运动估计,偏移量叫做运动矢量,我们把这种描述邻近帧差别的方式叫做运动补偿。

_注:这里所说的“预测”,实际上和“参考”是一个意思,就是找到被参考对象,与自己计算差异_。

变换编码与量化:绝大多数图像都有一个共同特征,平坦区域和内容缓慢变化区域占据一幅图像的大部分,而细节区域和内容突变区域则占小部分,即图像中直流和低频区占大部分,高频区占小部分。因此把图像从时空域变换到频域,更利于压缩。这个变换的过程,就叫变换编码,变换方法最常用的是离散余弦变换(Discrete Cosine Transform, DCT)。变换编码之后再把变换系数映射为较小的数值,这个过程叫做量化。

熵编码:利用信源的统计特性进行码率压缩的编码就称为熵编码,也叫统计编码。高频符号赋予短码,低频符号赋予长码,即可减少整体比特数。视频编码常用的熵编码有可变长编码(Variable Length Coding, VLC,也叫哈夫曼编码)和算术编码(Binary Arithmetic Coding, BAC)。

预测编码、变换编码、熵编码这样的编码框架,其实上世纪七十年代末就已经确定了下来,直到今天尚未发布的 H.266 规范依然在用,这四十年来基本都是处于老瓶装新酒的状态,当然细节之处还是在不断进行优化的。

H.264 码流结构

我们先了解 H.264 的码流结构,以及这样设计的原因,了解了码流结构后,编解码的过程就有了具体的依托。实际上 H.264 规范也是先规定了码流结构,再规定解码器的结构(_对于编码器的结构和实现模式没有具体的规定_),都是同样的道理。

句法元素分层

编码器输出的码流中,数据的基本单位是句法元素(_可以理解为码流结构每一个基本字段_),句法(Syntax)表征句法元素的组织结构,语义(Semantics)阐述句法元素的具体含义,所有的视频编码标准都是通过定义句法和语义来规范编码器工作流程的。

在 H.264 中,句法元素被组织成序列、图像、片、宏块(Macro Block, MB)、子块五个层次,如下图所示:

分层有利于节省码流,例如下一层中的共用信息可以在上一层保存,而不是每个下层结构都携带一份。但在 H.264 的分层结构中,各层数据组织并没有形成强依赖关系,这样有助于提高鲁棒性。因为分组交换容易出错,如果存在强依赖关系,一旦头部丢失,那后面的数据就无法使用了。

相较于以往标准,H.264 取消了序列层和图像层(概念上存在,但实际上取消了),把原本属于序列和图像头部的大部分句法元素抽离出来,形成了序列参数集(Sequence Parameter Set, SPS)和图像参数集(Picture Parameter Set, PPS),其余的句法元素则放入片层。参数集是独立的数据单位,不依赖参数集外的其他句法元素,它们可以单独传输、重点保护。

取消了序列层和图像层的分层结构及各层关系如下图所示:

从上图中我们可以看到,一幅图像由多个片组成,片数据会引用 PPS,PPS 又会引用 SPS,而 PPS 和 SPS 可以单独传输,重点保护

那片、宏块、子块这三层数据又是什么组织结构呢?请看下图:

  • skip_run:当图像采用帧间预测编码时,H.264 允许在图像平坦的区域使用“跳跃”块,“跳跃”块本身不携带任何数据,解码器通过周围已重建的宏块的数据来恢复“跳跃”块;
  • mb_type 是宏块类型,例如 I 帧的宏块,P 帧的宏块(_注:关于帧类型,可以搜索相关维基词条,关键词:I 帧,P 帧,B 帧,SP 帧,SI 帧_);
  • mb_predsub_mb_pred 是预测编码过程的预测信息,比如宏块如何划分,参考宏块的 id 等;
  • 残差数据(resisual)则是预测编码过程中,预测块和本块数据之间的差值;

宏块是解码的基本单元,解码器根据预测信息和残差数据,进行解码

功能分层

除了句法元素的分层,H.264 功能上也分为两层:视频编码层(Video Coding Layer, VCL)和网络抽象层(Network Abstraction Layer, NAL)。VCL 数据即编码处理的输出,正是它被分为了上述的五层结构。VCL 数据在传输或存储之前,会先封装进 NAL 单元,每个 NAL 单元则分为原始字节序列负荷(Raw Byte Sequence Payload, RBSP)和描述 RBSP(即 VCL 数据)的头部。

top Created with Sketch.