图像编码
一张图片可以使用一个二维矩阵表示,矩阵中的每一个点被称为像素。每个像素的颜色使用三原色来表示,即红、绿、蓝。
每个像素可以用不同的数据位数来表示,常用的量化位数有 16 位、24 位、32 位等。
- 24 位比特模式:每像素 24 位(bits per pixel,bpp)编码的 RGB 值:使用三个 8 位无符号整数(0 到 255)表示红色、绿色和蓝色的强度。
- 16 位比特模式:分配给每种原色各为 5 比特,其中绿色为 6 比特,因为人眼对绿色分辨的色调更精确。但某些情况下每种原色各占 5 比特,余下的 1 比特不使用。
- 32 位比特模式:同 24 位比特模式,余下的 8 比特用来表示象素的透明度(Alpha)。
图片还有一个很重要的属性就是分辨率,采用宽 x 高表示。
在处理图像或视频时另一个属性是长宽比,它描述了图像或像素的宽度和高度之间的比例关系。常见的比例有 4:3、16:9、21:9,通常指显示宽高比(DAR),同样像素也有不同的宽高比,称之为像素长宽比(PAR)。
YUV 颜色模型
RGB 诉求于人眼对色彩的感应,YUV 则着重于视觉对于亮度的敏感程度,Y 代表的是亮度(Luminance、Luma),UV 代表的是色度(Chrominance/Chroma)(因此黑白电影可省略 UV,相近于 RGB),分别用 Cr 和 Cb 来表示,因此 YUV 的记录通常以 Y:UV 的格式呈现。
为节省带宽起见,大多数 YUV 格式平均使用的每像素位数都少于 24 位。主要的抽样(subsample)格式有 YCbCr 4:2:0、YCbCr 4:2:2、YCbCr 4:1:1 和 YCbCr 4:4:4。YUV 的表示法称为 A:B:C 表示法:
- 4:4:4 表示完全取样。
- 4:2:2 表示 2:1 的水平取样,垂直完全采样。
- 4:2:0 表示 2:1 的水平取样,垂直 2:1 采样。
- 4:1:1 表示 4:1 的水平取样,垂直完全采样。
RGB 与 YUV 的转换
# 第一步计算亮度
Y = 0.299R + 0.587G + 0.114B
# 一旦有了亮度,可以分割颜色(色度蓝色和红色):
Cb = 0.564(B - Y)
Cr = 0.713(R - Y)
# 也可以通过使用 YCbCr 进行转换,甚至获得 RGB
R = Y + 1.402Cr
B = Y + 1.772Cb
G = Y - 0.344Cb - 0.714Cr
采样率、码率、帧以及场的概念
图像则是对模拟信号进行采样量化后获得,而视频则是由一系列的图像组成,采集时图像的分辨率及量化位数越高,所能表达的信息越多,画面则越清晰。视频存在一个采样频率的属性,即单位时间内采样的次数。视频的采样频率也受人眼的限制,通常在每秒 20 ~ 30 帧之间。当采样频率在每秒 1020 帧时,对于快速运动的图像,人眼可以感觉到不流畅,而采样频率提高到 2030 帧时,人眼看起来比较流畅了。如果将采样频率在提高,人眼是很难感觉这种差异的,这也是目前电影拍摄时使用 24 帧或者 30 帧采样频率的原因。
显示视频所需要的每秒位数称作为比特率,也叫码率。计算公式为 比特率=宽 高 位深度 每秒帧数
* 例如,如果我们不采用任何类型的压缩,每秒 30 帧,每像素 24 位,480×240 分辨率的视频将需要 82,944,000 位/秒 或 82.944 Mbps(30x480x240x24)。
当比特率几乎恒定时,称为恒定比特率(CBR),但它也可以变化,然后称为可变比特率(VBR)。下图显示了一个受限的 VBR,在帧为黑色时不会花太多位。
视频采样中通过逐行扫描得到一幅完整的图像称之为一帧,通常帧频率为 25 帧(PAL 制)、30 帧每秒(NTSC 制),而通过隔行扫描(奇、偶数行),那么一帧图像就被分成两场,通常场频为 50Hz(PAL 制)、60Hz(NTSC 制)。这是在早起,工程师们提出的一种技术,能够在不消耗额外的带宽的情况下,使得显示器的感知帧率倍增。这种技术称为隔行视频;它基本上在 1 帧中发送一半的屏幕,而在下一帧中发送另一半。
H264 简介
H264 是属于视频的编码层的标准格式,视频编码显然是为了压缩大小。
概念
- SODB:数据比特串 -> 最原始的编码数据
- RBSP:原始字节序列载荷 -> 在 SODB 的后面填加了结尾比特(RBSP trailing bits 一个 bit“1”)若干比特“0”,以便字节对齐。
- EBSP:扩展字节序列载荷 -> 在 RBSP 基础上填加了仿校验字节(0X03)它的原因是:在 NALU 加到 Annexb 上时,需要填加每组 NALU 之前的开始码 StartCodePrefix,如果该 NALU 对应的 slice 为一帧的开始则用 4 位字节表示,0x00000001,否则用 3 位字节表示 0x000001,为了使 NALU 主体中不包括与开始码相冲突的,在编码时,每遇到两个字节连续为 0,就插入一个字节的 0x03。解码时将 0x03 去掉,也称为脱壳操作。
H.264 的功能分为两层,视频编码层(VCL)和网络提取层(NAL)
VCL 数据即被压缩编码后的视频数据序列。在 VCL 数据要封装到 NAL 单元中之后,才可以用来传输或存储。
H.264 的编码视频序列包括一系列的 NAL 单元,每个 NAL 单元包含一个 RBSP,见表 1。编码片(包括数据分割片 IDR 片)和序列 RBSP 结束符被定义为 VCL NAL 单元,其余为 NAL 单元。典型的 RBSP 单元序列如图 2 所示。
每个单元都按独立的 NAL 单元传送。单元的信息头(一个字节)定义了 RBSP 单元的类型,NAL 单元的其余部分为 RBSP 数据。
NAL 单元
每个 NAL 单元是一个一定语法元素的可变长字节字符串,包括包含一个字节的头信息(用来表示数据类型),以及若干整数字节的负荷数据。一个 NAL 单元可以携带一个编码片、A/B/C 型数据分割或一个序列或图像参数集。
NALU 头由一个字节组成,它的语法如下:
NAL 单元按 RTP 序列号按序传送。其中,T 为负荷数据类型,占 5 bit;R 为重要性指示位,占 2 个 bit;最后的 F 为禁止位,占 1 bit。具体如下:
- NALU 类型位可以表示 NALU 的 32 种不同类型特征,类型 1~12 是 H.264 定义的,类型 24~31 是用于 H.264 以外的,RTP 负荷规范使用这其中的一些值来定义包聚合和分裂,其他值为 H.264 保留。
- 重要性指示位用于在重构过程中标记一个 NAL 单元的重要性,值越大,越重要。值为 0 表示这个 NAL 单元没有用于预测,因此可被解码器抛弃而不会有错误扩散;值高于 0 表示此 NAL 单元要用于无漂移重构,且值越高,对此 NAL 单元丢失的影响越大。
- 禁止位 编码中默认值为 0,当网络识别此单元中存在比特错误时,可将其设为 1,以便接收方丢掉该单元,主要用于适应不同种类的网络环境(比如有线无线相结合的环境)。
264 常见的帧头数据为:
- 00 00 00 01 67 (SPS)
- 00 00 00 01 68 (PPS)
- 00 00 00 01 65 (IDR 帧)
- 00 00 00 01 61 (P 帧)
上述的 67、68、65、61,还有 41 等,都是该 NALU 的识别级别。
F:禁止为,0 表示正常,1 表示错误,一般都是 0
NRI:重要级别,11 表示非常重要。
TYPE:表示该 NALU 的类型是什么,
见下表,由此可知 7 为序列参数集(SPS),8 为图像参数集(PPS),5 代表 I 帧。1 代表非 I 帧。
{% asset_img h264-header.png h264-header %}
由此可知,61 和 41 其实都是 P 帧(type 值为 1),只是重要级别不一样(它们的 NRI 一个是 11 BIN,一个是 10 BIN)
H264 (NAL 简介与 I 帧判断)
我们还是接着看最上面图的码流对应的数据来层层分析,以 00 00 00 01 分割之后的下一个字节就是 NALU 类型,将其转为二进制数据后,
解读顺序为从左往右算,如下:
(1)第 1 位禁止位,值为 1 表示语法出错
(2)第 2~3 位为参考级别
(3)第 4~8 为是 nal 单元类型
例如上面 00000001 后有 67、68 以及 65
其中 0x67 的二进制码为:
0110 0111
4-8 为 00111,转为十进制 7,参考第二幅图:7 对应序列参数集 SPS
其中 0x68 的二进制码为:
0110 1000 4-8 为 01000,转为十进制 8,参考第二幅图:8 对应图像参数集 PPS
其中 0x65 的二进制码为:
011 00101
4-8 位为 00101,转为十进制 5,参考第二幅图:5 对应 IDR 图像中的片 (I 帧)
所以判断是否为 I 帧的算法为:
(NALU 类型 & 0001 1111) = 5 即 (NALU 类型 & 31) = 5 比如 0x65 & 31 = 5
RTP 打包发送 H264 之封包详解
RFC3984 是 H.264 的 baseline 码流在 RTP 方式下传输的规范
H264 的码流结构
单个 NALU
12 字节的 RTP 头后面的就是音视频数据,比较简单。一个封装单个 NAL 单元包到 RTP 的 NAL 单元流的 RTP 序号必须符合 NAL 单元的解码顺序。对于 NALU 的长度小于 MTU 大小的包,一般采用单一 NAL 单元模式。对于一个原始的 H.264 NALU 单元常由[Start Code] [NALU Header] [NALU Payload]
三部分组成,其中 Start Code 用于标示这是一个 NALU 单元的开始,必须是“00 00 00 01”或“00 00 01”, NALU 头仅一个字节,其后都是 NALU 单元内容。
打包时去除“00 00 01”或“00 00 00 01”的开始码,把其他数据封包的 RTP 包即可。
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|F|NRI| type | |
+-+-+-+-+-+-+-+-+ |
| |
| Bytes 2..n of a Single NAL unit |
| |
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| :...OPTIONAL RTP padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
如有一个 H.264 的 NALU 是这样的:
[00 00 00 01 67 42 A0 1E 23 56 0E 2F …]
这是一个序列参数集 NAL 单元。[00 00 00 01] 是四个字节的开始码,67 是 NALU 头,42 开始的数据是 NALU 内容。
封装成 RTP 包将如下:
[RTP Header] [67 42 A0 1E 23 56 0E 2F]
即只要去掉 4 个字节的开始码就可以了。
包聚合
当 NALU 的长度特别小时,可以把几个 NALU 单元封在一个 RTP 包中。
为了体现/应对有线网络和无线网络的 MTU 巨大差异,RTP 协议定义了包聚合策略:
- STAP-A:聚合的 NALU 时间戳都一样,无 DON(decoding order number);
- STAP-B:聚合的 NALU 时间戳都一样,有 DON;
- MTAP16:聚合的 NALU 时间戳不同,时间戳差值用 16 bit 记录;
- MTAP24:聚合的 NALU 时间戳不同,时间戳差值用 24 bit 记录;
- 包聚合时,RTP 的时间戳是所有 NALU 时间戳的最小值;
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|F|NRI| Type | |
+-+-+-+-+-+-+-+-+ |
| |
| one or more aggregation units |
| |
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| :...OPTIONAL RTP padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Figure 3. RTP payload format for aggregation packets
STAP-A 示例:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| RTP Header |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|STAP-A NAL HDR | NALU 1 Size | NALU 1 HDR |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| NALU 1 Data |
: :
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| | NALU 2 Size | NALU 2 HDR |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| NALU 2 Data |
: :
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| :...OPTIONAL RTP padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Figure 7. An example of an RTP packet including an STAP-A
containing two single-time aggregation units
FU-A 的分片格式
数据比较大的 H264 视频包,被 RTP 分片发送。12 字节的 RTP 头后面跟随的就是 FU-A 分片:而当 NALU 的长度超过 MTU 时,就必须对 NALU 单元进行分片封包。也称为 Fragmentation Units (FUs).
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| FU indicator | FU header | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
| |
| FU payload |
| |
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| :...OPTIONAL RTP padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Figure 14. RTP payload format for FU-A
FU indicator 有以下格式:
+---------------+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|F|NRI| Type |
+---------------+
FU 指示字节的类型域 Type=28 表示 FU-A。NRI 域的值必须根据分片 NAL 单元的 NRI 域的值设置。
FU header 的格式如下:
+---------------+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|S|E|R| Type |
+---------------+
S: 1 bit 当设置成 1 表明这是 NALU 的首个 fragmnet。当跟随的 FU 荷载不是分片 NAL 单元荷载的开始,开始位设为 0。
E: 1 bit 当设置成 1 表明是 NALU 的最后一个 fragment,即,荷载的最后字节也是分片 NAL 单元的最后一个字节。当跟随的 FU 荷载不是分片 NAL 单元的最后分片,结束位设置为 0。
R: 1 bit 保留位必须设置为 0,接收者必须忽略该位。
Type: 5 bits
NAL 单元荷载类型定义见下表
单元类型以及荷载结构总结
.Type Packet Type name
---------------------------------------------------------
0 undefined -
1-23 NAL unit Single NAL unit packet per H.264
24 STAP-A Single-time aggregation packet
25 STAP-B Single-time aggregation packet
26 MTAP16 Multi-time aggregation packet
27 MTAP24 Multi-time aggregation packet
28 FU-A Fragmentation unit
29 FU-B Fragmentation unit
30-31 undefined
拆包和解包
拆包:当编码器在编码时需要将原有一个 NAL 按照 FU-A 进行分片,原有的 NAL 的单元头与分片后的 FU-A 的单元头有如下关系:
原始的 NAL 头的前三位为 FU indicator 的前三位,原始的 NAL 头的后五位为 FU header 的后五位,
FU indicator 与 FU header 的剩余位数根据实际情况决定。
**解包:**当接收端收到 FU-A 的分片数据,需要将所有的分片包组合还原成原始的 NAl 包时,FU-A 的单元头与还原后的 NAL 的关系如下:
还原后的 NAL 头的八位是由 FU indicator 的前三位加 FU header 的后五位组成,即:
nal_unit_type = (fu_indicator & 0xe0) | (fu_header & 0x1f)