H.264/AVC 的分层结构与画面划分

上篇我们对H.264/AVC做了简单的介绍,尤其是它的主要改进方面。在接下来的几篇中,我们会由浅入深,一步一步来探索H.264。在讲解顺序方面,我参考了毕厚杰的《新一代视频压缩编码标准》。先从H.264标准的编码方法入手,这包括它的分层结构、画面划分、帧内和帧间编码方式、整数变换和量化、CAVLC和CABAC,然后再从H.264码流的角度,来分析它的句法和语义。

当然这里并不会全部介绍,毕竟不是写论文或者出书。我在写的时候,会尽力从新手的角度出发,化繁就简,一些难点或者我现在也没理解的点,只一笔带过。也许以后经验丰富了,会就某个点,回过头来重新研究,那时再写也不迟。

那么在本篇里,我们就从H.264/AVC的分层结构以及画面的划分开始。之所以从这两个点开始,是因为这是和MPEG-2、MPEG-1不同的地方。我们上篇说H.264做了几点重要的改进,比如预测图块可以小到4x4像素,而这一点改进,就是建立在H.264的分层结构和画面划分之上的。

1、H.264/AVC的分层结构

看到这里的分层结构,有的同学可能会想到之前讲解的,MPEG-1和MPEG-2的数据位流的结构。比如MPEG-2的数据位流结构(允许我直接放图,这样比较一目了然):

在这里插入图片描述
MPEG-2视像数据位流的结构

注意这是MPEG-2的数据位流分层结构,重点是MPEG-2和数据位流。我们在本篇开篇的时候说,H.264码流的句法和语义,会放在最后讲,所以这里要讲的H.264/AVC的分层结构,并不是指H.264的比特流结构。

那这里的分层结构指的是什么呢?其实就是H.264分层处理数据的结构,H.264区别于之前的标准,将视频压缩标准,分成了两个层次:

  • (1)视像编码层(Video Coding Layer, 简称VCL),用于有效的表达视像内容,主要负责数据编码。
  • (2)网络抽象层(Network Abstraction Layer,简称NAL),用于按照一定格式,对视像编码层输出的数据进行打包和封装,并提供包头(header)等信息,以在不同速率的网络上传输或进行存储。

这两个层次,完成了从RGB/YUV像素数据输入,到编码并输出适应不同速率网络的H264码流的过程。

在这里插入图片描述
H.264/AVC的分层结构

虽然我们说,这里只是对H.264标准数据处理流程的分层,但它确确实实影响到了H264比特流的分层结构。比如上面说的,NAL的作用是对编码层输出的二进制数据进行打包,并提供包头信息,然后输出H264码流。所以H264的码流结构为:

在这里插入图片描述
NALU序列

当然上图只是NAL的单元(Unit)序列,简称NALU序列,一个NALU为NAL头 + RBSP组成。而这里把NALU序列说成是H264码流,是为了说明,H264码流的组成部分,就是一个个NALU。但是实际上的H264码流还需在NALU序列上,加上起始码。

说了这一段,是为了说明H264的比特流结构,和H264的分层结构,确实联系紧密。而H264的比特流结构,我们先点到为止,等到后面讲比特流的句法时,再详细介绍。下面我们开始介绍H264的画面划分,和它的分层结构一样,了解画面划分,对之后理解它的比特流结构,也很有帮助。

2、H.264/AVC画面划分

与MPEG-2和MPEG-1不同的是,H.264/AVC把一帧画面当做一片像片(slice),或分割成若干个像片来表示。如:

在这里插入图片描述
标准的H.264像片

有的同学一看,这和之前的MPEG-2不是差不多嘛,在MPEG-2和MPEG-1里,每帧图像分成许多像片,每个像片由16行组成。对比H.264,感觉H.264除了多了两行,还拐了个弯,好像没啥变化。

恩,首先说,这种感觉是对的。其次,有这种感觉,是因为还有一个重要的知识点没讲,那就是FMO。

FMO(Flexible Macroblock Ordering,灵活宏块次序)

要详细的了解FMO,还得从图像的扫描方式说起。我们都知道,图像扫描的顺序,是从左上角,一行行扫到右下角。而一帧图片由单个或多个像片组成,每个像片(slice)包含了一系列的宏块(MB),这些宏块的处理顺序,和扫描顺序一致,也即像片中的所有MB,均按照光栅扫描的次序被编码。

这样一来,在解码的时候,就得等待所有的片都接收到,并且按照扫描的顺序排好后,才能进行解码。这样一来,解码端是很受限制的,而当使用了任意像片次序(Arbitrary slice ordering,ASO)之后,就能够以数据接收的顺序进行解码。

而FMO呢,则是建立在ASO之上,所以它不仅有任意像片次序的功能,还在这个基础上,增加了像片分组的功能。而像片分组这个功能,不仅是FMO,也是H.264的一大重点。

那什么是像片分组(简称:片组)呢?

片组,其实就是使用某一规则,将一帧画面中的某些宏块划分成一个组,也即片组。这个过程也就是宏块(MB)到片组的映射,而像片(slice)呢,则是在片组内,对宏块做进一步划分。

比如上面我们说一帧画面,由一个或多个像片组成,其实这是不严谨的。严格点来说,一帧画面,是由一个或多个“片组”组成,而“片组”,是由一个或多个“像片”组成,“像片”则由宏块组成。

上面说的一帧画面,由一个或多个“像片”组成,其实这是不使用FMO的情况,这个时候,默认只有一个“片组”。也就是一帧画面,由一个“片组”组成,而“片组”由一个或多个“像片”组成。这就是上图显示的,标准的H.264像片。

这是不使用FMO的情况,那使用了FMO的图像像片是怎样的呢

当使用FMO时,图像根据不同宏块需要的扫描方式的不同,划分成多个片组,这样一来,不同片组的扫描方向和顺序互不影响。H.264规定了6种典型的分组模式,也即宏块到片组的映射规则,这几种模式使用 slice_group_map_type 来指定,数值分别为0~5,如果设置为6则需要自定义映射规则。不过具体slice_group_map_type在哪个地方设置,则是后面比特流句法部分的内容。

下面就是FMO规定的几种分组模式:
在这里插入图片描述

下图是表中的配图:
在这里插入图片描述
FMO规定的片组

从规则1到规则5,在维基百科上有几张彩图:
在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 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、付费专栏及课程。

余额充值