3.5 残差解码

残差解码

作者:chai51
出处:https://www.cnblogs.com/chai51
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任

引言

残差解码(Residual Decoding)是AV1解码的最后一步,它负责解码变换系数、进行逆变换和逆量化,最终将残差与预测值相加得到重建图像。残差解码包括变换树解码、变换块解码、系数解码和重建等步骤,是AV1解码流程的关键环节。

源码说明: 本文档基于作者自己编写的AV1解码器Python实现,所有代码示例和实现细节均来自实际可运行的源码。源码仓库:GitHub - av1_learning


残差解码概述

基本概念

残差解码是在预测解码之后进行的步骤,主要工作包括:

  1. 分块处理:将块划分为chunk(64x64)进行处理
  2. 变换树解码:对于帧间块,递归解码变换树结构
  3. 变换块解码:解码每个变换块,包括预测、系数解码、重建
  4. 系数解码:解码变换系数(EOB、系数值、符号)
  5. 重建:逆量化、逆变换,与预测值相加

解码路径

残差解码根据块类型和条件选择不同的解码路径:

残差解码(__residual)
  → 计算chunk数量
  → 遍历每个chunk
    → 遍历每个平面(Y、U、V)
      → 判断解码方式:
        ├─> 帧间块且非无损:变换树解码(__transform_tree)
        └─> 其他:变换块解码(__transform_block)
          → 帧内预测(如果是帧内块)
          → 系数解码(__coeffs)
          → 重建(reconstruct)

残差解码主函数

位置: src/tile/tile_group.py - __residual()

规范文档: 5.11.34 Residual syntax

核心实现

def __residual(self, av1: AV1Decoder):
    """
    残差解码主函数
    规范文档 5.11.34 Residual syntax
    """
    seq_header = av1.seq_header
    frame_header = av1.frame_header
    tile_group = self.tile_group
    use_128x128_superblock = seq_header.use_128x128_superblock
    subsampling_x = seq_header.color_config.subsampling_x
    subsampling_y = seq_header.color_config.subsampling_y
    MiRows = frame_header.MiRows
    MiCols = frame_header.MiCols
    MiRow = tile_group.MiRow
    MiCol = tile_group.MiCol
    MiSize = tile_group.MiSize
    HasChroma = tile_group.HasChroma

    # 1. 计算Superblock掩码
    sbMask = 31 if use_128x128_superblock else 15

    # 2. 计算chunk数量(64x64块单位)
    widthChunks = max(1, Block_Width[MiSize] >> 6)
    heightChunks = max(1, Block_Height[MiSize] >> 6)

    # 3. 确定chunk尺寸
    miSizeChunk = SUB_SIZE.BLOCK_64X64 if (
        widthChunks > 1 or heightChunks > 1) else MiSize

    # 4. 遍历每个chunk
    for chunkY in range(heightChunks):
        for chunkX in range(widthChunks):
            miRowChunk = MiRow + (chunkY << 4)
            miColChunk = MiCol + (chunkX << 4)
            subBlockMiRow = miRowChunk & sbMask
            subBlockMiCol = miColChunk & sbMask

            # 5. 遍历每个平面(Y、U、V)
            for plane in range(1 + HasChroma * 2):
                # 6. 获取变换尺寸
                txSz = TX_SIZE.TX_4X4 if tile_group.Lossless else self.__get_tx_size(
                    av1, plane, tile_group.TxSize)
                stepX = Tx_Width[txSz] >> 2
                stepY = Tx_Height[txSz] >> 2
                
                # 7. 计算平面尺寸
                planeSz = get_plane_residual_size(av1, miSizeChunk, plane)
                num4x4W = Num_4x4_Blocks_Wide[planeSz]
                num4x4H = Num_4x4_Blocks_High[planeSz]
                subX = subsampling_x if plane > 0 else 0
                subY = subsampling_y if plane > 0 else 0
                baseX = (miColChunk >> subX) * MI_SIZE
                baseY = (miRowChunk >> subY) * MI_SIZE
                
                # 8. 根据条件选择解码方式
                if tile_group.is_inter and not tile_group.Lossless and not plane:
                    # 帧间块且非无损:使用变换树解码
                    self.__transform_tree(
                        av1, baseX, baseY, num4x4W * 4, num4x4H * 4)
                else:
                    # 其他情况:直接变换块解码
                    baseXBlock = (MiCol >> subX) * MI_SIZE
                    baseYBlock = (MiRow >> subY) * MI_SIZE
                    for y in range(0, num4x4H, stepY):
                        for x in range(0, num4x4W, stepX):
                            self.__transform_block(av1, plane, baseXBlock, baseYBlock, txSz,
                                                   x + ((chunkX << 4) >> subX),
                                                   y + ((chunkY << 4) >> subY))

关键概念

  1. Chunk:64x64像素的处理单元,大块会被划分为多个chunk
  2. 平面处理:分别处理亮度(Y)和色度(U、V)平面
  3. 变换树:帧间块可以使用变换树结构,递归划分变换块
  4. 变换块:基本的变换单元,进行预测、系数解码、重建

变换树解码

位置: src/tile/tile_group.py - __transform_tree()

规范文档: 5.11.36 Transform tree syntax

功能说明

变换树解码用于帧间块,允许递归划分变换块,提供更灵活的变换尺寸选择。

核心实现

def __transform_tree(self, av1: AV1Decoder, startX: int, startY: int, w: int, h: int):
    """
    变换树解码
    规范文档 5.11.36 Transform tree syntax
    """
    frame_header = av1.frame_header
    tile_group = self.tile_group
    MiRows = frame_header.MiRows
    MiCols = frame_header.MiCols

    # 1. 边界检查
    maxX = MiCols * MI_SIZE
    maxY = MiRows * MI_SIZE
    if startX >= maxX or startY >= maxY:
        return

    # 2. 获取亮度变换尺寸
    row = startY >> MI_SIZE_LOG2
    col = startX >> MI_SIZE_LOG2
    lumaTxSz = tile_group.InterTxSizes[row][col]
    lumaW = Tx_Width[lumaTxSz]
    lumaH = Tx_Height[lumaTxSz]
    
    # 3. 判断是否需要递归划分
    if w <= lumaW and h <= lumaH:
        # 不需要划分,直接解码变换块
        from utils.tile_utils import find_tx_size
        txSz = find_tx_size(w, h)
        self.__transform_block(av1, 0, startX, startY, txSz, 0, 0)
    else:
        # 需要递归划分
        if w > h:
            # 水平划分
            self.__transform_tree(av1, startX, startY, w // 2, h)
            self.__transform_tree(av1, startX + w // 2, startY, w // 2, h)
        elif w < h:
            # 垂直划分
            self.__transform_tree(av1, startX, startY, w, h // 2)
            self.__transform_tree(av1, startX, startY + h // 2, w, h // 2)
        else:
            # 四等分
            self.__transform_tree(av1, startX, startY, w // 2, h // 2)
            self.__transform_tree(av1, startX + w // 2, startY, w // 2, h // 2)
            self.__transform_tree(av1, startX, startY + h // 2, w // 2, h // 2)
            self.__transform_tree(av1, startX + w // 2, startY + h // 2, w // 2, h // 2)

变换树划分规则

  1. 判断条件:如果当前区域尺寸小于等于亮度变换尺寸,则不需要划分
  2. 水平划分:宽度大于高度时,水平划分为两个子区域
  3. 垂直划分:高度大于宽度时,垂直划分为两个子区域
  4. 四等分:宽度等于高度时,四等分为四个子区域

变换块解码

位置: src/tile/tile_group.py - __transform_block()

规范文档: 5.11.35 Transform block syntax

核心实现

def __transform_block(self, av1: AV1Decoder, plane: int, baseX: int, baseY: int, txSz: TX_SIZE, x: int, y: int):
    """
    变换块解码
    规范文档 5.11.35 Transform block syntax
    """
    seq_header = av1.seq_header
    frame_header = av1.frame_header
    tile_group = self.tile_group
    use_128x128_superblock = seq_header.use_128x128_superblock
    subsampling_x = seq_header.color_config.subsampling_x
    subsampling_y = seq_header.color_config.subsampling_y
    MiRows = frame_header.MiRows
    MiCols = frame_header.MiCols
    AvailU = tile_group.AvailU
    AvailL = tile_group.AvailL

    # 1. 计算起始位置
    startX = baseX + 4 * x
    startY = baseY + 4 * y
    subX = subsampling_x if plane > 0 else 0
    subY = subsampling_y if plane > 0 else 0
    row = (startY << subY) >> MI_SIZE_LOG2
    col = (startX << subX) >> MI_SIZE_LOG2
    
    # 2. 边界检查
    maxX = (MiCols * MI_SIZE) >> subX
    maxY = (MiRows * MI_SIZE) >> subY
    if startX >= maxX or startY >= maxY:
        return

    # 3. 帧内预测(如果是帧内块)
    if not tile_group.is_inter:
        if ((plane == 0 and tile_group.PaletteSizeY) or (plane != 0 and tile_group.PaletteSizeUV)):
            # 调色板预测
            from frame.decoding_process import predict_palette
            predict_palette(av1, plane, startX, startY, x, y, txSz)
        else:
            # 标准帧内预测
            isCfl = (plane > 0 and tile_group.UVMode == Y_MODE.UV_CFL_PRED)
            if plane == 0:
                mode: Y_MODE = tile_group.YMode
            else:
                mode = Y_MODE.DC_PRED if isCfl else tile_group.UVMode

            log2W = Tx_Width_Log2[txSz]
            log2H = Tx_Height_Log2[txSz]
            from frame.decoding_process import predict_intra
            predict_intra(av1, plane, startX, startY,
                          (AvailL if plane == 0 else av1.tile_group.AvailLChroma) or x > 0,
                          (AvailU if plane == 0 else av1.tile_group.AvailUChroma) or y > 0,
                          tile_group.BlockDecoded[plane][...],
                          mode, log2W, log2H)
            if isCfl:
                # CFL预测
                from frame.decoding_process import predict_chroma_from_luma
                predict_chroma_from_luma(av1, plane, startX, startY, txSz)

    # 4. 系数解码和重建(如果不是skip)
    if not tile_group.skip:
        eob = self.__coeffs(av1, plane, startX, startY, txSz)
        if eob > 0:
            # 有残差,进行重建
            from frame.decoding_process import reconstruct
            reconstruct(av1, plane, startX, startY, txSz)

    # 5. 更新解码标志
    for i in range(stepY):
        for j in range(stepX):
            tile_group.LoopfilterTxSizes[plane][(row >> subY) + i][(col >> subX) + j] = txSz
            tile_group.BlockDecoded[plane][(subBlockMiRow >> subY) + i][(subBlockMiCol >> subX) + j] = 1

变换块解码流程

  1. 位置计算:计算变换块的起始位置和尺寸
  2. 边界检查:检查是否超出图像边界
  3. 帧内预测:如果是帧内块,进行帧内预测(调色板或标准帧内)
  4. 系数解码:如果不是skip,解码变换系数
  5. 重建:如果有残差(EOB > 0),进行逆变换和逆量化,与预测值相加
  6. 更新标志:更新解码标志和变换尺寸信息

系数解码

位置: src/tile/tile_group.py - __coeffs()

规范文档: 5.11.39 Coefficients syntax

核心实现

系数解码是残差解码的核心,包括EOB解码、系数值解码、符号解码等:

def __coeffs(self, av1: AV1Decoder,
             plane: int, startX: int, startY: int, txSz: TX_SIZE) -> int:
    """
    系数解码
    规范文档 5.11.39 Coefficients syntax
    
    Returns:
        EOB值(End of Block)
    """
    tile_group = self.tile_group

    # 1. 计算位置和尺寸
    x4 = startX >> 2
    y4 = startY >> 2
    w4 = Tx_Width[txSz] >> 2
    h4 = Tx_Height[txSz] >> 2
    txSzCtx = (Tx_Size_Sqr[txSz] + Tx_Size_Sqr_Up[txSz] + 1) >> 1
    ptype = plane > 0

    # 2. 检查all_zero标志
    all_zero = read_S(av1, 'all_zero', plane=plane, txSz=txSz,
                      txSzCtx=txSzCtx, x4=x4, y4=y4, w4=w4, h4=h4)
    if all_zero:
        # 全零块,直接返回
        if plane == 0:
            for i in range(w4):
                for j in range(h4):
                    tile_group.TxTypes[y4 + j][x4 + i] = DCT_DCT
        return 0

    # 3. 变换类型解码(仅亮度)
    if plane == 0:
        self.__transform_type(av1, x4, y4, txSz)

    # 4. 获取扫描顺序
    tile_group.PlaneTxType = compute_tx_type(av1, plane, txSz, x4, y4)
    scan = self.__get_scan(av1, txSz)

    # 5. EOB解码
    eobMultisize = min(Tx_Width_Log2[txSz], 5) + min(Tx_Height_Log2[txSz], 5) - 4
    # 根据eobMultisize选择不同的EOB解码方式
    if eobMultisize == 0:
        eob_pt_16 = read_S(av1, 'eob_pt_16', ...)
        eobPt = eob_pt_16 + 1
    # ... 其他尺寸的EOB解码 ...
    
    # 计算实际EOB值
    eob = eobPt if eobPt < 2 else ((1 << (eobPt - 2)) + 1)
    # EOB扩展位解码
    if eobShift >= 0:
        eob_extra = read_S(av1, 'eob_extra', ...)
        if eob_extra:
            eob += (1 << eobShift)
        # ... 更多扩展位 ...

    # 6. 系数值解码(从EOB-1到0,逆序)
    for c in range(eob - 1, -1, -1):
        pos = scan[c]
        if c == (eob - 1):
            # EOB位置的系数
            coeff_base_eob = read_S(av1, 'coeff_base_eob', ...)
            level = coeff_base_eob + 1
        else:
            # 其他位置的系数
            coeff_base = read_S(av1, 'coeff_base', ...)
            level = coeff_base

        # 大系数范围解码
        if level > NUM_BASE_LEVELS:
            for idx in range(COEFF_BASE_RANGE // (BR_CDF_SIZE - 1)):
                coeff_br = read_S(av1, 'coeff_br', ...)
                level += coeff_br
                if coeff_br < (BR_CDF_SIZE - 1):
                    break

        tile_group.Quant[pos] = level

    # 7. 符号解码
    for c in range(eob):
        pos = scan[c]
        if tile_group.Quant[pos] != 0:
            if c == 0:
                # DC系数符号
                dc_sign = read_S(av1, 'dc_sign', ...)
                sign = dc_sign
            else:
                # AC系数符号
                sign_bit = read_L(av1, 1)
                sign = sign_bit
        else:
            sign = 0

        # 超大系数Golomb解码
        if tile_group.Quant[pos] > (NUM_BASE_LEVELS + COEFF_BASE_RANGE):
            # Golomb编码解码
            length = 0
            while True:
                length += 1
                golomb_length_bit = read_L(av1, 1)
                if golomb_length_bit:
                    break
            # ... Golomb数据解码 ...
            tile_group.Quant[pos] = (x + COEFF_BASE_RANGE + NUM_BASE_LEVELS)

        # 应用符号
        if sign:
            tile_group.Quant[pos] = -tile_group.Quant[pos]

    # 8. 更新上下文
    culLevel = min(63, culLevel)
    for i in range(w4):
        tile_group.AboveLevelContext[plane][x4 + i] = culLevel
        tile_group.AboveDcContext[plane][x4 + i] = dcCategory
    for i in range(h4):
        tile_group.LeftLevelContext[plane][y4 + i] = culLevel
        tile_group.LeftDcContext[plane][y4 + i] = dcCategory

    return eob

系数解码步骤

  1. all_zero检查:如果块全为零,直接返回
  2. 变换类型:解码变换类型(DCT、ADST、FLIPADST等)
  3. 扫描顺序:根据变换类型获取扫描顺序
  4. EOB解码:解码End of Block位置
  5. 系数值解码:从EOB-1到0逆序解码系数值
  6. 符号解码:解码每个非零系数的符号
  7. 超大系数:使用Golomb编码解码超大系数
  8. 上下文更新:更新上下文信息供后续块使用

残差解码流程图

graph TD A[开始残差解码<br/>__residual] --> B[计算chunk数量<br/>widthChunks heightChunks] B --> C[遍历chunkY] C --> D[遍历chunkX] D --> E[遍历平面plane] E --> F{帧间块且非无损?} F -->|是| G[变换树解码<br/>__transform_tree] F -->|否| H[变换块解码<br/>__transform_block] G --> G1{需要划分?} G1 -->|否| H G1 -->|是| G2[递归划分] G2 --> G H --> H1{帧内块?} H1 -->|是| H2[帧内预测<br/>predict_intra/palette] H1 -->|否| H3[使用已计算的预测] H2 --> H4{Skip?} H3 --> H4 H4 -->|否| I[系数解码<br/>__coeffs] H4 -->|是| J[跳过系数解码] I --> I1[all_zero检查] I1 -->|是| J I1 -->|否| I2[变换类型解码] I2 --> I3[EOB解码] I3 --> I4[系数值解码] I4 --> I5[符号解码] I5 --> I6{EOB > 0?} I6 -->|是| K[重建<br/>reconstruct] I6 -->|否| J K --> L[更新解码标志] J --> L L --> M{还有平面?} M -->|是| E M -->|否| N{还有chunk?} N -->|是| D N -->|否| O[残差解码完成] style A fill:#e1f5ff style F fill:#fff9c4 style I6 fill:#fff9c4 style O fill:#c8e6c9

关键数据结构

系数相关字段

class TileGroup:
    # 量化系数
    Quant: List[int]              # 量化后的系数值
    Dequant: List[List[int]]      # 逆量化查找表
    
    # 变换信息
    TxSize: TX_SIZE               # 变换尺寸
    InterTxSizes: List[List]      # 帧间变换尺寸
    TxTypes: List[List]           # 变换类型
    PlaneTxType: int              # 平面变换类型
    
    # 上下文
    AboveLevelContext: List[List] # 上方块级别上下文
    AboveDcContext: List[List]    # 上方块DC上下文
    LeftLevelContext: List[List]  # 左侧块级别上下文
    LeftDcContext: List[List]     # 左侧块DC上下文
    
    # 解码标志
    BlockDecoded: List[List[List]] # 块解码标志
    LoopfilterTxSizes: List[List[List]] # 环路滤波变换尺寸

与解码流程的集成

残差解码在解码流程中的位置:

块划分解码
  → 模式信息解码
  → 调色板语法解码
  → 变换尺寸解码
  → 计算预测(__compute_prediction)
    → Inter-Intra预测
    → 帧间预测
  → 残差解码(__residual)
    → 变换树/变换块解码
      → 帧内预测(如果是帧内块)
      → 系数解码
      → 重建
  → 更新块信息

总结

残差解码是AV1解码的最后一步,主要特点包括:

  1. 分块处理:使用chunk(64x64)为单位处理大块
  2. 变换树:帧间块支持递归变换树结构
  3. 系数解码:使用上下文自适应熵解码,支持多种编码方式
  4. 重建过程:逆量化、逆变换,与预测值相加
  5. 上下文管理:更新上下文信息供后续块使用

残差解码将编码的残差信息转换为像素值,与预测值结合得到最终的重建图像,是AV1解码流程的关键环节。


参考资源:

  • AV1规范文档
  • 源码实现: GitHub - av1_learning
    • 残差解码: src/tile/tile_group.py - __residual()
    • 变换树: src/tile/tile_group.py - __transform_tree()
    • 变换块: src/tile/tile_group.py - __transform_block()
    • 系数解码: src/tile/tile_group.py - __coeffs()

上一篇: 模式信息解码

posted @ 2026-01-24 09:38  chai51  阅读(1)  评论(0)    收藏  举报