VideoCodec 入门篇 - 00 (编解码简介)
冗余信息指的是一帧或者多帧之间的多余信息,比如,一帧图片内的信息,很多都是蓝色,那么是否可以考虑用一个蓝色就来编码其他部分的内容信息呢?比如一个码流,有连续 N 帧的内容都是静止或者变化不多的,那这之间的每帧信息都有大量的重复冗余信息;
目录
1.5.1、显示宽高比 (Display Aspect Ratio)
1.5.2、像素宽高比 (Pixel Aspect Ratio)
1.8、扫描 (Interlaced | progressive)
3、编解码器 (CODEC - enCOder / DECoder)
3.1、视频编解码 vs 容器 (CODEC Vs Container)
3.3.1、图片分区 (Picture partitioning)
3.3.6、比特流格式 (BitStream Format)
本文介绍了一些关于视频编解码入门相关的一些知识,旨在能够讲述清楚关于视频编解码相关的一些基础知识,为后续的深入打基础;
1、基本术语 (Basic Terminology)
1.1、图像 (Image)
一个图像可以视作一个二维矩阵。如果将色彩考虑进来,我们可以做出推广:将这个图像视作一个三维矩阵——多出来的维度用于储存色彩信息。
如果我们选择三原色(红、绿、蓝)代表这些色彩,这就定义了三个平面:第一个是红色平面,第二个是绿色平面,最后一个是蓝色平面。
我们把这个矩阵里的每一个点称为像素(图像元素)。像素的色彩由三原色的强度(通常用数值表示)表示。例如,一个红色像素是指强度为 0 的绿色,强度为 0 的蓝色和强度最大的红色。粉色像素可以通过三种颜色的组合表示。如果规定强度的取值范围是 0 到 255,红色 255、绿色 192、蓝色 203 则表示粉色。
1.2、像素 (Pixel)
像素 (Pixel) 是光栅图像 (Raster Graphics) 中的物理点,或所有点可寻址显示设备中的最小可寻址元素;因此,它是屏幕上表示的图片的最小可控元素;
1.3、颜色深度 (Color Depth)
存储颜色的强度,需要占用一定大小的数据空间,这个大小被称为颜色深度,用于指示单个像素颜色的位数。假如每个颜色(平面)的强度占用 8 bit(取值范围为 0 到 255),那么颜色深度就是 24(8*3)bit,我们还可以推导出我们可以使用 2 的 24 次方种不同的颜色;
1.4、分辨率 (Resolution)
一个平面内像素的数量。通常表示成 width × height ,例如下面这张 4x4 的图片
1.5、宽高比 (Aspect Ratio)
1.5.1、显示宽高比 (Display Aspect Ratio)
显示宽高比,也叫 DAR,它描述了图像或像素的宽度和高度之间的比例关系。
一般来说,有 16:9 (1280/720) 和 4:3 (1024/768);
1.5.2、像素宽高比 (Pixel Aspect Ratio)
像素宽高比,也叫 PAR,是一个数学比率,用于描述数字图像中像素的宽度与该像素的高度相比;
1.6、帧率 (FPS)
将单位时间内连续的 n 帧定义为 视频;这可以视作一个新的维度,n 即为帧率,若单位时间为秒,则等同于 FPS (每秒帧数 Frames Per Second)。
1.7、码率 (BR)
播放一段视频每秒所需的数据量就是它的 Bit rate 比特率(即常说的 码率);
Bitrate = WIDTH * HEIGHT * BITS_PER_PIXEL * FPS
例如,一段每秒 30 帧,每像素 24 bits,分辨率是 480x240 的视频,如果我们不做任何压缩,它将需要 82,944,000 比特每秒或 82.944 Mbps (30x480x240x24)。
1.7.1、恒定码率 (CBR)
当比特率几乎恒定时称为恒定比特率(Constant bitrate 即 CBR);
比如以恒定的 1.2Mbps 码率播放如下内容:
1.7.2、可变码率 (VBR)
当比特率几乎可变时称为可变比特率(Variable Bitrate 即 VBR);
比如以可变码率播放如下内容:
可以看到,前面连续黑帧的时候,较低的码率 200Kbps,后面有实际的信息量的内容,以较高的码率 2.4Mbps 呈现;这样可以有效优化带宽,并能够较好的呈现有价值的信息;
1.7.3、平均码率 (ABR)
还有一个平均码率的概念, Average bitrate,可以将他视为可变码率:
1.8、扫描 (Interlaced | progressive)
1.8.1、隔行扫描 (Interlaced)
在早期,工程师们想出了一项技术能将视频的感官帧率加倍而没有消耗额外带宽。这项技术被称为隔行扫描;总的来说,它在一个时间点发送一个画面——画面用于填充屏幕的一半,而下一个时间点发送的画面用于填充屏幕的另一半。
我们常看到的 1080i,720i,这些都属于隔行扫描;
1.8.2、逐行扫描 (progressive)
在带宽充裕的今天,基本上都使用逐行,按照每行来依次绘制
我们的 1080p,720p,480p,都是逐行扫描;
2、消除冗余 (Redundancy Removal)
2.1、什么是冗余 ?(What)
冗余信息指的是一帧或者多帧之间的多余信息,比如,一帧图片内的信息,很多都是蓝色,那么是否可以考虑用一个蓝色就来编码其他部分的内容信息呢?
比如一个码流,有连续 N 帧的内容都是静止或者变化不多的,那这之间的每帧信息都有大量的重复冗余信息;
2.2、为何需要消除冗余?(Why)
打个比方,播放一个小时分辨率为 720p 和 30fps 的码流,需要多大空间呢?结论是 278GB
1280 x 720 x 24 x 30 x 3600 (宽,高,每像素比特数,fps 和秒数)
所以,不管是从数据传输的带宽和存储上来说,这个数据率太大,这基本上不可接受;
带宽计算:
WIDTH * HEIGHT * BITS_PER_PIXEL * FPS
1280 * 720 * 24 * 30
663,552,000 (663.552Mbps)
所以呢,我们需要对视频进行压缩,去除资料中的冗余信息,一方面是为了降低存储压力,另一方面是减小带宽压力;
2.3、怎么消除冗余?(How)
前面说了,可以通过压缩视频连续帧数据的方式来减小带宽和存储压力,那么具体是如何有效的压缩的呢?
下面分为几个方面来有效的压缩数据,为了能够讲清楚,并且比较生动的对比,可能会涉及一部分额外的知识,但都是为了能够更好的阐述整个压缩过程;
2.3.1、颜色空间 (Color Space)
一般来说,我们最熟悉的颜色空间是由 RGB 三基色组成的颜色空间,RGB 组成的颜色空间,比较适合在显示器上使用:
除了熟悉的 RGB 颜色空间以外,其实还有很多种方式来表达我们可见到的图像的颜色;
眼睛是一个复杂的器官,有许多部分组成,但我们最感兴趣的是视锥细胞和视杆细胞。眼睛有大约 1.2亿个视杆细胞和 6百万个视锥细胞。
让我们把颜色和亮度放在眼睛的功能部位上。视杆细胞主要负责亮度,而视锥细胞负责颜色,有三种类型的视锥,每个都有不同的颜料,叫做:
S-视锥(蓝色)
M-视锥(绿色)
L-视锥(红色)
既然我们的视杆细胞(亮度)比视锥细胞(色度)多很多,一个合理的推断是相比颜色,我们有更好的能力去区分亮度,也就是黑暗和光亮。
除了 RGB 颜色模型以外,有一种模型将亮度(光亮)和色度(颜色)分离开,它被称为 YCbCr;
这个颜色模型使用 Y 来表示亮度,还有两种颜色通道:Cb(蓝色色度) 和 Cr(红色色度)。YCbCr 可以由 RGB 转换得来,也可以转换回 RGB。使用这个模型我们可以创建拥有完整色彩的图像,如下图。
YCbCr 既然是颜色空间,RGB 也是,他们之间可以通过公式进行转换 ( ITU-R 的 BT.601标准):
第一步是计算亮度,我们将使用 ITU 建议的常量,并替换 RGB 值。
Y = 0.299R + 0.587G + 0.114B
一旦我们有了亮度后,我们就可以拆分颜色(蓝色色度和红色色度):
Cb = 0.564(B - Y) Cr = 0.713(R - Y)
并且我们也可以使用 YCbCr 转换回来,甚至得到绿色。
R = Y + 1.402Cr B = Y + 1.772Cb G = Y - 0.344Cb - 0.714Cr
色度子采样 (Chroma subsampling)
OK,现在知道一幅图的像素可以用 YCbCr 来表示,Y 分量是亮度分量,也就是前面说的,人眼最敏感的部分,Cb 和 Cr 是色度分量,人眼没那么敏感的部分,那么是否有一种可能性,在我们的一个像素的表示中,我们让 Y 分量占多一点,Cb 和 Cr 分量少占点,但是呢,又能够较为真实的还原像素 (也就是人眼大致分辨不出来的程度);这就是色度子采样;
色度子采样是一种编码图像时,使色度分辨率低于亮度的技术。
常用的 YCbCr 分为了 YCbCr 4:4:4、YCbCr 4:2:2、YCbCr 4:2:0
1. YCbCr 4:4:4,即YCbCr 4:4:4采样,每一个Y对应一组CbCr分量8+8+8 = 24bits, 占用 3个字节。
2. YCbCr 4:2:2,即YCbCr 4:2:2采样,每两个Y共用一组CbCr分量,一个YUV占8+4+4 = 16bits 占用 2个字节
3. YCbCr 4:2:0,即YCbCr 4:2:0采样,每四个Y共用一组CbCr分量一个YUV占8+2+2 = 12bits 占用 1.5个字节。
在 YCbCr 4:4:4 的情况下,每个通道采样一样:
在 YCbCr 4:2:0 的情况下:
如果是在 720p 的图中,YCbCr 4:2:0 如下:
下图是同一张图片使用几种主要的色度子采样技术进行编码,第一行图像是最终的 YCbCr,而最后一行图像展示了色度的分辨率。这么小的损失确实是一个伟大的胜利。
结论
按照将 RGB (24bit per pixel) 使用 YCbCr 4:2:0 (12 bit per pixel)来表示,我们能减少一半的视频大小 (139GB),但是依然很大;
带宽也减小了一半:
WIDTH * HEIGHT * BITS_PER_PIXEL * FPS
1280 * 720 * 12 * 30
331,776,000 (331.776Mb) bits per second
2.3.2、时间冗余 (帧间预测)
时间冗余,指的是站在时间流逝的维度上,在视频中的帧与帧之间的概念;
在谈时间冗余 (帧间预测) 之前,需要先了解帧的分类;
2.3.2.1、帧的类型 (Frame Type)
前面谈到的是,所有帧都是 YCbCr 图片,每一帧都是包含了完整的信息;视频播放本质上多图片连续播放的过程,图片与图片之间会存在一定的相关性,比如,播放一段踢足球的视频,那么足球在每一帧一定是逐步移动的一个渐变过程;那么相邻两帧之间的差别不会很大,是否可以利用某种方式来记录帧与帧之间差别,来达到不用完整信息来记录每一帧呢?
如下所示,一个 “吃豆豆” 的图,对这个图做如下先验解释:
- 这里有 4 帧图片,4 帧图片方框内的内容,是完整的图的内容;
- 这 4 帧图片代表了一个连续的运动过程,从左到右;
从图可以看出,视频中的帧的划分,分为了 3 种:I 帧、P 帧 和 B 帧,做如下说明:
I 帧 (关键帧,参考帧)
I 帧(Intra-coded picture 可参考,关键帧)是一个自足的帧。它不依靠任何东西来渲染,I 帧与静态图片相似。第一帧通常是 I 帧,但我们将看到 I 帧被定期插入其它类型的帧之间。
P 帧 (预测)
P 帧(Predicted picture)仅保存图像中与上一帧相比的变化。例如,在汽车在静止背景中移动的场景中,只需要对汽车的运动进行编码。编码器不需要将不变的背景像素存储在P帧中,从而节省了空间。P 帧也称为 delta‑frames;
B 帧 (双向预测)
B 帧(Bi-predictive picture)通过使用当前帧与前一帧和后帧之间的差异来指定其内容,从而节省更多空间;
可以知道,I 帧占空间大,比较 “值钱”,其次是 P 帧,B 帧 “最棒”;
Video 中,有多种类型,可以用 B 帧,也可以不用 B 帧;
2.3.2.2、帧间预测 (Temporal redundancy)
前面说了:
I 帧的可压缩性最低,但不需要其他视频帧来解码。
P 帧可以使用先前帧中的数据进行解压缩,并且比 I 帧更易于压缩。
B 帧可以使用前一帧和前一帧作为数据参考,以获得最大量的数据压缩。
上面的图,是一个红色的小球逐步运动的过程,我们可以把他理解为视频;
第一行有 5 帧,original frames 是重建后的帧;
第二行也有 5 帧,encoded frames 代表了用 I 帧和 P 帧编码的过程,首先是 I 帧,接着用 3 个 P 帧来构建预测帧 (替代第一行的完整的图像信息);
我们利用这样编码后,得到的帧的 Size:
可以明显的看到,I 帧 103Kb,在 P 帧的地方,被压缩到了 2Kb;
2.3.2.3、残差 (Frame difference)
相邻的两帧,抽取出不同的部分,来进行编码,如下所示:
Frame 0 和 Frame 1,描述了一个小球的滚动,可以简单的想到一个办法就是利用两张图的 Diff 来编码第二帧图:
这样看起来,我们只需要记录 difference 的部分就可以根据 I 帧来还原其他帧了;但我们有一个更好的方法来节省数据量。首先,我们将
0 号帧 视为一个个分块的集合,然后我们将尝试将
帧 1 和
帧 0 上的块相匹配。我们可以将这看作是运动预测
2.3.2.4、运动预测 (Motion estimation)
运动补偿是一种描述相邻帧(相邻在这里表示在编码关系上相邻,在播放顺序上两帧未必相邻)差别的方法,具体来说是描述前面一帧(相邻在这里表示在编码关系上的前面,在播放顺序上未必在当前帧前面)的每个小块怎样移动到当前帧中的某个位置去;
第一帧这个小球在 (x=0, y=25) 的地方,第二帧,小球在 (x=7, y=26);这里的 x,y 就是运动向量 (motion vector);进一步节省数据量的方法是,只编码这两者运动向量的差。所以,最终运动向量就是
x=7 (7-0), y=1 (26-25);
如下所示,能看到当我们使用运动预测时,编码的数据量少于使用简单的残差帧技术。
可以利用 FFmpeg 来生成带帧间预测 (运动向量) 的视频;
2.3.3、空间冗余 (帧内预测)
空间预测,指的是在一帧图像中,不同位置,可能具有多处地方有类似的像素;
比如下面的图,红色框框空间的部分,都是类似的颜色;
那是否也可以将一帧图内的相似处,也进行压缩呢?
我们来看如下例子;
我们将分为 4 个过程 A、B、C、D 来阐述;
- A 过程:首先摘取了一个 4x4 大小的像素框,写了数字的地方是代表我们知道它的实际像素值;问号部分我们不管他 (其实也是已知的值,只不过为了展示帧内编码过程,我们先假装不知道它)
- B 过程:我们已知的部分 (横向 100、100、100、200 和纵向的 100、100、100、100),以预测像素的方向向下 (Direction);就可以得到预测的像素 ;这里预测,在同一个向下的方向上,他们都像素值一样;
- C 过程:是真实的这 4x4 大小的像素框的实际像素值,可以看到,我们的预测其实是有点错误的(最后一行的 2 个像素),不过还好,错误不大;
- D 过程:将 B 和 C 的值做差值,得出的数据是高度可压缩的内容;
这里,我们假设预测的方向是向下的,实际上呢,预测的方向还可以是多方向的:
3、编解码器 (CODEC - enCOder / DECoder)
编解码器是用于对数字数据流或信号进行编码或解码的设备或计算机程序;
之前计算过 720p@30fps 的数据量很大,所以我们需要一种行之有效的方式来压缩数据;将图像数据压缩的部分称之为编码器,将压缩数据恢复成为图像的部分,成为解码器;
3.1、视频编解码 vs 容器 (CODEC Vs Container)
一个常见的错误是,混淆视频编码格式和数字视频容器格式;
VP8、VP9、H.264、H.265 这种我们称为编解码格式,指的是将帧按照对应的协议压缩/解压缩的过程;
MP4、MKV 我们成为容器,我们可以将容器视为包含视频(也很可能包含音频、字幕)元数据 (Metadata)的包装格式,压缩过的视频可以看成是它承载的内容;
默认情况下,我们按照对应的容器名称来命名文件,比如 video.mp4,大概率可能是 MPEG-4 Part 14 容器,一个叫 video.mkv 的文件可能是 matroska。
我们可以使用 ffmpeg 或 mediainfo 来完全确定编解码器和容器格式
不要认为修改文件后缀就可以改变它的容器封装格式 —— 谢谢;
下面的图展示了一个容器,可能包含了 audio、video 以及 subtitles;
常见的容器有:
- OGG
- MP4
- WMA
- AVI
- MKV, WebM
- TS
- MOV
常见的编解码格式:
- H264 / AVC
- H265 / HEVC
- MPEG-4
- VP9
- AV1
- Theora
- Daala
编解码格式和容器呢,并不是可以完全随意配合使用的:
我们的重点是介绍编解码,下面来看编解码的历史;
3.2、历史 (History)
这些编解码器中的每一个都是为了解决特定问题而创建的(如传输视频56kbps,电话,视频会议),互联网带宽变大,分辨率变大,CPU功率变大,等等;
视频编解码器 H.261 诞生在 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 是由 Google, Mozilla, Microsoft, Amazon, Netflix, AMD, ARM, NVidia, Intel, Cisco 等公司组成的开放媒体联盟(AOMedia)设计的一种新的免版税和开源的视频编解码器。
AV1 的诞生
2015 年早期,Google 正在开发VP10,Xiph (Mozilla) 正在开发Daala,Cisco 开源了其称为 Thor 的免版税视频编解码器。
接着 MPEG LA 宣布了 HEVC (H.265) 每年版税的的上限,比 H.264 高 8 倍,但很快他们又再次改变了条款:
- 不设年度收费上限
- 收取内容费(收入的 0.5%)
- 每单位费用高于 h264 的 10 倍
开放媒体联盟由硬件厂商(Intel, AMD, ARM , Nvidia, Cisco),内容分发商(Google, Netflix, Amazon),浏览器维护者(Google, Mozilla),等公司创建。
这些公司有一个共同目标,一个免版税的视频编解码器,所以 AV1 诞生时使用了一个更简单的专利许可证。Timothy B. Terriberry 做了一个精彩的介绍,关于 AV1 的概念,许可证模式和它当前的状态,就是本节的来源。
前往 Analyzer, 你会惊讶于使用你的浏览器就可以分析 AV1 编解码器。 av1 浏览器分析器
3.3、通用解码器
我们接下来要介绍通用视频编解码器背后的主要机制,几乎所有的编解码器都以类似的方式工作,并被现代编解码器如 VP9, AV1 和 HEVC 使用。需要注意:我们将简化许多内容。有时我们会使用真实的例子(主要是 H.264)来演示技术;
整个编码和解码的过程由几部分组成:
- 图片分区 (Picture partitioning);
- 预测 (Predictions);
- 转换 (Transform);
- 量化 (Quantization);
- 熵编码 (Entropy coding);
3.3.1、图片分区 (Picture partitioning)
Codec 流程的第一步是将帧分成几个分区,子分区甚至更多,叫做 picture partitioning;
比如一幅图,经过分区后如下所示:
当我们分割图片时,我们可以更精确的处理预测;在微小移动的部分使用较小的分区,而在静态背景上使用较大的分区;
通常,编解码器将这些分区组织成 slices(或 tiles),macroblock(或 coding tree units)和许多子分区。这些分区的最大大小有所不同,HEVC 设置成 64x64,而 AVC 使用 16x16,但子分区可以达到 4x4 的大小。
3.3.2、预测 (Predictions)
一旦我们有了分区,我们就可以在它们之上做出预测 (注意,我们是在 macroblocks 和 slices 上来讨论预测)。
- 对于帧间预测,我们需要发送运动向量和残差;
- 至于帧内预测,我们需要发送预测方向和残差。
帧间预测和帧内预测之前的章节有描述,这里就不多说了;
3.3.3、转换 (Transform)
转换的目的是将像素块转为更加容易编码的内容;
在我们得到残差块(predicted partition - real partition)之后,我们可以用一种方式变换它,这样我们就知道哪些像素我们应该丢弃,还依然能保持整体质量。这个确切的行为有几种变换方式。
尽管有其它的变换方式,但我们重点关注离散余弦变换(DCT)。DCT 的主要功能有:
- 将像素块转换为相同大小的频率系数块。
- 压缩能量,更容易消除空间冗余。
- 可逆的,也意味着你可以还原回像素。
2017 年 2 月 2 号,F. M. Bayer 和 R. J. Cintra 发表了他们的论文:图像压缩的 DCT 类变换只需要 14 个加法。
我们来看下面的像素块(8x8):
下面是其渲染的块图像(8x8):
当我们对这个像素块应用 DCT 时, 得到如下系数块(8x8):
接着如果我们渲染这个系数块,就会得到这张图片:
如你所见它看起来完全不像原图像,我们可能会注意到第一个系数与其它系数非常不同。第一个系数被称为直流分量,代表了输入数组中的所有样本,有点类似于平均值。
这个系数块有一个有趣的属性:高频部分和低频部分是分离的。
在一张图像中,大多数能量会集中在低频部分,所以如果我们将图像转换成频率系数,并丢掉高频系数,我们就能减少描述图像所需的数据量,而不会牺牲太多的图像质量。
频率是指信号变化的速度。
让我们通过实验学习这点,我们将使用 DCT 把原始图像转换为频率(系数块),然后丢掉最不重要的系数。
首先,我们将它转换为其频域。
然后我们丢弃部分(67%)系数,主要是它的右下角部分。
然后我们从丢弃的系数块重构图像(记住,这需要可逆),并与原始图像相比较。
如我们所见它酷似原始图像,但它引入了许多与原来的不同,我们丢弃了67.1875%,但我们仍然得到至少类似于原来的东西。我们可以更加智能的丢弃系数去得到更好的图像质量,但这是下一个主题。
3.3.4、量化 (Quantization)
当我们丢弃一些系数时,在最后一步(变换),我们做了一些形式的量化。这一步,我们选择性地剔除信息(有损部分)或者简单来说,我们将量化系数以实现压缩。
我们如何量化一个系数块?一个简单的方法是均匀量化,我们取一个块并将其除以单个的值(10),并舍入值。
我们如何逆转(重新量化)这个系数块?我们可以通过乘以我们先前除以的相同的值(10)来做到。
这不是最好的方法,因为它没有考虑到每个系数的重要性,我们可以使用一个量化矩阵来代替单个值,这个矩阵可以利用 DCT 的属性,多量化右下部,而少(量化)左上部,JPEG 使用了类似的方法,你可以通过查看源码看看这个矩阵。
3.3.5、熵编码 (Entropy coding)
在我们量化数据(图像块/切片/帧)之后,我们仍然可以以无损的方式来压缩它。有许多方法(算法)可用来压缩数据,主要围绕熵编码。
首先,熵编码是一种无损压缩的编码,即,恢复的时候,可以完全恢复到之前的数据而不失真;
其次,熵编码有很多种方式:
- 霍夫曼编码 (Huffman)
- 算术编码
- 行程编码 (RLE)
- 基于上下文的自适应变长编码(CAVLC)
- 基于上下文的自适应二进制算术编码(CABAC)
JPEG 用的是 Huffman 编码和算术编码,H264 用的是 CAVLC 和 CABAC。
H264 (chroma subsampling, motion estimation, intra prediction, CABAC…) 之后,我们得到视频的带宽和大小为:
WIDTH * HEIGHT * BITS_PER_PIXEL * FPS
1280 * 720 * 0.031 * 30
857,088 (837Kb) bits per second
3,085,516,800 (367.82MB)
整个压缩过程从 278GB -> 139GB -> 367.82MB;
3.3.6、比特流格式 (BitStream Format)
上述 5 个过程完毕后,我们可以得到编码后的原始视频数据了
完成前面的所有这些步之后,我们得到的是按照我们预设的一系列操作后,得到的裸数据流;
为了能让解码器知道我们在编码过程中,使用了哪些不同的配置,以及图像原生的一些信息,我们需要明确告知解码器编码定义,如颜色深度,颜色空间,分辨率,预测信息(运动向量,帧内预测方向),配置,层级,帧率,帧类型,帧号等等更多信息。
将码流的信息一起打包到码流中,生成 Bitstream;
bit depth, color space, resolution, predictions info (motion vectors, intra-prediction direction), profile, level, frame rate, frame type, frame number and etc
这里以 H.264 的 Bitstream 为例;
3.3.6.1、H.264 比特流
H.264 的 Bitstream 的封装如下所示:
由同步字段和 NAL Unit (Network Abstraction Layer,有的地方叫 NALU) 组成;
同步字段的开始是 0x00, 0x00, 0x00, 0x01;
后面的同步字段是 0x00, 0x00, 0x01;
打开一段 H.264 的码流,可以看到开始的部分,至少由 3 个同步字段:
这个 NAL 呢,是 H.264 最基本的数据存储单元,它可以存储很多类型的数据,前面说过,H.264 呢,不仅仅只包含了码流部分,也保留了很多的信息来描述编码的码流信息,如:帧、颜色、使用的参数等;所以这里 NAL 的真实 Payload 就有很多种类型;
NAL Unit 分为了 header (1 Byte) + payload 部分,header 部分包含了如下内容:
- forbidden_bit (1 bit):传输中发生错误时,会被置为1,告诉接收方丢掉该单元;否则为 0;
- nal_ref_idc (2 bit):指示当前 NALU 的优先级,或者说重要性,数值越大表明越重要。
On one hand, if it is a reference field / frame / picture, nal_ref_idc is not equal to 0. According to the Recommendation, non-zero nal_ref_idc specifies that the content of the NAL unit contains a sequence parameter set (SPS), a SPS extension, a subset SPS, a picture parameter set (PPS), a slice of a reference picture, a slice data partition of a reference picture, or a prefix NAL unit preceding a slice of a reference picture.
On the other hand, if it is a non-reference field / frame / picture, nal_ref_idc is equal to 0.
一方面,如果它是参考字段/帧/图片,则nal_ref_idc不等于0。根据H264标准,非零nal_ref_idcs指示NALU的内容可能为:序列参数集(SPS),SPS扩展,子SPS,图片参数集(PPS),参考图片的切片,参考图片的切片数据分区,或参考图片的切片之前的前缀 NALU。 另一方面,如果它是非参考场/帧/图片,则nal_ref_idc等于0。
- nal_unit_type (5 bit):表示 NALU 的类型;
参考:Introduction to H.264: NAL Unit
这个 nal_unit_type 包含了一下可能的内容(这里为了不引入更多的其他概念,进行简要的说明):
nal_type_id | Description | NAL 类型 |
0 | Undefined | non-VCL |
1 | Coded slice of a non-IDR picture | VCL |
2 | Coded slice data partition A | VCL |
3 | Coded slice data partition B | VCL |
4 | Coded slice data partition C | VCL |
5 | IDR Coded slice of an IDR picture | VCL |
6 | SEI Supplemental enhancement information | non-VCL |
7 | SPS Sequence parameter set | non-VCL |
8 | PPS Picture parameter set | non-VCL |
9 | Access unit delimiter (AU) | non-VCL |
10 | End of sequence | non-VCL |
11 | End of stream | non-VCL |
... | ... | ... |
更多详细的说明,参考另一面专门分析 H.264 的文章;
包含图像数据的 unit 属于VCL NAL units.
SPS、PPS、和 SEI 属于Non-VCL NAL Units;
- 1-4:I/P/B帧,合起来介绍的原因是,他们是依据VLC的slice区分的,本文暂不涉及;
- 5:IDR帧。I 帧的一种,告诉解码器,之前依赖的解码参数集合(接下来要出现的SPS\PPS等)可以被刷新了。
- 6:SEI,英文全称 Supplemental Enhancement Information,翻译为“补充增强信息”,提供了向视频码流中加入额外信息(自定义信息)的方法。
- 7:SPS,全称Sequence Paramater Set,翻译为“序列参数集”。SPS中保存了一组编码视频序列(Coded Video Sequence)的全局参数。因此该类型保存的是和编码序列相关的参数。
- 8: PPS,全称Picture Paramater Set,翻译为“图像参数集”。该类型保存了整体图像相关的参数。
- 9:AU分隔符,AU全称Access Unit,它是一个或者多个NALU的集合,代表了一个完整的帧。
通常,比特流的第一个 NAL 是 SPS,这个类型的 NAL 负责传达通用编码参数,如配置,层级,分辨率
3.3.6.2、H.264 Vs H.265
我们已经更多地了解了编解码器的工作原理,那么就容易理解新的编解码器如何使用更少的数据量传输更高分辨率的视频。
我们将比较 AVC(H.264) 和 HEVC(H.265),要记住的是:我们几乎总是要在压缩率和更多的 CPU 周期(复杂度)之间作权衡。
HEVC 比 AVC 有更大和更多的分区(和子分区)选项,更多帧内预测方向,改进的熵编码等,所有这些改进使得 H.265 比 H.264 的压缩率提升 50%。
4、针对 H.264 补充
有了前面的先验知识后,这里补充几点内容;
4.1、H.264/AVC
H.264/AVC标准是由ITU-T和ISO/IEC联合开发的,定位于覆盖整个视频应用领域,包括:低码率的无线应用、标准清晰度和高清晰度的电视广播应用、Internet上的视频流应用,传输高清晰度的DVD视频以及应用于数码相机的高质量视频应用等等。
ITU-T给这个标准命名为 H.264(以前叫做H.26L),而ISO/IEC 称它为 MPEG-4 高级视频编码(Advanced Video Coding,AVC),并且它将成为MPEG-4标准的第10部分。
4.2、帧的说明 (I、B、P)
4.2.1、I 帧
I 帧:即Intra-coded picture(帧内编码图像帧),I帧表示关键帧,你可以理解为这一帧画面的完整保留;解码时只需要本帧数据就可以完成(因为包含完整画面)。又称为内部画面 (intra picture),I 帧通常是每个 GOP(MPEG 所使用的一种视频压缩技术)的第一个帧,经过适度地压缩,做为随机访问的参考点,可以当成图象。在MPEG编码的过程中,部分视频帧序列压缩成为I帧;部分压缩成P帧;还有部分压缩成B帧。I帧法是帧内压缩法,也称为“关键帧”压缩法。I帧法是基于离散余弦变换DCT(Discrete Cosine Transform)的压缩技术,这种算法与JPEG压缩算法类似。采用I帧压缩可达到1/6的压缩比而无明显的压缩痕迹。
【I帧特点】
- 它是一个全帧压缩编码帧。它将全帧图像信息进行JPEG压缩编码及传输;
- 解码时仅用I帧的数据就可重构完整图像;
- I帧描述了图像背景和运动主体的详情;
- I帧不需要参考其他画面而生成;
- I帧是P帧和B帧的参考帧(其质量直接影响到同组中以后各帧的质量);
- I帧是帧组GOP的基础帧(第一帧),在一组中只有一个I帧;
- I帧不需要考虑运动矢量;
- I帧所占数据的信息量比较大。
【I帧编码流程】
- 进行帧内预测,决定所采用的帧内预测模式。
- 像素值减去预测值,得到残差。
- 对残差进行变换和量化。
- 变长编码和算术编码。
- 重构图像并滤波,得到的图像作为其它帧的参考帧。
4.2.2、P帧
P帧:即Predictive-coded Picture(前向预测编码图像帧)。P帧表示的是这一帧跟之前的一个关键帧(或P帧)的差别,解码时需要用之前缓存的画面叠加上本帧定义的差别,生成最终画面。(也就是差别帧,P帧没有完整画面数据,只有与前一帧的画面差别的数据)
【P帧的预测与重构】
P帧是以I帧为参考帧,在I帧中找出P帧“某点”的预测值和运动矢量,取预测差值和运动矢量 (MV) 一起传送。在接收端根据运动矢量从I帧中找出P帧“某点”的预测值并与差值相加以得到P帧“某点”样值,从而可得到完整的P帧。
【P帧特点】
- P帧是I帧后面相隔1~2帧的编码帧;
- P帧采用运动补偿的方法传送它与前面的I或P帧的差值及运动矢量(预测误差);
- 解码时必须将I帧中的预测值与预测误差求和后才能重构完整的P帧图像;
- P帧属于前向预测的帧间编码。它只参考前面最靠近它的I帧或P帧;
- P帧可以是其后面P帧的参考帧,也可以是其前后的B帧的参考帧;
- 由于P帧是参考帧,它可能造成解码错误的扩散;
- 由于是差值传送,P帧的压缩比较高。
4.2.3、B帧
B帧:即Bidirectionally predicted picture(双向预测编码图像帧)。B帧是双向差别帧,也就是B帧记录的是本帧与前后帧的差别,换言之,要解码B帧,不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面的与本帧数据的叠加取得最终的画面。B帧压缩率高,但是解码时CPU会比较累。
【B帧的预测与重构】
B帧以前面的I或P帧和后面的P帧为参考帧,“找出”B帧“某点”的预测值和两个运动矢量,并取预测差值和运动矢量传送。接收端根据运动矢量在两个参考帧中“找出(算出)”预测值并与差值求和,得到B帧“某点”样值,从而可得到完整的B帧。采用运动预测的方式进行帧间双向预测编码
【B帧特点】
- B帧是由前面的I或P帧和后面的P帧来进行预测的;
- B帧传送的是它与前面的I帧或P帧和后面的P帧之间的预测误差及运动矢量;
- B帧是双向预测编码帧;
- B帧压缩比最高,因为它只反映丙参考帧间运动主体的变化情况,预测比较准确;
- B帧不是参考帧,不会造成解码错误的扩散
【为什么需要B帧】
从上面的看,我们知道I和P的解码算法比较简单,资源占用也比较少,I只要自己完成就行了,P呢,也只需要解码器把前一个画面缓存一下,遇到P时就使用之前缓存的画面就好了,如果视频流只有I和P,解码器可以不管后面的数据,边读边解码,线性前进,大家很舒服。那么为什么还要引入B帧?
网络上的电影很多都采用了B帧,因为B帧记录的是前后帧的差别,比P帧能节约更多的空间,但这样一来,文件小了,解码器就麻烦了,因为在解码时,不仅要用之前缓存的画面,还要知道下一个I或者P的画面(也就是说要预读预解码),而且,B帧不能简单地丢掉,因为B帧其实也包含了画面信息,如果简单丢掉,并用之前的画面简单重复,就会造成画面卡(其实就是丢帧了),并且由于网络上的电影为了节约空间,往往使用相当多的B帧,B帧用的多,对不支持B帧的播放器就造成更大的困扰,画面也就越卡。
4.3、帧间的参考关系
I、B、P之间的参考关系如下所示:
注意一点:
- B 帧只能向前参考 I 或者 P;
- P 帧只能可以参考 I 和 另一个 P;
4.3.1、序列 (GOP)
编码器将多张图像进行编码后生产成一段一段的 GOP ( Group of Pictures ) , 解码器在播放时则是读取一段一段的 GOP 进行解码后读取画面再渲染显示。GOP ( Group of Pictures) 是一组连续的画面,由一张 I 帧和数张 B / P 帧组成,是视频图像编码器和解码器存取的基本单位,它的排列顺序将会一直重复到影像结束。
所以,有的地方也说 GOP 指的是两个 I 帧之间的距离;
由于P、B帧的复杂度大于I帧,所以过多的P、B帧会影响编码效率,使编码效率降低。另外,过长的GOP还会影响Seek操作的响应速度,由于P、B帧是由前面的I或P帧预测得到的,所以Seek操作需要直接定位,解码某一个P或B帧时,需要先解码得到本GOP内的I帧及之前的N个预测帧才可以,GOP值越长,需要解码的预测帧就越多,seek响应的时间也越长。
4.4、播放顺序与编解码顺序
对于不带 B 帧的编解码,帧的播放顺序和解码顺序是一致的:
对于带了 IPB 帧的,他们之间有相互参考,所以编码的帧的显示顺序和编码顺序是可能不一样的;
4.4.1、PTS 和 DTS
由于播放顺序和解码顺序可能存在不一致,那么就引入了 2 个概念:
- DTS(Decoding Time Stamp):即解码时间戳,这个时间戳的意义在于告诉播放器该在什么时候解码这一帧的数据。
- PTS(Presentation Time Stamp):即显示时间戳,这个时间戳用来告诉播放器该在什么时候显示这一帧的数据。
虽然 DTS、PTS 是用于指导播放端的行为,但它们是在编码的时候由编码器生成的;
当视频流中没有 B 帧时,通常 DTS 和 PTS 的顺序是一致的。但如果有 B 帧时,就回到了我们前面说的问题:解码顺序和播放顺序不一致了。
在视频采集的时候是录制一帧就编码一帧发送一帧的,在编码的时候会生成 PTS,这里需要特别注意的是 frame(帧)的编码方式,在通常的场景中,编解码器编码一个 I 帧,然后向前跳过几个帧,用编码 I 帧作为基准帧对一个未来 P 帧进行编码,然后跳回到 I 帧之后的下一个帧。编码的 I 帧和 P 帧之间的帧被编码为 B 帧。之后,编码器会再次跳过几个帧,使用第一个 P 帧作为基准帧编码另外一个 P 帧,然后再次跳回,用 B 帧填充显示序列中的空隙。这个过程不断继续,每 12 到 15 个 P 帧和 B 帧内插入一个新的 I 帧。P 帧由前一个 I 帧或 P 帧图像来预测,而 B 帧由前后的两个 P 帧或一个 I 帧和一个 P 帧来预测,因而编解码和帧的显示顺序有所不同;
假设这里实际的视频送入编码器的帧是这样子的:
I B B P
那么它的 PTS 对应的就是
PTS:1 2 3 4
编码顺序是:
1 4 2 3
推流顺序也是按照编码顺序去推的,即
I P B B
那么接收断收到的视频流也就是
I P B B
这时候去解码,也是按照收到的视频流一帧一帧去解的了,接收一帧解码一帧,因为在编码的时候已经按照 I、B、P 的依赖关系编好了,接收到数据直接解码就好了。
那么解码的顺序就是:
可以看到解码出来对应的 PTS 不是顺序的,为了正确显示视频流,这时候我们就必须按照 PTS 调整解码后的 frame(帧),即
参考下面的图:
4.5、IDR 帧
IDR 帧属于 I 帧。解码器收到 IDR frame 时,将所有的参考帧队列丢弃,这点是所有 I 帧共有的特性,但是收到 IDR 帧时,解码器另外需要做的工作就是:把所有的 PPS 和 SPS 参数进行更新。IDR 的出现其实是相当于向解码器发出了一个清理 reference buffer 的信号吧,上面说前于这一帧的所有已编码帧不能为 inter 做参考帧了。
IDR是强制刷新帧,因为P和B都不是记录完整信息,记录的是与其它帧的差异,所以如果I帧本身有错误,这个错误就会遗传到下面的帧,但碰到IDR帧的时候,播放器丢弃以前所以的信息,从新开始解码,错误也就到此为止。
I和IDR帧都使用帧内预测,在编码解码中为了方便,首个I帧要和其他I帧区别开,把第一个I帧叫IDR,这样方便控制编码和解码流程,所以IDR帧一定是I帧,但I帧不一定是IDR帧;IDR帧的作用是立刻刷新,使错误不致传播,从IDR帧开始算新的序列开始编码。I帧有被跨帧参考的可能,IDR不会。
IDR(Instantaneous Decoding Refresh)--即时解码刷新。
I帧:帧内编码帧是一种自带全部信息的独立帧,无需参考其它图像便可独立进行解码,视频序列中的第一个帧始终都是I帧。
I和IDR帧都是使用帧内预测的。它们都是同一个东西而已,在编码和解码中为了方便,要首个I帧和其他I帧区别开,所以才把第一个首个I帧叫IDR,这样就方便控制编码和解码流程。 IDR帧的作用是立刻刷新,使错误不致传播,从IDR帧开始,重新算一个新的序列开始编码。而I帧不具有随机访问的能力,这个功能是由IDR承担。 IDR会导致DPB(DecodedPictureBuffer 参考帧列表——这是关键所在)清空,而I不会。IDR图像一定是I图像,但I图像不一定是IDR图像。一个序列中可以有很多的I图像,I图像之后的图象可以引用I图像之间的图像做运动参考。
对于IDR帧来说,在IDR帧之后的所有帧都不能引用任何IDR帧之前的帧的内容,与此相反,对于普通的I-帧来说,位于其之后的B-和P-帧可以引用位于普通I-帧之前的I-帧。从随机存取的视频流中,播放器永远可以从一个IDR帧播放,因为在它之后没有任何帧引用之前的帧。但是,不能在一个没有IDR帧的视频中从任意点开始播放,因为后面的帧总是会引用前面的帧 。
收到 IDR 帧时,解码器另外需要做的工作就是:把所有的 PPS 和 SPS 参数进行更新。
对IDR帧的处理(与I帧的处理相同):(1) 进行帧内预测,决定所采用的帧内预测模式。(2) 像素值减去预测值,得到残差。(3) 对残差进行变换和量化。(4) 变长编码和算术编码。(5) 重构图像并滤波,得到的图像作为其它帧的参考帧。
4.6、防止竞争
H.264 的 NAL 的起始码 (Start Code) 为 0x000001,所以当读到 0x000001 的时候,认为是一个 NAL 结束,下一个 NAL 开始;H.264 规定,当监测到 0x000000 时,也可以认为当前 NAL 结束;这是因为,连着 3 个字节的 0x00,中的任何一个字节的 0,要么属于起始码,要么是起始码前面添加的 0;
但,如果在 NAL 的内部中,出现了 0x000001 或者 0x000000 序列时,咋办?此刻解码器就不知道该怎么办了,会造成解码错位;大量的实验证明,NAL 内部经常会出现这样的字节序列;
所以 H.264 提出一种方式,叫 “防止竞争”,在编码完一个 NAL 的时候,应该监测 NAL 内是否出现下面 4 个字节序列:
- 0x000000
- 0x000001
- 0x000002 保留
- 0x000003
防止他们和 Start Code 出现竞争;
如果检测到这些序列存在,编码器将在最后一个字节前,插入一个新字节,0x03,从而变成:
- 0x000000 -------> 0x00000300
- 0x000001 -------> 0x00000301
- 0x000002 -------> 0x00000302
- 0x000003 -------> 0x00000303
在解码器在 NAL 内部检测到有 0x000003 序列的时候,将扔掉 03,恢复原始的数据;
4.7、POC (Picture Order Count)
表示了图像的播放顺序,使用了 B 帧,所以播放顺序和编解码顺序可能不一致;
POC 可以由 frame-num 通过映射关系计算得来;也可以由编码器显示的传输;
4.8、AVSYNC (音视频同步)
上面说了视频帧、DTS、PTS 相关的概念。我们都知道在一个媒体流中,除了视频以外,通常还包括音频。音频的播放,也有 DTS、PTS 的概念,但是音频没有类似视频中 B 帧,不需要双向预测,所以音频帧的 DTS、PTS 顺序是一致的。
音频视频混合在一起播放,就呈现了我们常常看到的广义的视频。在音视频一起播放的时候,我们通常需要面临一个问题:怎么去同步它们,以免出现画不对声的情况。
要实现音视频同步,通常需要选择一个参考时钟,参考时钟上的时间是线性递增的,编码音视频流时依据参考时钟上的时间给每帧数据打上时间戳。在播放时,读取数据帧上的时间戳,同时参考当前参考时钟上的时间来安排播放。这里的说的时间戳就是我们前面说的 PTS。实践中,我们可以选择:同步视频到音频、同步音频到视频、同步音频和视频到外部时钟。
![Logo](https://devpress.csdnimg.cn/79de2bf0b7994defa4242ef90d5513fa.jpg)
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)