【深度解析H266/VVC编码协议-帧内预测】

预测编码技术

预测编码(Prediction Coding)是指利用已编码的一个或多个样本值,根据某种模型或方法,对当前的样本值进行预测,对样本真实值和预测值之间的差值编码。视频中的每个像素看成一个信源符号,它通常与空域上或时域上邻近的像素具有较强的相关性,因此视频是一种有记忆信源。预测编码技术通过预测模型消除像素间的相关性,得到的差值信号可以认为没有相关性,或者相关性很小,因此可以作为无记忆信源进行编码。

视频预测编码的主要思想是通过预测来消除像素间的相关性;主要分为帧内预测和帧间预测。
帧内预测:利用当前图像内已编码像素生成预测值。
帧间预测:利用当前图像之前已编码图像的重建像素生成预测值。

VVC 帧内预测编码过程

帧内预测技术是利用同一帧中相邻像素的相关性,利用当前块相邻区域的重建像素预测当前块中像素的技术,如下图所示,当前CU可以利用相邻A、B、C、D和E位置处的重建像素来预测当前CU中的像素。通过帧内预测之后,再将预测残差通过变换、量化等,可以有效地去除视频的空间冗余。
在这里插入图片描述
帧内预测是利用的视频的空间相关性,利用当前图像已经编码的像素预测当前像素,VVC的帧内预测方法主要分为两种方式:

  • 利用相邻(左边和上方)已经编码的像素预测当前像素(如角度预测模式)
  • 利用已经编码的颜色分量预测当前颜色分量的像素(如CCLM模式,用Y分量的像素预测色度分量的像素)

为了提高帧内预测模式的性能,VVC中的帧内预测模式改进主要有以下几点:
引入了更大尺寸的预测块

  • 为了提高预测的精度,引入了更多的参考像素(MRL多参考行模式),针对参考像素的处理,使用了新的滤波技术(MDIS)
  • 引入了更多的方向性(65种角度预测模式)
  • 引入了跨分量预测(CCLM模式)
  • 引入了神经网络的思想(MIP技术)
  • 对预测后的像素进一步改进(PDPC技术)
  • 更小预测和变换块(ISP技术)

一、角度预测模式

为了捕捉自然视频中呈现的更多的边缘方向,VVC在HEVC的33个角度预测模式的基础上,将角度预测模式扩展到了65个,再加上Planar模式和DC模式,共67个传统角度模式。

角度预测模式的步骤为:

1.1 参考像素获取

参考像素值获取模块是对当前CU相邻参考像素是否可用进行判断。

H266沿用大范围边界像素作为当前CU的参考,当参考像素不可用或不存在时使用默认值填充的方式得等到参考像素值。
H266引用多参考行内预测(Multiple Reference Line Intra Prediction, MRLP)技术,领域像素可选范围扩展到当前CU上侧三行和左侧三列。得到邻域像素后,进行平滑滤波或差值滤波,引入依赖模式的帧内平滑(Mode Dependent Smoothing,MDIS)技术,根据预测模式和CU尺寸进行不同的滤波处理。

VVC帧内预测模板

1.2 参考像素范围

  1. 单参考行像素

    • 当参考像素不存在或者不可用时(比如图像边界、Slice边界、Tile边界或尚未编码块),H266使用最邻近的像素进行填充,比如下图A的参考像素不存在,则A所有像素都用B的最下方的像素进行填充。
    • 如果所有区域参考像素都不可用,则用固定值填充;Mid=1<<(bitdepth-1);
    • 如果像素比特深度是8,则固定值是128,如果是10,则固定值是512;例如第一个CU的参考像素就是用固定值填充。
      在这里插入图片描述
  2. 多参考行像素(MRLP)
    MRLP技术允许使用邻近的3行(列)参考像素,选择其中的1行(列)生成预测值,对于不存在或者不可用的像素,采用于单行相同的填充方式。为了平衡性能,仅允许MPM列表中的模式使用MRLP技术。
    在这里插入图片描述

1.3 参考像素平滑滤波(MDIS)

在HEVC中,对角度预测模式(不包括Planar和DC)使用两抽头线性插值滤波器进行滤波。
在VVC中,采用四抽头高斯插值滤波器来进行滤波,从而提高帧内角度预测精度。

VVC协议中MDIS 帧内参考像素滤波总共存在三种滤波器,即满足条件下的整数平滑滤波、非整像素下的三次插值滤波器、非整像素的高斯插值滤波器。

  • 一种是对满足一定条件下的参考像素进行平滑滤波 ([1 2 1]/4 滤波器);
  • 另外两种是对预测时非整数像素位置的插值滤波器,VVC共使用两组四抽头的插值滤波器:三次插值滤波器和高斯插值滤波器,其中三次插值滤波器能保留更多细节纹理,高斯插值滤波器的滤波效果更为平滑。
  1. 整数平滑滤波
    是否对参考像素进行滤波由当前CU的大小、预测模式等条件,需要同时满足如下表格中的五个条件才能使用平滑滤波。
    滤波方法为3抽头滤波器,抽头系数为[0.25, 0.5, 0.25]。
序号条件
1参考行限制:预测过程使用单参考行像素
2大小限制:预测过程使用单参考行像素
3仅对亮度分量使用
4不适用ISP模式
5模式限制:当前CU选择的模式属于Planar模式或者对角模式
  1. 三次插值滤波器
    在非整像素可以保留更多的细节纹理,满如条件中(使用了MRLP技术或ISP技术、使用了Planar模式或对角模式、 Distmin <=Thr[n] )其中一个即可使用。

  2. 高斯插值滤波器
    滤波效果更加平滑,应用更加广泛,不满足三次插值滤波器时使用。

二、预测值计算

根据参考像素值,采用特定的预测模式计算待编码CU每个像素的预测值。

  • H266将角度预测模式扩展到了65种,加上DC模式和Planar模式一共67种模式称为传统预测模式。
  • 针对宽高不等的方形CU,宽角度帧内预测(Wide Angle Intra Prediction,WAIP)技术表达了更多的预测方向,模式编码扩展[-14, 80]。
  • H266还引入了基于矩阵的帧内预测(Matrix-based Intra Prediction,MIP)技术,借助神经网络离线训练得到的多个权重矩阵生成预测值,对传统预测模式有效补充。

在这里插入图片描述

2.1 Planar模式

编号0,适用于像素值缓慢变化的区域,其预测像素可以看成是水平、垂直两个方向预测值的平均值。

2.2 DC 模式

编号1,适用于大面积平坦区域,DC模式需要计算出当前CU左侧及上方参考像素的平均值。

2.3 传统角度预测模式

位于-135°~45°内,水平类模式编号为2 ~ 33,垂直类编号34 ~ 66;每种角度预测模式都相当于在水平或垂直方向做了角度偏移,如下表。
在这里插入图片描述
为了捕捉自然视频中呈现的任意边缘方向,VVC中的帧内传统角度预测模式数从HEVC中使用的33个扩展到65个。
在下图中,VVC中的新的角度预测模式被描绘成红色虚线箭头,并且planar和DC模式保持不变。
在这里插入图片描述
在HEVC中,每个帧内编码块都是正方形的且每边的长度是2的幂,因此,使用DC模式进行帧内预测时不需要除法运算。
在VVC中,帧内编码块可以是矩形,为了避免用DC模式进行帧内预测时需要进行除法运算,VVC只计算较长一边的均值作为预测值。
在这里插入图片描述

2.4 宽角度预测模式

在HEVC中,由于帧内预测块都是正方形的,所以各个角度预测模式使用的概率是相等的。
在VVC中,帧内预测块可能是矩形块,对于水平类的块(宽大于高)上边的参考像素使用概率大于左边参考像素的使用概率,对于垂直类的块(高大于宽)上边的参考像素使用概率小于左边参考像素的使用概率。

因此,VVC引入了宽角度预测模式,在对矩形块预测时,将传统的角度预测模式转换为宽角度预测模式。
如图,模式2 ~ 66表示传统的帧内预测模式,模式 -1 ~ -14以及模式67 ~ 80表示宽角度预测模式。
在这里插入图片描述
宽角度预测模式仍然使用传统角度模式索引发出信号,在解码端在将传统角度模式再转换为宽角度预测模式,这样的话帧内预测模式的总数和帧内模式编码方法保持不变, 并且帧内模式编码方法不变。

在这里插入图片描述
在H266中,二叉树划分和三叉树划分都会导致非方形CU的出现,传统的角度模式范围可能会限制非方形CU对参考像素的选择。
相应的角度偏移如下表。针对非方形CU,增加宽角度预测模式后,仍使用65重候选角度预测模式,即增加的宽角度预测模式替换了部分传统角度预测模式;一般会根据宽高比来替换不同的传统角度编号。
在这里插入图片描述

宽角度帧内预测模式的替换取决于块的宽高比。
具体如下表所示:

宽高比被替换的角度模式替换后的角度模式
W / H == 16Modes 2,3,4,5,6,7,8,9,10,11,12, 13,14,15Modes 67,68,69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80
W / H == 8Modes 2,3,4,5,6,7,8,9,10,11,12, 13Modes 67,68,69, 70, 71, 72, 73, 74, 75, 76, 77, 78
W / H == 4Modes 2,3,4,5,6,7,8,9,10,11Modes 67,68,69, 70, 71, 72, 73, 74, 75, 76
W / H == 2Modes 2,3,4,5,6,7,8,9Modes 67,68,69, 70, 71, 72
W / H == 1NoneNone
W / H == 1/2Modes 59,60,61,62,63,64,65,66Modes -6,-5,-4,-3,-2,-1
W / H == 1/4Mode 57,58,59,60,61,62,63,64,65,66-10,-9,-8,-7,-6,-5,-4,-3,-2,-1
W / H == 1/8Modes 55, 56,57,58,59,60,61,62,63,64,65,66-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1
W / H == 1/16Modes 53, 54, 55, 56,57,58,59,60,61,62,63,64,65,66Modes -14,-13,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1

在这里插入图片描述
如上图所示,当对矩形块使用宽角度预测时,垂直相邻的两个预测像素可能使用两个不相邻的参考像素。
为了减少参考像素间距∆pα带来的负面影响,需要对参考像素进行低通滤波和平滑处理。
当预测模式是[-14, -12, -10, -6, 72, 76, 78, 80]时,参考像素可以不经处理直接使用。

角度模式向宽角度模式的转换代码如下所示:

int PU::getWideAngle( const TransformUnit &tu, const uint32_t dirMode, const ComponentID compID )
{
  //This function returns a wide angle index taking into account that the values 0 and 1 are reserved 
  //for Planar and DC respectively, as defined in the Spec. Text.
  if( dirMode < 2 )
  {
    return ( int ) dirMode;
  }
 
  const CompArea&  area         = tu.cu->ispMode && isLuma(compID) ? tu.cu->blocks[compID] : tu.blocks[ compID ];
  int              width        = area.width;
  int              height       = area.height;
  int              modeShift[ ] = { 0, 6, 10, 12, 14, 15 };
  int              deltaSize    = abs( floorLog2( width ) - floorLog2( height ) );
  int              predMode     = dirMode;
 
  if( width > height && dirMode < 2 + modeShift[ deltaSize ] )
  {
    predMode += ( VDIA_IDX - 1 );
  }
  else if( height > width && predMode > VDIA_IDX - modeShift[ deltaSize ] )
  {
    predMode -= ( VDIA_IDX + 1 );
  }
 
  return predMode;
}

2.5 基于矩阵的预测模式(MIP)

传统预测模式及宽角度预测模式都是以像素映射或线性渐变方式计算预测值,无法对不规则纹理做出有效的预测。
H266标准使用了MIP技术,采用离线训练神经网络的方法,得到多个固定的权重矩阵,进而利用权重矩阵计算预测值。
参考像素经过处理后得到输入向量,输入向量与权重矩阵相乘得到输出向量,经过进一步排列和上采样得到待编码CU的预测值。
在这里插入图片描述

三、 预测值修正

3.1 位置决定的帧内预测组合(PDPC)

3.2 帧内子区域划分(ISP)

ISP技术旨在充分利用与待预测像素距离相近的参考像素进行预测。
根据编码块得到大小,将亮度帧内预测块垂直划分或水平划分为若干个子区域,并按照从左到右、从上到下的额顺序依次进行编码及重建。
ISP技术使帧内预测编码基于CU子区域进行,前一子区域编码之后的重建像素为下一子区域提供参考,各子区域共用一种帧内预测模式。
在这里插入图片描述

3.3 跨分支线性模型预测(CCLM)

在H266中,CU色度分量进行预测编码前,亮度分量已经完成编码获得亮度重建值,因此亮度分量可以作为色度分量预测的参考信息。CCLM技术通过参考像素的亮度重建值和色度重建值建立分量间线性关系,根据待预测像素的亮度重建值计算色度预测值,过程如下图。
在这里插入图片描述

3.4 亮度分量的最可能模式(Most Probable Mode,MPM)技术

MPM技术充分利用相邻块预测模式之间的相关性,来进行亮度预测模式的编码。
如果直接对预测块的模式进行编码,那么对于67种模式需要7bit来编码,数据量很大。

VVC采取和HEVC一样的方法,先构建最可能模式列表(most probable mode ,MPM)
在VVC内MPM list里有6个预测模式(无论是否应用MRL和ISP),

  • 如果该块的预测模式在MPM中只需要编码其索引号(只需要3bit),

  • 如果该块的预测模式不在MPM中而是在61个non-MPM模式中,熵编码时使用截断二元码(TBC)编码其模式。
    在这里插入图片描述
    MPM列表是基于左边相邻块和上方相邻块的帧内模式构造的,如上图所示,构造方法如下:

  • 当左边块和上边块不可参考时,其帧内模式默认设置为Planar模式

  • 如果左边块和上边块都是非角度模式时, MPM list -> {Planar,DC,V,H,V–4,V+4}

  • 如果左边块和上边块其中一个是角度模式,另一个不是角度模式时
    设Max是左边块和上边块较大的模式 , MPM list ->{Planar, Max, Max − 1, Max + 1, Max − 2, Max + 2}

  • 如果左边块和上边块都是角度模式且它们不同时
    设Max是左边块和上边块较大的模式
    设Min是左边块和上边块较小的模式

    • 如果Max - Min 的等于 1, 则 MPM list -> {Planar, Left, Above, Min – 1, Max + 1, Min – 2}
    • 如果Max - Min 的大于等于62, 则MPM list ->{Planar, Left, Above, Min + 1, Max – 1, Min + 2}
    • 如果Max - Min 的等于2 则MPM list -> {Planar, Left, Above, Min + 1, Min – 1, Max + 1}
      否则MPM list -> {Planar, Left, Above, Min – 1, –Min + 1, Max – 1}
  • 如果左边块和上边块时相同的角度模式时,
    MPM list –>{Planar, Left, Left − 1, Left + 1, Left − 2, Left + 2}}

MPM索引码字的第一个bin是CABAC上下文编码的。
总共使用三个上下文,分别对应于当前帧内块是启用了MRL、启用了ISP还是正常的帧内块。
生成MPM列表时候,需要删除重复的模式,以便包含在MPM列表的模式唯一。

具体建立过程:

  1. 获得左下和右上相邻像素,分别记为A和B
    在这里插入图片描述
  2. 获取相邻像素A和B所在PU的帧内预测模式,获取方法如下:
  • 如果以下条件之一成立,则相邻PU的帧内预测模式设置为Planar模式
  • 其相邻PU不可用
  • 相邻PU的编码模式不是帧内编码模式
  • 相邻PU是MIP模式
  • 相邻PU和当前PU不是位于同一个CTU
  • 否则,获取其相邻PU的帧内预测模式
  1. 将相邻像素A、B所处PU的预测模式分别记为A、B,则MPM列表构建如下:
    (1) A =B 且 A > B
MPM[0]Planar
MPM[1]A
MPM[2]2 + ( ( A +61 ) % 64 )
MPM[2]2 + ( ( A -1 ) % 64 )
MPM[3]2 + ( ( A +60 ) % 64 )
MPM[4]2 + ( A % 64 )
MPM[5]VER+4

(2) A ≠ B,A > DC且B > DC
记MinAB = Min(A,B), MaxAB = max(A,B)

MPM[0]Planar
MPM[4]A
MPM[5]B
  • 若maxAB - minAB = 1,则
MPM[3]2+((minAB+61))%64
MPM[4]2+((maxAB-1))%64
MPM[5]2+(minAB)%64
  • 若maxAB - minAB >= 62,则
MPM[3]2+((minAB+61))%64
MPM[4]2+((maxAB-1))%64
MPM[5]2+(minAB+60)%64
  • 若maxAB - minAB >=62,则
MPM[3]2+((minAB-1))%64
MPM[4]2+((maxAB+61))%64
MPM[5]2+(minAB)%64
  • 若maxAB - minAB = 2,则
MPM[3]2+((minAB-1))%64
MPM[4]2+((minAB+61))%64
MPM[5]2+(maxAB)%64
  • 否则
MPM[3]2+((minAB+61))%64
MPM[4]2+((minAB-1))%64
MPM[5]2+(maxAB+61)%64

(3) A ≠ B,A > DC 或 B > DC

MPM[0]Planar
MPM[1]maxAB
MPM[2]2 + ( ( maxAB + 61 ) % 64 )
MPM[3]2 + ( ( maxAB − 1 ) % 64 )
MPM[4]2 + ( ( maxAB + 60 ) % 64 )
MPM[5]2 + ( maxAB % 64 )

(4) 否则

MPM[0]Planar
MPM[1]DC
MPM[2]DC
MPM[2]VER
MPM[3]HOR
MPM[4]VER-4
MPM[5]VER+4

具体代码:

int PU::getIntraMPMs( const PredictionUnit &pu, unsigned* mpm, const ChannelType &channelType /*= CHANNEL_TYPE_LUMA*/ )
{
  const int numMPMs = NUM_MOST_PROBABLE_MODES;
  {
    CHECK(channelType != CHANNEL_TYPE_LUMA, "Not harmonized yet");
    int numCand      = -1;
    int leftIntraDir = PLANAR_IDX, aboveIntraDir = PLANAR_IDX;//将左相邻块和上相邻块预测模式设置为Planar模式
 
    const CompArea &area = pu.block(getFirstComponentOfChannel(channelType));
    const Position posRT = area.topRight();//当前块的左上角
    const Position posLB = area.bottomLeft();//当前块的右下角
 
    // Get intra direction of left PU
    // 获得左相邻PU
    const PredictionUnit *puLeft = pu.cs->getPURestricted(posLB.offset(-1, 0), pu, channelType);
    if (puLeft && CU::isIntra(*puLeft->cu))
    {
      leftIntraDir = PU::getIntraDirLuma( *puLeft );//获得左相邻PU的预测模式
    }
 
    // Get intra direction of above PU
    // 获得上相邻PU
    const PredictionUnit *puAbove = pu.cs->getPURestricted(posRT.offset(0, -1), pu, channelType);
    if (puAbove && CU::isIntra(*puAbove->cu) && CU::isSameCtu(*pu.cu, *puAbove->cu))
    {
      aboveIntraDir = PU::getIntraDirLuma( *puAbove );//获得左相邻PU的预测模式
    }
 
    CHECK(2 >= numMPMs, "Invalid number of most probable modes");
 
    const int offset = (int)NUM_LUMA_MODE - 6;//61
    const int mod = offset + 3;//64
 
    {
      mpm[0] = PLANAR_IDX;//Planar
      mpm[1] = DC_IDX;//DC
      mpm[2] = VER_IDX;//50
      mpm[3] = HOR_IDX;//18
      mpm[4] = VER_IDX - 4;//46
      mpm[5] = VER_IDX + 4;//54
 
      if (leftIntraDir == aboveIntraDir)
      {
        numCand = 1;
        if (leftIntraDir > DC_IDX)
        {
          mpm[0] = PLANAR_IDX;
          mpm[1] = leftIntraDir;
          mpm[2] = ((leftIntraDir + offset) % mod) + 2;
          mpm[3] = ((leftIntraDir - 1) % mod) + 2;
          mpm[4] = ((leftIntraDir + offset - 1) % mod) + 2;
          mpm[5] = ( leftIntraDir               % mod) + 2;
        }
      }
      else //L!=A
      {
        numCand = 2;
        int  maxCandModeIdx = mpm[0] > mpm[1] ? 0 : 1;
 
        if ((leftIntraDir > DC_IDX) && (aboveIntraDir > DC_IDX))
        {
          mpm[0] = PLANAR_IDX;
          mpm[1] = leftIntraDir;
          mpm[2] = aboveIntraDir;
          maxCandModeIdx = mpm[1] > mpm[2] ? 1 : 2;
          int minCandModeIdx = mpm[1] > mpm[2] ? 2 : 1;
          if (mpm[maxCandModeIdx] - mpm[minCandModeIdx] == 1)
          {
            mpm[3] = ((mpm[minCandModeIdx] + offset)     % mod) + 2;
            mpm[4] = ((mpm[maxCandModeIdx] - 1)          % mod) + 2;
            mpm[5] = ((mpm[minCandModeIdx] + offset - 1) % mod) + 2;
          }
          else if (mpm[maxCandModeIdx] - mpm[minCandModeIdx] >= 62)
          {
            mpm[3] = ((mpm[minCandModeIdx] - 1)      % mod) + 2;
            mpm[4] = ((mpm[maxCandModeIdx] + offset) % mod) + 2;
            mpm[5] = ( mpm[minCandModeIdx]           % mod) + 2;
          }
          else if (mpm[maxCandModeIdx] - mpm[minCandModeIdx] == 2)
          {
            mpm[3] = ((mpm[minCandModeIdx] - 1)      % mod) + 2;
            mpm[4] = ((mpm[minCandModeIdx] + offset) % mod) + 2;
            mpm[5] = ((mpm[maxCandModeIdx] - 1)      % mod) + 2;
          }
          else
          {
            mpm[3] = ((mpm[minCandModeIdx] + offset) % mod) + 2;
            mpm[4] = ((mpm[minCandModeIdx] - 1)      % mod) + 2;
            mpm[5] = ((mpm[maxCandModeIdx] + offset) % mod) + 2;
          }
        }
        else if (leftIntraDir + aboveIntraDir >= 2)
        {
          mpm[0] = PLANAR_IDX;
          mpm[1] = (leftIntraDir < aboveIntraDir) ? aboveIntraDir : leftIntraDir;
          maxCandModeIdx = 1;
          mpm[2] = ((mpm[maxCandModeIdx] + offset)     % mod) + 2;
          mpm[3] = ((mpm[maxCandModeIdx] - 1)          % mod) + 2;
          mpm[4] = ((mpm[maxCandModeIdx] + offset - 1) % mod) + 2;
          mpm[5] = ( mpm[maxCandModeIdx]               % mod) + 2;
        }
      }
    }
    for (int i = 0; i < numMPMs; i++)
    {
      CHECK(mpm[i] >= NUM_LUMA_MODE, "Invalid MPM");
    }
    CHECK(numCand == 0, "No candidates found");
    return numCand;
  }
}

getPURestricted函数主要是根据位置获取PU


const PredictionUnit* CodingStructure::getPURestricted( const Position &pos, const PredictionUnit& curPu, const ChannelType _chType ) const
{
  const PredictionUnit* pu = getPU( pos, _chType );
  // exists       same slice and tile                  pu precedes curPu in encoding order
  //                                                  (thus, is either from parent CS in RD-search or its index is lower)
  //存在 相同slice和Tile    PU在编码顺序中先于当前PU
  //(因此,在RD搜索中是来自父CS的,或者其索引较低)
  const bool wavefrontsEnabled = curPu.cu->slice->getSPS()->getEntropyCodingSyncEnabledFlag();
  int ctuSizeBit = floorLog2(curPu.cs->sps->getMaxCUWidth());
  int xNbY  = pos.x << getChannelTypeScaleX( _chType, curPu.chromaFormat );//相邻PU的左上角位置x
  int xCurr = curPu.blocks[_chType].x << getChannelTypeScaleX( _chType, curPu.chromaFormat );//当前PU的左上角位置x
  //判断相邻PU和当前PU是否在同一CTU中
  bool addCheck = (wavefrontsEnabled && (xNbY >> ctuSizeBit) >= (xCurr >> ctuSizeBit) + 1 ) ? false : true;
  if( pu && CU::isSameSliceAndTile( *pu->cu, *curPu.cu ) && ( pu->cs != curPu.cs || pu->idx <= curPu.idx ) && addCheck )
  {
    return pu;
  }
  else
  {
    return nullptr;
  }
}

3.5 色度分量亮度派生模式(Derived Mode,DM)技术

针对色度预测模式编码,H266使用了DM模式,即直接使用对应位置的亮度预测查模式信息。

  • 14
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

九天之遥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值