一直在学习编解码相关的知识,但是在这个社会中,似乎自学算法,难度确实会有点大;很多人从入门便开始放弃;而我现在也差点步入放弃的队伍之中;因为总是很难找到学习方法,很难看懂一些东西;
但是,经过我很长时间的摸索,我发现了一个问题;那就是与其自己去长时间的看编码的东西,不如花时间去先看解码的东西;因为如果解码搞懂了,编码自然而然的就不是问题了;因为步骤只是反过来而已;而且通过解码,可以搞懂每个参数到底有什么用,编码器的实现可能多种多样,而且算法复杂造成函数写起来实现会比较庞大;但是解码不同;拿HM的简单的小解码器,就很容易把265的框架搞通;
===================================================
这里,我会做到原创,因为抄袭是在是没有用,除非是文章写得很好,我看明白了,我会放到这里,并注明引用。来保证自己的原创精神,也希望如果有缘看到我的文章,可以提出各种各样的建议,学习方法。错误;不胜感激
===================================================
首先来说,这里我分析的是基于HM15.0版本的265解码部分;
先看NAL.h
因为代码比较短,所以我还是把它贴过来
#pragma once
#include <vector>
#include <sstream>
#include "CommonDef.h"
class TComOutputBitstream;
/**
* Represents a single NALunit header and the associated RBSPayload
*/
struct NALUnit
{
NalUnitType m_nalUnitType; ///< nal_unit_type
UInt m_temporalId; ///< temporal_id
UInt m_reservedZero6Bits; ///< reserved_zero_6bits
/** construct an NALunit structure with given header values. */
NALUnit(
NalUnitType nalUnitType,
Int temporalId = 0,
Int reservedZero6Bits = 0)
:m_nalUnitType (nalUnitType)
,m_temporalId (temporalId)
,m_reservedZero6Bits(reservedZero6Bits)
{}
/** default constructor - no initialization; must be perfomed by user */
NALUnit() {}
/** returns true if the NALunit is a slice NALunit */
Bool isSlice()
{
return m_nalUnitType == NAL_UNIT_CODED_SLICE_TRAIL_R
|| m_nalUnitType == NAL_UNIT_CODED_SLICE_TRAIL_N
|| m_nalUnitType == NAL_UNIT_CODED_SLICE_TSA_R
|| m_nalUnitType == NAL_UNIT_CODED_SLICE_TSA_N
|| m_nalUnitType == NAL_UNIT_CODED_SLICE_STSA_R
|| m_nalUnitType == NAL_UNIT_CODED_SLICE_STSA_N
|| m_nalUnitType == NAL_UNIT_CODED_SLICE_BLA_W_LP
|| m_nalUnitType == NAL_UNIT_CODED_SLICE_BLA_W_RADL
|| m_nalUnitType == NAL_UNIT_CODED_SLICE_BLA_N_LP
|| m_nalUnitType == NAL_UNIT_CODED_SLICE_IDR_W_RADL
|| m_nalUnitType == NAL_UNIT_CODED_SLICE_IDR_N_LP
|| m_nalUnitType == NAL_UNIT_CODED_SLICE_CRA
|| m_nalUnitType == NAL_UNIT_CODED_SLICE_RADL_N
|| m_nalUnitType == NAL_UNIT_CODED_SLICE_RADL_R
|| m_nalUnitType == NAL_UNIT_CODED_SLICE_RASL_N
|| m_nalUnitType == NAL_UNIT_CODED_SLICE_RASL_R;
}
Bool isSei()
{
return m_nalUnitType == NAL_UNIT_PREFIX_SEI
|| m_nalUnitType == NAL_UNIT_SUFFIX_SEI;
}
Bool isVcl()
{
return ( (UInt)m_nalUnitType < 32 );
}
};
struct OutputNALUnit;
/**
* A single NALunit, with complete payload in EBSP format.
*/
struct NALUnitEBSP : public NALUnit
{
std::ostringstream m_nalUnitData;
/**
* convert the OutputNALUnit #nalu# into EBSP format by writing out
* the NALUnit header, then the rbsp_bytes including any
* emulation_prevention_three_byte symbols.
*/
NALUnitEBSP(OutputNALUnit& nalu);
};
//! \}
//! \}
这里主要是看NALUnit这个结构体,首先定义结构体的成员,包括naltype,temporalId,reservedzero6bits;这三个,第一个指的是nal的类型;第二个指的是时域层标识号,这个标识号是nal header的最后3bits -1得来的;最后一个参数是6位reseved nalheader的东西;这个我们不用特别的关心,感觉用处是不大的;
这里,如果概念不是很清楚的话,请看《HEVC原理,标准与实现》第9章网络适配层的部分;就会一目了然
紧接着是构造函数,这个不多说;
然后是判断该nal是否为slice nal,这个参考265的nal类型可以看出哪些nal是slice类型;当然,通过代码可以很清楚的看出来;
然后是SEI nal判断,这个判断同上
然后是是否为图像判断,这个通过看书还是看代码都很清楚的可以看出,当nal<32的时候,是代表这个nal承载的内容是图像内容,不是non-vcal内容;
=======================================================
上面讲到这里,nal的东西,也就是说,nal的基础类其实就这些东西;按道理来说,这一章,我没有必要去分析下面的nalunitebsp内容;因为这部分是集成的nalunit;不过也可以看一下;
首先说什么是ebsp,我们的图像,编码出来以后,是裸流,二进制的裸流,这个没有争议,这个流称之为SODB;然后对SODB会进行封装,封装成为RBSP,这个封装其实就是把裸流封成一个字节一个字节的样子;
我们说,sodb是一个bit一个bit的;俺么肯定最后是不一定凑得齐1个字节的;那么RBSP就会补充好最后,让他成为1个字节,补充的东西就是一个1,和若干个0(还有可能有0x0000补充在最后面);当RBSP封装好了以后,加上nal同步字节头,就是0x00000001或者是0x000001,结尾加上0x000000,那么这个nal就封装好了;
但是这样封装好了,真的可以用嘛;当然不行,因为如果在nal里面有0x000000或者0x000001怎么办呢?所以要因为防止竞争的机制;就是如果出现了连续两个0x0000了,那么后面如果是01/02/03的话,我们就会补充上03,防止竞争;也就是说,如果你发现在nal里,有0x00000301,那么这个03其实是为了防止竞争,也就是怕人们当做成为nal起始,而加入的东西;
讲到这里,主要是如果你配上书的话,那么就会很容易懂得这块代码;如果你没有理论,那么这块简直不知道在说啥,理论就去参考HEVC原理,标准与实现》第9章网络适配层的部分
=========================================================
讲到这里,大概对nal有了一个比较清楚的认识,下面我们看一下HM是如何开始读取nal的;这里我以一个我自己的例子,也就是265的vps nal为例;先看一下二进制的数据,如下:
然后我们直接讲nalread.cpp
先看read函数
void read(InputNALUnit& nalu, vector<uint8_t>& nalUnitBuf)
{
/* perform anti-emulation prevention */
TComInputBitstream *pcBitstream = new TComInputBitstream(NULL);
convertPayloadToRBSP(nalUnitBuf, pcBitstream, (nalUnitBuf[0] & 64) == 0);
nalu.m_Bitstream = new TComInputBitstream(&nalUnitBuf);
nalu.m_Bitstream->setEmulationPreventionByteLocation(pcBitstream->getEmulationPreventionByteLocation());
delete pcBitstream;
readNalUnitHeader(nalu);
}
这里面代码较为简单,我一句句的分析:
1、首先,初始化一个输入的码流;
2、需要把EBSP开始化成RBSP,也就是把这些03防止竞争码都去掉;
3、初始化nalu里的码流;
4、把除去了竞争码以后的数据,给到nalu里面;这里面的函数操作简单的很,其实就是一个容器的操作;push-back;
5、读取nalu头;这里面就是我们上面分析的那一坨;
下面我把EBSP转成RBSP的函数以及读取nail头都粘贴到这里;仅供参考:
static void convertPayloadToRBSP(vector<uint8_t>& nalUnitBuf, TComInputBitstream *bitstream, Bool isVclNalUnit)
{
UInt zeroCount = 0;
vector<uint8_t>::iterator it_read, it_write;
UInt pos = 0;
bitstream->clearEmulationPreventionByteLocation();
for (it_read = it_write = nalUnitBuf.begin(); it_read != nalUnitBuf.end(); it_read++, it_write++, pos++)
{
assert(zeroCount < 2 || *it_read >= 0x03);
if (zeroCount == 2 && *it_read == 0x03)
{
bitstream->pushEmulationPreventionByteLocation( pos );
pos++;
it_read++;
zeroCount = 0;
if (it_read == nalUnitBuf.end())
{
break;
}
assert(*it_read <= 0x03);
}
zeroCount = (*it_read == 0x00) ? zeroCount+1 : 0;
*it_write = *it_read;
}
assert(zeroCount == 0);
if (isVclNalUnit)
{
// Remove cabac_zero_word from payload if present
Int n = 0;
while (it_write[-1] == 0x00)
{
it_write--;
n++;
}
if (n > 0)
{
//拖尾0
printf("\nDetected %d instances of cabac_zero_word", n/2);
}
}
nalUnitBuf.resize(it_write - nalUnitBuf.begin());
}
Void readNalUnitHeader(InputNALUnit& nalu)
{
TComInputBitstream& bs = *nalu.m_Bitstream;
Bool forbidden_zero_bit = bs.read(1); // forbidden_zero_bit
assert(forbidden_zero_bit == 0);
nalu.m_nalUnitType = (NalUnitType) bs.read(6); // nal_unit_type
nalu.m_reservedZero6Bits = bs.read(6); // nuh_reserved_zero_6bits
assert(nalu.m_reservedZero6Bits == 0);
nalu.m_temporalId = bs.read(3) - 1; // nuh_temporal_id_plus1
if ( nalu.m_temporalId )
{
assert( nalu.m_nalUnitType != NAL_UNIT_CODED_SLICE_BLA_W_LP
&& nalu.m_nalUnitType != NAL_UNIT_CODED_SLICE_BLA_W_RADL
&& nalu.m_nalUnitType != NAL_UNIT_CODED_SLICE_BLA_N_LP
&& 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_VPS
&& nalu.m_nalUnitType != NAL_UNIT_SPS
&& nalu.m_nalUnitType != NAL_UNIT_EOS
&& nalu.m_nalUnitType != NAL_UNIT_EOB );
}
else
{
assert( nalu.m_nalUnitType != NAL_UNIT_CODED_SLICE_TSA_R
&& nalu.m_nalUnitType != NAL_UNIT_CODED_SLICE_TSA_N
&& nalu.m_nalUnitType != NAL_UNIT_CODED_SLICE_STSA_R
&& nalu.m_nalUnitType != NAL_UNIT_CODED_SLICE_STSA_N );
}
}
到此,我们知道如何获取一个nal的数据,并将它转换从EBSP--->RBSP得到字节表示的二进制码流;后面,也就开始了解析;