x265对语法元素cu_qp_delta_abs的编码(前缀部分)

HEVC所使用的熵编码方法为CABAC(基于上下文的自适应二进制算术编码),大致分为三个步骤:

1、二值化

2、上下文建模(模型初始化、更新)

3、二进制算术编码    

对该语法元素的编码是在codeDeltaQP函数中完成的。

void Entropy::codeDeltaQP(const CUData& cu, uint32_t absPartIdx)
{
    int dqp = cu.m_qp[absPartIdx] - cu.getRefQP(absPartIdx);

    int qpBdOffsetY = QP_BD_OFFSET;

    dqp = (dqp + 78 + qpBdOffsetY + (qpBdOffsetY / 2)) % (52 + qpBdOffsetY) - 26 - (qpBdOffsetY / 2);

    uint32_t absDQp = (uint32_t)((dqp > 0) ? dqp  : (-dqp));//语法元素cu_qp_delta_abs的值
    uint32_t TUValue = X265_MIN((int)absDQp, CU_DQP_TU_CMAX);//语法元素的前缀值
    writeUnaryMaxSymbol(TUValue, &m_contextState[OFF_DELTA_QP_CTX], 1, CU_DQP_TU_CMAX);//使用截断一元码(TU)二元化方法将前缀转换为二元码,在对每一位比特进行常规编码。
    if (absDQp >= CU_DQP_TU_CMAX)
        writeEpExGolomb(absDQp - CU_DQP_TU_CMAX, CU_DQP_EG_k);使用EGK二元化方法将后缀转换为二元码,在对每一位比特进行旁路编码。

    if (absDQp > 0)
    {
        uint32_t sign = (dqp > 0 ? 0 : 1);
        encodeBinEP(sign);对符号进行旁路编码
    }
}

一、二值化

根据HEVC的标准,cu_qp_delta_abs语法元素的前缀部分的值为:prefixVal=Min(cu_qp_delta_abs,5)。并且对prefixVal的编码使用截断莱斯二进制化方法(TR),TR方法的两个参数cMax=5、cRiceParam = 0。x265中将cu_DQP_TU_CMAX(cMAX)和TUValue(prefixVal)两个参数传递给writeUnaryMaxSymbol函数并在其中完成二值化,该函数也同时对其进行编码,这之后会讲。

void Entropy::writeUnaryMaxSymbol(uint32_t symbol, uint8_t* scmModel, int offset, uint32_t maxSymbol)
{
    X265_CHECK(maxSymbol > 0, "maxSymbol too small\n");

    encodeBin(symbol ? 1 : 0, scmModel[0]);

    if (!symbol)
        return;

    bool bCodeLast = (maxSymbol > symbol);

    while (--symbol)
        encodeBin(1, scmModel[offset]);

    if (bCodeLast)
        encodeBin(0, scmModel[offset]);
}

该函数中symbol为语法元素值,maxSymbol为门限值cMAX,莱斯参数R(cRiceParam)=0,其他参数先不管。TR分为前缀码和后缀码,两者分开求取。

前缀值P=V>>R,所以P=symbol,第一种情况:若P小于值(cMax>>R)=maxSymbol,则前缀码由P个‘1’和一个‘0’组成,当symbol为0时,P等于0,所以前缀就只有一个0,而p不等于0时,前缀码的第一个值就为1,所以对第一位进行编码的encodeBin函数的第一个参数就可以确定,剩余的‘1’在while循环中编码完成,因为循环条件为(--symbol)所以会编码symbol-1个‘1’。到此也就完成了P个‘1’的编码,且symbol<maxSymbol,bCodeLast=1,满足最后一个if条件,编码最后一个‘0’。

第二种情况:P大于等于值(cMax>>R)时,前缀码由maxSymbol个‘1’组成,由第一个编码函数加上while中的编码函数完成。

后缀值S=V-(P<<R),说明S始终为零,而后缀码为S的二元化串,无后缀码。

二、上下文模型

1、初始化

codeDeltaQP传递给writeUnaryMaxSymbol的两个参数:

&entropy->m_contextState[OFF_DELTA_QP_CTX](最大概率符号MPS、概率状态索引)和1(初始化类型)

确定了对该语法元素编码的上下文模型。下面讲解对上下文的初始化:

首先先给出标准中对该步骤的描述

InitType

0

1

2

initVariable

0..1

2..3

4..5


initVariable

0

1

2

3

4

5

initValue

154

154

154

154

154

154


我们最终所要的就是initValue,初始化的函数为:
initBuffer(&m_contextState[OFF_DELTA_QP_CTX], sliceType, qp, (uint8_t*)INIT_DQP, NUM_DELTA_QP_CTX);

static void initBuffer(uint8_t* contextModel, SliceType sliceType, int qp, uint8_t* ctxModel, int size)
{
    ctxModel += sliceType * size;

    for (int n = 0; n < size; n++)
        contextModel[n] = sbacInit(qp, ctxModel[n]);
}

先看initBuffer函数的参数列表,size=NUM_DELTA_QP_CTX=3,表示上下文模型个数,根据上面的表格可知,实际上该语法元素需要6个上下模型,但因为initValue的值都相同,初始化出来的模型也都相同,也就不需要initVariable的中转,可以直接与initType对应。sliceType就代表initType

typedef enum SliceType
{
    B_SLICE,
    P_SLICE,
    I_SLICE
}SliceType;
INIT_DQP=ctxModel存储initValue
static const uint8_t INIT_DQP[3][NUM_DELTA_QP_CTX] =
{
    { 154,  154,  154, },
    { 154,  154,  154, },
    { 154,  154,  154, },
};

contextModel就是存储地址,qp为片层亮度量化参数,初始化算法中会使用。将initValue和qp传给sbacInit实现初始化。

uint8_t sbacInit(int qp, int initValue)
{
    qp = x265_clip3(QP_MIN, QP_MAX_SPEC, qp);

    int  slope      = (initValue >> 4) * 5 - 45;
    int  offset     = ((initValue & 15) << 3) - 16;
    int  initState  =  X265_MIN(X265_MAX(1, (((slope * qp) >> 4) + offset)), 126);
    uint32_t mpState = (initState >= 64);
    uint32_t state = ((mpState ? (initState - 64) : (63 - initState)) << 1) + mpState;

    return (uint8_t)state;
}

初始变量计算方法(标准中已给出),mpState?(initState-64):(63-initState)得到概率状态索引,mpState为最大概率符号,最后将概率状态索引左移一位,末尾赋值最大概率符号,state同时存储两个参数。

contextModel[]=m_contextState[]数组内存储的就是state,也就是上下文模型。

2、更新

writeUnaryMaxSymbol函数中的encodeBin函数就是实现对二进制值得编码,第一个参数就是二进制值,第二个参数就是所使用的上下文模型,其参数offset表示在确定initType中的偏移量,第一个二进制位比较特殊需要与后几位想区分,使用第一个上下文模型,而后几位就使用offset所确定的上下文模型。

void Entropy::encodeBin(uint32_t binValue, uint8_t &ctxModel)
{
    uint32_t mstate = ctxModel;

    ctxModel = sbacNext(mstate, binValue);

    if (!m_bitIf)
    {
        m_fracBits += sbacGetEntropyBits(mstate, binValue);
        return;//对该位编码采用熵编码估计值(即以查表的方式)而不是直接进行编码,从而在不增加码率的条件下增加编码速度。
    }

    uint32_t range = m_range;
    uint32_t state = sbacGetState(mstate);
    uint32_t lps = g_lpsTable[state][((uint8_t)range >> 6)];
    range -= lps;

    X265_CHECK(lps >= 2, "lps is too small\n");

    int numBits = (uint32_t)(range - 256) >> 31;
    uint32_t low = m_low;

    // NOTE: MPS must be LOWEST bit in mstate
    X265_CHECK((uint32_t)((binValue ^ mstate) & 1) == (uint32_t)(binValue != sbacGetMps(mstate)), "binValue failure\n");
    if ((binValue ^ mstate) & 1)
    {
        // NOTE: lps is non-zero and the maximum of idx is 8 because lps less than 256
        //numBits = g_renormTable[lps >> 3];
        unsigned long idx;
        idx=CLZ(lps);
        X265_CHECK(state != 63 || idx == 1, "state failure\n");

        numBits = 8 - idx;
        if (state >= 63)
            numBits = 6;
        X265_CHECK(numBits <= 6, "numBits failure\n");

        low += range;
        range = lps;
    }
    m_low = (low << numBits);
    m_range = (range << numBits);
    m_bitsLeft += numBits;

    if (m_bitsLeft >= 0)
        writeOut();
}
函数中是使用sbacNext通过查表的方法对模型进行更新的。实际上就是标准中所给的概率更新表,只是根据x265中的实际情况进行改写,就是考虑到mstate最后一位为MPS。
const uint8_t g_nextState[128][2] =
{
    { 2, 1 }, { 0, 3 }, { 4, 0 }, { 1, 5 }, { 6, 2 }, { 3, 7 }, { 8, 4 }, { 5, 9 },
    { 10, 4 }, { 5, 11 }, { 12, 8 }, { 9, 13 }, { 14, 8 }, { 9, 15 }, { 16, 10 }, { 11, 17 },
    { 18, 12 }, { 13, 19 }, { 20, 14 }, { 15, 21 }, { 22, 16 }, { 17, 23 }, { 24, 18 }, { 19, 25 },
    { 26, 18 }, { 19, 27 }, { 28, 22 }, { 23, 29 }, { 30, 22 }, { 23, 31 }, { 32, 24 }, { 25, 33 },
    { 34, 26 }, { 27, 35 }, { 36, 26 }, { 27, 37 }, { 38, 30 }, { 31, 39 }, { 40, 30 }, { 31, 41 },
    { 42, 32 }, { 33, 43 }, { 44, 32 }, { 33, 45 }, { 46, 36 }, { 37, 47 }, { 48, 36 }, { 37, 49 },
    { 50, 38 }, { 39, 51 }, { 52, 38 }, { 39, 53 }, { 54, 42 }, { 43, 55 }, { 56, 42 }, { 43, 57 },
    { 58, 44 }, { 45, 59 }, { 60, 44 }, { 45, 61 }, { 62, 46 }, { 47, 63 }, { 64, 48 }, { 49, 65 },
    { 66, 48 }, { 49, 67 }, { 68, 50 }, { 51, 69 }, { 70, 52 }, { 53, 71 }, { 72, 52 }, { 53, 73 },
    { 74, 54 }, { 55, 75 }, { 76, 54 }, { 55, 77 }, { 78, 56 }, { 57, 79 }, { 80, 58 }, { 59, 81 },
    { 82, 58 }, { 59, 83 }, { 84, 60 }, { 61, 85 }, { 86, 60 }, { 61, 87 }, { 88, 60 }, { 61, 89 },
    { 90, 62 }, { 63, 91 }, { 92, 64 }, { 65, 93 }, { 94, 64 }, { 65, 95 }, { 96, 66 }, { 67, 97 },
    { 98, 66 }, { 67, 99 }, { 100, 66 }, { 67, 101 }, { 102, 68 }, { 69, 103 }, { 104, 68 }, { 69, 105 },
    { 106, 70 }, { 71, 107 }, { 108, 70 }, { 71, 109 }, { 110, 70 }, { 71, 111 }, { 112, 72 }, { 73, 113 },
    { 114, 72 }, { 73, 115 }, { 116, 72 }, { 73, 117 }, { 118, 74 }, { 75, 119 }, { 120, 74 }, { 75, 121 },
    { 122, 74 }, { 75, 123 }, { 124, 76 }, { 77, 125 }, { 124, 76 }, { 77, 125 }, { 126, 126 }, { 127, 127 }
};

三、二进制算术编码

encodeBin后续完成的就是算术编码,取出当前的编码区间m_range和区间下限m_low,sbacGetState函数将mstate右移一位得到state概率状态索引,通过取出m_range的后8位(unit8)&3得到计算索引值,与state仍通过查编码区间索引表的方法确定出当前的最小概率符号区间lps,x265中直接计算range=range-lps得到最大概率区间。

变量numBits表示的是重归一化过程中输出的码元数。重归一化流程图如图所示

                     

numBits第一次被赋的值为(range - 256) >> 31,range为32位,取出符号位,也就得到range是否大于256,。该numBits是在当前二进制值等于MPS的情况下使用的,而其取值为什么只能为‘0’和‘1’,是因为在

编码区间索引表中相邻概率状态索引值所对应的lps取值相近,表明当前m_range减去lps若小于256,只要左移一位就可以使其值大于256。

函数的最后根据所得的numBits,实现部分的归一化操作,并将numBits存入全局变量中。

if语句中实现的是二进制值等于(最小概率符号)LPS的情况,if中的条件就是Binvalue和mstate最后一位的值不相同时执行。

CLZ函数得到lps二进制串最高位‘1’的位置。

int clz(unsigned int a)
{
	int i,count=0;
	for(i=0;i<32;i++)
	{
		 if(((a<<i)&0x80000000)!=0)
					break;
		 count++;
	}
	return count^31;
}

通过index的值就可以得到lps需要左移几位可以大于256。numBits = 8 - idx该式成立,2的8次就是256。之后的if语句是当state大于等于63时,lps始终为2,而2左移6位刚好等于256,numBits = 6成立。对range和low做调整L=L+RMPS    R=RLPS。最后仍然是实现部分重归一化。

writeOut函数完成剩余的重归一化工作,并完成流程图中输出‘0’和输出‘1’的步骤。

void Entropy::writeOut()
{
    uint32_t leadByte = m_low >> (13 + m_bitsLeft);
    uint32_t low_mask = (uint32_t)(~0) >> (11 + 8 - m_bitsLeft);

    m_bitsLeft -= 8;
    m_low &= low_mask;

    if (leadByte == 0xff)
        m_numBufferedBytes++;
    else
    {
        uint32_t numBufferedBytes = m_numBufferedBytes;
        if (numBufferedBytes > 0)
        {
            uint32_t carry = leadByte >> 8;
            uint32_t byteTowrite = m_bufferedByte + carry;
            m_bitIf->writeByte(byteTowrite);

            byteTowrite = (0xff + carry) & 0xff;
            while (numBufferedBytes > 1)
            {
                m_bitIf->writeByte(byteTowrite);
                numBufferedBytes--;
            }
        }
        m_numBufferedBytes = 1;
        m_bufferedByte = (uint8_t)leadByte;
    }
}

该函数实现流程图中根据不同情况对L进行调整的步骤,并根据L的值决定输出的是‘0’还是‘1’。但这里使用的是相当复杂的算法,最终以字节为单位将编码下限m_low中的高位作为码流输出。到此就完成了熵编码一条支线上的全过程。

感谢阅读!!上述都是个人的理解,如果有什么原则性的错误还请提出!!

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值