3.5 残差解码
残差解码
作者:chai51
出处:https://www.cnblogs.com/chai51
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任
引言
残差解码(Residual Decoding)是AV1解码的最后一步,它负责解码变换系数、进行逆变换和逆量化,最终将残差与预测值相加得到重建图像。残差解码包括变换树解码、变换块解码、系数解码和重建等步骤,是AV1解码流程的关键环节。
源码说明: 本文档基于作者自己编写的AV1解码器Python实现,所有代码示例和实现细节均来自实际可运行的源码。源码仓库:GitHub - av1_learning
残差解码概述
基本概念
残差解码是在预测解码之后进行的步骤,主要工作包括:
- 分块处理:将块划分为chunk(64x64)进行处理
- 变换树解码:对于帧间块,递归解码变换树结构
- 变换块解码:解码每个变换块,包括预测、系数解码、重建
- 系数解码:解码变换系数(EOB、系数值、符号)
- 重建:逆量化、逆变换,与预测值相加
解码路径
残差解码根据块类型和条件选择不同的解码路径:
残差解码(__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))
关键概念
- Chunk:64x64像素的处理单元,大块会被划分为多个chunk
- 平面处理:分别处理亮度(Y)和色度(U、V)平面
- 变换树:帧间块可以使用变换树结构,递归划分变换块
- 变换块:基本的变换单元,进行预测、系数解码、重建
变换树解码
位置: 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)
变换树划分规则
- 判断条件:如果当前区域尺寸小于等于亮度变换尺寸,则不需要划分
- 水平划分:宽度大于高度时,水平划分为两个子区域
- 垂直划分:高度大于宽度时,垂直划分为两个子区域
- 四等分:宽度等于高度时,四等分为四个子区域
变换块解码
位置: 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
变换块解码流程
- 位置计算:计算变换块的起始位置和尺寸
- 边界检查:检查是否超出图像边界
- 帧内预测:如果是帧内块,进行帧内预测(调色板或标准帧内)
- 系数解码:如果不是skip,解码变换系数
- 重建:如果有残差(EOB > 0),进行逆变换和逆量化,与预测值相加
- 更新标志:更新解码标志和变换尺寸信息
系数解码
位置: 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
系数解码步骤
- all_zero检查:如果块全为零,直接返回
- 变换类型:解码变换类型(DCT、ADST、FLIPADST等)
- 扫描顺序:根据变换类型获取扫描顺序
- EOB解码:解码End of Block位置
- 系数值解码:从EOB-1到0逆序解码系数值
- 符号解码:解码每个非零系数的符号
- 超大系数:使用Golomb编码解码超大系数
- 上下文更新:更新上下文信息供后续块使用
残差解码流程图
关键数据结构
系数相关字段
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解码的最后一步,主要特点包括:
- 分块处理:使用chunk(64x64)为单位处理大块
- 变换树:帧间块支持递归变换树结构
- 系数解码:使用上下文自适应熵解码,支持多种编码方式
- 重建过程:逆量化、逆变换,与预测值相加
- 上下文管理:更新上下文信息供后续块使用
残差解码将编码的残差信息转换为像素值,与预测值结合得到最终的重建图像,是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()
- 残差解码:
上一篇: 模式信息解码

浙公网安备 33010602011771号