简介
H264从1999年开始,到2003年形成草案,最后在2007年定稿有待核实。在ITU的标准⾥称 为H.264,在MPEG的标准⾥是MPEG-4的⼀个组成部分–MPEG-4 Part 10,⼜叫Advanced Video Codec,因此常常称为MPEG-4 AVC或直接叫AVC。
H264编码原理
在⾳视频传输过程中,视频⽂件的传输是⼀个极⼤的问题;⼀段分辨率为1920*1080,每个像素点为RGB占⽤3个字节,帧率是25的视频,对于传输带宽的要求是:
1920*1080*3*25 /1024/1024 = 148.315MB/s
换成bps则意味着视频每秒带宽为1186.523Mbps,这样的速率对于⽹络存储是不可接受的。因此视频压缩和编码技术应运⽽⽣。
压缩的本质
帧内压缩
帧内压缩,指的是一帧图片的内部压缩。当H.264对图片进行 16 * 16 分块后,会对每个小块内的图像进行分析,如果2个小块图像比较相近,那么住需要存储一张即可,无需存储重复图块。这样可以有效压缩图片的存储大小。
帧内压缩是生成I帧的算法。
帧间压缩
帧间压缩也称为时间压缩,指的是图片间的图像压缩。在每帧图片划分成16 * 16 小块的图像进行分析基础上,比图片间的数据,如果两张图片比较相近,对相同的图像模块只需存储一份,对不同的部分再做存储。避免了重复数据的存储,极大改善了图片压缩空间。它通过比较时间轴上不同帧之间的数据进行压缩。帧间压缩是无损的,它通过比较本帧与相邻帧之间的差异,仅记录本帧与其相邻帧的差值,这样可以大大减少数据量。
帧间压缩是生成B帧和P帧的算法。
H264图像原理
H264中的I帧、P帧和B帧 IDR帧
H26使⽤帧内压缩和帧间压缩的⽅式提⾼编码压缩率;H264采⽤了独特的I帧、P帧和B帧策略 来实现,连续帧之间的压缩;
IDR 图像(Instantaneous Decoding Refresh)
-
IDR 图像是 H.264 中一种特殊的 I 帧。
-
IDR 图像的特点是:它之后的帧不能参考它之前的任何帧进行预测编码。
-
IDR帧因为附带SPS、PPS等信息,解码器在收到 IDR 帧时,需要做的工作就是:把所有的 PPS 和 SPS 参数进行更新。 将参考帧队列清空,将已解码的数据全部输出或抛弃,重新查找参数集,开始一个新的序列。这样,如果前一个序列出现错误,在这里可以获得重新同步的机会。IDR帧之后的帧永远不会使用IDR帧之前的数据来解码。
I 帧(Intra-coded Picture)
-
I 帧是常规的帧内编码帧,不需要参考其他帧即可独立解码。
-
I 帧可以被后续的 P 帧和 B 帧所参考。
那么:
-
I 帧不一定就是 IDR 图像,普通的 I 帧也可以被后续帧参考。
-
但 IDR 图像一定是 I 帧,因为它具有 I 帧的特性,同时又有 IDR 的额外限制。
I P B三种帧的说明
1.I帧:帧内编码帧 ,I帧表示关键帧,你可以理解为这⼀帧画⾯的完整保留;解码时只需要本帧数据就可以完成(因为包含完整画⾯)
I帧特点:
1) 它是⼀个全帧压缩编码帧。它将全帧图像信息进⾏JPEG压缩编码及传输;
2) 解码时仅⽤I帧的数据就可重构完整图像;
3) I帧描述了图像背景和运动主体的详情;
4) I帧不需要参考其他画⾯⽽⽣成;
5) I帧是P帧和B帧的参考帧(其质量直接影响到同组中以后各帧的质量);
6) I帧是帧组GOP的基础帧(如果为IDR则为第⼀帧),在⼀组中只有⼀个IDR帧,⼀个或多个I
帧(包括IDR帧);
7) I帧不需要考虑运动⽮量;
8) I帧所占数据的信息量⽐较⼤。
2、P帧
P帧:前向预测编码帧。P帧表示的是这⼀帧跟之前的⼀个关键帧(或P帧)的差别,解码时需要⽤之前缓存的画⾯叠加上本帧定义的差别,⽣成最终画⾯。(也就是差别帧,P帧没有完整画⾯数据,只有与前⼀帧的画⾯差别的数据)
P帧的预测与重构:P帧是以I帧为参考帧,在I帧中找出P帧“某点”的预测值和运动⽮量,取预测差值和运动⽮量⼀起传送。在接收端根据运动⽮量从I帧中找出P帧“某点”的预测值并与差值相加以得到P帧“某点”样值,从⽽可得到完整的P帧。
P帧特点:
1) P帧是I帧后⾯相隔1~2帧的编码帧;
2) P帧采⽤运动补偿的⽅法传送它与前⾯的I或P帧的差值及运动⽮量(预测误差);
3) 解码时必须将I帧中的预测值与预测误差求和后才能重构完整的P帧图像;
4) P帧属于前向预测的帧间编码。它只参考前⾯最靠近它的I帧或P帧;
5) P帧可以是其后⾯P帧的参考帧,也可以是其前后的B帧的参考帧;
6) 由于P帧是参考帧,它可能造成解码错误的扩散;
7) 由于是差值传送,P帧的压缩⽐较⾼。
3、B帧
B帧:双向预测内插编码帧。B帧是双向差别帧,也就是B帧记录的是本帧与前后帧的差别(具体⽐较复杂,有4种情况,但我这样说简单些),换⾔之,要解码B帧,不仅要取得之前的缓存画⾯,还要解码之后的画⾯,通过前后画⾯的与本帧数据的叠加取得最终的画⾯。B帧压缩率⾼,但是解码时CPU会⽐较累。
B帧的预测与重构
B帧以前⾯的I或P帧和后⾯的P帧为参考帧,“找出”B帧“某点”的预测值和两个运动⽮量,并取预测差值和运动⽮量传送。接收端根据运动⽮量在两个参考帧中“找出(算出)”预测值并与差值求和,得到B帧“某点”样值,从⽽可得到完整的B帧。
B帧特点
1)B帧是由前⾯的I或P帧和后⾯的P帧来进⾏预测的;
2)B帧传送的是它与前⾯的I或P帧和后⾯的P帧之间的预测误差及运动⽮量;
3)B帧是双向预测编码帧;
4)B帧压缩⽐最⾼,因为它只反映两参考帧间运动主体的变化情况,预测⽐较准确;
5)B帧不是参考帧,不会造成解码错误的扩散。
注:I、B、P各帧是根据压缩算法的需要,是⼈为定义的,它们都是实实在在的物理帧。⼀般来说,I帧的压缩率是7(跟JPG差不多),P帧是20,B帧可以达到50。可⻅使⽤B帧能节省⼤量空间,节省出来的空间可以⽤来保存多⼀些I帧,这样在相同码率下,可以提供更好的画质。
找到在前后参考帧中最相似的位置。这个相似位置的像素值就称为"预测值"。
但实际 B 帧中该点的像素值可能与预测值存在差异。这个差异就称为"差值"。
预测值与差值求和:然后将这个预测值与 B 帧中传输的"差值"相加。相加的结果就是 B 帧该点的最终像素值。
H264编码结构解析
H264除了实现了对视频的压缩处理之外,为了⽅便⽹络传输,提供了对应的视频编码和分⽚ 策略;类似于⽹络数据封装成IP帧,在H264中将其称为组(GOP, group of pictures)、⽚ (slice)、宏块(Macroblock)这些⼀起组成了H264的码流分层结构;H264将其组织成为 序列(GOP)、图⽚(pictrue)、⽚(Slice)、宏块(Macroblock)、⼦块(subblock)五个层次。 GOP (图像组)主要⽤作形容⼀个IDR帧 到下⼀个IDR帧之间的间隔了多少个帧。
举例来说:GOP 指的就是两个I帧之间的间隔. ⽐较说GOP为120,如果是720 p60 的话,那就是2s⼀次I帧.
一个视频图像可编码成一个或更多个片,每片包含整数个宏块( MB,Macroblock),即每片至少一个宏块 ,最多时每片包含整个图像的宏块。总之,一幅图像中每片的宏块数不一定固定。
设片的目的是为了限制误码的扩散和传输,应使编码片相互间是独立的。某片的预测不能以其它片中的宏块为参考图像,这样某一片中的预测误差才不会传播到其它片中去。对于切片(slice)来讲,分为以下几种类型:
I片:只包 I宏块。I 宏块利用从当前片中已解码的像素作为参考进行帧内预测(不能取其它片中的已解码像素作为参考进行帧内预测);
P片:可包 P和I宏块。P 宏块利用前面已编码图象作为参考图象进行帧内预测,一个帧内编码的宏块可进一步作宏块的分割:即 16×16、16×8、8×16 或 8×8 亮度像素块(以及附带的彩色像素);如果选了 8×8 的子宏块,则可再分成各种子宏块的分割,其尺寸为 8×8、8×4、4×8 或 4×4 亮度像素块(以及附带的彩色像素);
B片:可包 B和I宏块。B 宏块则利用双向的参考图象(当前和未来的已编码图象帧)进行帧内预测;
SP片(切换P):用于不同编码流之间的切换,包含 P 和/或 I 宏块;
SI片:扩展档次中必须具有的切换,它包了一种特殊类型的编码宏块,叫做 SI 宏块,SI 也是扩展档次中的必备功能。
宏块是视频信息的主要承载者,因为它包含着每一个像素的亮度和色度信息。视频解码最主要的工作则是提供高效的方式从码流中获得宏块中的像素阵列。
一个编码图像通常划分成若干宏块组成,一个宏块由一个 16×16 亮度像素和附加的一个 8×8 Cb 和一个 8×8 Cr 彩色像素块组成。每个图像中,若干宏块被排列成片的形式。
子块就是宏块分割之后的结果。每个宏块( 16×16 像素)可以 4 种方式分割:一个 16×16,两个 16×8,两个 8×16,四个 8×8。其运动补偿也相应有四种。而 8×8 模式的每个子宏块还可以四种方式分割:一个 8×8,两个 4×8 或两个 8×4 及 4 个 4×4。这些分割和子宏块大大提高了各宏块之间的关联性。这种分割下的运动补偿则称为树状结构运动补偿。
NALU
NALU(NAL Unit),也就是NAL 单元。NAL是H.264码流的基本结构,每个NALU包含了一个字节大小的NALU头信息
(NAL header),以及一个原始字节序列负荷(RBSP,Raw Byte Sequence Payload)。下图展示了多个NALU排列:
NALU结构单元的主体结构如下所示;⼀个原始的H.264 NALU单元通常由[StartCode] [NALU Header] [NALU Payload]三部分组成,其中 Start Code ⽤于标示这是⼀个NALU 单元的开 始,必须是"00 00 00 01" 或"00 00 01",除此之外基本相当于⼀个NAL header + RBSP
接下来看下面这个NALU示意图:
SPS:序列参数集,SPS中保存了⼀组编码视频序列(Coded video sequence)的全局参数。
PPS:图像参数集,对应的是⼀个序列中某⼀幅图像或者某⼏幅图像的参数。
I帧:帧内编码帧,可独⽴解码⽣成完整的图⽚。
P帧: 前向预测编码帧,需要参考其前⾯的⼀个I 或者B 来⽣成⼀张完整的图⽚。
B帧: 双向预测内插编码帧,则要参考其前⼀个I或者P帧及其后⾯的⼀个P帧来⽣成⼀张完整的图⽚。注意:
1.从图我们可以知道,一张图片可以有多个NALU;
2.对解码器来说,需要先收到SPS和PPS进行初始化,否则解码器无法解出正常的帧数据。
3.发I帧之前,至少要发送一次SPS和PPS,因此如果在实际应用中遇到H264无法解码的时候,检查SPS和PPS是否有接收到并正常初始化。传统的H264 编码内存布局例⼦:
0x00 00 00 01 67 …
0x00 00 00 01 68 …
NAL头信息(NAL header)
T为负荷数据类型,占5bit nal_unit_type
这个NALU单元的类型,1~12由H.264使⽤,24~31由H.264以外的应⽤
IDR帧通常对应于nal_unit_type=5的NAL单元。
R为重要性指示位,占2个bit nal_ref_idc (R)
取00~11,似乎指示这个NALU的重要性,如00的NALU解码器可以丢弃它⽽不影
响图像的回放,0~3,取值越⼤,表示当前NAL越重要,需要优先受到保护。
如果当前NAL是属于参考帧的⽚,或是序列参数集,或是图像参数集这些重要
的单位时,本句法元素必需⼤于0。
最后的F为禁⽌位,占1bit
forbidden_zero_bit (F,占1bit)在 H.264 规范中规定了这⼀位必须为 0。
NALU类型的详细解析表 :
nal_unit_type | NAL单元和RBSP语法结构的内容 |
---|---|
0 | 未指定 |
1 | 一个非 IDR 图像的编码条带(p帧/b帧) |
2 | 编码条带数据分割块 A |
3 | 编码条带数据分割块 B |
4 | 编码条带数据分割块 C |
5 | IDR 图像的编码条带(IDR帧) |
6 | 辅助增强信息 (SEI) |
7 | 序列参数集 |
8 | 图像参数集 |
9 | 访问单元分隔符 |
10 | 序列结尾 |
11 | 流结尾 |
12 | 填充数据 |
13 | 序列参数集扩展 |
14-18 | 保留 |
19 | 未分割的辅助编码图像的编码条带 |
20-23 | 保留 |
24-31 | 未指定 |
下表是常见的NALU类型:
十六进制、二进制 | 类型 | 重要性 | 类型值 |
---|---|---|---|
0x67 (0 11 00111) | SPS | 非常重要 | type = 7 |
0x68 (0 11 01000) | PPS | 非常重要 | type = 8 |
0x65 (0 11 00101) | IDR帧 | 关键帧 非常重要 | type = 5 |
0x61 (0 11 00001) | I帧 | 非常重要 | type=1非IDR的I帧不大常见 |
0x41 (0 10 00001) | P帧 | 重要 | type = 1 |
0x01 (0 00 00001) | B帧 | 不重要 | type = 1 |
0x06 (0 00 00110) | SEI | 不重要 | type = 6 |
RBSP
原始字节序列负荷(RBSP,Raw Byte Sequence Payload)
RBSP 指原始字节序列载荷,它是 NAL 单元的数据部分的封装格式,封装的数据来自 SODB(String Of Data Bits,原始数据比特流),SODB 是编码后的原始数据, SODB 经封装为 RBSP 后放入 NAL 的数据部分 。最后,加上 RBSP Trailing Bits(RBSP尾部补齐字节,一个 bit 1 若干比特 0)做8位字节补齐。
SODB为原始数据比特流 (String Of Data Bits)就是最原始的编码/压缩得到的数据。
EBSP为扩展字节序列载荷(Encapsulated Byte Sequence Payload)
EBSP = RBSP插入防竞争字节(0x03)
RBSP = SODB + RBSP Trailing Bits(RBSP尾部补齐字节);
引入RBSP Trailing Bits做8位字节补齐
H264 annexb模式
H264有两种封装 ⼀种是annexb模式,传统模式,有startcode,SPS和PPS是在ES中
⼀种是mp4模式,⼀般mp4 mkv都是mp4模式,没有startcode,SPS和PPS以及其它信息 被封装在container中,每⼀个frame前⾯4个字节是这个frame的⻓度 很多解码器只⽀持annexb这种模式,因此需要将mp4转换flv:在ffmpeg中⽤ h264_mp4toannexb_filter可以做转换
在处理 H.264 视频时,需要注意容器格式和 SPS/PPS 信息的正确处理,以确保视频可以正确播放。
比如 FLV/MP4/MKV等结构中,h264需要h264_mp4toannexb处理。添加SPS/PPS等信息。
测试代码
读取MP4文件,解复用并将视频帧写入新后缀。测试了h264,flv,mkv,ts都不能直接读取mp4裸数据 必须加上nal头字段。如果已经加入了nal头字段的h264,则使用裸数据转换成MP4依然可以播放,因为数据中有nal头字段。
#include "test_h264.h"
#include <stdio.h>
#include <libavutil/log.h>
#include <libavformat/avio.h>
#include <libavformat/avformat.h>
static char err_buf[128] = {0};
static char* av_get_err(int errnum)
{
av_strerror(errnum, err_buf, 128);
return err_buf;
}
void test_h264_code()
{
char *in_filename ="believe.mp4";
char *video_filename = "believe.h264";
FILE *outfp=fopen(video_filename,"wb");
printf("in:%s out:%s\n", in_filename, video_filename);
// 分配解复用器的内存,使用avformat_close_input释放
AVFormatContext* ifmt_ctx = avformat_alloc_context();
if (!ifmt_ctx)
{
printf("[error] Could not allocate context.\n");
return -1;
}
// 根据url打开码流,并选择匹配的解复用器
int ret = avformat_open_input(&ifmt_ctx,in_filename,NULL,NULL);
if(ret != 0)
{
printf("[error]avformat_open_input: %s\n", av_get_err(ret));
return -1;
}
// 读取媒体文件的部分数据包以获取码流信息
ret = avformat_find_stream_info(ifmt_ctx,NULL);
if(ret < 0)
{
printf("[error]avformat_find_stream_info: %s\n", av_get_err(ret));
avformat_close_input(&ifmt_ctx);
return -1;
}
int video_index = av_find_best_stream(ifmt_ctx,AVMEDIA_TYPE_VIDEO,-1,-1,NULL,0);
if(video_index == -1)
{
printf("Didn't find a video stream.\n");
avformat_close_input(&ifmt_ctx);
return -1;
}
// 分配数据包
AVPacket* pkt = av_packet_alloc();
av_init_packet(pkt);
// 1 获取相应的比特流过滤器 什么意思?扩展接收frame信息 增加头部 比如自定义头部
const AVBitStreamFilter* bsfilter = av_bsf_get_by_name("h264_mp4toannexb");
// 2 初始化过滤器上下文
AVBSFContext* bsf_ctx=NULL;
av_bsf_alloc(bsfilter,&bsf_ctx);
// 3 添加解码器属性
avcodec_parameters_copy(bsf_ctx->par_in,ifmt_ctx->streams[video_index]->codecpar);
av_bsf_init(bsf_ctx);
int file_end=0;
while (file_end == 0){
if((ret = av_read_frame(ifmt_ctx, pkt)) < 0){ // 没有更多包可读
file_end = 1;
printf("read file end: ret:%d\n", ret);
}
if(ret == 0 && pkt->stream_index == video_index){
#if 0
int input_size = pkt->size;
int out_pkt_count = 0;
//将数据包送入FFmpeg的bitstream filter上下文bsf_ctx中。这个过程会增加“h264_mp4toannexb”头部
if (av_bsf_send_packet(bsf_ctx, pkt) != 0) {
av_packet_unref(pkt); // send内部会自己处理引用计数
continue; // 继续送
}
av_packet_unref(pkt);
while (av_bsf_receive_packet(bsf_ctx,pkt) == 0) {
out_pkt_count++;
printf("fwrite size:%d\n", pkt->size);
size_t size = fwrite(pkt->data,1,pkt->size,outfp);
if(size != pkt->size){
printf("fwrite failed-> write:%u, pkt_size:%u\n", size, pkt->size);
}
av_packet_unref(pkt);
}
if (out_pkt_count >= 2 ) {
printf("cur pkt(size:%d) only get 1 out pkt, it get %d pkts\n",
input_size, out_pkt_count);
}
#else
// TS流可以直接写入
size_t size = fwrite(pkt->data,1,pkt->size,outfp);
if(size != pkt->size)
printf("fwrite failed-> write:%u, pkt_size:%u\n", size, pkt->size);
av_packet_unref(pkt);
#endif
}
else {
if(ret == 0)
av_packet_unref(pkt); // 释放内存
}
}
if(outfp)
fclose(outfp);
if(bsf_ctx)
av_bsf_free(&bsf_ctx);
if(pkt)
av_packet_free(&pkt);
if(ifmt_ctx)
avformat_close_input(&ifmt_ctx);
printf("finish\n");
}
总结
补充gop pictures
GOP 指的就是两个I帧之间的间隔. ⽐较说GOP为120,如果是720 p60 的话,那就是2s⼀次I帧.
在视频编码序列中,主要有三种编码帧:I帧、P帧、B帧,如下所示:
1. I帧即Intra-coded picture(帧内编码图像帧),不参考其他图像帧,只利⽤本帧的信息进⾏编码。
2. P帧即Predictive-codedPicture(预测编码图像帧),利⽤之前的I帧或P帧,采⽤运动预测的⽅式进⾏帧间预测编码。
3. B帧即Bidirectionallypredicted picture(双向预测编码图像帧),提供最⾼的压缩⽐,它既需要之前的图像帧(I帧或P帧),也需要后来的图像帧(P帧),采⽤运动预测的⽅式进⾏帧间双向预测编码。
在视频编码序列中,GOP即Group of picture(图像组),指两个I帧之间的距离,Reference(参考周期)指两个P帧之间的距离。⼀个I帧所占⽤的字节数⼤于⼀个P帧,⼀个P帧所占⽤的字节数⼤于⼀个B帧。
所以在码率不变的前提下,GOP值越⼤,P、B帧的数量会越多,平均每个I、P、B帧所占⽤的字节数就越多,也就更容易获取较好的图像质量;Reference越⼤,B帧的数量越多,同理也更容易获得较好的图像质量。
理解上述:gop是两个i帧间隔,间隔越长则图像越多,p帧b帧越多 码率就很高 图像质量就很好。需要说明的是,通过提⾼GOP值来提⾼图像质量是有限度的,在遇到场景切换的情况时,H.264编码器会⾃动强制插⼊⼀个I帧,此时实际的GOP值被缩短了。另⼀⽅⾯,在⼀个GOP中,P、B帧是由I帧预测得到的,当I帧的图像质量⽐较差时,会影响到⼀个GOP中后续P、B帧的图像质量,直到下⼀个GOP开始才有可能得以恢复,所以GOP值也不宜设置过⼤。同时,由于P、B帧的复杂度⼤于I帧,所以过多的P、B帧会影响编码效率,使编码效率降低。另外,过⻓的GOP还会影响Seek操作的响应速度,由于P、B帧是由前⾯的I或P帧预测得到的,所以Seek操作需要直接定位,解码某⼀个P或B帧时,需要先解码得到本GOP内的I帧及之前的N个预测帧才可以,GOP值越⻓,需要解码的预测帧就越多,seek响应的时间也越⻓。
理解上述:
1.由于之前的间隔长高质量定律,当i帧质量下滑时,则后面pb帧图像也受影响下滑。所有gop值不能太大也不能太小。
2.由于seek操作是以i帧为开始点查找pb帧,所有seek响应时间和gop值挂钩。
本文介绍图像压缩基本原理和概念,H.264编码的基础概念:序列、图像、片、宏块、子块。然后介绍H.264的NAL层,可以了解H.264码流是由一个个NALU组成,每个NALU又是由00 00 00 01
或00 00 01
来分隔开,NALU都有一个NAL头和一个RBSP。
参考文献
https://blog.csdn.net/qq_29350001/article/details/78226286