此系列是为了记录自己学习VTM10.0的过程和锻炼表达能力,主要是从解码端进行入手。由于本人水平有限,出现的错误恳请大家指正,欢迎与大家一起交流进步。
int poc;//picture order count
PicList* pcListPic = NULL;//存有图片的线性表
ifstream bitstreamFile(m_bitstreamFileName.c_str(), ifstream::in | ifstream::binary);//c_str()将string类型转换为c语言的字符串,in代表输入,binary代表为二进制模式。创建一个文件输入比特流。
InputByteStream bytestream(bitstreamFile);//将比特流转为字节流
// 创建解码器类
xCreateDecLib();
//舍弃RAP的前置图像中为RASL,更新the last displayed POC?
m_iPOCLastDisplay += m_iSkipFrame; // set the last displayed POC correctly for skip forward.
bool loopFiltered[MAX_VPS_LAYERS] = {
false };//标记是否已进行环路滤波
bool bPicSkipped = false;//表示是否跳过解码图像
bool isEosPresentInPu = false;//表示前一个NALU所在的PU是否是Eos
poc:帧的播放顺序
pcListPic:存放着解码出来的帧
bitstreamFile和bytestream:解码端的输入码流,一个是以比特为单位,另一个是以字节为单位
xCreateDecLib():函数包含着解码器类的创建和初始化,存在ROM上变量的初始化,量化和变换相关的初始化
m_iPOCLastDisplay += m_iSkipFrame :不确定
loopFiltered:标记是否已经环路滤波
bPicSkipped:是否跳过解码上一个NALU所在的图像
isEosPresentInPu:判断前一个NALU是否是EOS
while (!!bitstreamFile)
{
//创建NALU类
InputNALUnit nalu;
nalu.m_nalUnitType = NAL_UNIT_INVALID;
bool bNewPicture = m_cDecLib.isNewPicture(&bitstreamFile, &bytestream);//将要解码的NALU是否是图像中的第一个NALU
bool bNewAccessUnit = bNewPicture && m_cDecLib.isNewAccessUnit( bNewPicture, &bitstreamFile, &bytestream );//将要解码的NALU是否是新的一帧中的第一个NALU,同时也是新的AU中的第一个NALU
if(!bNewPicture)
{
//分支1
}
if ((bNewPicture || !bitstreamFile || nalu.m_nalUnitType == NAL_UNIT_EOS) && !m_cDecLib.getFirstSliceInSequence(nalu.m_nuhLayerId) && !bPicSkipped)
{
//分支2
//满足不是跳过解码的图像,同时满足不是sequence中的第一个slice,同时满足以下至少一个条件:1)将要解码的NALU是图像中的第一个NALU;2)比特流文件eof?;3)上一个NALU的类型是EOS
}
else if ( (bNewPicture || !bitstreamFile || nalu.m_nalUnitType == NAL_UNIT_EOS ) &&
m_cDecLib.getFirstSliceInSequence(nalu.m_nuhLayerId))//在下一个NALU所在的slice将是sequence中的第一个slice的情况下,同时满足以下至少一个条件:1)将要解码的NALU是图像中的第一个NALU;
{
//2)比特流文件eof?;3)上一个NALU的类型是EOS。则下一个NALU所在的slice也是picture中的第一个slice。
m_cDecLib.setFirstSliceInPicture (true);
}
if( pcListPic )
{
//分支3
}
if( bNewPicture )
{
}
if (bNewPicture || !bitstreamFile || nalu.m_nalUnitType == NAL_UNIT_EOS)
{
}
if (bNewAccessUnit || !bitstreamFile)
{
}
if(bNewAccessUnit)
{
}
}
进入循环只要bitstreamFile有效,就进行NALU解码
这里有两个重要的flag,bNewPicture和bNewAccessUnit
bNewPicture:将要解码的NALU是否是一帧中的第一个NALU
bNewAccessUnit:将要解码的NALU是否是AU中第一个NALU
bNewPicture为false进入第一个分支,具体参考2.1节
本节分支2:满足以下条件之一
要解码的NALU是一帧中的第一个NALU
eof
上一个NALU的类型是EOS
如果同时满足目前的解码过程不处于CLVS中的第一个slice且上一个NALU所处的帧未被跳过解码则进行一些操作,具体参考2.2节
如果同时满足目前的解码过程处于CLVS中的第一个slice则标志着解码过程进入一帧中的第一个slice。
说明:m_cDecLib.setFirstSliceInPicture (true)会使bNewPicture判断为False
本节分支3:存储的帧不为空,则进行一些操作,具体参考2.3节
之后还有四个分支和之前两个flag有关,由于能力有限就不展开了
只要解码的NALU不是一帧中的第一个NALU就可进入此分支
AnnexBStats stats = AnnexBStats();//JVET-S2001中AnnexB有关的信息
// 将字节流的下一个NALU的所有比特流信息存入NALU类中的m_Bitstream的m_fifo,将统计信息存入stats,具体过程可以参考JVET-S2001中的AnnexB
byteStreamNALUnit(bytestream, nalu.getBitstream().getFifo(), stats);
// 读取NALU头信息,参考JVET-S2001 7.3.1.2 P83
read(nalu);
// 判断是否是IDR图像中的第一个slice
if(m_cDecLib.getFirstSliceInPicture() &&//是否是图片中的第一个slice,在解码器类初始化时设置为true
(nalu.m_nalUnitType == NAL_UNIT_CODED_SLICE_IDR_W_RADL ||
nalu.m_nalUnitType == NAL_UNIT_CODED_SLICE_IDR_N_LP))
{
//分支1
m_newCLVS[nalu.m_nuhLayerId] = true; // m_newCLVS标记是否是一个新的CLVS
xFlushOutput(pcListPic, nalu.m_nuhLayerId);//将pcListPic中存有的图片清空,并写入文件
}
if (m_cDecLib.getFirstSliceInPicture() && nalu.m_nalUnitType == NAL_UNIT_CODED_SLICE_CRA && isEosPresentInPu)
{
//分支2
// 在EOS后面紧接着的CRA图像是CLVSS
m_newCLVS[nalu.m_nuhLayerId] = true;
}
else if (m_cDecLib.getFirstSliceInPicture() && nalu.m_nalUnitType == NAL_UNIT_CODED_SLICE_CRA && !isEosPresentInPu)
{
// 如果CRA图像前面不是EOS,那CRA图像就不是CLVSS
m_newCLVS[nalu.m_nuhLayerId] = false;
}
// temporal_id应该小于cfg中的m_iMaxTemporalLayer,同时nuh_layer_id应该在cfg的m_targetDecLayerIdSet中
if( ( m_iMaxTemporalLayer < 0 || nalu.m_temporalId <= m_iMaxTemporalLayer ) && xIsNaluWithinTargetDecLayerIdSet( &nalu ) )
{
//分支3
}
else//不满足条件,跳过解码此图像
{
bPicSkipped = true;
}
if (nalu.m_nalUnitType == NAL_UNIT_EOS)
{
//分支4
isEosPresentInPu = true;//当NALU的类型为EOS,将isEosPresentInPu设置为true
m_newCLVS[nalu.m_nuhLayerId] = true; //The presence of EOS means that the next picture is the beginning of new CLVS
}
byteStreamNALUnit():主要是将字节流掐头去尾,详细过程参考JVET-S2001中AnnexB一章,这里不再展开
read():读取NALU的头信息,相应格式在JVET-S2001 7.3.1.2 P83
本小节分支1:判断是否进入IDR图像中的第一个slice解码过程中,主要是由解码器类来决定。如果是则意味着进入新的CLVS,并将之前缓存的帧清除
本小节分支2:只有当前一个NALU是EOS(end of sequence)时,当前CRA图像才意味着进入新的CLVS
本小节分支3:是整个函数中最重要的分支,包含调用解码器类解码的过程。但是需要满足NALU的时域层在输出范围内,多图像层也在输出范围内。不满足就跳过解码。具体参考2.1.1节
本小节分支4:当前解码NALU为EOS类型时,就将isEosPresentInPu设置为true。并意味着下一个NALU就是CLVS的开始
if (bPicSkipped)
{
if ((nalu.m_nalUnitType == NAL_UNIT_CODED_SLICE_TRAIL) || (nalu.m_nalUnitType == NAL_UNIT_CODED_SLICE_STSA) || (nalu.m_nalUnitType == NAL_UNIT_CODED_SLICE_RASL) || (nalu.m_nalUnitType == NAL_UNIT_CODED_SLICE_RADL) || (nalu.m_nalUnitType == NAL_UNIT_CODED_SLICE_IDR_W_RADL) || (nalu.m_nalUnitType == NAL_UNIT_CODED_SLICE_IDR_N_LP) || (nalu.m_nalUnitType == NAL_UNIT_CODED_SLICE_CRA) || (nalu.m_nalUnitType == NAL_UNIT_CODED_SLICE_GDR))
{
//满足前一个NALU所在的图像是被跳过解码的,且当前NALU的nal_unit_type属于VCL(除去保留的)
//分支1
if (m_cDecLib.isSliceNaluFirstInAU(true, nalu))//图片中的第一个VCL类型的NALU是否是AU中的第一个VCL类型的NALU
{
//清除一些AU相关的缓存信息
m_cDecLib.resetAccessUnitNals();
m_cDecLib.resetAccessUnitApsNals();
m_cDecLib.resetAccessUnitPicInfo();
}
bPicSkipped = false;
}
}
m_cDecLib.decode(nalu, m_iSkipFrame, m_iPOCLastDisplay, m_targetOlsIdx);//调用解码器类进行解码NALU
if (nalu.m_nalUnitType == NAL_UNIT_VPS)//如果NALU类型是VPS,则提取一些信息
{
//分支2
m_cDecLib.deriveTargetOutputLayerSet( m_targetOlsIdx );
m_targetDecLayerIdSet = m_cDecLib.getVPS()->m_targetLayerIdSet;//更新需要解码图片的nuh_layer_id集
m_targetOutputLayerIdSet = m_cDecLib.getVPS()->m_targetOutputLayerIdSet;//更新需要输出图片的nuh_layer_id集
}
本小节的分支1:前一个NALU所在的图像是被跳过解码的,当前要解码NALU所在的图像不是被跳过解码的。当前NALU的类型又恰巧是VCL(除去保留的),又很恰巧这是AU中第一个VCL类型的NALU。那么就要调用解码器类进行以下三步操作
resetAccessUnitNals()
resetAccessUnitApsNals()
resetAccessUnitPicInfo()
都是跟AU相关的,没有跟进去看,具体啥作用也不知道。同时也要把bPicSkipped设置为false。
m_cDecLib.decode():调用解码器类进行解码的函数,需要另开篇幅仔细描述的。
本小节分支2:如果解码过的NALU类型是VPS(video parameter set),还需要提取一些信息。
if (!loopFiltered[nalu.m_nuhLayerId] || bitstreamFile)
{
//满足以下至少一个条件:1)eof且还未进行环路滤波?;2)将要解码的NALU是图像中的第一个NALU;3)上一个NALU的类型是EOS
m_cDecLib.executeLoopFilters();//调用解码器类进行环路滤波
m_cDecLib.finishPicture(poc, pcListPic, INFO, m_newCLVS[nalu.m_nuhLayerId]);//一张图像解码完后的一些操作?
}
loopFiltered[nalu.m_nuhLayerId] = (nalu.m_nalUnitType == NAL_UNIT_EOS);//如果NALU的类型为EOS,则将loopFiltered设置为true
if (nalu.m_nalUnitType == NAL_UNIT_EOS)
{
m_cDecLib.setFirstSliceInSequence(true, nalu.m_nuhLayerId);//如果NALU的类型为EOS,下一个NALU所在的slice将是sequence中的第一个slice
}
//图像解码完成后有关于IRAP和GDR的操作
m_cDecLib.updateAssociatedIRAP();
m_cDecLib.updatePrevGDRInSameLayer();
m_cDecLib.updatePrevIRAPAndGDRSubpic();
只要不是eof并且已经滤波那么执行以下操作
如果上一个NALU的类型是EOS,那还需要将loopFiltered设置为true,并标记解码过程处于CLVS中的第一个slice
之后还有一些与IRAP和GDR相关的操作,没有跟进去看,具体啥作用也不知道。
if( !m_reconFileName.empty() && !m_cVideoIOYuvReconFile[nalu.m_nuhLayerId].isOpen() )//存在m_reconFileName,且m_cVideoIOYuvReconFile不能使用
{
//分支1
// 使用pcListPic中的第一张图的BitDepths作为m_outputBitDepth
const BitDepths &bitDepths=pcListPic->front()->cs->sps->getBitDepths();
for( uint32_t channelType = 0; channelType < MAX_NUM_CHANNEL_TYPE; channelType++ )
{
if( m_outputBitDepth[channelType] == 0 )
{
m_outputBitDepth[channelType] = bitDepths.recon[channelType];
}
}
std::string reconFileName = m_reconFileName;
if( ( m_cDecLib.getVPS() != nullptr && ( m_cDecLib.getVPS()->getMaxLayers() == 1 || xIsNaluWithinTargetOutputLayerIdSet( &nalu ) ) ) || m_cDecLib.getVPS() == nullptr )
{
//要么不存在VPS,要么当VPS存在的时候满足以下条件之一:1)最大允许层等于1;2)上一个NALU的nuh_layer_id在m_targetOutputLayerIdSet中
m_cVideoIOYuvReconFile[nalu.m_nuhLayerId].open( reconFileName, true, m_outputBitDepth, m_outputBitDepth, bitDepths.recon ); // 将文件流设置为write mode
}
}
// write reconstruction to file
if( bNewPicture )//如果要解码的NALU是图像中的第一个NALU,将重构图像写入文件
{
xWriteOutput( pcListPic, nalu.m_temporalId );
}
if (nalu.m_nalUnitType == NAL_UNIT_EOS)//如果上一个NALU类型是EOS,将重构图像写入文件,将m_bFirstSliceInPicture设置为false
{
xWriteOutput( pcListPic, nalu.m_temporalId );
m_cDecLib.setFirstSliceInPicture (false);
}
// write reconstruction to file -- for additional bumping as defined in C.5.2.3
if (!bNewPicture && ((nalu.m_nalUnitType >= NAL_UNIT_CODED_SLICE_TRAIL && nalu.m_nalUnitType <= NAL_UNIT_RESERVED_IRAP_VCL_12)
|| (nalu.m_nalUnitType >= NAL_UNIT_CODED_SLICE_IDR_W_RADL && nalu.m_nalUnitType <= NAL_UNIT_CODED_SLICE_GDR)))
{
xWriteOutput( pcListPic, nalu.m_temporalId );
}
本节分支1:如果存在输出文件名,且输出文件流未打开。则取pcListPic中的第一张图的BitDepths作为以后输出的比特位数。然后打开相应的输出文件流
之后三个分支都与将重构图像写入文件有关,分别是当:
如果要解码的NALU是图像中的第一个NALU
上一个NALU类型是EOS
是C.5.2.3定义的情况
第二种情况还要标记解码过程未进入一帧中的第一个slice
xFlushOutput( pcListPic );//结束解码,清空pcListPic
// get the number of checksum errors
uint32_t nRet = m_cDecLib.getNumberOfChecksumErrorsDetected();
// delete buffers
m_cDecLib.deletePicBuffer();
// destroy internal classes
xDestroyDecLib();
destroyROM();//清除存放在ROM的变量
xFlushOutput():清空之前的缓存帧
m_cDecLib.getNumberOfChecksumErrorsDetected():统计checksum errors的数量,并将其返回
m_cDecLib.deletePicBuffer():清除解码器类的picture buffer
xDestroyDecLib():摧毁解码器类
destroyROM():清除存放在ROM的变量