A-LOAM源码详解(一)scanRegistration.cpp

 文章主要是对A-LOAM源码的注释,在理解代码时,主要参考了以下两篇文章
 A-LOAM源码详解(一):scanRegistration.cpp] https://zhuanlan.zhihu.com/p/398149559
 LOAM笔记] https://www.cnblogs.com/wellp/p/8877990.html
 代码如下,有点长

#include <cmath>
#include <iostream>
#include <vector>
#include <string>
#include "aloam_velodyne/common.h"
#include "aloam_velodyne/tic_toc.h"
#include <nav_msgs/Odometry.h>
#include <opencv/cv.h>
#include <pcl_conversions/pcl_conversions.h>
#include <pcl/point_cloud.h>
#include <pcl/point_types.h>
#include <pcl/filters/voxel_grid.h>
#include <pcl/kdtree/kdtree_flann.h>
#include <ros/ros.h>
#include <sensor_msgs/Imu.h>
#include <sensor_msgs/PointCloud2.h>
#include <tf/transform_datatypes.h>
#include <tf/transform_broadcaster.h>

using std::atan2;
using std::cos;
using std::sin;

const double scanPeriod = 0.1;

const int systemDelay = 0; 
int systemInitCount = 0;
bool systemInited = false;
int N_SCANS = 0;
float cloudCurvature[400000];  //保存单帧16线扫描的点云的曲率
int cloudSortInd[400000];
int cloudNeighborPicked[400000];  //用于标记已处理的点
int cloudLabel[400000];           //赋予点云特征标签,用于将点云特征分类  

//如果点i的曲率小于j 则返回true
bool comp (int i,int j) { return (cloudCurvature[i]<cloudCurvature[j]); }

ros::Publisher pubLaserCloud;
ros::Publisher pubCornerPointsSharp;
ros::Publisher pubCornerPointsLessSharp;
ros::Publisher pubSurfPointsFlat;
ros::Publisher pubSurfPointsLessFlat;
ros::Publisher pubRemovePoints;
std::vector<ros::Publisher> pubEachScan;

bool PUB_EACH_LINE = false;

double MINIMUM_RANGE = 0.1; 

template <typename PointT>
void removeClosedPointCloud(const pcl::PointCloud<PointT> &cloud_in,
                              pcl::PointCloud<PointT> &cloud_out, float thres)
{
    if (&cloud_in != &cloud_out)
    {
        cloud_out.header = cloud_in.header;
        cloud_out.points.resize(cloud_in.points.size());
    }

    size_t j = 0; 
    //size_t在32位架构上是4字节,在64位架构上是8字节,在不同架构上进行编译时需要注意这个问题。
    //而int在不同架构下都是4字节,与size_t不同;且int为带符号数,size_t为无符号数。
    //与int固定四个字节有所不同,size_t的取值range是目标平台下最大可能的数组尺寸,
    //一些平台下size_t的范围小于int的正数范围,又或者大于unsigned int. 使用Int既有可能浪费,又有可能范围不够大。
    
    for (size_t i = 0; i < cloud_in.points.size(); ++i)
    {   
        //去除以激光雷达为中心原点,半径thres以内的点
        if (cloud_in.points[i].x * cloud_in.points[i].x + cloud_in.points[i].y * cloud_in.points[i].y + cloud_in.points[i].z * cloud_in.points[i].z < thres * thres)
            continue;  //跳出当前循环,即执行到这个语句,后面的都不执行了,强制开始下一次循环,该语句只能在循环中使用
        cloud_out.points[j] = cloud_in.points[i];
        j++;  //对滤除后的点进行计数
    }
    if (j != cloud_in.points.size()) 
    {
        cloud_out.points.resize(j); //如果有点被滤除掉,则对输出点云进行resize
    }

    cloud_out.height = 1;   //如果cloud是无序的 height是1
    cloud_out.width = static_cast<uint32_t>(j);  //14241左右 16线数据
    //printf("cloud_out_width:%d\n",cloud_out.width);
    cloud_out.is_dense = true;
}

//对获取的每一帧点云数据进行处理
void laserCloudHandler(const sensor_msgs::PointCloud2ConstPtr &laserCloudMsg)
{
    if (!systemInited)
    { 
        systemInitCount++;
        if (systemInitCount >= systemDelay)
        {
            systemInited = true;
        }
        else
            return;
    }

    TicToc t_whole;     
    TicToc t_prepare;  
    //对计算时间间隔的类初始化
    //vector是一个封装了动态大小数组的顺序容器
    std::vector<int> scanStartInd(N_SCANS, 0);   //长度为N_SCANS
    std::vector<int> scanEndInd(N_SCANS, 0);
    pcl::PointCloud<pcl::PointXYZ> laserCloudIn;
    //将ros系统中接受到的LaserCloudMsg转为LaserCloudIn作为A-LOAM的点云输入 其包含的数据是相同的,只是格式不同,便于处理
    pcl::fromROSMsg(*laserCloudMsg, laserCloudIn);  
    std::vector<int> indices;
    //首先对点云滤波,去除NaN值的无效点,以及在Lidar坐标系原点MINIMUM_RANGE距离以内的点
    pcl::removeNaNFromPointCloud(laserCloudIn, laserCloudIn, indices);
    removeClosedPointCloud(laserCloudIn, laserCloudIn, MINIMUM_RANGE);


    int cloudSize = laserCloudIn.points.size();  //点云数目 与刚才的cloud_out.width相同
    // printf("cloudSize is %d \n",cloudSize);
    float startOri = -atan2(laserCloudIn.points[0].y, laserCloudIn.points[0].x);
    float endOri = -atan2(laserCloudIn.points[cloudSize - 1].y,
                          laserCloudIn.points[cloudSize - 1].x) +
                   2 * M_PI;

    if (endOri - startOri > 3 * M_PI)
    {
        endOri -= 2 * M_PI;
    }
    else if (endOri - startOri < M_PI)
    {
        endOri += 2 * M_PI;
    }
    //printf("end Ori %f\n", endOri);
    //printf("atan30 is :%.1f \n",float(atan(-1/2)));
    bool halfPassed = false;
    int count = cloudSize;
    PointType point;  //typedef pcl::PointXYZI PointType;
    std::vector<pcl::PointCloud<PointType>> laserCloudScans(N_SCANS); //定义一个长度为16的点云向量
    //这个for循环,主要实现对无序点云中每个点所属的scadid(线数)划分、同线数内,点位置的确定
    for (int i = 0; i < cloudSize; i++)
    {
        point.x = laserCloudIn.points[i].x;
        point.y = laserCloudIn.points[i].y;
        point.z = laserCloudIn.points[i].z;

        float angle = atan(point.z / sqrt(point.x * point.x + point.y * point.y)) * 180 / M_PI;
        int scanID = 0;
        
        if (N_SCANS == 16)
        {
            scanID = int((angle + 15) / 2 + 0.5);  //确定当前点的scanid 0-15
            //printf("scanid is %d \n",scanID);
            if (scanID > (N_SCANS - 1) || scanID < 0)
            {
                count--;
                continue;
            }
        }
        else if (N_SCANS == 32)
        {
            scanID = int((angle + 92.0/3.0) * 3.0 / 4.0);
            if (scanID > (N_SCANS - 1) || scanID < 0)
            {
                count--;
                continue;
            }
        }
        else if (N_SCANS == 64)
        {   
            if (angle >= -8.83)
                scanID = int((2 - angle) * 3.0 + 0.5);
            else
                scanID = N_SCANS / 2 + int((-8.83 - angle) * 2.0 + 0.5);

            // use [0 50]  > 50 remove outlies 
            if (angle > 2 || angle < -24.33 || scanID > 50 || scanID < 0)
            {
                count--;
                continue;
            }
        }
        else
        {
            printf("wrong scan number\n");
            ROS_BREAK();
        }
        //printf("angle %f scanID %d \n", angle, scanID);

        float ori = -atan2(point.y, point.x);  //对每一个点都求一次y/x的反正切
        if (!halfPassed)  
        { 
            if (ori < startOri - M_PI / 2)
            {
                ori += 2 * M_PI;
            }
            else if (ori > startOri + M_PI * 3 / 2)
            {
                ori -= 2 * M_PI;
            }

            if (ori - startOri > M_PI)
            {
                halfPassed = true;
            }
        }
        else
        {
            ori += 2 * M_PI;
            if (ori < endOri - M_PI * 3 / 2)
            {
                ori += 2 * M_PI;
            }
            else if (ori > endOri + M_PI / 2)
            {
                ori -= 2 * M_PI;
            }
        }

        //归一化 每个点的夹角比上扫描一圈的整个夹角 用realtime给每个点分配fireid
        float relTime = (ori - startOri) / (endOri - startOri); 
        //小数部分 扫描周期*点所在位置  得到在一圈扫描中扫描到该点时的时间  intensity  强度
        point.intensity = scanID + scanPeriod * relTime; // intensity字段的整数部分存放scanID,小数部分存放归一化后的fireID
        laserCloudScans[scanID].push_back(point); // 将点根据scanID放到对应的子点云laserCloudScans中
    }
    
    cloudSize = count;
    printf("points size %d \n", cloudSize);

    //一帧点云 有序点云 按照scanID和fireid进行顺序存放
    pcl::PointCloud<PointType>::Ptr laserCloud(new pcl::PointCloud<PointType>());
    //将子点云laserCloudScans合成一帧点云laserCloud 这里laserCloud也可以认为是有序点云按照scanid fireid的顺序存放
    for (int i = 0; i < N_SCANS; i++)
    { 
        scanStartInd[i] = laserCloud->size() + 5;  //记录每个scan开始的index 忽略前5个点
        *laserCloud += laserCloudScans[i];
        //记录每个scan结束的index,忽略后5个点,开始和结束处的点云容易产生不闭合的“接缝”对提取edge feature不利
        scanEndInd[i] = laserCloud->size() - 6;   
    }
    // printf("scanStartInd %d \n",scanStartInd[0]);   5
    // printf("scanEndInd %d \n",scanEndInd[0]);       900左右
    // printf("laserCloudScans %d \n",laserCloudScans[0]);    
    printf("prepare time %f \n", t_prepare.toc());

    //计算点的曲率
    for (int i = 5; i < cloudSize - 5; i++)   //cloudSize 是16条线的点数  900*16 =14400 左右
    {   
        //从第六个点开始,计算曲率时,选取前后5个点进行计算  点的编号从0开始
        float diffX = laserCloud->points[i - 5].x + laserCloud->points[i - 4].x + laserCloud->points[i - 3].x + laserCloud->points[i - 2].x + laserCloud->points[i - 1].x - 10 * laserCloud->points[i].x + laserCloud->points[i + 1].x + laserCloud->points[i + 2].x + laserCloud->points[i + 3].x + laserCloud->points[i + 4].x + laserCloud->points[i + 5].x;
        float diffY = laserCloud->points[i - 5].y + laserCloud->points[i - 4].y + laserCloud->points[i - 3].y + laserCloud->points[i - 2].y + laserCloud->points[i - 1].y - 10 * laserCloud->points[i].y + laserCloud->points[i + 1].y + laserCloud->points[i + 2].y + laserCloud->points[i + 3].y + laserCloud->points[i + 4].y + laserCloud->points[i + 5].y;
        float diffZ = laserCloud->points[i - 5].z + laserCloud->points[i - 4].z + laserCloud->points[i - 3].z + laserCloud->points[i - 2].z + laserCloud->points[i - 1].z - 10 * laserCloud->points[i].z + laserCloud->points[i + 1].z + laserCloud->points[i + 2].z + laserCloud->points[i + 3].z + laserCloud->points[i + 4].z + laserCloud->points[i + 5].z;
        //printf("diffX %f \n",laserCloud->points[i - 5].x);
        cloudCurvature[i] = diffX * diffX + diffY * diffY + diffZ * diffZ;   //curvature 曲率
        cloudSortInd[i] = i;  //一轮循环之后变成cloudSize - 5
        cloudNeighborPicked[i] = 0;  //点有没有被选择为feature点
        cloudLabel[i] = 0;  // Label 2: corner_sharp 
                            // Label 1: corner_less_sharp  包含label 2
                            // Label -1: surf_flat
                            // Label 0: surf_less_flat 包含label -1 因为点太多,最后会降采样
    }


    TicToc t_pts;

    pcl::PointCloud<PointType> cornerPointsSharp;
    pcl::PointCloud<PointType> cornerPointsLessSharp;
    pcl::PointCloud<PointType> surfPointsFlat;
    pcl::PointCloud<PointType> surfPointsLessFlat;

    printf("cloudSortInd  %d %d %d %d %d\n",cloudSortInd,&cloudSortInd[0],cloudSortInd[2],cloudSortInd[3],cloudSortInd[5]);
    //输出结果 cloudSortInd  -638208576 -638208576 0 0 5   
    //直接用cloudSortInd打印出的是数组的首地址对应于cloudSortInd[0] 通过地址来索引数据
    float t_q_sort = 0;
    //for循环  根据曲率计算四种特征点
    for (int i = 0; i < N_SCANS; i++)  //按照scan的顺序 即线束的顺序 提取4种特征点
    {
        if( scanEndInd[i] - scanStartInd[i] < 6)  //如果该scan的点少于7个就跳过
            continue;
        pcl::PointCloud<PointType>::Ptr surfPointsLessFlatScan(new pcl::PointCloud<PointType>);
        for (int j = 0; j < 6; j++)  //该scan分成6小段 执行特征检测
        {
            int sp = scanStartInd[i] + (scanEndInd[i] - scanStartInd[i]) * j / 6;      
            int ep = scanStartInd[i] + (scanEndInd[i] - scanStartInd[i]) * (j + 1) / 6 - 1;
            //假设scanStartInd[0]=5  scanEndInd[0]=900
            //sp 0    154  303    subscan的起始index
            //ep 153  302  ....   subscan的结束index

            TicToc t_tmp;
            //根据曲率由小到大对subscan的点进行sort sort是排序函数  每一线都得到6个由小到大的点集
            //cloudSortInd为数组首元素地址,该函数表示在内存块 cloudSortInd+sp 到 cloudSortInd + ep + 1
            //通过比较曲率之间的大小改变对应内存块储存的曲率值实现排序
            //bool comp (int i,int j) { return (cloudCurvature[i]<cloudCurvature[j]); }
            //使用cloudSortInd是使用地址来索引求曲率的点
            std::sort (cloudSortInd + sp, cloudSortInd + ep + 1, comp); 
            t_q_sort += t_tmp.toc();

            int largestPickedNum = 0;
            for (int k = ep; k >= sp; k--) //从后往前,即从曲率大的点开始提取corner feature
            {
                int ind = cloudSortInd[k];   //ind 从5-14155左右 为曲率最大的点的索引
                //printf("ind %d \n",ind);
                if (cloudNeighborPicked[ind] == 0 &&
                    cloudCurvature[ind] > 0.1)  //如果该点没有被选择,且曲率大于0.1
                {

                    largestPickedNum++;
                    if (largestPickedNum <= 2)//该subscan中曲率最大的前2个点认为是corner_sharp的特征点
                    {                        
                        cloudLabel[ind] = 2;
                        cornerPointsSharp.push_back(laserCloud->points[ind]); //push_back 在末尾行添加元素
                        cornerPointsLessSharp.push_back(laserCloud->points[ind]);
                    }
                    else if (largestPickedNum <= 20)//曲率中大于0.1的前20个点被认为是corner_less_sharp的特征点
                    {                        
                        cloudLabel[ind] = 1; 
                        cornerPointsLessSharp.push_back(laserCloud->points[ind]);
                    }
                    else
                    {
                        break;
                    }

                    cloudNeighborPicked[ind] = 1;  //标记该点被选择过了

                    //与当前点距离的平方 <=0.05的点标记为选择过,避免特征点密集分布
                    for (int l = 1; l <= 5; l++)  // 参数5的选择
                    {
                        float diffX = laserCloud->points[ind + l].x - laserCloud->points[ind + l - 1].x;
                        float diffY = laserCloud->points[ind + l].y - laserCloud->points[ind + l - 1].y;
                        float diffZ = laserCloud->points[ind + l].z - laserCloud->points[ind + l - 1].z;
                        if (diffX * diffX + diffY * diffY + diffZ * diffZ > 0.05)
                        {
                            break;
                        }

                        cloudNeighborPicked[ind + l] = 1;
                    }
                    for (int l = -1; l >= -5; l--)
                    {
                        float diffX = laserCloud->points[ind + l].x - laserCloud->points[ind + l + 1].x;
                        float diffY = laserCloud->points[ind + l].y - laserCloud->points[ind + l + 1].y;
                        float diffZ = laserCloud->points[ind + l].z - laserCloud->points[ind + l + 1].z;
                        if (diffX * diffX + diffY * diffY + diffZ * diffZ > 0.05)
                        {
                            break;
                        }

                        cloudNeighborPicked[ind + l] = 1;
                    }
                }
            }

            //提取surfer平面feature 与上述类似,选取该subscan曲率最小的前4个点为surf_flat
            int smallestPickedNum = 0;
            for (int k = sp; k <= ep; k++)
            {
                int ind = cloudSortInd[k];

                if (cloudNeighborPicked[ind] == 0 &&
                    cloudCurvature[ind] < 0.1)
                {

                    cloudLabel[ind] = -1; 
                    surfPointsFlat.push_back(laserCloud->points[ind]);

                    smallestPickedNum++;
                    if (smallestPickedNum >= 4)
                    { 
                        break;
                    }

                    cloudNeighborPicked[ind] = 1;
                    for (int l = 1; l <= 5; l++)
                    { 
                        float diffX = laserCloud->points[ind + l].x - laserCloud->points[ind + l - 1].x;
                        float diffY = laserCloud->points[ind + l].y - laserCloud->points[ind + l - 1].y;
                        float diffZ = laserCloud->points[ind + l].z - laserCloud->points[ind + l - 1].z;
                        if (diffX * diffX + diffY * diffY + diffZ * diffZ > 0.05)
                        {
                            break;
                        }

                        cloudNeighborPicked[ind + l] = 1;
                    }
                    for (int l = -1; l >= -5; l--)
                    {
                        float diffX = laserCloud->points[ind + l].x - laserCloud->points[ind + l + 1].x;
                        float diffY = laserCloud->points[ind + l].y - laserCloud->points[ind + l + 1].y;
                        float diffZ = laserCloud->points[ind + l].z - laserCloud->points[ind + l + 1].z;
                        if (diffX * diffX + diffY * diffY + diffZ * diffZ > 0.05)
                        {
                            break;
                        }

                        cloudNeighborPicked[ind + l] = 1;
                    }
                }
            }

            //其他的非corner特征点与surf_flat特征点一起组成surf_flat_less特征点
            //eg: corner_sharp:192 corner_less_sharp:1763 surf_flat:375 surf_less_flat:6944 来源于对应的发布数据topic的width值
            for (int k = sp; k <= ep; k++)
            {
                if (cloudLabel[k] <= 0)
                {
                    surfPointsLessFlatScan->push_back(laserCloud->points[k]);
                }
            }
        }

        //最后对该scan点云中提取的所有surf_less_flat点进行降采样
        pcl::PointCloud<PointType> surfPointsLessFlatScanDS;
        pcl::VoxelGrid<PointType> downSizeFilter;  //体素网格滤波
        //surfPointsLessFlatScan存放surf_less_flat的特征点
        downSizeFilter.setInputCloud(surfPointsLessFlatScan); 
        downSizeFilter.setLeafSize(0.2, 0.2, 0.2);  //设置体素大小
        downSizeFilter.filter(surfPointsLessFlatScanDS);  //执行滤波

        surfPointsLessFlat += surfPointsLessFlatScanDS;
    }
    printf("sort q time %f \n", t_q_sort);
    printf("seperate points time %f \n", t_pts.toc());

    //发布滤波后的当前帧点云数据
    //将四种特征点云转为ros格式的点云
    sensor_msgs::PointCloud2 laserCloudOutMsg;
    pcl::toROSMsg(*laserCloud, laserCloudOutMsg);
    //laserCloudMsg 从激光雷达得到的点云数据 stamp的作用 --> 和原始点云数据的stamp保持一致
    laserCloudOutMsg.header.stamp = laserCloudMsg->header.stamp; 
    laserCloudOutMsg.header.frame_id = "/camera_init";
    pubLaserCloud.publish(laserCloudOutMsg);

    //发布corner_sharp特征点数据
    sensor_msgs::PointCloud2 cornerPointsSharpMsg;
    pcl::toROSMsg(cornerPointsSharp, cornerPointsSharpMsg);
    cornerPointsSharpMsg.header.stamp = laserCloudMsg->header.stamp;
    cornerPointsSharpMsg.header.frame_id = "/camera_init";
    pubCornerPointsSharp.publish(cornerPointsSharpMsg);

    //发布corner_less_sharp特征点数据
    sensor_msgs::PointCloud2 cornerPointsLessSharpMsg;
    pcl::toROSMsg(cornerPointsLessSharp, cornerPointsLessSharpMsg);
    cornerPointsLessSharpMsg.header.stamp = laserCloudMsg->header.stamp;
    cornerPointsLessSharpMsg.header.frame_id = "/camera_init";
    pubCornerPointsLessSharp.publish(cornerPointsLessSharpMsg);

    //发布surf_flat特征点数据
    sensor_msgs::PointCloud2 surfPointsFlat2;
    pcl::toROSMsg(surfPointsFlat, surfPointsFlat2);
    surfPointsFlat2.header.stamp = laserCloudMsg->header.stamp;
    surfPointsFlat2.header.frame_id = "/camera_init";
    pubSurfPointsFlat.publish(surfPointsFlat2);

    //发布surf_less_flat特征点数据
    sensor_msgs::PointCloud2 surfPointsLessFlat2;
    pcl::toROSMsg(surfPointsLessFlat, surfPointsLessFlat2);
    surfPointsLessFlat2.header.stamp = laserCloudMsg->header.stamp;
    surfPointsLessFlat2.header.frame_id = "/camera_init";
    pubSurfPointsLessFlat.publish(surfPointsLessFlat2);

    // pub each scam
    if(PUB_EACH_LINE)
    {
        for(int i = 0; i< N_SCANS; i++)
        {
            sensor_msgs::PointCloud2 scanMsg;
            pcl::toROSMsg(laserCloudScans[i], scanMsg);
            scanMsg.header.stamp = laserCloudMsg->header.stamp;
            scanMsg.header.frame_id = "/camera_init";
            pubEachScan[i].publish(scanMsg);
        }
    }

    printf("scan registration time %f ms *************\n", t_whole.toc());
    if(t_whole.toc() > 100)
        ROS_WARN("scan registration process over 100ms");
}

int main(int argc, char **argv)
{
    ros::init(argc, argv, "scanRegistration");   //注册节点
    ros::NodeHandle nh;

    nh.param<int>("scan_line", N_SCANS, 16);    //参数定义 雷达线数

    nh.param<double>("minimum_range", MINIMUM_RANGE, 0.1);   //定义最小有效距离

    printf("scan line number %d \n", N_SCANS);

    if(N_SCANS != 16 && N_SCANS != 32 && N_SCANS != 64)
    {
        printf("only support velodyne with 16, 32 or 64 scan line!");
        return 0;
    }

    //话题订阅 首先实例化对象  参数依次为 消息类型  topic  消息队列 回调函数
    ros::Subscriber subLaserCloud = nh.subscribe<sensor_msgs::PointCloud2>("/velodyne_points", 100, laserCloudHandler);

    pubLaserCloud = nh.advertise<sensor_msgs::PointCloud2>("/velodyne_cloud_2", 100);

    pubCornerPointsSharp = nh.advertise<sensor_msgs::PointCloud2>("/laser_cloud_sharp", 100);

    pubCornerPointsLessSharp = nh.advertise<sensor_msgs::PointCloud2>("/laser_cloud_less_sharp", 100);

    pubSurfPointsFlat = nh.advertise<sensor_msgs::PointCloud2>("/laser_cloud_flat", 100);

    pubSurfPointsLessFlat = nh.advertise<sensor_msgs::PointCloud2>("/laser_cloud_less_flat", 100);

    pubRemovePoints = nh.advertise<sensor_msgs::PointCloud2>("/laser_remove_points", 100);

    if(PUB_EACH_LINE)
    {
        for(int i = 0; i < N_SCANS; i++)
        {
            ros::Publisher tmp = nh.advertise<sensor_msgs::PointCloud2>("/laser_scanid_" + std::to_string(i), 100);
            pubEachScan.push_back(tmp);
        }
    }
    ros::spin();

    return 0;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

摸鱼带师小弟

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

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

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

打赏作者

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

抵扣说明:

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

余额充值