TEncRCPic帧级别码率控制
先开看成员属性
补充:m_totalCostIntra是指该帧(I帧)的帧内所有LCU的代价总和,即该帧的复杂度(SATD),该值通过TEncSlice::calCostSliceI函数计算,同时LCU的m_costIntra参数也在该函数中赋值
m_remainingCostIntra是指该帧(I帧)的帧内所有还未编码LCU的代价总和,在getLCUInitTargetBits函数中将m_remainingCostIntra赋值为m_totalCostIntra
m_LCUs是一个数组,结构体TRLCU记录该帧中所有LCU的参数
struct TRCLCU
{
Int m_actualBits; //LCU实际比特
Int m_QP; // QP of skip mode is set to g_RCInvalidQPValue
Int m_targetBits;//LCU的目标比特
Double m_lambda;
Double m_bitWeight; //根该LCU的根据BU分配的bits,m_targetBits由该参数进行修正,但非I帧修正才用这个
Int m_numberOfPixel;//像素总量
Double m_costIntra; //I帧该LCU编码的代价,I帧修正用这个
Int m_targetBitsLeft;//包括该LCU在内和之后的LCU编码需要的总bits,在getLCUInitTargetBits函数中进行初始化
};
构造函数和析构函数还是初始化为0
再开看create函数
Void create( TEncRCSeq* encRCSeq, TEncRCGOP* encRCGOP, Int frameLevel, list<TEncRCPic*>& listPreviousPictures );
传参有个frameLevel,这个是帧级别,是在TEncRateCtrl的init函数中进行了指定
Void TEncRateCtrl::initRCPic( Int frameLevel )
{
m_encRCPic = new TEncRCPic;
m_encRCPic->create( m_encRCSeq, m_encRCGOP, frameLevel, m_listRCPictures );
}
xEstPicTargetBits是计算该帧的目标比特,xEstPicHeaderBits是该帧的预估头部bits,后面介绍
// at least allocate 100 bits for picture data,每帧至少分配100bits
从encRCSeq获取一些参数赋值给该类的成员属性
picWidthInLCU和picHeightInLCU是计算宽和高的LCU数量
注意,这里的剩余bit要减去m_estHeaderBits
m_bitsLeft -= m_estHeaderBits;
这里同时对LCU的所有参数进行了初始化
currWidth和currHeight是因为最右边和最小的LCU不一定是最大LCU,即64X64,计算一下该LCU的像素总量。
xEstPicTargetBits
xEstPicTargetBits函数,计算该帧的目标bits,对视频序列中剩余的帧数大于16时,对分配为当前帧的目标比特数进行调整。
算法原理
xEstPicHeaderBits函数,计算预估头bits
记录和当前帧处于同一level的帧有多少,以及它们的头信息花费的真正的比特数之和,求平均值。
getRefineBitsForIntra
在create函数中通过xEstPicTargetBits函数获取了该帧的目标比特,但对于I帧编码形式,在计算I帧目标比特时需要进行修正,先调用calCostSliceI函数计算I帧的SATD复杂度,即m_totalCostIntra。再调用getLeftAverageBits获取该序列平均帧比特(对于IPPP编码来说就是该视频的平均比特),然后调用getRefineBitsForIntra,传参就是平均比特进行修正,获得该I帧真正的目标比特。调用setTargetBits设置该I帧真正的比特。
留个疑问,那计算I帧的目标比特还有什么意义吗?反正都要修正?修正也没有用到I帧通过xEstPicTargetBits计算得到的目标比特
else if ( frameLevel == 0 ) // intra case, but use the model
{
m_pcSliceEncoder->calCostSliceI(pcPic); // TODO: This only analyses the first slice segment - what about the others?
if ( m_pcCfg->getIntraPeriod() != 1 ) // do not refine allocated bits for all intra case
{
Int bits = m_pcRateCtrl->getRCSeq()->getLeftAverageBits();
bits = m_pcRateCtrl->getRCPic()->getRefineBitsForIntra( bits );
...
...
m_pcRateCtrl->getRCPic()->setTargetBits( bits );
这是HM16.9中的I帧目标比特修正,之前的版本是
bpp | bpp>0.2 | 0.2≥bpp>0.1 | 其他 |
修正Rtarget | 5XRtarget | 7XRtarget | 10XRtarget |
calCostSliceI函数
for( UInt ctuTsAddr = startCtuTsAddr, ctuRsAddr = pcPic->getPicSym()->getCtuTsToRsAddrMap( startCtuTsAddr);
ctuTsAddr < boundingCtuTsAddr;
ctuRsAddr = pcPic->getPicSym()->getCtuTsToRsAddrMap(++ctuTsAddr) )
{
// initialize CU encoder
TComDataCU* pCtu = pcPic->getCtu( ctuRsAddr );
pCtu->initCtu( pcPic, ctuRsAddr );
Int height = min( sps.getMaxCUHeight(),sps.getPicHeightInLumaSamples() - ctuRsAddr / pcPic->getFrameWidthInCtus() * sps.getMaxCUHeight() );
Int width = min( sps.getMaxCUWidth(), sps.getPicWidthInLumaSamples() - ctuRsAddr % pcPic->getFrameWidthInCtus() * sps.getMaxCUWidth() );
Int iSumHad = m_pcCuEncoder->updateCtuDataISlice(pCtu, width, height);
(m_pcRateCtrl->getRCPic()->getLCU(ctuRsAddr)).m_costIntra=(iSumHad+offset)>>shift;
iSumHadSlice += (m_pcRateCtrl->getRCPic()->getLCU(ctuRsAddr)).m_costIntra;
}
m_pcRateCtrl->getRCPic()->setTotalIntraCost(iSumHadSlice);
代码主体部分如上,updateCtuDataISlice函数计算出该LCU的m_costIntra基于哈达玛变换的SATD,然后赋值给该LCU的帧内编码代价m_costIntra,for循环接受把所有LCU的m_costIntra加起来就是该I帧的帧内代价。计算出该帧内代价后就可以通过getRefineBitsForIntra函数计算出I帧修正比特。
estimatePicLambda函数
上面已经计算出了I帧和非I帧的目标比特,自然就知道该帧的bpp,调用该函数计算出计算帧的预估lambda,同时这个函数也对一个图像中所有的LCU的码率控制参数进行初始化,后面的代码是先计算帧的Lamda,再估计帧QP
在I帧情况下调用了getLCUInitTargetBits函数,该函数的作用是初始化LCU的m_targetBitsLeft参数,用于后续的I帧LCU层码率分配更新,同时该函数还有一个重要点,就是把上面calCostSliceI函数计算得到的m_totalCostIntra赋值给m_remainingCostIntra
{ //I帧
list<TEncRCPic*> listPreviousPicture = m_pcRateCtrl->getPicList();
m_pcRateCtrl->getRCPic()->getLCUInitTargetBits();
lambda = m_pcRateCtrl->getRCPic()->estimatePicLambda( listPreviousPicture, pcSlice->getSliceType());
sliceQP = m_pcRateCtrl->getRCPic()->estimatePicQP( lambda, listPreviousPicture );
}
else // normal case 非I帧
{
list<TEncRCPic*> listPreviousPicture = m_pcRateCtrl->getPicList();
lambda = m_pcRateCtrl->getRCPic()->estimatePicLambda( listPreviousPicture, pcSlice->getSliceType());
sliceQP = m_pcRateCtrl->getRCPic()->estimatePicQP( lambda, listPreviousPicture );
}
先估计当前帧的lambda,I帧和P帧有不同的估计算法,底下的代码都是为了约束当前帧的lambda
lastLevelLambda记录和当前帧处于同一level的帧的lambda
lastPicLambda记录上一帧的lambda
启用LCU的分层模式,同一个图片层的不同LCU的参数是不同的,所以要遍历整个图像层的所有LCU ,分配不同的参数
该LCU的BU(基本单元)所占比重初始化,LCU的权重计算使用的是模型参数alpha和beta。
m_bitWeight刚开始是指该LCU的BU比重,后来计算后变成了该LCU根据BU比重所分配的比特,我对m_bitWeight的理解是根据比重计算的每个BU的初始比特,没有考虑该帧内的编码情况。有的BU可能多一点。有的可能少一点,只是一个初始化,并不是该BU的目标比特,后续会在compressSlice函数中调用getLCUTargetBpp进行修正。
BU的比重是
estimatePicQP函数,预估QP,上面已经预估了Lambda
之后的代码同样是对QP的约束
getLCUTargetBpp
xEstPicTargetBits是计算该帧的目标比特,那getLCUTargetBpp就是计算该帧下LCU的目标比特。
但说计算其实也不准确,LCU在estimatePicLambda函数中已经计算了BU的比特,但只是简单的分配,并未考虑编码过程中实际和目标的差距,getLCUTargetBpp我更认为是根据该帧剩余比特、剩余LCU动态调整目标比特的过程。
该函数计算了LCU的目标比特,同时计算LCU的bpp值,通过该bpp得到该CLU的estLambda。
下面这段程序的目的是为了获得LCU层的目标比特数Bpp,那么说先要获得图片层的各单元的总的目标比特数,再除以整个LCU层的总像素数。
分两种情况:I帧和非I帧。
(1)当为I帧时,考虑getLCU(LCUIdx).m_costIntra这个变量,他是表征各索引LCU块的costIntra(我的理解是帧内比重的意思),
在帧内又有两种情况,剩余的总的costIntra>0.1时,就要考虑各个LCU块的比重,并根据比重对各个LCU块分配比特。当剩余的比重小于0.1时,就对
剩余的块平均分配剩余的比特数。
(2)当为非I帧时,使用bitWeight表示各个LCU块的比重(这里bitWeight其实是根据BU分配的比特),求出各个块的分配到的比特数avgBits 。
两种情况的Bpp都是通过下面的公式求得:bpp = ( Double )avgBits/( Double )m_LCUs[ LCUIdx ].m_numberOfPixel;
Int LCUIdx = getLCUCoded() = m_numberOfLCU - m_LCULeft
总LCU数量 – 未编码的LCU数量 = 已经编码的LCU数量 =正在编码的LCU的索引
先看看I帧情况
Int noOfLCUsLeft = m_numberOfLCU - LCUIdx + 1;
noOfLCUsLeft=总LCU数量-已经编码的LCU数量+1 = 未编码LCU+1
Int bitrateWindow = min(4,noOfLCUsLeft);//比特率窗口,4和noOfLCUsLeft取最小
Double MAD = getLCU(LCUIdx).m_costIntra;//编号为LCUIdx的LCU块的比重值为MAD
if (m_remainingCostIntra > 0.1 )//帧内未编码LCU占总比重
{
Double weightedBitsLeft =(m_bitsLeft*bitrateWindow+(m_bitsLeft-getLCU(LCUIdx).m_targetBitsLeft)*noOfLCUsLeft)/(Double)bitrateWindow;
//weightedBitsLeft其实是修正后的剩余比特,简化一下该等式
weightedBitsLeft=
m_bitsLeft(m_bitsLeftgetLCU(LCUIdx).m_targetBitsLeft)*noOfLCUsLeft)/(Double)bitrateWindow
当bitsLeft> targetBitsLeft时说明后续用不了那么多bits,该帧剩余比特可以加一点
反之当bitsLeft<targetBitsLeft时说明该帧的剩余比特不够用了,该帧剩余比特减一点
avgBits = Int( MAD*weightedBitsLeft/m_remainingCostIntra );//就是剩余的比特数*当前块在总块中的比重
m_remainingCostIntra -= MAD;// m_remainingCostIntra进行更新
m_LCUs[ LCUIdx ].m_targetBits = avgBits; //更新该LCU的目标比特
}
后续的P帧也是类似情况,需要对剩余比特avgBits进行修正
for ( Int i=LCUIdx; i<m_numberOfLCU; i++ )
{
totalWeight += m_LCUs[i].m_bitWeight;
}
Int realInfluenceLCU =min( g_RCLCUSmoothWindowSize, getLCULeft() );
avgBits = (Int)( m_LCUs[LCUIdx].m_bitWeight - ( totalWeight - m_bitsLeft ) / realInfluenceLCU + 0.5 );
其中m_bitWeight代表:为每个BU(基本单元)分配的初始目标bit数。上式中为何会有减号后一项?这是因为HM会根据上一个LCU实际所用bits数来更新下一个LCU的目标bits(动态调整)。(若初始剩余比特totalWeight > 实际剩余比特m_bitsLeft,说明前面编码过的LCU分配多了,则应该减少下一个LCU的目标bits。否则若还按照m_bitWeight分配目标bits,则会导致总bits数分完后还有剩余LCU未编码。)realInfluenceLCU是平滑窗口,即平滑比特波动。+0.5达到向上取整的目的。
可以参考该帖HEVC帧间码率控制https://www.codetd.com/article/922772
LCU块的bpp由以上函数已经知道,根据bpp计算CLU的预估lambda
函数getLCUEstLambda
后续都是约束
//for Lambda clip, LCU level clip
Double clipNeighbourLambda = -1.0;
for ( Int i=LCUIdx - 1; i>=0; i-- )
{
if ( m_LCUs[i].m_lambda > 0 )
{
clipNeighbourLambda = m_LCUs[i].m_lambda;
break;
}
}
if ( clipNeighbourLambda > 0.0 )
{
estLambda = Clip3( clipNeighbourLambda * pow( 2.0, -1.0/3.0 ), clipNeighbourLambda * pow( 2.0, 1.0/3.0 ), estLambda );
}
if ( clipPicLambda > 0.0 )
{
estLambda = Clip3( clipPicLambda * pow( 2.0, -2.0/3.0 ), clipPicLambda * pow( 2.0, 2.0/3.0 ), estLambda );
}
else
{
estLambda = Clip3( 10.0, 1000.0, estLambda );
}
if ( estLambda < 0.1 )
{
estLambda = 0.1;
}
return estLambda;
函数getLCUEstQP
同理,知道LCU的lambda后计算QP,不再赘述
updateAfterCTU是编码完一个LCU后更新参数、
updateAfterPicture编码完成一帧后的参数更新
addToPictureLsit是把编码完的帧加入list中