11.13Z周报-HRNet

前言

本周学习了HRNet,看了论文,并根据模型图复现代码,以及复习机器学习,计算机网络准备考试。模型目前还没有复现成功,所以下面的代码的结构是根据源码进行的解析,等下周复现了会把自己的代码再粘贴上来。

文章

标题Deep High-Resolution Representation Learning for Human Pose Estimation

创新点

HRNet不同于之前的固有套路,对于输入的图像先进行下采样得到局部特征,再进行上采样得到我们想要的高分辨率特征图。HRNet网络改串联变并联,始终保持一个高分辨率主体网络,局部特征的获取不再是一个单独的过程,下采样和上采样不是按顺序进行,而是在保持高分辨率主体的同时进行卷积得到更多的局部信息,然后将不同的信息不断进行融合,最终得到全局信息和局部信息都丰富的高分辨率特征图。
在这里插入图片描述

对比

之前的CPN和Simple Baseline都是先下采样再上采样最后得到我们需要的高分辨率特征图,但是在卷积的过程中,很多特征信息就丢失了,比如simplebaseline,经过4层特征提取后,特征图分辨率已经很小了,虽然模型简单,但是信息丢失太多,对于关键点预测精准度的提升效果不大。而HRNnet不同于之前下采样和上采样单独分阶段进行,HRNet一直保持高分辨率主体网络,然后进行卷积得到更多的局部特征,然后将局部和全局特征不断进行融合,最后得到信息丰富的高分辨率特征图。

思考

HRNet模型获得较好的效果是主要是因为并行多分辨率子网吗,如果我们也用单层的结构,但是将每一层的特征进行多尺度融合是否也可以达到类似的效果。对于卷积核的参数以及堆叠的层进行更改会有什么效果,在多层次之间也用残差结构效果会怎么样。这些都可以自己做实验来验证。

实现思路

1.连续多分辨率子网络

有的位姿估计网络是通过串联高分辨率子网来建立的,每个子网形成一个stage,由一系列卷积组成,并且在相邻的子网之间有一个下样本层来将分辨率减半
在这里插入图片描述
2.并行多分辨率子网

在第一个 stage 开始了一个高分辨率的网络分支,然后逐步增加高分辨率到低分辨率的子网路,形成一个新的 stages,并将多分辨率子网并行连接。因此,后一阶段并行子网的分辨率由前一阶段的分辨率和一个更低的分辨率组成,一个包含4个并行子网络的网络结构示例如下
在这里插入图片描述

3.重复的多尺度融合

引入了平行网络信息交换单元,比如每个子网络重复接受来自其他平行子网络的信息。下面是一个例子,展示了信息交换的方案。我们将第三 stage 分为几个(例如3个)交换模块,每个模块由3个并行卷积单元和一个跨并行单元的交换单元组成,其结构如下
在这里插入图片描述
卷积实现
在这里插入图片描述

模型图

在这里插入图片描述

损失

均方差

评价指标

在这里插入图片描述
其中,di是预测值与真实值的欧式距离,vi表示真值是否可见标志。s是目标缩放的比例,ki是一个控制衰减的每个关键点长度。

Training: 按照等比例,将人体检测结果扩展到高度:宽度 = 4 :3,然后剪裁到固定尺寸256×192或者384×288。

使用了 Adam 优化器,基础学习率设置为1e-3,在迭代170个 epochs 以及 200 个 epoch 进行10倍的学习率衰减。训练过程在210个epochs 内结束。

Testing: 使用2个阶段的方式 - 使用person检测器检测person实例,然后预测检测关键点。对于验证集和测试开发集,我们使SimpleBaseline2提供的person检测器。计算了原图,和水平反转图估算出来 heatmap 的平均值。每个关键点的位置,都是通过调整最高热值来进行判断的

在这里插入图片描述

代码结构
1.stem

对于输入的图片进行两个3x3卷积进行初始特征提取,使其分辨率下降到1/4

2.Layer1

Layer1模块,这里的Layer1其实和之前讲的ResNet中的Layer1类似,就是重复堆叠Bottleneck,注意这里的Layer1只会调整通道个数,并不会改变特征层大小

3.transation 不同层数分支创建

每通过一个Transition结构都会新增一个branch

        def _make_transation(self):

            # 不同层数分支进行创建
            def _make_transition_layer(self, num_channels_pre_layer, num_channels_cur_layer):
                """
                    :param num_channels_pre_layer: 上一个stage平行网络的输出通道数目,为一个list,
                        stage=2时, num_channels_pre_layer=[256]
                        stage=3时, num_channels_pre_layer=[32,64]
                        stage=4时, num_channels_pre_layer=[32,64,128]
                    :param num_channels_cur_layer:
                        stage=2时, num_channels_cur_layer = [32,64]
                        stage=3时, num_channels_cur_layer = [32,64,128]
                        stage=4时, num_channels_cur_layer = [32,64,128,256]
                """

                num_branches_cur = len(num_channels_cur_layer)
                num_branches_pre = len(num_channels_pre_layer)

                transition_layers = []
                # 对stage的每个分支进行处理
                # stage1的时候,num_channels_cur_layer为2,所以有两个循环,i=0、1
                for i in range(num_branches_cur):
                    # 如果不为最后一个分支
                    if i < num_branches_pre:
                        # 如果当前层的输入通道和输出通道数不相等,则通过卷积对通道数进行变换
                        # 如果branches_cur通道数=branches_pre通道数,那么这个分支直接就可以用,不用做任何变化
                        # 如果branches_cur通道数!=branches_pre通道数,那么就要用一个cnn网络改变通道数
                        # 注意这个cnn是不会改变特征图的shape
                        # 在stage1中,pre通道数是256,cur通道数为32,所以要添加这一层cnn改变通道数
                        # 所以transition_layers第一层为
                        # conv2d(256,32,3,1,1)
                        # batchnorm2d(32)
                        # relu
                        if num_channels_cur_layer[i] != num_channels_pre_layer[i]:
                            transition_layers.append(
                                nn.Sequential(
                                    nn.Conv2d(
                                        num_channels_pre_layer[i],
                                        num_channels_cur_layer[i],
                                        3, 1, 1, bias=False
                                    ),
                                    nn.BatchNorm2d(num_channels_cur_layer[i]),
                                    nn.ReLU(inplace=True)
                                )
                            )
                        else:
                            # 如果当前层的输入通道和输出通道数相等,则什么都不做
                            transition_layers.append(None)
                    else:
                        # 如果为最后一个分支,则再新建一个分支(该分支分辨率会减少一半)
                        # 由于branches_cur有两个分支,branches_pre只有一个分支
                        # 所以我们必须要利用branches_pre里的分支无中生有一个新分支
                        # 这就是常见的缩减图片shape,增加通道数提特征的操作
                        conv3x3s = []
                        for j in range(i + 1 - num_branches_pre):
                            # 利用branches_pre中shape最小,通道数最多的一个分支(即最后一个分支)来形成新分支
                            inchannels = num_channels_pre_layer[-1]
                            outchannels = num_channels_cur_layer[i] \
                                if j == i - num_branches_pre else inchannels
                            conv3x3s.append(
                                nn.Sequential(
                                    nn.Conv2d(
                                        inchannels, outchannels, 3, 2, 1, bias=False
                                    ),
                                    nn.BatchNorm2d(outchannels),
                                    nn.ReLU(inplace=True)
                                )
                            )
                            # 所以transition_layers第二层为:
                            # nn.Conv2d(256, 64, 3, 2, 1, bias=False),
                            # nn.BatchNorm2d(64),
                            # nn.ReLU(inplace=True)

                        transition_layers.append(nn.Sequential(*conv3x3s))

                return nn.ModuleList(transition_layers)


3._make_stage进行特征提取和特征融合

stage对于每个尺度分支都是先通过4个BasicBlock,然后融合不同尺度上的信息,每个尺度分支上的输出都是由所有分支上的输出进行融合得到的。

# 同级stage设计,通过 HighResolutionModule
    def _make_stage(self, layer_config, num_inchannels,
                    multi_scale_output=True):
        """
        当stage=2时: num_inchannels=[32,64]           multi_scale_output=Ture
        当stage=3时: num_inchannels=[32,64,128]       multi_scale_output=Ture
        当stage=4时: num_inchannels=[32,64,128,256]   multi_scale_output=False
        """
        # 当stage=2,3,4时,num_modules分别为:1,4,3
        # 表示HighResolutionModule(平行之网络交换信息模块)模块的数目
        num_modules = layer_config['NUM_MODULES']

        # 当stage=2,3,4时,num_branches分别为:2,3,4,表示每个stage平行网络的数目
        num_branches = layer_config['NUM_BRANCHES']

        # 当stage=2,3,4时,num_blocks分别为:[4,4], [4,4,4], [4,4,4,4],
        # 表示每个stage blocks(BasicBlock或者BasicBlock)的数目
        num_blocks = layer_config['NUM_BLOCKS']

        # 当stage=2,3,4时,num_channels分别为:[32,64],[32,64,128],[32,64,128,256]
        # 在对应stage, 对应每个平行子网络的输出通道数
        num_channels = layer_config['NUM_CHANNELS']

        # 当stage=2,3,4时,分别为:BasicBlock,BasicBlock,BasicBlock
        block = blocks_dict[layer_config['BLOCK']]

        # 当stage=2,3,4时,都为:SUM,表示特征融合的方式
        fuse_method = layer_config['FUSE_METHOD']

        modules = []
        # 根据num_modules的数目创建HighResolutionModule
        for i in range(num_modules):
 #num_modules表示一个融合块中要进行几次融合,前几次融合是将其他分支的特征融合到最高分辨率的特征图上,只输出最高分辨率特征图(multi_scale_output = False)
#只有最后一次的融合是将所有分支的特征融合到每个特征图上,输出所有尺寸特征(multi_scale_output=True)

            # multi_scale_output 只被用再最后一个HighResolutionModule
            if not multi_scale_output and i == num_modules - 1:
                reset_multi_scale_output = False
            else:
                reset_multi_scale_output = True

            # 根据参数,添加HighResolutionModule到
            modules.append(
                HighResolutionModule(
                    num_branches,   # 当前stage平行分支的数目
                    block,          # BasicBlock,BasicBlock
                    num_blocks,     # BasicBlock或者BasicBlock的数目
                    num_inchannels, # 输入通道数目
                    num_channels,   # 输出通道数
                    fuse_method,    # 通特征融合的方式
                    reset_multi_scale_output     # 是否使用多尺度方式输出
                )
            )
            # 获得最后一个HighResolutionModule的输出通道数
            num_inchannels = modules[-1].get_num_inchannels()

        return nn.Sequential(*modules), num_inchannels

stage搭建
stage2/3

forward函数中

      # 对应论文中的stage2,在配置文件中self.stage2_cfg['NUM_BRANCHES']为2
        # 其中包含了创建分支的过程,即 N11-->N21,N22 这个过程
        # N22的分辨率为N21的二分之一,总体过程为:
        # x[b,256,64,48] ---> y[b, 32, 64, 48]  因为通道数不一致,通过卷积进行通道数变换
        #                     y[b, 64, 32, 24]  通过新建平行分支生成
        x_list = []
        for i in range(self.stage2_cfg['NUM_BRANCHES']):
            if self.transition1[i] is not None:
                x_list.append(self.transition1[i](x))
            else:
                x_list.append(x)
        # 总体过程如下(经过一些卷积操作,但是特征图的分辨率和通道数都没有改变):
        # x[b, 32, 32, 24] --->  y[b, 32, 32, 24]
        # x[b, 64, 16, 12]  --->  y[b, 64, 16, 12]
        y_list = self.stage2(x_list)

        # 对应论文中的stage3
        # 其中包含了创建分支的过程,即 N22-->N32,N33 这个过程
        # N33的分辨率为N32的二分之一,
        # y[b, 32, 64, 48] ---> x[b, 32,  64, 48]   因为通道数一致,没有做任何操作
        # y[b, 64, 32, 24] ---> x[b, 64,  32, 24]   因为通道数一致,没有做任何操作
        #                       x[b, 128, 16, 12]   通过新建平行分支生成

        x_list = []
        for i in range(self.stage3_cfg['NUM_BRANCHES']):
            if self.transition2[i] is not None:
                x_list.append(self.transition2[i](y_list[-1]))
            else:
                x_list.append(y_list[i])
        # 总体过程如下(经过一些卷积操作,但是特征图的分辨率和通道数都没有改变):
        # x[b, 32, 32, 24] ---> y[b, 32, 32, 24]
        # x[b, 32, 16, 12] ---> y[b, 32, 16, 12]
        # x[b, 64, 8, 6] --->   y[b, 64, 8, 6]

        y_list = self.stage3(x_list)

# stage4
        x_list = []
        for i in range(self.stage4_cfg['NUM_BRANCHES']):
            if self.transition3[i] is not None:
                x_list.append(self.transition3[i](y_list[-1]))
            else:
                x_list.append(y_list[i])
        y_list = self.stage4(x_list)

        x = self.final_layer(y_list[0])

        return x
stage4

需要注意的是在Stage4中的最后一个Exchange Block只输出下采样4倍分支的输出(即只保留分辨率最高的特征层),然后接上一个卷积核大小为1x1卷积核个数为17(因为COCO数据集中对每个人标注了17个关键点)的卷积层。最终得到的特征层(64x48x17)就是针对每个关键点的heatmap

简洁模型代码

import torch
from torch import nn

# 残差块
class Bottleneck(nn.Module):
    expansion = 4
    def __init__(self, input_channel, output_channel, stride=1, downsample = None, bn_momentum =0.1):
        super(Bottleneck, self).__init__()
        self.conv1 = nn.Conv2d(input_channel, output_channel, kernel_size=1, bias=False)
        self.bn1 = nn.BatchNorm2d(output_channel, bn_momentum)
        self.relu = nn.ReLU(inplace=True)
        self.conv2 = nn.Conv2d(output_channel, output_channel, kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(output_channel,bn_momentum)
        self.conv3 = nn.Conv2d(output_channel, output_channel * self.expansion, kernel_size=1, bias=False)
        self.bn3 = nn.BatchNorm2d(output_channel * self.expansion, momentum=bn_momentum)
        self.downsample = downsample
        self.stride = stride


    def forward(self, x):
        residual = x
        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)
        out = self.relu(out)

        out = self.conv3(out)
        out = self.bn3(out)

        if self.downsample is not None:
            residual = self.downsample(x)

        out += residual
        out = self.relu(out)

        return out
class BasicBlock(nn.Module):
    expansion = 1
    def __init__(self, input_channel, output_channel, stride=1, downsample=None, bn_momentum=0.1):
        super(BasicBlock, self).__init__()
        self.conv1 = nn.Conv2d(input_channel, output_channel, kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(output_channel, momentum=bn_momentum)
        self.relu = nn.ReLU(inplace=True)
        self.conv2 = nn.Conv2d(input_channel, output_channel, kernel_size=3, stride=1, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(output_channel, momentum=bn_momentum)
        self.downsample = downsample
        self.stride = stride

    def forward(self, x):
        residual = x
        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)

        if self.downsample is not None:
            residual = self.downsample(x)

        out += residual

        out = self.relu(out)

        return out

#stage 特征融合阶段
class Stage(nn.Module):
    def __init__(self, stage, output_branches, c, bn_momentum):
        """
        stage:第几阶段
        output_branches:输出几个分支
        c:通道
        """
        super(Stage, self).__init__()
        self.stage = stage
        self.output_branches = output_branches

        self.branches = nn.ModuleList()

        # 每一分支都要先经过4个BasicBlock进行特征提取
        for i in range(self.stage):
            w = c * (2 ** i)
            branch = nn.Sequential(
                BasicBlock(w, w, bn_momentum=bn_momentum),
                BasicBlock(w, w, bn_momentum=bn_momentum),
                BasicBlock(w, w, bn_momentum=bn_momentum),
                BasicBlock(w, w, bn_momentum=bn_momentum),
            )
            self.branches.append(branch)

        self.fuse_layers = nn.ModuleList()

        for i in range(self.output_branches):
            self.fuse_layers.append(nn.ModuleList())
            for j in range(self.stage):
                if i == j:
                    self.fuse_layers[-1].append(nn.Sequential())
                    # i<j 进行上采样
                elif i < j:
                    self.fuse_layers[-1].append(nn.Sequential(
                        nn.Conv2d(c * (2 ** j), c * (2 ** i), kernel_size=1, stride=1, bias=False),
                        nn.BatchNorm2d(c * (2 ** i), momentum=0.1),
                        nn.Upsample(scale_factor=(2.0 ** (j - i)), mode='nearest'),
                    ))
    
                # i>j时,是为了将所有分支采样到和i相同的分辨率融合,这个时候j所代表分支的分辨率比i高
                else:
                    conv3x3s = []
                    # 又内嵌的一个循环,作用是i-j>1时,两个分支的分辨率差了不止两倍,此时还是两倍两倍往上采样,
                    # i-j=2时,j分支的分辨率比i分支大了4倍,需要上采样两次,循环两次
                    for k in range(i - j):
                        #当k==i-j-1时,仅循环一次,并采用当前模块,j分支进行conv3x3,stride=2,不用bias,接BN,不用RELU
                        if (k == i - j - 1):
                            conv3x3s.append(
                                nn.Sequential(
                                    nn.Conv2d(in_channels=c * (2 ** j), out_channels=c * (2 ** i), kernel_size=3,
                                              stride=2, padding=1,bias=False),
                                    nn.BatchNorm2d(c * (2 ** i), momentum=0.1)
                                )
                            )
                        else:
                            #k!=i-j-1时,此时需要循环两次,先采用当前模块,将j分支进行Conv3x3,stride=2下采样两倍,+BN+RELU
                            # 再用k == i-j-1模块(保证最后一次二倍下采样的卷积操作不使用RELU)---看别人的博客,猜测是为了保证融合后特征的多样性
                            conv3x3s.append(
                                nn.Sequential(
                                    nn.Conv2d(in_channels=c * (2 ** j), out_channels=c * (2 ** j), kernel_size=3,
                                              stride=2, padding=1,bias=False),
                                    nn.BatchNorm2d(c * (2 ** j), momentum=0.1),
                                    nn.ReLU(True)
                                )
                            )
                    self.fuse_layers[-1].append(nn.Sequential(*conv3x3s))
        self.relu = nn.ReLU(inplace=True)

    def forward(self, x):
    # 判断分支数
        assert len(self.branches) == len(x)

        x = [branch(b) for branch, b in zip(self.branches, x)]
        # 进行特征融合
        x_fused = []
        for i in range(len(self.fuse_layers)):
            for j in range(0, len(self.branches)):
                if j == 0:
                    x_fused.append(self.fuse_layers[i][0](x[0]))
                else:
                    x_fused[i] = x_fused[i] + self.fuse_layers[i][j](x[j])

        for i in range(len(x_fused)):
            x_fused[i] = self.relu(x_fused[i])

        return x_fused


class HRNet(nn.Module):
    def __init__(self, c=32, nof_joints=17, bn_momentum=0.1):
        super(HRNet, self).__init__()

        # stem层,进行初始特征提取
        self.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=2, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(64, momentum=bn_momentum)
        self.conv2 = nn.Conv2d(64, 64, kernel_size=3, stride=2, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(64, momentum=bn_momentum)
        self.relu = nn.ReLU(inplace=True)

        # Stage 1 (layer1)
        downsample = nn.Sequential(
            nn.Conv2d(64, 256, kernel_size=1, stride=1, bias=False),
            nn.BatchNorm2d(256, momentum=bn_momentum),
        )
        # 经过4个Bottleneck
        self.layer1 = nn.Sequential(
            Bottleneck(64, 64, downsample=downsample),
            Bottleneck(256, 64),
            Bottleneck(256, 64),
            Bottleneck(256, 64),
        )

        # transition1 两个分支
        self.transition1 = nn.ModuleList([
            nn.Sequential(
                nn.Conv2d(256, c, kernel_size=3, stride=1, padding=1, bias=False),
                nn.BatchNorm2d(c, momentum=bn_momentum),
                nn.ReLU(inplace=True),
            ),
            nn.Sequential(nn.Sequential(
                nn.Conv2d(256, c * (2 ** 1), kernel_size=3, stride=2, padding=1, bias=False),
                nn.BatchNorm2d(c * (2 ** 1), momentum=bn_momentum),
                nn.ReLU(inplace=True),
            )),
        ])

        # stage2   1
        self.stage2 = nn.Sequential(
            Stage(stage=2, output_branches=2, c=c, bn_momentum=bn_momentum),
        )

        # transition2 三个分支
        self.transition2 = nn.ModuleList([
            nn.Sequential(),
            nn.Sequential(),
            nn.Sequential(nn.Sequential(
                nn.Conv2d(c * (2 ** 1), c * (2 ** 2), kernel_size=3, stride=2, padding=1, bias=False),
                nn.BatchNorm2d(c * (2 ** 2), momentum=bn_momentum),
                nn.ReLU(inplace=True),
            )),
        ])

        # Stage 3 (stage3)  4
        self.stage3 = nn.Sequential(
            Stage(stage=3, output_branches=3, c=c, bn_momentum=bn_momentum),
            Stage(stage=3, output_branches=3, c=c, bn_momentum=bn_momentum),
            Stage(stage=3, output_branches=3, c=c, bn_momentum=bn_momentum),
            Stage(stage=3, output_branches=3, c=c, bn_momentum=bn_momentum),
        )

        # transition3 四个分支
        self.transition3 = nn.ModuleList([
            nn.Sequential(),
            nn.Sequential(),
            nn.Sequential(),
            nn.Sequential(nn.Sequential(
                nn.Conv2d(c * (2 ** 2), c * (2 ** 3), kernel_size=3, stride=2, padding=1, bias=False),
                nn.BatchNorm2d(c * (2 ** 3), momentum=bn_momentum),
                nn.ReLU(inplace=True),
            )),
        ])

        # Stage 4 (stage4)  3
        self.stage4 = nn.Sequential(
            Stage(stage=4, output_branches=4, c=c, bn_momentum=bn_momentum),
            Stage(stage=4, output_branches=4, c=c, bn_momentum=bn_momentum),
            Stage(stage=4, output_branches=1, c=c, bn_momentum=bn_momentum),
        )

        # final_layer
        self.final_layer = nn.Conv2d(c, nof_joints, kernel_size=1, stride=1)

    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.conv2(x)
        x = self.bn2(x)
        x = self.relu(x)

        x = self.layer1(x)

        x = [trans(x) for trans in self.transition1]

        x = self.stage2(x)

        x = [
            self.transition2[0](x[0]),
            self.transition2[1](x[1]),
            self.transition2[2](x[-1])
        ]

        x = self.stage3(x)

        x = [
            self.transition3[0](x[0]),
            self.transition3[1](x[1]),
            self.transition3[2](x[2]),
            self.transition3[3](x[-1])
        ]

        x = self.stage4(x)

        x = self.final_layer(x[0])

        return x


if __name__ == '__main__':
    model = HRNet()
    print(model)

    input = torch.randn(1, 3, 256, 192)
    out = model(input)
    print(out.shape)

参考

HighResolutionModule(高分辨率模块),这篇帖子写的很详细,可以作为参考
HRNet代码理解https://icver.blog.csdn.net/article/details/111867755

总结

通过看了,CPN,Simple baseline , HRNet这三篇论文,我感觉最大的收获就是学习到了人体姿态估计的研究思想,要将关键点精准的提取出来,就要将全局特征和局部特征都要保留,然后进一步思考怎样构建模型可以达到这样的效果,在考虑精准度的同时还要考虑计算量的问题。就是这次的代码我感觉和之前相比难很多,所以这周还没复现成功,下周再花点时间完成。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值