一、H.264的预测编码
H.264 视频编码标准相较于之前的编码标准扩充了精确度更高的分层结
构、运动估计和整数变换等新特征,拥有更高的压缩效率以及图像压缩质量,因此其标准格式被广泛用于实时视频通信。H.264 编码器大体分为两个流程,即从左向右的压缩编码流程和由右向左的重建流程如下图所示。首先将输入的原始数据与预测数据相减,得到残差数据。其次对残差数据进行变换,得到变换系数,变换系数包含残差数据的低频分量和高频分量。低频分量即图像中的背景部分,通常变化较小。高频分量即图像中的轮廓部分,通常变化较大。然后进行量化,对低频分量使用较高的量化精度,对高频分量使用较低的量化精度,得到量化后的变换系数,即残差系数。最后对控制信息(预测模式和量化参数等)、残差系数以及运动信息进行熵编码,得到码流(二进制比特流)。其中,帧间预测使用的参考帧因为量化带来的精度损失,会造成图像的失真,出现类似马赛克的现象,因此需要先进行去块效应滤波。
(一)基于空间域的帧内预测编码
H.264/AVC标准的帧内预测编码(Intra Prediction)是一种空间预测技术,它允许编码器使用已经编码的像素来预测当前要编码的像素值,从而减少图像内的冗余信息。帧内预测主要应用于I帧(Intra-coded frames)或者某些P帧和B帧中的宏块,这些宏块不依赖于其他帧进行预测。
H.264支持多种宏块划分方式,以适应不同的视频内容和编码需求。以下是H.264中常见的宏块划分类型:
- 16x16宏块:H.264标准中最基本的宏块划分方式,每个宏块包含16x16的像素,适用于图像中变化平缓的区域,如背景和渐变区域。
- 16x8宏块:将16x16的宏块分为两个16x8的子宏块,适用于图像中水平方向上有较大变化的区域。
- 8x16宏块: 将16x16的宏块分为两个8x16的子宏块,适用于图像中垂直方向上有较大变化的区域。
- 8x8宏块:将16x16的宏块分为四个8x8的子宏块,适用于图像中包含复杂细节或快速运动的区域。
- 8x4, 4x8, 4x4宏块:宏块被分为更小的子宏块,从而提供更高的编码灵活性,适用于图像中包含精细纹理或小尺寸对象的区域。
H.264帧内预测由两种亮度块预测和一种色度块预测组成,其中亮度
块预测由 4×4 预测模式和 16×16 预测模式构成,4×4 预测模式共计九种,包括水平、垂直以及多个不同角度的预测模式,后文会详细介绍。16×16 模式四种,色度块预测为 8×8 预测模式,共四种,后文同样会分别介绍。
(1)Intra4×4 帧内预测
Intra4×4 帧内预测是以宏块为单位进行编码,将 16×16 像素的宏块分割为
16 个 4×4 的子块。如下图所示,4×4亮度块的上方和左方像素A~M为已编码和重构像素,用作编解码器中的预测参考像素。a~p为待预测像素,利用A~M值和9种模式实现。
H.264定义了多种帧内预测模式,每种模式都试图找到最合适的预测方向来减少预测误差。这些模式大致可以分为两类:直流(DC)预测和方向性(Angular)预测。
①直流(DC)预测:直流预测假设一个宏块内的所有像素值大致相同,因此它使用邻近宏块中同一位置的像素的平均值来预测当前宏块的像素值。这种预测方式适用于那些颜色变化平缓的区域,如背景和渐变区域。
②方向性(Angular)预测:方向性预测根据像素值的变化趋势选择一个预测方向,然后沿着这个方向对像素值进行预测。H.264/AVC标准的方向性(Angular)预测是帧内预测编码中的一种方法,它利用邻近已编码像素的特定方向上的趋势来预测当前宏块中的像素值。这种预测方法适用于图像中存在明显方向性变化的情况,如边缘、纹理和细节丰富的区域。
下面的图和表格是Intra4×4 帧内预测 9 种预测模式的描述,其中模式2(DC预测)根据A~M中已编码像素预测,而其余模式只有在所需预测像素全部提供才能使用,被称为方向性预测。图中箭头表明了每种模式预测方向。对模式3~8,预测像素由A~M加权平均而得。例如,在预测模式4中,d=round(B/4+C/2+D/4)。
模式 | 描 述 |
---|---|
模式0(垂直) | A~D 四个参考点从垂直方向插补到 P 块中 |
模式1(水平) | I~L 四个参考点从水平方向插补到 P 块中 |
模式2(DC) | 由 A~M 平均值计算得到 |
模式3(左下对角线) | 从右上到左下 45。方向插补全部参考值 |
模式4(右下对角线) | 从左上到右下 45。方向插补全部参考值 |
模式 5(垂直向右) | 垂直向右 22.5。方向插补全部参考值 |
模式 6(水平向下) | 水平向下 22.5。方向插补全部参考值 |
模式 7(垂直向左) | 垂直向左 22.5。方向插补全部参考值 |
模式 8(水平向上) | 水平向上22.5。方向插补全部参考值 |
(2)Intra16×16 帧内预测
Intra16×16 帧内预测以整个 16×16 宏块为单位,常用于宏块亮度较为平缓
的情况,共有 4 种预测模式如下图 所示,其详细描述情况如下表所示。
模式 | 描述 |
---|---|
0(垂直预测) | 插补垂直方向参考值进行预测 |
1(水平预测) | 插补水平方向参考值进行预测 |
2(DC) | 插补参考值的均值进行预测 |
3(平面预测) | 插补左边、上边的参考值,适用于平坦部分 |
(3)Intra8×8 帧内预测
Intra8×8 模式用于预测色度(U)和饱和度(V)信号,其编码参考点在顶
部和左侧,色度参考值用于得出编码宏块的色度值。U、V 色度信号预测常用同一模式。Intra8×8 有以下四种模式:DC(0)、水平(1)、垂直(2)、平面(3),与帧内Intra16×16 亮度快预测模式类似,只是编号有所不同。
编码器会尝试所有可能的预测模式,并选择产生最小预测误差的模式进行编码。
(二)帧间预测编码
帧间预测编码主要用于减少视频序列中的时间冗余,通过预测当前帧与之前已编码帧之间的关系来压缩数据。H.264 帧间预测编码的宏块分割方式如下图所示,这种从树根到树枝的橡树根式扩张被称为树状结构运动补偿。
解码器端接收传输的运动矢量和各个块的分割方式。一般来说,面对平滑
图像使用大块分割方式,其优点是每个块产生很少需要传输的运动矢量和块分割方式,从而减少传输内容;对于相对复杂的图像,使用小块分割方式,虽然增大传输量,但可使图像质量有明显提高。分割块大小很大程度上影响传输数据量和图像质量。
帧间预测编码顺序和步骤通常如下:
- 输入帧分割:将图像帧分割成宏块(Macroblocks),通常是16x16像素的区域。对于隔行视频,宏块可以是16x8或8x16像素的区域。
- 参考帧选择:选择一个或多个参考帧(通常是之前已编码的I帧或P帧)用于预测。H.264支持多参考帧,可以从前面的多个帧中选择最佳匹配帧。
- 运动估计:对每个宏块执行运动估计,以找到参考帧中的最佳匹配块。运动估计可以通过不同的算法进行,如整像素搜索、亚像素搜索(1/2像素、1/4像素等)。
- 运动向量编码:将找到的运动向量(Motion Vectors, MVs)编码,以便在解码时能够重建宏块。运动向量通常指示了宏块在参考帧中位置偏移。
- 运动补偿:使用运动向量从参考帧中提取对应的宏块,并将其作为当前宏块的预测值。当前宏块与预测值之间的差异(残差)将被编码。
我们利用一个简单的MATLAB代码示例来理解帧间预测编码的基本流程。代码如下:
% 读取多个图像文件作为参考帧
reference_frames = cell(1, 3);
reference_frames{1} = imread('reference_frame1.png');
reference_frames{2} = imread('reference_frame2.png');
reference_frames{3} = imread('reference_frame3.png');
% 读取当前帧
current_frame = imread('current_frame.png');
% 将图像转换为灰度
current_frame = rgb2gray(current_frame);
for i = 1:size(reference_frames, 1)
reference_frames{i} = rgb2gray(reference_frames{i});
end
% 执行帧间预测
inter_frame_prediction(current_frame, reference_frames);
%帧间预测基本流程
function inter_frame_prediction(current_frame, reference_frames)
% 假设 current_frame 是当前帧,reference_frames 是一个包含多个参考帧的细胞数组
block_size = 16; % 块大小
search_window = 16; % 搜索窗口大小
sub_pixel_precision = 0.5; % 亚像素精度
% 获取图像尺寸
[height, width] = size(current_frame);
% 初始化预测帧和运动向量数组
predicted_frame = zeros(height, width);
motion_vectors = zeros(height/block_size, width/block_size, 2);
% 遍历当前帧的每个块
for y = 1:block_size:height-block_size
for x = 1:block_size:width-block_size
% 计算当前块与每个参考帧中各个位置的差值
min_diff = inf;
best_vector = [0, 0];
best_reference_index = 1;
for i = -search_window:search_window
for j = -search_window:search_window
for k = 1:size(reference_frames, 1)
% 确保搜索范围在图像边界内
if (x+i >= 1) && (x+i+block_size-1 <= width) && ...
(y+j >= 1) && (y+j+block_size-1 <= height)
% 计算差值
diff = sum(sum(abs(current_frame(y:y+block_size-1, x:x+block_size-1) - ...
reference_frames{k}(y+j:y+j+block_size-1, x+i:x+i+block_size-1))));
% 更新最小差值和最佳运动向量
if diff < min_diff
min_diff = diff;
best_vector = [i, j];
best_reference_index = k;
end
end
end
end
end
% 存储运动向量
motion_vectors(y/block_size, x/block_size, :) = best_vector;
% 根据运动向量进行运动补偿
predicted_frame(y:y+block_size-1, x:x+block_size-1) = ...
reference_frames{best_reference_index}(y+best_vector(2):y+best_vector(2)+block_size-1, ...
x+best_vector(1):x+best_vector(1)+block_size-1);
end
end
% 显示运动向量场和预测帧
figure, imshow(motion_vectors(:,:,1));
figure, imshow(predicted_frame);
end
二、变换编码
H.264/AVC变换编码是视频压缩过程中的一个关键步骤,它通过对图像块的残差数据进行变换和量化来去除空间冗余,从而减少数据量。在H.264中,变换编码主要使用的是离散余弦变换(DCT),但与以前的视频压缩标准相比,H.264在变换编码方面做了一些改进和创新。
以下是H.264变换编码的基本步骤:
- 残差计算:
- 对于帧间预测,残差是当前宏块与运动补偿后的预测宏块之间的差异。
- 对于帧内预测(I帧),残差是当前宏块与帧内预测宏块之间的差异。
- 变换:
- H.264使用两种尺寸的DCT变换:4x4和16x16。
- 对于亮度分量,通常首先对每个4x4的残差块进行DCT变换。
- 对于色度分量,通常对8x8的残差块进行DCT变换。
- 对于16x16的宏块,可以对其中的4个4x4亮度块进行组合,然后进行16x16的DCT变换。
- 量化:
- 对变换后的系数进行量化,以减少非零系数的数量和大小。
- 量化是一个可逆过程,它将变换系数除以量化步长,并四舍五入到最接近的整数。
- 扫描和编码:
- 对量化后的系数进行扫描,通常是使用Zigzag扫描或水平/垂直扫描,将2D的系数矩阵转换为1D的系数序列。
- 使用熵编码(如CAVLC或CABAC,后文详细说明)对扫描后的系数进行编码。
- 反量化:
- 在解码过程中,对量化后的系数进行反量化,以重建变换系数。
- 反变换:
- 对反量化后的系数进行反变换(IDCT),以重建残差数据。
- 重建:
- 对于帧间预测,将重建的残差数据加上预测宏块,得到重建的当前宏块。
- 对于帧内预测,将重建的残差数据加上帧内预测宏块,得到重建的当前宏块。
H.264的变换编码过程中,4x4的DCT变换是标准的一部分,而16x16的变换是一个可选的扩展,称为变换 SKIP 模式。这种模式在某些情况下可以提供更好的压缩效率,尤其是在图像细节较少的区域。
我们利用一个简单的MATLAB代码示例来理解变换编码的基本流程。代码如下:
function h264_4x4_transform_encodingExample()
% 读取一个示例图像
I = imread('example.png');
I = rgb2gray(I);
% 选择一个图像块进行变换编码
block_size = 4;
block = I(1:block_size, 1:block_size);
% 执行DCT变换
dct_block = dct2(block);
% 量化参数,根据H.264标准,QPS范围通常为0-51
QP = 28;
% 标准量化矩阵,这里使用了一个简化的例子
QM = ones(block_size);
QM = QM * (2^(QP/6));
% 执行量化
quantized_block = round(dct_block ./ QM);
% 执行反量化
dequantized_block = quantized_block .* QM;
% 执行IDCT变换
idct_block = idct2(dequantized_block);
% 显示结果
figure, imshow(block), title('原始图像块');
figure, imshow(dct_block), title('DCT变换系数');
figure, imshow(quantized_block), title('量化后的DCT系数');
figure, imshow(idct_block), title('重建的图像块');
end
% 调用示例函数
h264_4x4_transform_encodingExample();
三、去块效应滤波
去块效应滤波(Deblocking Filter)是H.264/AVC视频压缩标准中用于改善视频质量的一个步骤。在H.264编码过程中,图像被分割成宏块(Macroblocks),这些宏块可能因为量化、变换和熵编码而出现边缘不连续性,导致在解码后的图像中出现块状效应(Blockiness)。去块效应滤波器的作用是平滑这些边缘,减少块状效应,从而提高视频质量。
去块效应滤波器通常在视频解码器中实现,用于处理解码后的图像。它可以在像素级上操作,以减少由于量化导致的块效应,也可以在宏块级上操作,以减少由于变换和熵编码导致的块效应。
去块效应滤波器的主要操作包括:
- 边缘检测:
- 检测图像中潜在的块状边缘。
- 通常使用梯度或其他边缘检测算法来找到边缘。
- 边缘平滑:
- 对检测到的边缘进行平滑处理。
- 平滑处理可以包括线性插值、高斯滤波、双线性滤波等。
- 边缘保持:
- 在平滑边缘的同时,保持边缘的尖锐度。
- 避免过度平滑导致边缘模糊。
- 滤波器设计:
- 设计滤波器以平衡平滑度和边缘保持。
- 滤波器的设计需要考虑编码参数(如量化参数QP)和视频内容。
- 滤波器应用:
- 将滤波器应用于解码后的图像。
- 滤波器通常在每个宏块上应用,以改善整个图像的质量。
去块效应滤波器的设计和实现可以非常复杂,需要考虑编码器的具体实现和视频内容的特性。在实际应用中,去块效应滤波器通常是根据编码参数和视频内容动态调整的,以实现最佳的图像质量。
四、基于上下文的自适应熵编码
H.264标准中提供了两种熵编码方法,上下文自适应可变长度编码(CAVLC, Context-Adaptive Variable-Length Coding)和上下文自适应二进制算术编码(CABAC, Context-Adaptive Binary Arithmetic Coding)。
(一)CAVLC
CAVLC是一种基于上下文的熵编码方法,它根据已编码的符号来调整后续符号的编码概率,从而实现更高效的编码。CAVLC主要用于编码变换系数、宏块类型、运动向量等视频压缩数据。在CAVLC中,每个符号的编码长度是根据其出现的概率来确定的,高概率符号使用较短的编码,而低概率符号使用较长的编码。
CAVLC编码过程主要包括以下几个步骤:
- 符号概率估计:
- 根据已编码的符号来估计当前符号的概率。
- 上下文模型用于估计符号的概率,上下文模型可以基于已编码的符号类型、位置、值等信息。
- 码表选择:
- 根据估计的概率选择合适的码表(码字)。
- 码表通常包含一系列可能的码字和对应的概率。
- 编码:
- 使用选定的码字对当前符号进行编码。
- 编码后的符号被添加到输出流中。
- 更新上下文:
- 更新上下文信息,以便用于下一个符号的编码。
- 上下文信息的更新可以帮助更准确地估计下一个符号的概率。
CAVLC编码的关键在于上下文模型的设计,它决定了如何根据已编码的符号来调整后续符号的编码概率。上下文模型可以是简单的,也可以是复杂的,取决于编码器的实现和性能要求。
在H.264中,CAVLC编码通常用于编码变换系数、宏块类型、运动向量等数据。这些数据通常具有不同的统计特性,因此需要使用不同的上下文模型来进行编码。
在MATLAB中,虽然没有直接支持CAVLC的函数,但我们可以通过一些基本的函数来模拟CAVLC的编码过程。以下是一个简化的MATLAB示例,它展示了如何使用基本的MATLAB函数来模拟CAVLC编码的过程:
function cavlc_encoding_example()
% 准备一些示例数据
data = [4, 2, 0, 0, 1, 1, 0, 0, 0, 0]; % 变换和量化后的系数
% 计算每个符号的出现次数
symbols = unique(data);
symbol_counts = zeros(1, max(symbols));
for i = 1:length(data)
symbol_counts(data(i) + 1) = symbol_counts(data(i) + 1) + 1;
end
% 根据出现次数生成码字长度
code_lengths = symbol_counts / length(data);
code_lengths = log2(code_lengths + 1); % 防止对数函数中的零值
% 生成码字
codes = zeros(size(symbols));
for i = 1:length(symbols)
codes(i) = dec2bin(symbols(i), ceil(code_lengths(symbols(i) + 1)));
end
% 显示结果
disp('原始数据: ');
disp(data);
disp('码字长度: ');
disp(code_lengths(2:end));
disp('码字: ');
disp(codes);
end
% 调用示例函数
cavlc_encoding_example();
在这个示例中,我们首先计算每个不同符号的出现次数,然后根据这些次数生成码字长度。最后,我们使用dec2bin
函数为每个符号生成一个二进制码字。
(二)CABAC
CABAC是一种更高级的熵编码方法,它通过上下文自适应算术编码来去除编码数据中的统计冗余。CABAC编码器在编码过程中能够根据已编码的数据动态地调整编码概率,从而实现更高的压缩率。
CABAC在H.264中的实现涉及以下几个关键方面:
- 上下文模型:
- CABAC使用上下文模型来估计符号的概率。
- 上下文模型基于已编码的符号类型、位置、值等信息。
- 上下文模型可以是简单的,也可以是复杂的,取决于编码器的实现和性能要求。
- 算术编码器:
- CABAC使用算术编码器来对符号进行编码。
- 算术编码器根据上下文模型计算每个符号的编码概率。
- 算术编码器将符号编码为一个连续的数值范围,并使用熵编码器(如CABAC熵编码器)将这个范围编码为比特流。
- 熵编码器:
- CABAC熵编码器用于将编码概率范围编码为比特流。
- 熵编码器可以基于上下文模型动态地调整编码概率。
- 上下文模型更新:
- 根据当前符号的编码结果更新上下文模型。
- 上下文模型的更新可以帮助更准确地预测下一个符号的概率。
- 序列编码:
- 重复上述步骤,对序列中的每个符号进行编码。
- 编码后的符号序列被添加到输出流中。
在H.264中,CABAC编码通常用于编码变换系数、宏块类型、运动向量等数据。这些数据通常具有不同的统计特性,因此需要使用不同的上下文模型来进行编码。
在上下文自适应二进制算术编码(CABAC)中,上下文模型是决定编码效率的关键因素。CABAC编码器利用上下文模型来估计符号的概率,并根据这些概率来动态调整编码。上下文模型的构建需要考虑编码数据的统计特性,以及这些特性如何随时间变化。
CABAC中的上下文模型构建涉及以下几个步骤:
- 上下文建模:
- 确定需要建模的上下文,这通常包括编码数据的各个部分,如变换系数、宏块类型、运动向量等。
- 选择适当的上下文模型,例如统计模型、决策树模型、神经网络模型等。
- 上下文初始化:
- 为每个上下文设置初始概率和状态。
- 初始概率和状态通常是根据预先定义的规则或通过训练数据来确定的。
- 上下文更新:
- 在编码过程中,根据已编码的符号更新上下文模型。
- 更新过程可以基于多种方法,如动态概率更新、决策树剪枝、神经网络训练等。
- 上下文传播:
- 上下文模型之间的传播,即当编码一个符号时,考虑其对其他符号上下文的影响。
- 上下文传播可以基于概率传播、状态传播等方法。
- 上下文合并:
- 在多参考帧编码中,上下文模型需要合并来自不同参考帧的信息。
- 上下文合并可以基于概率合并、状态合并等方法。
- 上下文选择:
- 根据编码器的当前状态和上下文模型,选择最合适的上下文模型进行编码。
- 上下文选择可以基于概率比较、状态匹配等方法。
- 上下文模型优化:
- 在编码过程中,不断优化上下文模型,以提高编码效率。
- 优化过程可以基于反馈机制、自适应调整等方法。
CABAC中的上下文模型构建是一个复杂的过程,需要根据具体的编码数据和编码器性能要求来设计。在实际应用中,上下文模型通常是根据大量训练数据来训练得到的,或者通过经验规则来确定。
由于CABAC编码器的设计和实现相对复杂,因此在MATLAB中直接实现CABAC编码器可能比较困难。如果你需要实现完整的H.264 CABAC编码,可能需要参考H.264标准文档,并使用更专业的工具或库。