题记:上一节解封装获取的streams
,包含了媒体文件的数据,这些数据通常是经过压缩的。压缩的过程称为编码,而解码就是重构图像和声音数据。编解码作为最核心内容,还是需要了解一下编码理论,本节梳理编码流程,下节介绍FFmpeg的解码,如果对理论不感兴趣可以暂时略过。同样可以结合ffplay和Demo更容易理解。
编码简介
如前文介绍的,编码实际上是利用信息冗余去除技术对数据进行压缩,编码后则可以降低到原来的几十分之一甚至更低,其中基本技术通常包括:
- 空间冗余:相邻像素之间有较强的相关性,帧内压缩技术
- 时间冗余:相邻图像之间内容变化不大,帧间压缩技术,I帧P帧B帧的由来
- 编码冗余:不同像素值出现的概率不同,如变长编码技术
- 视觉冗余:视觉系统对某些细节不敏感,如有损压缩技术
- 知识冗余:根据已有知识进行优化,如智能编码技术
视频编码
常见的视频编码有3个系列:
- ISO-MPEG/ITU-T 系列
- H.264 高级视频编码(Advanced Video Coding,简称 AVC)
- H.265 高效率视频编码(High Efficiency Video Coding,简称 HEVC)
- H.266 多功能视频编码(Versatile Video Coding,简称 VVC)
- AOM 系列
- VP8 一个开放的图像压缩格式,随后由Google发布
- VP9 是 Google 提供的开源的免费视频编码格式,是 VP8 的后续版本
- AV1 Alliance for Open Media Video 1 是由 AOM(Alliance for Open Media,开放媒体联盟)制定的一个开源、免版权费的视频编码格式
- AVS 系列 中国具备自主知识产权的系列编码标准
- AVS2 第二代数字音视频编解码技术标准(AVS2)
- AVS3 AVS3 增加了对 8K 分辨率的支持
H.264的计算复杂度相对较低,其编码和解码算法相对简单,使得它在早期的硬件设备上也能得到较好的支持。后续的系列往往有更高的压缩比,但也需要更强大的计算资源来进行复杂的预测、变换和熵编码等操作,本章着重介绍H.264。
H.264编码流程
编码流程
H.264编码流程大致分为
- 输入处理
- 帧类型分析
- 帧内/帧间预测
- 变换+量化
- 环路滤波
- 熵编码
- 网络抽象层处理
下图是H.264编码框架图(红色帧间,蓝色帧内),但实际上图中没有标出帧操作还是块操作,理解起来还是比较难,此处可以先略过,结合后文解释再来理解。
1. 输入处理
原始视频数据的颜色空间(一般是RGB),通常会被转成YUV格式。这里其实是视觉冗余
的第一个应用。
- RGB
- 每个像素包含R、G、B三个分量,每个分量一个字节;
- 色彩直观,易于理解和使用,位图存储就是RGBA形成。
- YUV
- 亮度和色度分离,每个像素有一个Y分量,表示亮度,也就是灰度图的值。
- U和V分量表示色度,根据采样格式的不同,U和V分量的数量会有所不同(如4:2:0采样中,每四个像素共用一个U分量和V分量)
- 适用于视频存储和传输,如视频压缩、广播电视等
- 相互转换
- RGB转YUV:
- Y=0.299R+0.587G+0.114B
- U=0.492(B-Y)
- V=0.877(R-Y)
- YUV转RGB:
- R=Y+1.140V
- G=Y-0.395U-0.581V
- B=Y+2.032U
- RGB转YUV:
由于人的视觉对亮度更敏感,色度方面都存在一定冗余,所以YUV会有不同的格式,如YUV444、YUV422、YUV420、YUV411等,如下简图。YUV444和RGB使用相同长度的字节,所表达的信息量是一样,而YUV420编码是RGB的一半长度。
- 使用更少的字节,如下图4个像素,RGB需要12字节,而YUV420仅需要6个字节
- 4个像素块共用一个UV
- Y和UV不等长,传输时一般是 YY...YYUV...UV或者YY...YYU...UV...V
- 编码上更容易实现压缩。
补充一点,在相机开发使用YUV420,Y和UVOpenGL通常会是两个纹理,亮度(UV)纹理宽高是亮度(Y)纹理的1/2,就是这个原因。以下是GPUImage的处理:
// 亮度纹理
CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, [[GPUImageContext sharedImageProcessingContext] coreVideoTextureCache], cameraFrame, **NULL**, GL_TEXTURE_2D, GL_LUMINANCE, bufferWidth, bufferHeight, GL_LUMINANCE, GL_UNSIGNED_BYTE, 0, &luminanceTextureRef);
// 色度纹理
CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, [[GPUImageContext sharedImageProcessingContext] coreVideoTextureCache], cameraFrame, **NULL**, GL_TEXTURE_2D, GL_LUMINANCE_ALPHA, bufferWidth/2, bufferHeight/2, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, 1, &chrominanceTextureRef);
2. 类型分析
对于视频来说,实际上是一幅幅图像构成,而在随意截取的片段中前后图像常常有很大相似性,这就是
时间冗余
。由于前后帧之间的相似性,衍生出了I帧P帧B帧和GOP的概念。
- GOP(Group of Pictures):
- GOP通常认为两个关键帧之间的图像序列,包括前一个I帧,不包括后一个I帧。但这个说法并不严谨;
- 在一些情况下,为了实现更高的随机访问能力、更好的错误恢复性能或更灵活的编辑操作,编码器可能会在一个GOP中插入多个I帧;
- GOP分为开放GOP和闭合GOP,区别在于闭合GOP只能参考GOP内部帧,开放GOP可以参考前一个GOP的帧;
- GOP的划分可以配置成固定长度的,也可以是可变的(根据视频内容变化,提高编码效率);
- I帧(Intra-coded frame,帧内编码帧)0x61 :
- I帧包含一个完整画面的信息,采用帧内压缩编码技术,
- I帧可以独立解码,通常用作GOP的起始帧。
- IDR帧(Instantaneous Decoding Refresh frame 即时解码刷新帧)0x65 :
- 特殊的I帧
- 在解码时会清空之前的解码信息
- IDR帧通常也用作划分GOP的边界。
- P帧(Predictive-coded frame,预测帧) :
- P帧需要参考之前的I帧或P帧解码
- B帧(Bi-predictive-coded frame,双向预测帧) :
- B帧需要参考前后的I帧和P帧解码
- B帧的存在会导致DTS(解码顺序)和PTS(播放顺序)不一致,解码时需要先解码后面的P帧
3. 帧内/帧间预测
类型分析完后就可以进入帧编码阶段了,实际上为了有效分析图像信息,还需要先对图像切片,这里又引出了H.264的重点概念宏块。帧内/帧间预测的单位实际上就是宏块 H.264结构中,一个视频图像编码后的数据为一帧
- 帧由一个或多个片构成
- 片由一个或多个宏块构成
- 宏块由16x16的YUV数据构成
- 宏块还可以划分子块,子块的大小可以是 8X16、 16X8、 8X8、 4X8、 8X4、 4X4
对于宏块而言,需要选择帧间预测还是帧内预测,根据帧的类型:
- I帧为帧内解码帧,所有宏块都会走帧内预测
- P帧和B帧的宏块以帧间编码为主,某些宏块也可以选择帧内预测。
帧内预测
帧内预测是利用图像本身的相关性进行预测,简单来说就是根据周围的像素信息猜测当前宏块或子块,就是空间冗余
的应用。
图中虚线部分实际是解码的部分流程,只所以编码也需要解码流程后面会讲到,这里主要表达帧内解码的参考信息不是来自原始图片,而是来自来对已经编码的块进行解码而获取到的信息。举例来说AB两个相邻块,A进行编码后得到A‘,然后使用A’而不是A的像素值进行对B的预测,这样可以保持和解码端处理一致。
预测有多种模式,根据色块大小不同,Y和UV分量也有差别,一般有如DC预测、水平预测、垂直预测等,如下图(44亮度9种,1616亮度4种),就是使用周边像素选择一定方式填充预测块,选择过程就是模式选择:
用实际值-预测试得到一个残差值。此时需要编码的有一个模式和残差值,比原来的值更多了。但由于图像上通常有一定相关性(
空间冗余
),残差值通常会有一定范围或者说规律,为后续处理提供了可能。
此外,如果没有无法找到合适的模式(图像变动没有规律)时,也可以直接使用本身的像素值不进行变化,即是I_PCM模式。
帧间预测
帧间预测是使用已经解码的帧对当前块进行预测(时间冗余
的应用),由于视频序列帧间具有很强的相关性,因此可以通过这种方式有效地去除视频中的时间冗余信息,从而实现压缩的目的,在框架图如下部分,是整个编码最耗时和复杂的部分。
帧间预测实际上就是找到与当前宏块或子块最接近的块,计算出运动向量和预测残差。运动向量指实际像素块与参考块之间的偏移(小范围的抖动);参考块与通过偏移得到预测像素,与真实像素的差值就是预测残差。需要把参考序号,运动向量和预测残差传入到编码才能构建出真实像素块。
这里列出涉及到3个概念:
- 宏块分割
- 宏块数据Y是16x16,UV是8x8
- 16x16模式可分割为一个16×16,两个16×8,两个8×16,四个8×8
- 8×8模式的子宏块还可以四种方式分割:一个8×8,两个4×8或两个8×4及4个4×4
- 具体分割会根据图像内容、预测精度、编码效率等因素决定
- 运动矢量&亚像素
- 按照一定算法从已经编解码的帧中找出与当前块对接近的参考块,计算出运动矢量(偏移)
- 运动矢量的最小精度,亮度是1/4,色度是1/8。
- 即Y分量矢量可以是(1/4, -1/2),整数对应Index,而像这样的(1.5, 0.125)不能对应真实的像素点,就是亚像素点,具体需要使用公式算出。
- F(n+0.5) = round([F(n-2)-5F(n-1)+20F(n)+20F(n+1)-5F(n+2)+F(n+3)] / 32)
- F(n+0.25) = round([F(n)+F(n+0.5)]/2)
- 色度还有1/8精度可参考其他文档
- 运动预测
为了进一步压缩运动向量(MV),还有运动估计(ME)的概念。具体根据周边已经计算过的MV,左MV(A),上MV(B)和右上MV(C),按照一定规则得到MVp(Motion Vector Prediction,运动矢量预测)。而预测的结果和实际的MV还是有差别的,这个差值就是MVD,即当前运动矢量MV与预测运动矢量MVp之间的差值。此时传输时需要传输参考信息,MVD和预测参差。
4. 变换量化与熵编码
注意,上述无论是帧间(4x4~ 16x16亮度、4x4~ 8x8色度)还是帧内(16x16和4x4亮度、8x8色度),实际上增加了信息,如残差块就和原本色大小相同,此外还需要额外的参考信息等。但如果是有效预测的话,残差块的值会有一个小范围的浮动,就可以使用DCT变换与量化进行压缩。
DCT与低通滤波应用示例
此处内容与H.264编码无关,是本人在学习OpenCV中的笔记,帮助大家理解DCT是怎么回事,如何在图片压缩上起作用,关于傅里叶变换自己也是上学时没明白,在用到OpenCV才懂,书到用时方恨少
- 傅里叶变换
抛开公式不谈,傅里叶变换就是把时域信号映射到频域的变换,如下图,把时域上的信号波形看作一系列不同频率的正余弦波。
- 离散傅里叶变换
- 时域上的离散对应频域上的周期,频域上的离散对应时域上的周期。把有限离散信号看成了可以无限延伸的周期离散信号,就是推出DFT变换。
- 偶对称信号的虚部为0.于是把有限离散信号先扩展成对称图形,再扩展成周期信号,就是DCT变形。
这里给出DCT的一个应用,图像处理(二维傅里叶变换,或者DCT、FFT)中低通滤波。就是把高频率(视觉冗余
)的部分过滤掉,达到JPEG或者H.264数据压缩的目的,实际上会丢失一部分信息造成模糊的效果。
# 读取图像
image = cv2.imread('input.png')
# 将图像转换为灰度
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 将图像转换为浮点型,方便后续处理
float_gray = np.float32(gray)
threshold = 100
# 对图像进行DCT变换
dct_matrix = cv2.dct(float_gray)
dct_abs = np.abs(dct_matrix)
dct_abs_normalized = cv2.normalize(dct_abs, None, 0, 255, cv2.NORM_MINMAX)
dct_abs_normalized_uint8 = np.uint8(dct_abs_normalized)
sub_dct = dct_abs_normalized_uint8[:threshold, :threshold]
np.savetxt('dct_context.txt', dct_abs_normalized_uint8, fmt='%d')
# 创建一个与dct_matrix形状相同的掩膜
mask = np.zeros(dct_matrix.shape, dtype=np.float32)
# 将低频部分置为零(或接近零的某个小值,以避免逆DCT后的伪影)
mask[:threshold, :threshold] = 1
# 应用掩膜到DCT矩阵上
filtered_dct = dct_matrix * mask
# 执行DCT逆变换
inverse_dct = cv2.idct(filtered_dct)
# 由于IDCT的结果可能包含负值或超出0-255范围的值,我们需要将其裁剪到有效的8位图像范围内
inverse_dct = np.clip(inverse_dct, 0, 255)
# 将结果转换回8位无符号整数类型
inverse_dct = np.uint8(inverse_dct)
input大小是(720,1080),右边是DCT变换后低通滤波的效果,中间是DCT变换数据展示,可以看到除了左上角其他地方都是黑色的(值比较小)。左上角对应的就是低频信号,右下对应高频信号。也就是说只需要存储(100,100)的数据就可以达到result效果,当然可以调整threshold
达到想要的效果。
打开保存的DCT变换的数据,如下图。可以看到右下的有效数据会越来越少,而这些高频信息对视觉上影响也较小。这就为压缩提供了可能(舍弃一定清晰度)。实际上对于H.264不是对图片本身做变换,而是对块的残差或者运动残差做DCT变换;也不是像低通滤波直接变成0而是采用量化的方式减小精度,再通过变长编码减小位数。
- H.264中的DCT
经过帧间/帧内预测,残差块亮度4x4 ~ 16x16,色度4x4 ~ 8x8.对于DCT变换(这里是整数DCT变换)得到的(0,0)位置上的为DC系数(代表了图像块的平均亮度或色度值),其他位置上为AC系数(代表了图像块中的细节信息,如边缘、纹理等),分别进行处理如图
分块成4x4区域做DCT变换,如果DC系数是4以上做Hadamard变换,变换后的图像数据可能更加集中或稀疏。这样16x16块分成16个4x4块,即有16个DC系数,8x8块有4个DC系数,分别做相应的Hadamard变换。最后分别对DC和AC系数做量化处理。
- 量化与熵编码
前面的步骤无论是帧间帧内还是DCT变换,还都不会造成图像画质上的损失。而量化本质上是把变换而来的连续数值离散化,简单来说就是(x / Qstep)取整
,Qstep
称为步长。量化会带来一定精度的损失,步长越大压缩越多,图像清晰度也越差。这时回头看一下DCT变换的结果,上一节中的图片(经过归一化取整了),可以看出左上的变动大,值也比较大,而越往右下变动越小,所以量化更影响高频(视觉冗余
)。
量化后的数据更方便熵编码,这一步是无损压缩。可类比大学课程中的霍夫曼编码,出现频率越高的编码短,以此来压缩数据。H.264采用两种熵编码方法:基于上下文的自适应可变长度编码(CAVLC)和基于上下文的自适应二进制算术编码(CABAC)。
6. 环路滤波
此时再回头看下本节开始的流程图,还有一部分没有用到。上文中经过熵编码的数据已经可以封装输出了,下图的路径其实是在解码,为什么编码端需要用到解码呢?
事实上这里是为了使用和解码端一致的参考块(帧内)和参考帧。编码端是有原图像的,无论是帧间还是帧内使用的参考帧都是已编码再解码得到的图像。此处的参考帧或者参考块是经过压缩的,和原图有差别,但能和解码端保持一致。
解码后的宏块组成图像,还要经过环路滤波才是解码端的效果,环路滤波是以图像为单位的。整体上在编码会以宏块(子块为单位),又有一定精度损失,块边界又常常是信号高频区域。因此会出现以下现象:
环路滤波对重建图像进行平滑处理,减少编码过程中引入的块效应和振铃效应等,具体大家可以查更详细的文档。
编码结构
- AnnexB格式和RTP格式
- AnnexB格式以起始位0X01开始,后面是NAL Unit
- RTP格式是NAL Unit封装成RTP数据包,如在线流媒体服务
- NAL Unit
- NALU Header 1字节
- forbidden_zero_bit:占用NAL单元头的第一个位,值默认0,值为1时表示错误
- nal_ref_idc:占用NAL单元头的第2、3位,取值00~11,取值越大表示此NAL单元越重要。
- nal_unit_type:4~8位,用来表示NAL单元的类型,标识IDR,数据帧,SPS和PPS,结束符等
- NALU 主体(preload)
- 视频压缩数据EBSP(Encapsulated Byte Sequence Payload)
- SODB(String Of Data Bits) :原始数据比特流
- RBSP(Raw Byte Sequence Payload) :在SODB的后面添加了trailing bits(一个bit 1和若干个bit 0),以便字节对齐
- EBSP :为了避免NAL单元内部的数据与开始码冲突,在NAL单元内部每出现两个连续的00时,就增加一个0x03(称为仿效字节),从而预防压缩后的数据与开始码产生冲突。
- 视频压缩数据EBSP(Encapsulated Byte Sequence Payload)
- NALU Header 1字节
- Slice层
- Slice Header
- first_mb_in_slice:开始宏块索引
- slice_type:I帧Slice P帧Slice B帧Slice
- pic_parameter_set_id:PPS的索引
- frame_num:当前Slice所属的帧的帧号
- slice_qp_delta:前Slice的量化参数偏移量
- disable_deblocking_filter_idc:表示是否禁用环形滤波器
- Slice Data
- 由宏块组成
- Slice Header
- Slice Data层
- flags 指示slice数据的不同特性和状态
- 宏块
- mb_type 标识宏块类型和编码方式等
- 块数据,分为I_PCM模式(宏块原数据)和预测模式,又化为子块等
音频编码
音频编码是指将模拟音频信号转换为数字信号的过程,主要通过抽样、量化和编码三个步骤实现,常用的编码。
- 脉冲代码调制编码(PCM) 最基础的音频编码方式,保持高保真度,但文件体积较大。
- 高级音频编码(AAC) :一种高效的压缩格式,保持高音质的同时,实现更小的文件体积。
- SBC :蓝牙音频传输中最基础的编解码器。
PCM是对声音量化后的基本编码,AAC是利用冗余信息的有损编码,这里简要介绍一下AAC(对音频学习比较少~)。
AAC编码
AAC两个格式:
- ADIF(Audio Data Interchange Format) 只有一个头信息,需要得到所有的数据后才能解码,适用于本地存储的音频文件。
- ADTS(Audio Data Transport Stream) 每一帧都有头信息,因此可以在任意帧解码,可用于网络直播等需要实时传输和处理的场景。ADTS帧结构如下: