H266/VVC 量化编码中 TCQ(或 DQ)技术
网格编码量化 TCQ(或依赖量化 DQ)
-
网格编码量化(Trellis-Coded Quantization,TCQ) 最先在 1990 年论文 [Trellis coded quantization of memoryless and Gauss-Markov sources] 中提出,它可以看成非常高维的矢量量化。仅使用标量量化很难避免率失真损失,而这些失真却可以通过矢量量化来降低。无约束的矢量量化复杂度非常高,TCQ 作为其中的一个变种可以直接用于现在的视频编码框架且对熵编码方法影响非常小,TCQ 是依赖量化(Dependent Quantization,DQ)。在高维数据空间用 TCQ 代替传统的标量量化可以获取更高的堆积密度。TCQ 很好的结合了标量量化和矢量量化的优点,复杂度低并且具有优良的性能。 【~虽然有文献介绍叫 TCQ 技术,但实际在一些提案中或代码中 VTM 或 VVenC 中叫 DQ 技术~】
-
TCQ 的结构:从解码器的视角看,TCQ 需要两个标量量化器和一个有限状态机在不同量化器间进行切换。标量量化器允许的重建值即反量化后的值都是量化步长( △ ) 的整数倍,一个量化器是 △ 的偶数倍,另一个是 △ 的奇数倍,注意两个量化器都包含重建值为 0 的情况可以提高低码率下的压缩效率。如图 43 是 TCQ 的两个标量量化器 Q0 和 Q1,横轴下方的值都是重建值 t,是△ 的整数倍。实心和空心圆点下面的数字是量化后的值 q。
-
两个量化器间的状态转换可以用图 44 中有四个状态的有限状态机描述。变换系数的重建(反量化)顺序和其编码顺序一样。初始状态设 s0 (对应第一个变换系数 t0 )设为 0。每个状态 sk+1 都由其前一个状态 sk 和前一个反量化前的值 qk 的奇偶性 pk 决定(即 pk 表示 qk 是奇数还是偶数),如图 44 右侧的表所示。对于变换系数 tk ,当其状态 sk 是 0 或 1 时使用量化器 Q0,当其状态 sk 是 2 或 3 时使用量化器 Q1。状态转换过程隐式的将每个量化其的量化结果划分为两个子集,一个包含奇数一个包含偶数。如图 43 中 Q0 的结果分为 A 和 B,Q1 的 结果分为 C 和 D。当前的状态和所在的子集将决定下一个状态。
-
TCQ 的反量化过程:不同于标量量化可以对每个变换系数独立处理,TCQ 需要按编码顺序反量化每个系数(因为当前状态取决于上一个状态)。加上一个块熵解码后有 N 个系数 qk ,则反量化后变换系数 tk’ 的获取过程如下:
- 其中 sgn(.) 表示获取符号,△ 是量化步长,二维的状态转换表 stateTransTable 是图 44 右侧的表,(sk >>1) 表示使用哪个量化器,(qk &1) 可以获得 qk 的奇偶性 pk 。
-
在 2020 年 1 月的 JVET-Q2002[Algorithm description for Versatile Video Coding and Test Model 8 (VTM 8)] 提案中对 TCQ(或 DQ) 技术进行了总结描述。
-
在 VVenC 编码器中 DepQuant.cpp 文件中定义了
DepQuant
类来处理 TCQ(或叫 DQ 技术)。
/*================================================================================*/
/*===== =====*/
/*===== T C Q =====*/
/*===== =====*/
/*================================================================================*/
class DepQuant : private RateEstimator, public DepQuantImpl
{
public:
DepQuant( bool enc );
void quant( TransformUnit& tu, const CCoeffBuf& srcCoeff, const ComponentID compID, const QpParam& cQP, const double lambda, const Ctx& ctx, TCoeff& absSum, bool enableScalingLists, int* quantCoeff );
private:
void xDecideAndUpdate ( const TCoeff absCoeff, const ScanInfo& scanInfo, bool zeroOut, int quantCoeff);
void xDecide ( const ScanInfo& scanInfo, const TCoeff absCoeff, const int lastOffset, Decision* decisions, bool zeroOut, int quantCoeff );
private:
CommonCtx m_commonCtx;
State m_allStates[ 12 ];
State* m_currStates;
State* m_prevStates;
State* m_skipStates;
State m_startState;
Decision m_trellis[ MAX_TB_SIZEY * MAX_TB_SIZEY ][ 8 ];
Rom m_scansRom;
};
- 其中
quant
函数是实现量化的核心函数,其中有关于 TCQ(或 DQ )技术的逻辑实现。
void DepQuant::quant( TransformUnit& tu, const CCoeffBuf& srcCoeff, const ComponentID compID, const QpParam& cQP, const double lambda, const Ctx& ctx, TCoeff& absSum, bool enableScalingLists, int* quantCoeff )
{
//===== reset / pre-init =====
const TUParameters& tuPars = *m_scansRom.getTUPars( tu.blocks[compID], compID );
m_quant.initQuantBlock ( tu, compID, cQP, lambda );
TCoeffSig* qCoeff = tu.getCoeffs( compID ).buf;
const TCoeff* tCoeff = srcCoeff.buf;
const int numCoeff = tu.blocks[compID].area();
::memset( qCoeff, 0x00, numCoeff * sizeof( TCoeffSig ) );
absSum = 0;
const CompArea& area = tu.blocks[ compID ];
const uint32_t width = area.width;
const uint32_t height = area.height;
const uint32_t lfnstIdx = tu.cu->lfnstIdx;
//===== scaling matrix ====
//const int qpDQ = cQP.Qp + 1;
//const int qpPer = qpDQ / 6;
//const int qpRem = qpDQ - 6 * qpPer;
//TCoeff thresTmp = thres;
bool zeroOut = false;
bool zeroOutforThres = false;
int effWidth = tuPars.m_width, effHeight = tuPars.m_height;
if( ( tu.mtsIdx[compID] > MTS_SKIP || ( tu.cs->sps->MTS && tu.cu->sbtInfo != 0 && tuPars.m_height <= 32 && tuPars.m_width <= 32 ) ) && compID == COMP_Y )
{
effHeight = ( tuPars.m_height == 32 ) ? 16 : tuPars.m_height;
effWidth = ( tuPars.m_width == 32 ) ? 16 : tuPars.m_width;
zeroOut = ( effHeight < tuPars.m_height || effWidth < tuPars.m_width );
}
zeroOutforThres = zeroOut || ( 32 < tuPars.m_height || 32 < tuPars.m_width );
//===== find first test position =====
int firstTestPos = std::min<int>( tuPars.m_width, JVET_C0024_ZERO_OUT_TH ) * std::min<int>( tuPars.m_height, JVET_C0024_ZERO_OUT_TH ) - 1;
if( lfnstIdx > 0 && tu.mtsIdx[compID] != MTS_SKIP && width >= 4 && height >= 4 )
{
firstTestPos = ( ( width == 4 && height == 4 ) || ( width == 8 && height == 8 ) ) ? 7 : 15 ;
}
const TCoeff defaultQuantisationCoefficient = (TCoeff)m_quant.getQScale();
const TCoeff thres = m_quant.getLastThreshold();
const int zeroOutWidth = ( tuPars.m_width == 32 && zeroOut ) ? 16 : 32;
const int zeroOutHeight = ( tuPars.m_height == 32 && zeroOut ) ? 16 : 32;
if( enableScalingLists )
{
for( ; firstTestPos >= 0; firstTestPos-- )
{
if( zeroOutforThres && ( tuPars.m_scanId2BlkPos[firstTestPos].x >= zeroOutWidth || tuPars.m_scanId2BlkPos[firstTestPos].y >= zeroOutHeight ) ) continue;
const TCoeff thresTmp = TCoeff( thres / ( 4 * quantCoeff[tuPars.m_scanId2BlkPos[firstTestPos].idx] ) );
if( abs( tCoeff[tuPars.m_scanId2BlkPos[firstTestPos].idx] ) > thresTmp ) break;
}
}
else
{
const TCoeff defaultTh = TCoeff( thres / ( defaultQuantisationCoefficient << 2 ) );
for( ; firstTestPos >= 0; firstTestPos-- )
{
if( zeroOutforThres && ( tuPars.m_scanId2BlkPos[firstTestPos].x >= zeroOutWidth || tuPars.m_scanId2BlkPos[firstTestPos].y >= zeroOutHeight ) ) continue;
if( abs( tCoeff[tuPars.m_scanId2BlkPos[firstTestPos].idx] ) > defaultTh ) break;
}
}
if( firstTestPos < 0 )
{
tu.lastPos[compID] = -1;
return;
}
//===== real init =====
RateEstimator::initCtx( tuPars, tu, compID, ctx.getFracBitsAcess() );
m_commonCtx.reset( tuPars, *this );
for( int k = 0; k < 12; k++ )
{
m_allStates[k].init();
}
m_startState.init();
int effectWidth = std::min( 32, effWidth );
int effectHeight = std::min( 32, effHeight );
for (int k = 0; k < 12; k++)
{
m_allStates[k].effWidth = effectWidth;
m_allStates[k].effHeight = effectHeight;
}
m_startState.effWidth = effectWidth;
m_startState.effHeight = effectHeight;
//===== populate trellis =====
for( int scanIdx = firstTestPos; scanIdx >= 0; scanIdx-- )
{
const ScanInfo& scanInfo = tuPars.m_scanInfo[ scanIdx ];
if( enableScalingLists )
{
m_quant.initQuantBlock( tu, compID, cQP, lambda, quantCoeff[scanInfo.rasterPos] );
xDecideAndUpdate( abs( tCoeff[scanInfo.rasterPos] ), scanInfo, zeroOut && ( scanInfo.posX >= effWidth || scanInfo.posY >= effHeight ), quantCoeff[scanInfo.rasterPos] );
}
else
xDecideAndUpdate( abs( tCoeff[scanInfo.rasterPos] ), scanInfo, zeroOut && ( scanInfo.posX >= effWidth || scanInfo.posY >= effHeight ), defaultQuantisationCoefficient );
}
//===== find best path =====
Decision decision = { std::numeric_limits<int64_t>::max(), -1, -2 };
int64_t minPathCost = 0;
for( int8_t stateId = 0; stateId < 4; stateId++ )
{
int64_t pathCost = m_trellis[0][stateId].rdCost;
if( pathCost < minPathCost )
{
decision.prevId = stateId;
minPathCost = pathCost;
}
}
//===== backward scanning =====
int scanIdx = 0;
for( ; decision.prevId >= 0; scanIdx++ )
{
decision = m_trellis[ scanIdx ][ decision.prevId ];
int32_t blkpos = tuPars.m_scanId2BlkPos[scanIdx].idx;
qCoeff[ blkpos ] = TCoeffSig( tCoeff[ blkpos ] < 0 ? -decision.absLevel : decision.absLevel );
absSum += decision.absLevel;
}
tu.lastPos[compID] = scanIdx - 1;
}
}; // namespace DQIntern
- 其中
xDecideAndUpdate
函数是其中核心函数,通过该函数计算每个变换系数的state和量化系数。
void DepQuant::xDecideAndUpdate( const TCoeff absCoeff, const ScanInfo& scanInfo, bool zeroOut, int quantCoeff )
{
Decision* decisions = m_trellis[ scanInfo.scanIdx ];
std::swap( m_prevStates, m_currStates );
xDecide( scanInfo, absCoeff, lastOffset(scanInfo.scanIdx), decisions, zeroOut, quantCoeff );
if( scanInfo.scanIdx )
{
if( scanInfo.insidePos == 0 )
{
m_commonCtx.swap();
m_currStates[0].updateStateEOS( scanInfo, m_prevStates, m_skipStates, decisions[0] );
m_currStates[1].updateStateEOS( scanInfo, m_prevStates, m_skipStates, decisions[1] );
m_currStates[2].updateStateEOS( scanInfo, m_prevStates, m_skipStates, decisions[2] );
m_currStates[3].updateStateEOS( scanInfo, m_prevStates, m_skipStates, decisions[3] );
::memcpy( decisions+4, decisions, 4*sizeof(Decision) );
}
else if( !zeroOut )
{
m_currStates[0].updateState( scanInfo, m_prevStates, decisions[0] );
m_currStates[1].updateState( scanInfo, m_prevStates, decisions[1] );
m_currStates[2].updateState( scanInfo, m_prevStates, decisions[2] );
m_currStates[3].updateState( scanInfo, m_prevStates, decisions[3] );
}
if( scanInfo.spt == SCAN_SOCSBB )
{
std::swap( m_prevStates, m_skipStates );
}
}
}