VVC/H.266 编码端自适应环路滤波器深度解析 —— EncAdaptiveLoopFilter.cpp 全面拆解

源文件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


目录

  1. [ALF 简介与 VVC 中的定位](#ALF 简介与 VVC 中的定位)
  2. [核心原理:Wiener 最优滤波](#核心原理:Wiener 最优滤波)
  3. [非线性 ALF:clipALF 机制](#非线性 ALF:clipALF 机制)
  4. 滤波器形状与分类机制
  5. [协方差矩阵 AlfCovariance 解析](#协方差矩阵 AlfCovariance 解析)
  6. 核心函数关系图
  7. [主流程 ALFProcess 源码拆解](#主流程 ALFProcess 源码拆解)
  8. [统计量收集:deriveStatsForFiltering / getBlkStats / calcCovariance](#统计量收集:deriveStatsForFiltering / getBlkStats / calcCovariance)
  9. [滤波器系数优化:deriveCoeffQuant + optimizeFilter](#滤波器系数优化:deriveCoeffQuant + optimizeFilter)
  10. 滤波器类合并:mergeClasses
  11. [CTB 级决策:alfEncoderCtb 与 deriveCtbAlfEnableFlags](#CTB 级决策:alfEncoderCtb 与 deriveCtbAlfEnableFlags)
  12. [CC-ALF(跨分量 ALF)流程](#CC-ALF(跨分量 ALF)流程)
  13. [Cholesky 分解求解器详解](#Cholesky 分解求解器详解)
  14. [C++ 语言应用技巧精析](#C++ 语言应用技巧精析)
  15. 踩坑点与调试要点
  16. 性能热点与优化建议
  17. 参考文献

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 的迭代过程

alfEncoderCtbfirstPass 基础上进一步迭代:

  1. 加载已有 APS(可能来自前几帧)
  2. 与新合成的滤波器做 RDO 比较
  3. 迭代更新 CTB 使能标志(偶数轮更新 CTB 决策,奇数轮重估系数)
  4. 最终选择代价最低的 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. 参考文献

  1. J. Chen et al., "Algorithm description for Versatile Video Coding and Test Model 16", JVET-W2002, 2022.
  2. H. Schwarz et al., "Adaptive Loop Filtering in the Versatile Video Coding Standard", IEEE T-CSVT, 2021.
  3. X. Zhao et al., "Joint separable and non-separable transforms with deep neural networks for image compression", IEEE TIP, 2019.
  4. ITU-T H.266, "Versatile Video Coding", ITU-T, 2020.
  5. P. Stoica & R. Moses, "Spectral Analysis of Signals", Prentice Hall, 2005. (Wiener 滤波理论基础)
  6. VTM 源码仓库https://vcgit.hhi.fraunhofer.de/jvet/VVCSoftware_VTM