H.264/AVC学习-参考帧队列重排

参考帧重排在H.264标准文档中的8.2.4节

1.参考帧重排目的

参考帧重排的意义:由于在解码每个(P/B)MB时,都要用到参考帧的索引ref_idx_l0 或 ref_idx_l1。假如有个参考帧(短期参考帧或者长期参考帧)对于解码一个图像特别有用,但是这个参考帧在缺省的队列中并不位于索引值为0的位置,所以编码数值较大的索引值需要花费更多比特。参考帧的重排序可以使这个参考帧位于索引值比较小的位置,以节省编码比特数。

以下图为例,假设参考帧队列中有4个参考图像,POC分别为100、101、102、103,参考帧队列中的索引值分别为0、1、2、3。假设下一个解码帧大部分宏块对应参考帧是POC为103的图像,那么宏块头中mb_pred需要编码ref_idx_l0为3(POC103图像在参考帧队列中的索引)。显然,编码数值3比数值0需要更多bit。如果把POC为103的图像移到参考帧队列最前面,则ref_idx_l0的值为0,编码数据量也随之减少。
在这里插入图片描述
参考帧队列重排的信息编码在每个slice header中的ref_pic_list_modification 中,我们需要在解码slice数据前解析这些信息。

2.参考帧队列

H264采用多参考帧提高编码性能。对于每一个P帧和B帧的解码都需要从参考图像队列中选择一个或多个参考帧。队列中的参考帧可分为短期参考帧和长期参考帧两种,分别按照PicNum和LongTermFrameIdx索引,通过这两个索引值可以在参考帧列表中获取对应的参考帧图像。

 问题:为什么要分成短期参考和长期参考?
参考《深入理解视频编码技术--基于H264标准及参考模型》
考虑以下场景:
1)新闻频道正在播放国际新闻,画面从主持人切换到现场,过一会又切回主持人
端坐画面。
2)网球比赛现场,镜头不断切换:纳达尔特写,费德勒特写,比赛过程.....
   很多类型的视频中,有规律的场景切换是非常常见的。H264标准规定参考帧
   数上限为16,而场景切换使得所有短期参考帧都无法使用,如果所有参考帧都是短期参考帧,那么场景切换后必须使用帧内编码,这就是长期参考帧存在的目的。

当解码一个P slice时,使用一个参考帧队列RefPicList0;解码B slice是,使用两个参考帧队列RefPicList0和RefPicList1。

3.参考帧队列解码过程

3.1 解码picture number

picture number需要用到以下场景:

  1. 解码参考帧列表
  2. 解码参考帧标记
  3. 处理非连续的frame_num时

主要用到以下几个参数FrameNum, FrameNumWrap, PicNum, LongTermFrameIdx 和 LongTermPicNum等。

对于每个短期参考图像,变量 FrameNum 和 FrameNumWrap 的计算过程如下:

  • FrameNum设置成slice header中解码所得得语法元素frame_num;
  • 变量 FrameNumWrap 这样计算:如 FrameNum 大于当前图像片头中的 frame_num,则 FrameNumWrap 等于 FrameNum 减去MaxFrameNum;否则 FrameNumWrap 等于 FrameNum。

对于每个长期参考图像,与变量 LongTermFrameIdx 相关(见H.264标准8.2.5节)。

对于每个短期参考图像,有一个对应的 PicNum 变量,对于每个长期参考图像,都存在一个LongTermPicNum 变量。这些变量的值由当前图像的 field_pic_flag 和 bottom_field_flag 的值决定。计算过程如下:
在这里插入图片描述

3.2初始化参考图像队列

初始化过程需要在解码slice header时调用,生成初始化好的参考图像列表 RefPicList0和RefPicList1。
它依赖于解码帧的参考标记(对应spec 8.2.5章)。参考队列初始化的前提是至少有一个参考slice被标记为‘短期参考’或‘长期参考’。
初始化实现过程对应标准文档8.2.4.2.1-8.2.4.2.5节,JM代码对应init_lists函数。

3.2.1 帧格式P slice的参考帧队列初始化

在对一个P slice解码之前通过参考帧列表的初始化生成 RefPicList0。RefPicList0 是经过排序的,排序的主要原则是:

  • 短期参考帧比长期参考帧索引值小;
  • 短期参考帧根据 PicNum 值降序排列
  • 长期参考帧LongTermPicNum 值升序排列

例如,当三个标记为"用于短期参考"的参考帧对应 PicNum分别为300,302,303 ,两个标记为"用于长期参考"的参考帧对应LongTermPicNum=0,3 。那么初始化列表顺序为:
RefPicList0[0] 设置为 PicNum = 303,
RefPicList0[1] 设置为 PicNum = 302,
RefPicList0[2] 设置为 PicNum = 300,
RefPicList0[3] 设置为 LongTermPicNum = 0,
RefPicList0[4] 设置为 LongTermPicNum = 3.

3.2.2 场格式P slice的参考帧队列初始化

在对一个编格式 P 或 SP slice解码之前同样需要先初始化RefPicList0 ,不同的是对一个场解码时,参考图像列表中的每个场都有单独的列表索引;而且可用的参考图像数将是解码一个帧时的两倍。
在此过程中,首先需要计算两个有序的参考帧列表refFrameList0ShortTerm 和 refFrameList0LongTerm,两个列表的产生过程如下:

  • 至少有一个场标记为“用于短期参考”的帧都放入refFrameList0ShortTerm ;
    当前场是参考场对的第二个场(按照解码顺序)且第一个场标记为“用于短期参考”,则第一场放入refFrameList0ShortTerm 。
    refFrameList0ShortTerm 按照 FrameNumWrap 值的降序进行排列;
  • 至少有一个场标记为“用于长期参考”的帧都refFrameList0LongTerm ;
    当前场是参考场对的第二个场(按照解码顺序)且第一个场标记为“用于长期期参考”,则第一场放入refFrameList0LongTerm 。
    refFrameList0LongTerm 按照 LongTermFrameIdx 值的升序进行排列。
    在这两个列表产生后,将根据标准文档8.2.4.2.5节定义的过程,使用 refFrameList0ShortTerm 和refFrameList0LongTerm 作为输入,生成 RefPicList0。

3.2.3 帧格式B slice的参考帧队列初始化

太繁琐了,具体参考8.2.4.2.3

3.2.4 场格式B slice的参考帧队列初始化

太繁琐了,具体参考8.2.4.2.4

3.2.5 场格式的参考帧列表初始化

太繁琐了,具体参考8.2.4.2.5

2.参考帧队列重排序

根据初始化过程生成的参考图像列表 RefPicList0 和 RefPicList1 后,需要根据slice header中的相关语法元素,如 ref_pic_list_reordering_flag_l0、ref_pic_list_reordering_flag_l1、
reordering_of_pic_nums_idc、abs_diff_pic_num_minus1 和 long_term_pic_num 等的规定,经过下面的
重排序过程进行列表的重排序。
在这里插入图片描述
slice header中的语法ref_pic_list_modification 指明了该slice参考帧队列是否需要修改,如果ref_pic_list_modification为0,则说明该slice不需要重排参考帧队列,按照初始化顺序即可。

其中,ref_pic_list_modification_flag_l0为1时,对参考帧列表RefPicList0进行修改;ref_pic_list_modification_flag_l1为1时,对参考帧列表RefPicList1进行修改。

本节中以P帧解码时修改参考帧列表RefPicList0为例讨论其执行过程。
1.设定一个值refIdxL0表示参考帧列表中参考帧的索引值,并初始化为0;
2.读取码流中的modification_of_pic_nums_idc值,并根据其取值进行计算:

  • 如果modification_of_pic_nums_idc为0或1,执行短期参考帧的修改过程;
  • 若modification_of_pic_nums_idc为2,执行长期参考帧的修改过程;
  • 若modification_of_pic_nums_idc为3,参考帧列表的修改过程完成;

重排序过程可以这么理解:与该解码slice关联较大的几个参考帧不在短期、长期参考帧队列序号靠前位置,或者新增参考帧时,根据ref_pic_list_modification得到想要放在队列前的参考帧frameNum,然后把该参考帧放在队列头,同时队列中的后续参考帧逐一后移。
代码实现可参考JM 中的reorder_lists 函数。

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
使用H.264/AVC压缩格式对视频流进行压缩,你可以使用FFmpeg库来实现。下面是一个基本的示例,展示了如何使用FFmpeg库对视频流进行压缩: 1. 安装FFmpeg库:首先,确保你已经安装了FFmpeg库。你可以从官方网站下载并按照说明进行安装。 2. 编写C++代码:创建一个C++源文件,并使用以下代码来实现视频流的压缩: ```cpp #include <iostream> #include <cstdio> #include <cstdlib> #include <cstring> #include <sstream> #include <fstream> extern "C" { #include <libavformat/avformat.h> #include <libavcodec/avcodec.h> #include <libswscale/swscale.h> } int main() { AVFormatContext* inputContext = nullptr; AVFormatContext* outputContext = nullptr; AVCodecContext* codecContext = nullptr; AVStream* stream = nullptr; AVPacket packet; const char* inputFilename = "your_input_video_file"; // 输入视频文件名 const char* outputFilename = "your_output_compressed_video_file.h264"; // 输出压缩后的视频文件名 av_register_all(); // 打开输入视频文件 if (avformat_open_input(&inputContext, inputFilename, nullptr, nullptr) != 0) { std::cerr << "Failed to open input video file!" << std::endl; return -1; } // 获取输入视频流信息 if (avformat_find_stream_info(inputContext, nullptr) < 0) { std::cerr << "Failed to retrieve input video stream information!" << std::endl; return -1; } // 查找视频流 int videoStreamIndex = -1; for (unsigned int i = 0; i < inputContext->nb_streams; i++) { if (inputContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { videoStreamIndex = i; break; } } if (videoStreamIndex == -1) { std::cerr << "Failed to find video stream in input video file!" << std::endl; return -1; } // 获取视频流编解码器参数 AVCodecParameters* codecParameters = inputContext->streams[videoStreamIndex]->codecpar; // 查找视频流编解码器 AVCodec* codec = avcodec_find_decoder(codecParameters->codec_id); if (codec == nullptr) { std::cerr << "Failed to find video decoder codec!" << std::endl; return -1; } // 创建编解码器上下文 codecContext = avcodec_alloc_context3(codec); if (codecContext == nullptr) { std::cerr << "Failed to allocate video decoder context!" << std::endl; return -1; } // 设置编解码器上下文参数 if (avcodec_parameters_to_context(codecContext, codecParameters) < 0) { std::cerr << "Failed to copy video decoder parameters to context!" << std::endl; return -1; } // 打开视频编解码器 if (avcodec_open2(codecContext, codec, nullptr) < 0) { std::cerr << "Failed to open video decoder!" << std::endl; return -1; } // 创建输出视频文件 if (avformat_alloc_output_context2(&outputContext, nullptr, nullptr, outputFilename) < 0) { std::cerr << "Failed to allocate output video file context!" << std::endl; return -1; } // 添加视频流到输出视频文件 stream = avformat_new_stream(outputContext, codec); if (stream == nullptr) { std::cerr << "Failed to create output video stream!" << std::endl; return -1; } // 复制视频流参数 if (avcodec_parameters_copy(stream->codecpar, codecParameters) < 0) { std::cerr << "Failed to copy video stream parameters!" << std::endl; return -1; } // 打开输出视频文件 if (!(outputContext->oformat->flags & AVFMT_NOFILE)) { if (avio_open(&outputContext->pb, outputFilename, AVIO_FLAG_WRITE) < 0) { std::cerr << "Failed to open output video file!" << std::endl; return -1; } } // 写视频文件头部信息 if (avformat_write_header(outputContext, nullptr) < 0) { std::cerr << "Failed to write output video file header!" << std::endl; return -1; } // 初始化视频帧 AVFrame* frame = av_frame_alloc(); if (frame == nullptr) { std::cerr << "Failed to allocate video frame!" << std::endl; return -1; } int frameNumber = 0; // 逐帧读取输入视频文件并进行压缩 while (av_read_frame(inputContext, &packet) >= 0) { if (packet.stream_index == videoStreamIndex) { // 发送解码器数据包 if (avcodec_send_packet(codecContext, &packet) < 0) { std::cerr << "Failed to send packet to video decoder!" << std::endl; return -1; } // 接收解码器输出帧 int ret = avcodec_receive_frame(codecContext, frame); if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { av_packet_unref(&packet); continue; } else if (ret < 0) { std::cerr << "Failed to receive frame from video decoder!" << std::endl; return -1; } // 压缩帧数据 AVPacket compressedPacket; av_init_packet(&compressedPacket); compressedPacket.data = nullptr; compressedPacket.size = 0; if (avcodec_encode_video2(codecContext, &compressedPacket, frame, nullptr) < 0) { std::cerr << "Failed to encode video frame!" << std::endl; return -1; } // 写入压缩后的帧数据到输出视频文件 if (av_write_frame(outputContext, &compressedPacket) < 0) { std::cerr << "Failed to write compressed video frame!" << std::endl; return -1; } av_packet_unref(&compressedPacket); frameNumber++; } av_packet_unref(&packet); } // 写视频文件尾部信息 av_write_trailer(outputContext); // 清理资源 avcodec_free_context(&codecContext); avformat_close_input(&inputContext); avformat_free_context(outputContext); av_frame_free(&frame); return 0; } ``` 请将"your_input_video_file"替换为你想要压缩的输入视频文件的路径,将"your_output_compressed_video_file.h264"替换为你想要保存的压缩后的视频文件路径。 3. 编译和运行:使用C++编译器(如g++)编译源文件,并链接FFmpeg库。 ```bash g++ your_source_file.cpp -o compress_video `pkg-config --cflags --libs libavformat libavcodec libswscale` ``` 然后运行可执行文件: ```bash ./compress_video ``` 这样,程序将读取输入视频文件并使用H.264/AVC压缩格式对视频流进行压缩,并将压缩后的视频保存到输出文件中。 请注意,上述代码仅提供了基本的功能实现,你可能需要根据具体需求进行更复杂的视频处理或参数设置。此外,确保你有适当的权限来读取输入视频文件并保存输出视频文件。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值