源文件 :
VVCSoftware_VTM-master/source/Lib/EncoderLib/EncAdaptiveLoopFilter.cpp(4339 行)所属模块 :VTM(VVC Test Model)编码器,环路滤波层
标准依据 :ITU-T H.266 / ISO/IEC 23090-3 (VVC),JVET-N1001
作者 :码流怪侠
日期:2026-06-17
目录
- [ALF 简介与 VVC 中的定位](#ALF 简介与 VVC 中的定位)
- [核心原理:Wiener 最优滤波](#核心原理:Wiener 最优滤波)
- [非线性 ALF:clipALF 机制](#非线性 ALF:clipALF 机制)
- 滤波器形状与分类机制
- [协方差矩阵 AlfCovariance 解析](#协方差矩阵 AlfCovariance 解析)
- 核心函数关系图
- [主流程 ALFProcess 源码拆解](#主流程 ALFProcess 源码拆解)
- [统计量收集:deriveStatsForFiltering / getBlkStats / calcCovariance](#统计量收集:deriveStatsForFiltering / getBlkStats / calcCovariance)
- [滤波器系数优化:deriveCoeffQuant + optimizeFilter](#滤波器系数优化:deriveCoeffQuant + optimizeFilter)
- 滤波器类合并:mergeClasses
- [CTB 级决策:alfEncoderCtb 与 deriveCtbAlfEnableFlags](#CTB 级决策:alfEncoderCtb 与 deriveCtbAlfEnableFlags)
- [CC-ALF(跨分量 ALF)流程](#CC-ALF(跨分量 ALF)流程)
- [Cholesky 分解求解器详解](#Cholesky 分解求解器详解)
- [C++ 语言应用技巧精析](#C++ 语言应用技巧精析)
- 踩坑点与调试要点
- 性能热点与优化建议
- 参考文献
1. ALF 简介与 VVC 中的定位
1.1 环路滤波器的作用
视频编码中,DCT 量化引入了块效应(blocking artifact)和振铃(ringing),重建帧质量不如原始帧。环路滤波器在解码端重建帧上进行后处理,使其更接近原始图像,从而提升编码效率(同等 PSNR 用更少码率)。
VVC 标准的环路滤波流水线(编码侧同步执行以供参考帧使用):
DCT 量化 → 熵编码 → 重建帧 → DBF (去块) → SAO (样本偏移) → ALF → 参考帧
1.2 ALF 在 VVC 相对 HEVC 的演进
| 特性 | HEVC ALF | VVC ALF |
|---|---|---|
| 滤波器形状 | 固定 5×5/7×7 | 菱形 5×5 + 7×7,自适应选择 |
| 分类数 | 1(亮度) | 25 类(亮度),多 alt(色度) |
| 非线性 | 无 | 有(clip 参数联合优化) |
| 跨分量 | 无 | CC-ALF(色度由亮度辅助修正) |
| APS 复用 | 无 | 多帧 APS 复用 |
| CTB 级开关 | 有 | 有(精细化 RDO) |
VVC 的 ALF 编码增益相比 HEVC 提升约 0.5~1.0 dB PSNR(Y 分量),在高分辨率视频上效果更为显著。
2. 核心原理:Wiener 最优滤波
2.1 问题建模
设重建像素邻域向量为 x k ∈ R N \mathbf{x}_k \in \mathbb{R}^N xk∈RN,原始像素为 y k y_k yk。我们寻找滤波器系数 f ∈ R N \mathbf{f} \in \mathbb{R}^N f∈RN 使得:
y ^ k = f T x k ≈ y k \hat{y}_k = \mathbf{f}^T \mathbf{x}_k \approx y_k y^k=fTxk≈yk
最小化均方误差(MSE):
J ( f ) = ∑ k ( y k − f T x k ) 2 J(\mathbf{f}) = \sum_k \left( y_k - \mathbf{f}^T \mathbf{x}_k \right)^2 J(f)=k∑(yk−fTxk)2
2.2 Wiener-Hopf 方程
对 J ( f ) J(\mathbf{f}) J(f) 求导并令其等于零:
∂ J ∂ f = − 2 ∑ k x k ( y k − f T x k ) = 0 \frac{\partial J}{\partial \mathbf{f}} = -2\sum_k \mathbf{x}_k(y_k - \mathbf{f}^T\mathbf{x}_k) = 0 ∂f∂J=−2k∑xk(yk−fTxk)=0
整理为标准 Wiener-Hopf 方程:
( ∑ k x k x k T ) ⏟ E f = ∑ k x k y k ⏟ y \underbrace{\left(\sum_k \mathbf{x}_k\mathbf{x}k^T\right)}{\mathbf{E}} \mathbf{f} = \underbrace{\sum_k \mathbf{x}k y_k}{\mathbf{y}} E (k∑xkxkT)f=y k∑xkyk
即 E f = y \mathbf{E}\mathbf{f} = \mathbf{y} Ef=y,其中:
- E i j = ∑ k x k , i ⋅ x k , j \mathbf{E}{ij} = \sum_k x{k,i} \cdot x_{k,j} Eij=∑kxk,i⋅xk,j:自相关矩阵 (代码中
AlfCovariance::E) - y i = ∑ k x k , i ⋅ y k \mathbf{y}i = \sum_k x{k,i} \cdot y_k yi=∑kxk,i⋅yk:互相关向量 (代码中
AlfCovariance::y)
2.3 最小 MSE(最小误差)
代入最优系数后,最小化后的误差为:
J min = pixAcc − f T y J_{\min} = \text{pixAcc} - \mathbf{f}^T \mathbf{y} Jmin=pixAcc−fTy
其中 pixAcc = ∑ k y k 2 \text{pixAcc} = \sum_k y_k^2 pixAcc=∑kyk2,对应代码中的 pixAcc 成员。
calculateError() 函数即实现此公式:
cpp
double AlfCovariance::calculateError(const AlfClipIdx* clip, const double* coeff, const int numCoeff) const
{
double sum = 0;
for (int i = 0; i < numCoeff; i++)
sum += coeff[i] * y(clip[i], i); // f^T * y
return pixAcc - sum; // pixAcc - f^T * y
}
2.4 率失真优化(RDO)
编码侧不仅最小化失真,还要平衡码率:
Cost = D + λ ⋅ R \text{Cost} = D + \lambda \cdot R Cost=D+λ⋅R
其中 D D D 为过滤后的 MSE, R R R 为 ALF 参数(系数、clip、CTB 使能标志)的编码比特数, λ \lambda λ 为拉格朗日因子(由 QP 决定)。
cpp
// ALFProcess() 中的 lambda 计算
m_lambda[COMPONENT_Y] = lambdas[COMPONENT_Y] * double(1 << shiftLuma);
3. 非线性 ALF:clipALF 机制
3.1 线性 ALF 的局限
标准 Wiener 滤波假设输入信号线性叠加,但在像素边缘处,邻域像素与中心像素的差值可能极大,线性系数会在平坦区过度修正。
3.2 非线性截断公式
VVC 引入 clipALF 函数对差值进行截断:
d k = Clip 3 ( − K i j , K i j , x k , j + x k , j ′ − 2 ⋅ x k , 0 ) d_k = \text{Clip}3(-K{ij}, K_{ij},\ x_{k,j} + x_{k,j'} - 2 \cdot x_{k,0}) dk=Clip3(−Kij,Kij, xk,j+xk,j′−2⋅xk,0)
其中 K i j K_{ij} Kij 是第 i i i 个 clip bin 级别对应的截断幅值,共 4 档(ALF_NUM_CLIP_VALS = 4):
cpp
// 实际截断值枚举(以 8bit 为例)
clip[0]=128, clip[1]=64, clip[2]=32, clip[3]=0 // 对应 bin 0~3
3.3 扩展后的协方差矩阵
引入 clip 参数 b i b_i bi 后,自相关矩阵扩展为 4D 张量:
E ( b 0 , b 1 , k , l ) = ∑ pixels clipALF ( K b 0 , ... , k ) ⋅ clipALF ( K b 1 , ... , l ) E(b_0, b_1, k, l) = \sum_{\text{pixels}} \text{clipALF}(K_{b_0}, \ldots, k) \cdot \text{clipALF}(K_{b_1}, \ldots, l) E(b0,b1,k,l)=pixels∑clipALF(Kb0,...,k)⋅clipALF(Kb1,...,l)
关键影响 :每个系数都有独立的 clip 参数,维度从 N × N N \times N N×N 扩展为 4 × 4 × N × N 4 \times 4 \times N \times N 4×4×N×N,内存翻 16 倍,也是 ALF 编码侧最耗内存的部分。
4. 滤波器形状与分类机制
4.1 滤波器形状
VVC 亮度 ALF 支持两种滤波器形状,通过 AlfFilterShape 描述:
7×7 菱形(Luma):13 系数
f0
f1 f2 f1
f3 f4 f5 f4 f3
f6 x f6
f7
5×5 菱形(Chroma):7 系数
f0
f1 f2 f1
f3 x f3
f4
4.2 方向对称与 transposeIdx
梯度方向分类时,ALF 将像素分为 4 种旋转方向(0/90/45/135°),通过 transposeIdx(0~3)控制系数的几何旋转映射,使同一套系数能处理各方向纹理。
4.3 25 类分类机制
亮度 ALF 将图像中的像素按其梯度幅度 + 方向 划分为 25 个类(MAX_NUM_ALF_CLASSES = 25):
类索引 = 梯度方向类 (0~4) × 5 + 梯度幅度类 (0~4)
deriveClassification() 函数(继承自 AdaptiveLoopFilter)通过计算 4×4 像素块内的水平、垂直、对角梯度完成分类。
5. 协方差矩阵 AlfCovariance 解析
AlfCovariance 是整个 ALF 编码侧的数据核心,存储了求解 Wiener 滤波器所需的所有统计量。
5.1 数据布局
cpp
struct AlfCovariance {
double* data; // 连续内存,存储 E + y(展平的多维张量)
double pixAcc; // pixAcc = sum(y_k^2)
int numCoeff; // 系数数量 N
int numBins; // clip 档位数 B (1 or 4)
// 访问器
double& E(int b0, int b1, int k, int l); // E[b0][b1][k][l]
double& y(int b, int k); // y[b][k]
};
内存布局(行主序):
data[0 .. B*B*N*N - 1] → E 张量
data[B*B*N*N .. B*B*N*N + B*N - 1] → y 向量
5.2 快速偏移计算
cpp
// getOffsetEfast() 避免四重乘法
ptrdiff_t getOffsetEfast(int b0, int b1, int k, int l) {
return ((b0 * numBins + b1) * numCoeff + k) * numCoeff + l;
}
5.3 协方差叠加(运算符重载)
cpp
AlfCovariance& operator+=(const AlfCovariance& other) {
pixAcc += other.pixAcc;
// SIMD 友好的连续内存拷贝叠加
for (int i = 0; i < size; i++)
data[i] += other.data[i];
return *this;
}
这是 CTB 级协方差 → 帧级协方差聚合的关键,也是 getFrameStat() 的核心操作。
6. 核心函数关系图
整个编码侧 ALF 的函数调用关系如下(已在博客头部图示中可视化):
ALFProcess()
├── deriveClassification() ← 像素梯度分类(25 类)
├── deriveStatsForFiltering() ← 统计量收集
│ └── getBlkStats()
│ └── calcCovariance() ← 填充 E, y, pixAcc
├── firstPass() ← 初步确定最优 APS
│ ├── getUnfilteredDistortion()
│ ├── getFilterCoeffAndCost()
│ │ ├── getFrameStats() ← 聚合 CTB 协方差
│ │ └── mergeFiltersAndCost()
│ │ ├── mergeClasses() ← 类合并(贪心 Δerr 最小)
│ │ └── deriveFilterCoeffs()
│ │ └── deriveCoeffQuant()
│ │ └── optimizeFilter() ← Wiener + clip 优化
│ └── deriveCtbAlfEnableFlags() ← CTB 使能 RDO
├── alfEncoderCtb() ← 精细化 CTB 级决策(含 APS 复用)
│ └── (与 firstPass 子树相同,增加 APS 管理)
├── alfReconstructor() ← 执行真实滤波(m_filter7x7Blk 等)
└── deriveCcAlfFilter() ← CC-ALF 系数推导
├── deriveStatsForCcAlfFiltering()
├── deriveCcAlfFilterCoeff()
└── determineControlIdcValues()
7. 主流程 ALFProcess 源码拆解
ALFProcess() 是编码端 ALF 的总入口,每帧调用一次。
7.1 IRAP 帧重置
cpp
if (cs.slice->getPendingRasInit() || cs.slice->isIDRorBLA() || ...) {
// 清除所有 APS 状态------关键:防止跨 IRAP 错误复用
m_apsMap->clearActive();
for (int i = ...; i < ...; i++) {
APS* alfAPS = m_apsMap->getPS(i);
alfAPS->getAlfAPSParam().reset();
}
}
踩坑点:IRAP 帧不重置 APS 会导致后续帧解码失败,标准强制要求。
7.2 Lambda 初始化与精度对齐
cpp
int shiftLuma = 2 * DISTORTION_PRECISION_ADJUSTMENT(m_inputBitDepth[ChannelType::LUMA]);
m_lambda[COMPONENT_Y] = lambdas[COMPONENT_Y] * double(1 << shiftLuma);
DISTORTION_PRECISION_ADJUSTMENT 将失真值归一化到参考位深(10bit),保证不同位深下 RDO 比较的一致性。
7.3 边界填充(关键细节)
cpp
m_tempBuf.copyFrom(cs.getRecoBuf());
PelUnitBuf recYuv = m_tempBuf.getBuf(cs.area);
recYuv.extendBorderPel(MAX_ALF_FILTER_LENGTH >> 1); // 填充 3 个像素边界
ALF 7×7 滤波器需要访问当前像素上下各 3 行,边界填充防止越界访问。
7.4 虚拟边界处理
cpp
if (isCrossedByVirtualBoundaries(...)) {
// 处理 slice/tile/subpicture 边界
// 在虚拟边界两侧各自处理,避免跨边界滤波(影响并行解码)
}
VVC 支持独立并行区域(slice、tile、subpicture),ALF 不能跨越这些边界进行滤波,否则解码端并行处理时出现数据依赖。
8. 统计量收集:deriveStatsForFiltering / getBlkStats / calcCovariance
8.1 三层结构
帧级 (m_alfCovarianceFrame)
↑ getFrameStat() 聚合
CTB 级 (m_alfCovariance[comp][shape][ctu][class])
↑ getBlkStats() 收集
像素级 calcCovariance()
8.2 calcCovariance ------ ELocal 计算
cpp
void EncAdaptiveLoopFilter::calcCovariance(
Pel ELocal[MAX_NUM_ALF_LUMA_COEFF][MAX_ALF_NUM_CLIP_VALS],
const Pel* rec, const ptrdiff_t stride,
const AlfFilterShape& shape, const int transposeIdx,
const ChannelType channel, int vbDistance)
{
// 遍历菱形滤波器的每个系数位置 k
// ELocal[k][b] = clipALF(clip[b], curr, rec0[j], rec1[-j])
// = Clip3(-clip[b], clip[b], rec0[j] + rec1[-j] - 2*curr)
}
ELocal[k][b] 存储的是:对于系数位置 k k k,使用 clip 值 b b b 时的截断差值。
8.3 getBlkStats ------ 协方差矩阵累积
cpp
// 外层:遍历像素
const double yLocal = org[j] - rec[j]; // 原始-重建差
// 构建局部 e 矩阵
for (int b = 0; b < numBins; b++)
for (int k = 0; k < shape.numCoeff; k++)
e[b][k] = invStrength * ELocal[k][b];
// 累积 E 矩阵(利用对称性,只计算上三角)
for (int b0 = 0; b0 < numBins; b0++)
for (int k = 0; k < shape.numCoeff; k++) {
const double we = weight * e[b0][k];
for (int b1 = 0; b1 <= b0; b1++) {
const int maxl = b0==b1 ? k+1 : shape.numCoeff;
for (int l = 0; l < maxl; l++) {
alfCovariance[classIdx].data[oe] += we * e[b1][l]; // 只填上三角
}
}
alfCovariance[classIdx].y(b0, k) += we * yLocal;
}
alfCovariance[classIdx].pixAcc += weight * yLocal * yLocal;
性能关键 :对称性利用使乘法次数减半,
getOffsetEfast避免重复乘法计算偏移量,是整个 ALF 最热的内循环。
9. 滤波器系数优化:deriveCoeffQuant + optimizeFilter
9.1 优化流程
optimizeFilter()
├── getClipMax() // 确定 clip 上界(不影响解的最大 clip)
├── setEyFromClip() // 从 4D E 张量提取当前 clip 下的 2D 矩阵 kE 和向量 ky
├── gnsSolveByChol() // Cholesky 分解求 f(浮点)
├── calculateError() // 计算当前 MSE
└── [循环] 步长搜索 clip 最优组合
├── 对每个系数试探 clip ± step
├── 重新 gnsSolveByChol()
└── 接受最优 clip 更新
9.2 浮点到整数的量化
cpp
double EncAdaptiveLoopFilter::deriveCoeffQuant(...)
{
// 1. 浮点优化
cov.optimizeFilter(shape, filterClipp, filterCoeff, optimizeClip);
// 2. 四舍五入量化
roundFiltCoeff(filterCoeffQuant, filterCoeff, numCoeff, factor);
// 3. 范围钳制 [-factor+1, factor-1]
filterCoeffQuant[i] = std::min(maxValue, std::max(minValue, filterCoeffQuant[i]));
// 4. 贪心整数搜索(微调量化误差)
while (modified) {
// 对每个系数试探 ±1 步
// 使用增量误差公式 calcErrorForCoeffsDelta(避免全量重算)
}
}
9.3 增量误差公式(数学推导)
当系数 f m f_m fm 变化量为 δ \delta δ 时,误差增量为:
Δ J = δ ⋅ c A m + δ 2 ⋅ E m m − 2 δ ⋅ y m \Delta J = \delta \cdot c_Am + \delta^2 \cdot E_{mm} - 2\delta \cdot y_m ΔJ=δ⋅cAm+δ2⋅Emm−2δ⋅ym
代码实现:
cpp
double AlfCovariance::calcErrorForCoeffsDelta(
double cAc, double *cA, double bc,
const AlfClipIdx *clip, const AlfCoeff *coeff,
const int numCoeff, double cDelta, int modInd) const
{
double error = cAc - bc; // 当前基础误差
error += cDelta * cA[modInd]; // 一阶项
error += cDelta*cDelta * E(clip[modInd],clip[modInd],modInd,modInd); // 二阶项
error -= 2 * y(clip[modInd],modInd) * cDelta; // 互相关项
return error;
}
设计亮点 :增量公式 O ( 1 ) O(1) O(1) 复杂度,避免了每次都做 O ( N 2 ) O(N^2) O(N2) 的完整重算,使整数搜索过程高效可行。
10. 滤波器类合并:mergeClasses
10.1 背景
25 个分类对应 25 套滤波器系数,但实际图像中某些类的像素数很少,单独估计会过拟合。类合并将相似的类共享一套系数,降低码率同时维持性能。
10.2 贪心合并算法
初始化:numRemaining = 25,每类各自一套协方差
while numRemaining >= 2:
遍历所有 (i,j) 类对:
计算合并后的误差增量 Δerr = err(i+j) - err(i) - err(j)
选择 Δerr 最小的对 (i*, j*) 合并
更新协方差:covMerged[i*] += covMerged[j*]
记录每一步的分配方案 filterIndices[numFilters-1][classIdx]
保存从 1 到 25 组滤波器的所有分配方案
cpp
void EncAdaptiveLoopFilter::mergeClasses(...)
{
while (numRemaining >= 2) {
double errorMin = std::numeric_limits<double>::max();
// ...
tmpCov.add(covMerged[i], covMerged[j]);
double errorMerged = tmpCov.optimizeFilterClip(...);
double error = errorMerged - error1 - error2; // Δerr
if (error < errorMin) { ... }
// 合并最优对
covMerged[bestToMergeIdx1] += covMerged[bestToMergeIdx2];
}
}
10.3 最优滤波器组数的确定
mergeFiltersAndCost() 遍历 1~25 组的所有分配方案,结合码率 RDO 选择最优组数:
Cost(numFilters) = D(numFilters) + λ * R(numFilters)
其中 R 通过 getCostFilterCoeff() 和 lengthFilterCoeffs() 基于 CABAC 估计计算。
11. CTB 级决策:alfEncoderCtb 与 deriveCtbAlfEnableFlags
11.1 设计思路
ALF 可以 CTB(Coding Tree Block)为单位独立关闭,对于某些 CTB(如包含大量强边缘或纹理复杂的区域),ALF 可能会降低质量,关闭反而更优。
11.2 deriveCtbAlfEnableFlags 源码分析
cpp
double EncAdaptiveLoopFilter::deriveCtbAlfEnableFlags(
CodingStructure& cs, const int shapeIdx, ChannelType channel, ...)
{
for (int ctuIdx = 0; ctuIdx < m_numCTUsInPic; ctuIdx++) {
// 1. 计算开启 ALF 的代价(过滤失真 + 比特数)
double costOn = distortion_on + lambda * bits_on;
// 2. 计算关闭 ALF 的代价(未过滤失真 + 比特数)
double costOff = m_ctbDistortionUnfilter[comp][ctuIdx] + lambda * bits_off;
// 3. CTB 级 RDO 决策
m_modes[comp][ctuIdx] = (costOn < costOff) ? AlfMode::LUMA0 : AlfMode::OFF;
// 4. 更新全局失真
if (costOff < costOn) {
cost += costOff;
distUnfilter += m_ctbDistortionUnfilter[comp][ctuIdx];
} else {
cost += costOn;
}
}
}
11.3 alfEncoderCtb 的迭代过程
alfEncoderCtb 在 firstPass 基础上进一步迭代:
- 加载已有 APS(可能来自前几帧)
- 与新合成的滤波器做 RDO 比较
- 迭代更新 CTB 使能标志(偶数轮更新 CTB 决策,奇数轮重估系数)
- 最终选择代价最低的 APS 组合(最多 8 个 APS 叠加使用)
12. CC-ALF(跨分量 ALF)流程
12.1 基本原理
CC-ALF(Cross-Component ALF)利用亮度重建样本 来修正色度重建样本的误差:
C ^ ( x , y ) = C rec ( x , y ) + ∑ k h k ⋅ L rec ( x k , y k ) \hat{C}(x,y) = C_{\text{rec}}(x,y) + \sum_{k} h_k \cdot L_{\text{rec}}(x_k, y_k) C^(x,y)=Crec(x,y)+k∑hk⋅Lrec(xk,yk)
其中 h k h_k hk 是 CC-ALF 系数, L rec L_{\text{rec}} Lrec 是亮度重建像素邻域(1×4 形状,4 个系数)。
12.2 相关代码路径
deriveCcAlfFilter()
├── countLumaSwingGreaterThanThreshold() ← 判断是否限制 CC-ALF(高 QP 时跳过)
├── deriveStatsForCcAlfFiltering()
│ └── getBlkStatsCcAlf()
│ └── calcCovarianceCcAlf()
├── deriveCcAlfFilterCoeff() ← 最小二乘求解 h_k
├── determineControlIdcValues() ← CTB 级是否启用 CC-ALF
└── getCoeffRateCcAlf() ← 比特数估计
12.3 系数量化
CC-ALF 系数量化使用候选码本(CCALF_SMALL_TAB),而非均匀量化:
cpp
void EncAdaptiveLoopFilter::roundFiltCoeffCCALF(...)
{
for (int i = 0; i < numCoeff; i++) {
// 在预定义候选值集合中找最近邻
for (int k = 0; k < CCALF_CANDS_COEFF_NR; k++) {
double diff = val - CCALF_SMALL_TAB[k];
if (diff*diff < bestErr) { bestIndex = k; }
}
filterCoeffQuant[i] = CCALF_SMALL_TAB[bestIndex] * sign;
}
}
13. Cholesky 分解求解器详解
AlfCovariance 内嵌了一套精巧的 Cholesky 分解求解器,用于求解正定对称线性系统 E f = y \mathbf{E}\mathbf{f} = \mathbf{y} Ef=y。
13.1 Cholesky 分解
将矩阵 A \mathbf{A} A 分解为 A = R T R \mathbf{A} = \mathbf{R}^T\mathbf{R} A=RTR(其中 R \mathbf{R} R 为上三角矩阵):
R i i = A i i − ∑ k = 0 i − 1 R k i 2 R_{ii} = \sqrt{A_{ii} - \sum_{k=0}^{i-1} R_{ki}^2} Rii=Aii−k=0∑i−1Rki2
R i j = 1 R i i ( A i j − ∑ k = 0 i − 1 R k i R k j ) , j > i R_{ij} = \frac{1}{R_{ii}}\left(A_{ij} - \sum_{k=0}^{i-1}R_{ki}R_{kj}\right), \quad j > i Rij=Rii1(Aij−k=0∑i−1RkiRkj),j>i
cpp
int AlfCovariance::gnsCholeskyDec(TE inpMatr, TE outMatr, int numEq) const
{
for (int i = 0; i < numEq; i++) {
for (int j = i; j < numEq; j++) {
double scale = inpMatr[i][j];
for (int k = i-1; k >= 0; k--)
scale -= outMatr[k][j] * outMatr[k][i];
if (i == j) {
if (scale <= REG_SQR) return 0; // 奇异矩阵保护
invDiag[i] = 1.0 / (outMatr[i][i] = sqrt(scale));
} else {
outMatr[i][j] = scale * invDiag[i];
outMatr[j][i] = 0.0; // 下三角置零
}
}
}
return 1;
}
13.2 回代求解
前向回代(下三角)→ R^T * z = rhs gnsTransposeBacksubstitution()
后向回代(上三角)→ R * x = z gnsBacksubstitution()
13.3 数值稳定性保护
cpp
constexpr double REG_SQR = 0.0000001; // 奇异矩阵时加正则化
if (scale <= REG_SQR) return 0; // 返回失败,调用方退化处理
当某分类统计样本不足时,矩阵可能接近奇异,REG_SQR 防止除零和精度问题。
14. C++ 语言应用技巧精析
14.1 TempCtx ------ CABAC 上下文快照
cpp
const TempCtx ctxStart(m_ctxPool, AlfCtx(m_CABACEstimator->getCtx()));
TempCtx ctxBest(m_ctxPool);
// ...
ctxBest = AlfCtx(m_CABACEstimator->getCtx()); // 保存当前最优上下文
// ...
m_CABACEstimator->getCtx() = AlfCtx(ctxBest); // 恢复最优上下文
TempCtx 通过 RAII 管理 CABAC 上下文池,保存/恢复状态零拷贝(内部使用引用计数)。这是 VTM 中 RDO 试探的标准模式。
14.2 基于范围的 for + initializer_list
cpp
for (const auto chType: { ChannelType::LUMA, ChannelType::CHROMA }) {
// 统一处理亮度/色度,避免代码重复
}
for (bool nonLinearFlag: { false, true }) {
if (nonLinearFlag && !useNonlinearAlf) continue;
// ...
}
利用 std::initializer_list 惯用法替代重复的 if/else,代码简洁且性能零开销。
14.3 constexpr + 浮点比较的正确方式
cpp
inline double essentiallyEqual(double a, double b) {
constexpr double REL_EPSILON = 0x1p-20; // C++14 十六进制浮点字面量
constexpr double ABS_EPSILON = 0x1p-30;
return std::abs(a-b) < std::max(ABS_EPSILON, REL_EPSILON * std::max(std::abs(a), std::abs(b)));
}
标准的 ULP 感知浮点近似比较,同时处理近零值(绝对误差)和大数(相对误差)。
14.4 std::fill_n + std::copy_n 替代 memset/memcpy
cpp
// 初始化 clip 值(类型安全)
std::fill_n(m_alfClipMerged[shapeIdx][0][0],
MAX_NUM_ALF_LUMA_COEFF * MAX_NUM_ALF_CLASSES * MAX_NUM_ALF_CLASSES,
m_alfParamTemp.nonLinearFlag[channel] ? ALF_NUM_CLIP_VALS[ChannelType::LUMA] / 2 : 0);
// 复制 clip 索引
std::copy_n(clipMerged[numFilters-1][classIdx], MAX_NUM_ALF_LUMA_COEFF, m_filterClippSet[filtIdx]);
相比 memset/memcpy,类型安全且对非 POD 类型友好。
14.5 多级指针内存管理(三维 new/delete)
cpp
// 分配 m_alfCovariance[comp][shape][ctu][class]
m_alfCovariance[compIdx] = new AlfCovariance **[m_filterShapes[chType].size()];
for (int i = 0; i != shapes; i++) {
m_alfCovariance[compIdx][i] = new AlfCovariance*[m_numCTUsInPic];
for (int j = 0; j < m_numCTUsInPic; j++) {
m_alfCovariance[compIdx][i][j] = new AlfCovariance[numClasses];
for (int k = 0; k < numClasses; k++)
m_alfCovariance[compIdx][i][j][k].create(numCoeff, numBins);
}
}
注意 :VTM 使用传统裸指针+手动
destroy(),现代 C++ 可改用std::vector<std::vector<...>>或智能指针避免内存泄漏风险。
14.6 函数指针(成员函数指针)实现多态滤波
cpp
// 根据系数个数选择不同的 SIMD 滤波函数
m_filter7x7Blk(m_classifier, recBuf, buf, blkDst, blkSrc, COMPONENT_Y, coeff, clip, ...);
m_filter5x5Blk(...);
m_filter7x7Blk 是函数指针成员,在 create() 时根据平台特性(SSE4/AVX2/NEON)绑定对应的优化实现,是策略模式的高效 C 风格实现。
14.7 sgn2 符号函数与整数舍入
cpp
// roundFiltCoeff 中的符号保留量化
const int sign = sgn2(filterCoeff[i]); // 返回 +1 或 -1
const double val = filterCoeff[i] * alfStrength * sign;
filterCoeffQuant[i] = int(std::min(val, 2.0) * factor + 0.5) * sign;
先取绝对值处理,再乘回符号,是处理有符号量化的经典模式,避免了对负数四舍五入不对称的问题。
14.8 #if MAX_NUM_CC_ALF_FILTERS > 1 条件编译
cpp
#if MAX_NUM_CC_ALF_FILTERS > 1
struct FilterIdxCount { uint64_t count; uint8_t filterIdx; };
bool compareCounts(FilterIdxCount a, FilterIdxCount b) { return a.count > b.count; }
#endif
标准规范通常只有 1 个 CC-ALF 滤波器,> 1 的路径为扩展实验预留,条件编译零运行时开销。
15. 踩坑点与调试要点
15.1 虚拟边界处理遗漏
症状 :在 tile/slice 边界处出现竖/横纹,主观质量严重下降。
原因 :并行区域边界两侧的 ALF 必须独立处理,不能使用边界另一侧的像素。
定位 :检查 isCrossedByVirtualBoundaries 的返回值,以及 clipTopRow/clipBotRow 的赋值。
15.2 Cholesky 分解失败时的退化
症状 :特定场景(黑屏、静止画面)下画质异常或崩溃。
原因 :当某分类没有足够样本时,E 矩阵行列式接近 0,Cholesky 分解返回 0(失败)。
处理 :gnsSolveByChol() 失败时,VTM 将对应系数置零(零系数 = ALF 不生效),不会崩溃。
15.3 APS 复用导致的误差累积
症状 :长 GOP 后期帧画质明显变差。
原因 :过多复用旧 APS,没有为变化区域生成新滤波器。
调优 :通过 getMaxNumALFAPS() 控制最大 APS 数量,m_encCfg->getMaxNumALFAPS() = 0 可禁用 APS 复用(纯帧内 ALF)。
15.4 ALF 强度参数 ALFStrengthLuma
cpp
double alfStrength = m_encCfg->getALFStrengthLuma(); // 默认 1.0
ALF 强度参数 < 1.0 时,系数按比例缩小,相当于弱化滤波效果,对弱纹理内容(屏幕内容、图形)有时更优。误设为 0 则 ALF 完全失效。
15.5 CC-ALF 依赖亮度 ALF
cpp
if (!cs.slice->getAlfEnabledFlag(COMPONENT_Y)) {
m_ccAlfFilterParam.ccAlfFilterEnabled[compID - 1] = false;
return; // CC-ALF 强制依赖亮度 ALF
}
标准规定:亮度 ALF 关闭时,CC-ALF 也必须关闭,否则码流非法。
16. 性能热点与优化建议
16.1 热点分析
| 函数 | 计算复杂度 | 优化手段 |
|---|---|---|
calcCovariance() |
O ( N 2 ⋅ B 2 ⋅ pixels ) O(N^2 \cdot B^2 \cdot \text{pixels}) O(N2⋅B2⋅pixels) | SIMD AVX2/SSE4,循环展开 |
getBlkStats() |
O ( N 2 ⋅ B 2 ⋅ pixels ) O(N^2 \cdot B^2 \cdot \text{pixels}) O(N2⋅B2⋅pixels) | 与 calcCovariance 融合 |
gnsCholeskyDec() |
O ( N 3 ) O(N^3) O(N3),N=13 | 较小,无需 SIMD |
optimizeFilter() |
O ( N 2 ⋅ clip_iters ) O(N^2 \cdot \text{clip\_iters}) O(N2⋅clip_iters) | clip 搜索步长折半策略 |
mergeClasses() |
O ( C 2 ⋅ N 2 ) O(C^2 \cdot N^2) O(C2⋅N2),C=25 | 增量误差 O(1) 避免重算 |
16.2 内存访问优化
// E 张量的最优访问顺序(行主序)
for b0 -> for b1 -> for k -> for l // ✓ 缓存友好
// 不要
for k -> for l -> for b0 -> for b1 // ✗ 缓存不友好
getBlkStats 中的 getOffsetEfast 已经保证了这一点。
16.3 帧并行潜力
CTB 级统计量收集(getBlkStats)按 CTB 独立计算,天然支持行级并行(wave-front)。VTM 目前是单线程,实际编码器(如 x265-style)可按行并行加速。
17. 参考文献
- J. Chen et al., "Algorithm description for Versatile Video Coding and Test Model 16", JVET-W2002, 2022.
- H. Schwarz et al., "Adaptive Loop Filtering in the Versatile Video Coding Standard", IEEE T-CSVT, 2021.
- X. Zhao et al., "Joint separable and non-separable transforms with deep neural networks for image compression", IEEE TIP, 2019.
- ITU-T H.266, "Versatile Video Coding", ITU-T, 2020.
- P. Stoica & R. Moses, "Spectral Analysis of Signals", Prentice Hall, 2005. (Wiener 滤波理论基础)
- VTM 源码仓库:https://vcgit.hhi.fraunhofer.de/jvet/VVCSoftware_VTM