H264是基于运动补偿的视频编码标准。所谓编码我的理解就是对数据进行压缩便于网络传输。而视频编码就是依据图像帧的像素块之间的相似性对图像进行压缩。
H264结构中,一幅图像编码后的数据叫一帧,一帧由一个或多个Slice片组成,一片由一个或多个MB宏块组成,一个宏块由16*16的yuv数据组成。宏块是H264编码的基本单位。
H264定义了三种帧,I帧,P帧,B帧。
group of picture 两个I帧之间的所有帧为一个GOP。
H264对关联度高的视频帧进行分组,其算法是在相邻几幅图像画面中,一般有差别的像素只有10%以内的点,亮度差值变化不超过2%,而色度差值的变化只有1%以内,我们认为这样的图可以分到一组GOP。
IDR帧都是I帧,但I帧不一定是IDR帧。当解码器遇到IDR帧就会清空参考队列,将已解码的数据全部输出或抛弃,开始解码新的序列。
而普通的I帧不会清理参考队列,也就是说IDR可以阻断误差的累计,而普通I帧不行。
PTS(Presentation Time Stamp):PTS 主要用于度量解码后的视频帧什么时候被显示出来
DTS(Decode Time Stamp):DTS 主要是标识内存中的 bit 流什么时候开始送入解码器中进行解码
DTS 主要用户视频的解码,在解码阶段使用。PTS主要用于视频的同步和输出,在显示的时候使用。
由于B帧的存在,要参考前后帧,所以在有B帧的情况下 DTS!=PTS。没有B帧则两者相等。
https://zhuanlan.zhihu.com/p/31056455
视频图像被送到H264编码器中,编码器给每一个图像划分宏块。H264默认使用16*16大小作为一个宏块。但为了更高的压缩率,还可以将宏块划分为8*16、 16*8、 8*8、 4*8、 8*4、 4*4大小的子块。
对划分好的宏块计算宏块的像素值。最终一幅图中每个宏块都处理完后如下图:
运动估计与运动补偿:
运动估计:当前帧的某个区域(A)在参考帧中寻找一个合适的匹配区域(B)。
运动补偿:找到区域A和区域B的不同。
这运动矢量就是炮二的区域移动到炮五的区域,移动后产生一个预测帧。预测帧和当前帧并不完全一样,他们的区别就是残差。 此时的残差则是炮二位置的棋格,以及炮五边框的颜色变化。
预测性编码的产出就是这些运动矢量和残差,通过这个例子我们能看到这些产出数据是远远小于一个完整帧的数据量的。
https://www.cnblogs.com/charybdis/p/6049108.html
对于一幅图像,相邻的两个像素的亮度和色度是比较接近的,所以在保存一个像素时不需要将这个像素的全部信息保存,只需要保存这个像素与其参考像素的插值即可。
使用上一个像素X’做参考像素,经过帧内预测获得当前像素X的预测像素Xp,X减去Xp就获得了差值d。
在解码的时候,同样利用X’获得预测像素Xp,Xp加上插值d,就可以获取原始值X。
同时这个X可以作为下一个像素的X’从而成为一个完整的循环。
当然在H264中,因为以像素为单位太小,所以以宏块为单位(16*16像素、4*4像素)进行计算。
上述遗漏的问题,预测值Xp怎么来的?Xp是通过X’利用某个公式计算的。在白皮书中 4*4 有9种预测模式,16*16有4种预测模式。
如何在这9种算法种选择,当然是希望误差越小越好,所以也有对应的算法去计算误差。例如:SAD,SATD等。
同时因为选择了不同的算法,所以解码器也需要知道每个宏块具体使用哪种算法。所以有1bit用于保存是由与上一个一样,如果不一样则用4bit保存具体选择哪个算法。
X’真的与原始X完全一样么?
理论上上讲按前面的算法应该是一样的,但因为差值传到解码器的过程种经过了量化、变换、反变换和反量化,有了精度算是,因此X’真的与原始X无法完全一致。
H264的主要目标是为了有高视频压缩比和良好的网络亲和性。
为了这两个目的H264将系统架构划分了两个层面 VCL 和 NAL。
对核心算法引擎、块、宏块及片的语法级别的定义,负责有效表示视频数据的内容,最终输出编码完的数据SODB(数据比特串)
定义了片级以上的语法级别(如序列参数集和图像参数集),负责以网络所要求的恰当方式去格式化数据并提供头信息,以保证数据适合各种信道和存储介质上的传输。
『NAL』就是为了包装『VCL』以达到更好网络传输效果。NAL层将 SODB 打包成 RBSP(原始字节序列负荷) 然后加上NAL header 组成一个NALU。
RBSP(Raw Byte Sequence Payload,原始字节序列载荷):
PBSP就是在SODB后添加了trailing bits,即一个bit 1和若干个bit 0,以便字节对齐。
传统的视频码流仅有VCL视频编码层,而H264可以根据不同应用增加不同的NAL header,用来适应不同的网络应用环境,减少码流的传输错误。VCL数据在传输前先被映射到NAL单元中。
EBSP:(Encapsulated Byte Sequence Payload, 扩展字节序列载荷)
H264规定,当检测到当检测到0x000000时,也可以表示当前NALU的结束。
那这样就会产生一个问题,就是如果在NALU的内部,出现了0x000001或0x000000时该怎么办?
在编码时,每遇到两个字节连续为0(0x0000),就插入一个字节的0x03。解码时将0x03去掉。
ps:H264有两种封装:⼀种是annexb模式,传统模式,有startcode。⼀种是mp4模式,⼀般mp4,mkv都是mp4模式,没有startcode,SPS和PPS以及其它信息被封装在container中,每⼀个frame前⾯4个字节是这个frame的⻓度很多解码器只⽀持annexb这种模式。
H264码流是一个个连续的NALU,一个NALU包含 [NALU Header][NALU Payload (RBSP)] 三部分。
主要是为了将相邻两个NALU划分开,让他们有一个界线,方便解码。必须是 0x00 00 00 01 或者0x00 00 01。
那么玩意数据中间正好有个 0x00 00 00 01 或者 0x00 00 01 怎么办?见上述EBSP。
并且h264有个防止竞争的机制,在编码一个NAL时,如果出现有连续两个0x00字节,就在连续两个0x00后面插入一个0x03(解码的时候这个0x03会被丢弃)。
由 1字节(8位)组成。禁止位(1位)、重要性指示位(2位)、NALU类型(5位)。
nal_unit_type取值说明:
SPS 和 PPS
https://zhuanlan.zhihu.com/p/27896239
从上图可知SPS和PPS是一个NALU的类型。
实际网络传输编码好的数据流的时候会出现丢包,而如果丢包数据为图像头等关键信息的时候甚至会导致后续解码失败。在H264之前,为了应对图像头关键信息被丢失的做法是在很多包(也有说法是每一个包)都会携带图像头关键信息(冗余做灾备的思想)。但是,在H264种,为了提高网络传输鲁棒性,重新设计出SPS和PPS。
SPS(序列参数集):SPS中保存了一组编码视频序列(Coded Video Sequence)的全局参数。因此该类型保存的是和编码序列相关的参数。
https://yinwenjie.blog.csdn.net/article/details/52771030
PPS(图像参数集):PPS中保存了整体图像相关的参数。
https://yinwenjie.blog.csdn.net/article/details/52877689
根据Vega分析,IDR帧中就包含了SPS,PPS和IDR本身的NALU。
SEI:补充增强信息
Access Unit分隔符:Access Unit:是一个或者多个 NALU 的集合,代表了一个完整的帧。
通过 Vega 分析,不同的 H264 文件有不用的 Profile 和 level。
计算支持1080P(1920*1080)的最低级别:
一个宏块大小16*16.。ceil是向上取整
水平宏块数(PicWidthInMbs)= ceil(视频宽度 / 16) = ceil(1080 / 16) = ceil(67.5) = 68
垂直宏块数(FrameHeightInMbs)= ceil(视频宽度 / 16) = ceil(1920 / 16) = ceil(120) = 120
每帧宏块数(Macroblocks per frame)= 水平宏块数 * 垂直宏块数 = 68 * 120 = 8160
查询上面的级别详表可知,支持每帧8160个宏块的最低级别是4。
级别4 允许的每秒最大宏块数是 245,760 。所以 245760 / 8160 =30.1,即最高支持每秒30.1帧。当然级别更高支持的帧数也更多。
MaxDpbMbs
表中最后一列为 MaxDpbMbs 最大解码缓冲区宏块数。也就是解码时参考缓冲区中的宏块数。
DpbMbs = ref(参考帧数) * PicWidthInMbs(水平宏块数) * FrameHeightInMbs(垂直宏块数)
我们可以根据 MaxDpbMbs 倒推出 最大参考帧数。
公式为:max_ref = min(floor(MaxDpbMbs / (PicWidthInMbs * FrameHeightInMbs)), 16)。floor是向下取整。
以1080P + Level 4 为例:
min(floor(32,768 / (68*120)),16) = 4 注:后面的16 是因为 参考帧数组大只能为16
所以1080P的视频在 Level 4 级别下,最高支持 4 个参考帧。
反推可知,在解码时参考帧的帧数并不只是前1帧,而是前多帧。同理编码时当前帧的参考帧也不只是前一帧,而是前多帧。
这也就应证了I帧和IDR帧的区别。虽然应证了,但还是存在疑问,既然I帧已经可以独立编码解码了,那么为什么在编码解码的时候还要参考I帧之前的帧?