VTM10.0代码学习1:DecApp

    科技2025-11-09  10

    此系列是为了记录自己学习VTM10.0的过程和锻炼表达能力,主要是从解码端进行入手。由于本人水平有限,出现的错误恳请大家指正,欢迎与大家一起交流进步。

    1.初始化

    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

    2.循环进行NALU解码

    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有关,由于能力有限就不展开了

    2.1 if(!bNewPicture)

    只要解码的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的开始

    2.1.1 分支3

    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),还需要提取一些信息。

    2.2 if((bNewPicture || !bitstreamFile || nalu.m_nalUnitType == NAL_UNIT_EOS)…)

    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并且已经滤波那么执行以下操作

    m_cDecLib.executeLoopFilters():调用解码器类进行环路滤波m_cDecLib.finishPicture():结束编码一帧并放入pcListPic中

    如果上一个NALU的类型是EOS,那还需要将loopFiltered设置为true,并标记解码过程处于CLVS中的第一个slice

    之后还有一些与IRAP和GDR相关的操作,没有跟进去看,具体啥作用也不知道。

    2.3 if( pcListPic )

    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

    3. 收尾

    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的变量

    Processed: 0.015, SQL: 9