IMU和视觉融合学习笔记

文章探讨了视觉与IMU融合在位姿估计中的重要性,特别是在高速运动和光照干扰等条件下提高定位鲁棒性。介绍了融合的两种方式——松耦合和紧耦合,以及它们在优化纯视觉SLAM中的作用,如单目尺度优化和视觉惯性联合优化。同时,文章还提及了初始化参数估计和IMU标定的过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

利用纯视觉信息进行位姿估计,对运动物体、光照干扰、场景纹理缺失等情况,定位效果不够鲁棒。当下,视觉与IMU融合(VI-SLAM)逐渐成为常见的多传感器融合方式。视觉信息与IMU 数据进行融合,根据融合方式同样可分为基于滤波器和基于优化两类。按照是否把图像特征信息加入状态向量,又可以分为松融合与紧融合两类。

双目相机加入imu模块的的好处,是解决了高速运动中间数据处理的问题。相机运动过程中间出现模糊,两帧之间重叠区域太少,因此很难做特征点匹配。相机自己又能在慢速运动中间解决 imu的漂移问题,这两者是互补的。在之前研究视觉和IMU的基础上,开展IMU和视觉融合学习,并做记录。

在这里插入图片描述

一、IMU和视觉融合的方法

1、IMU

IMU以高频率(100HZ或200HZ)输出载体的角速度w和线加速度a,解算出高频率(100HZ或200HZ)的载体速度V、位置P以及旋转R

2、相机

零偏和噪声会比较大,以至于长时间使用后偏移的就很快,但是如果使用高精惯导,这个漂移误差会降低些,因为它是一种积分状态,从开始时间一直在持续积分,积分到不再使用为止,也就是它的V、P、R,他们分别每一个时刻都有一个误差,这个误差会产生迭代,所以长时间使用后就会漂移,

相机以30Hz或20Hz获得场景中的图像信息,利用图像中的特征信息,解算载体的旋转和平移
相机可以获得丰富的环境信息,并在长时间内的漂移误差较小,但在快速运动或旋转的环境中容易发生跟踪丢失的情况,且在面对挑战环境时定位精度会明显下降

3、融合的目标

融合的目标就是进行一个相互的补偿,主要有三个目标可以进行相互的补偿:

  • 利用视觉里程计对IMU的累积漂移进行补偿,降低惯导的漂移误差
  • 对于单目视觉传感器,可以利用IMU进行场景深度的校正,缓解单目相机尺度不确定性问题
  • IMU的输出与环境无关,不受环境变化的约束,利用IMU与视觉进行补偿可以提高视觉里程计位姿估计的鲁棒性

在这里插入图片描述

note:IMU加进视觉里面,并不是提高了视觉的定位精度,比如ORB-SLAM3的纯视觉定位精度是高于视觉+IMU的定位精度的,纯视觉是更高精度的里程计,过程相当于一个高精度融合低精度的,所以最后多模态的定位精度没有纯视觉的高,所以视觉+IMU提高的是系统的鲁棒性,并非提高精度

4、IMU如何和视觉传感器实现融合

(1)松耦合

松耦合是每一个传感器算出一个轨迹,对算出的结果进行再一次的融合

(2)紧耦合

紧耦合是在估计的时候,就是把IMU的状态,视觉传感器的状态等传感器的状态放在一起进行位姿估计,最后融合出来是只存在一个轨迹误差,其实不需要单独计算各个传感器的轨迹。
在这里插入图片描述
在这里插入图片描述
现在大多数研究使用的都是紧耦合的状态,如果系统特别大,视觉惯性SLAM所有导航模块中间的一个小分支,那么更多的用的是松耦合的情况,因为要避免单个模块失效的情况。

现在大多数系统上看到的问题都是由基于优化的,比如VINS、ORB-SLAM,都是基于优化算法的紧耦合。滤波只考虑当前的状态,相当于“得过且过”,想要得到当前的状态,只需要直达搜上一个状态,是一步一步推算出来的;优化过程是长期的一个过程,想要得到当前的状态,从过去很久之前到当前的所有的一个状态变化

以前的情况是基于优化的算法计算量比较大,基于滤波的计算量稍小一些,但是发展中,基于滤波的种方法也不仅仅值依靠前一个状态,也涉及了一个滑动窗模型,而基于优化的方法,使用稀疏边缘化的方式,实现了计算量的降低,从而减负,优化算法的实时性还是不错的

5、IMU如何优化纯视觉SLAM

(1)单目尺度优化

单目尺度可以理解为比例尺,计算视觉的轨迹需要特征的匹配,特征匹配就是每两个图像帧之间非常快速的运动,纯视觉开始运动的时候比较依赖前几帧选择出来的尺度,慢慢进行迭代。

遇到大场景的情况下,运动速度非常的剧烈,这个时候尺度是非常的不准的,这个时候引入IMU,IMU预积分的过程中是可以解算出PRV,IMU单独的模块可以解算出载体的平移旋转和速度,IMU解算出来的平移不是靠特征匹配的方式得到的,和场景是没有关系的,所以IMU在短时间内效果还是不错的,长时间就很很容易漂移(比如说100Hz,也就是0.01秒之内,对平移来说还是相对而言比较准确的)。

载体上是固连的,相机和IMU两个模块,那么我们就可以通过这两个模块,一个是已知的、计算的还算比较准确的平移,一个是通过图像匹配估计出来的平移,这样就可以计算出来这两个之间的一个尺度,
在这里插入图片描述

尺度的大小主要与相机的平移运动有关,IMU可以解算出PRV,因此可以利用IMU估计得到的平移优化纯视觉的平移尺度

(2)视觉惯性联合优化

当我们视觉的信息在模糊的情况下,整个的联合优化的过程,可以保证轨迹估计继续进行,IMU预积分会存在一些估计的误差,而视觉存在投影误差,PnP、3D到3D投影误差,2D到3D的投影误差,把这两个同时放到一个优化方程里面,我们想要缩小所有的残差,进一步的到当前更好的当前状态X,相当于实现了扩大考虑的因素,

在这里插入图片描述
如果是一个纯视觉的里程计,那就是C这一部分,
在这里插入图片描述

IMU的话就是加了一个B,用了一个边缘化的操作。
在这里插入图片描述

二、IMU和视觉的初始化及参数估计方法

1、VI初始化需要初始化那些参数?

(1)视觉

确定载体的初始位姿、初始关键帧以及初始空间点的位置

保留在算法中的始终都是关键帧,确定载体的初始位姿为母关键帧,第一个关键帧设置为i,当前的位姿为单位阵,设置为初始关键,此刻确定的是初始关键帧所对应的初始空间点的位置确定好着三个量之后,完成的视觉初始化

(2)IMU

估计IMU的偏置、异构传感器的时延、重力方向对准

IMU的偏置分为两个:陀螺仪的偏置、加速度计的偏置
现在的研究大部分没有考虑到异构传感器的很长的时延

(3)VI

传感器旋转平移外参、单目尺度因子

传感器旋转平移外参就是所谓的标定

标定算法现在还是再出很多好论文,标定所发也就是这视觉、IMU和VI这三方面的内容,也可以从这几方面入手,写一下自己的标定算法

2、IMU标定

(1)陀螺仪

ω ~ m 是测量量; ω t 是真值; b ω 是零偏; \tilde{ω}_{m} 是测量量;{ω}_{t} 是真值;{b}_{ω} 是零偏; ω~m是测量量;ωt是真值;bω是零偏;

n ω 是高斯白噪声,出场预设的,不会去拿来标定 {n}_{ω}是高斯白噪声,出场预设的,不会去拿来标定 nω是高斯白噪声,出场预设的,不会去拿来标定
在这里插入图片描述

(2)加速度计测得的线加速度

a ω ~ 是测得的值, R W B ( a w t − a w g + b a + n a ) 是真值, \tilde{a _{ω} }是测得的值,R_W^B ( a_{wt}-a_{wg}+ b_{a}+n_{a} )是真值, aω~是测得的值,RWB(awtawg+ba+na)是真值,
加速度计测的是重力方向的加速度,需要把世界坐标系转化到 B 系, 加速度计测的是重力方向的加速度,需要把世界坐标系转化到B系, 加速度计测的是重力方向的加速度,需要把世界坐标系转化到B系,
后面是 b a 是零偏; n a 是高斯白噪声 后面是{b}_{a} 是零偏; {n}_{a}是高斯白噪声 后面是ba是零偏;na是高斯白噪声
在这里插入图片描述

3、ORB-SLAM3的IMU初始化

先进行视觉初始化,然后IMU状态变量是和Map紧密关联的,而这在一定程度上也就导致了IMU和Map的耦合,不易分开

void LocalMapping::InitializeIMU(float priorG, float priorA, bool bFIBA)
{
    if (mbResetRequested)
        return;

    float minTime;
    int nMinKF;
    if (mbMonocular)
    {
        minTime = 2.0;
        nMinKF = 10;
    }
    else
    {
        minTime = 1.0;
        nMinKF = 10;
    }


    if(mpAtlas->KeyFramesInMap()<nMinKF)
        return;

    // Retrieve all keyframe in temporal order
    list<KeyFrame*> lpKF;
    KeyFrame* pKF = mpCurrentKeyFrame;
    while(pKF->mPrevKF)
    {
        lpKF.push_front(pKF);
        pKF = pKF->mPrevKF;
    }
    lpKF.push_front(pKF);
    vector<KeyFrame*> vpKF(lpKF.begin(),lpKF.end());

    if(vpKF.size()<nMinKF)
        return;

    mFirstTs=vpKF.front()->mTimeStamp;
    if(mpCurrentKeyFrame->mTimeStamp-mFirstTs<minTime)
        return;

    bInitializing = true;

    while(CheckNewKeyFrames())
    {
        ProcessNewKeyFrame();
        vpKF.push_back(mpCurrentKeyFrame);
        lpKF.push_back(mpCurrentKeyFrame);
    }

    const int N = vpKF.size();
    IMU::Bias b(0,0,0,0,0,0);

    // Compute and KF velocities mRwg estimation
    if (!mpCurrentKeyFrame->GetMap()->isImuInitialized())
    {
        Eigen::Matrix3f Rwg;
        Eigen::Vector3f dirG;
        dirG.setZero();
        for(vector<KeyFrame*>::iterator itKF = vpKF.begin(); itKF!=vpKF.end(); itKF++)
        {
            if (!(*itKF)->mpImuPreintegrated)
                continue;
            if (!(*itKF)->mPrevKF)
                continue;

            dirG -= (*itKF)->mPrevKF->GetImuRotation() * (*itKF)->mpImuPreintegrated->GetUpdatedDeltaVelocity();
            Eigen::Vector3f _vel = ((*itKF)->GetImuPosition() - (*itKF)->mPrevKF->GetImuPosition())/(*itKF)->mpImuPreintegrated->dT;
            (*itKF)->SetVelocity(_vel);
            (*itKF)->mPrevKF->SetVelocity(_vel);
        }

        dirG = dirG/dirG.norm();
        Eigen::Vector3f gI(0.0f, 0.0f, -1.0f);
        Eigen::Vector3f v = gI.cross(dirG);
        const float nv = v.norm();
        const float cosg = gI.dot(dirG);
        const float ang = acos(cosg);
        Eigen::Vector3f vzg = v*ang/nv;
        Rwg = Sophus::SO3f::exp(vzg).matrix();
        mRwg = Rwg.cast<double>();
        mTinit = mpCurrentKeyFrame->mTimeStamp-mFirstTs;
    }
    else
    {
        mRwg = Eigen::Matrix3d::Identity();
        mbg = mpCurrentKeyFrame->GetGyroBias().cast<double>();
        mba = mpCurrentKeyFrame->GetAccBias().cast<double>();
    }

    mScale=1.0;

    mInitTime = mpTracker->mLastFrame.mTimeStamp-vpKF.front()->mTimeStamp;

    std::chrono::steady_clock::time_point t0 = std::chrono::steady_clock::now();
    Optimizer::InertialOptimization(mpAtlas->GetCurrentMap(), mRwg, mScale, mbg, mba, mbMonocular, infoInertial, false, false, priorG, priorA);

    std::chrono::steady_clock::time_point t1 = std::chrono::steady_clock::now();

    if (mScale<1e-1)
    {
        cout << "scale too small" << endl;
        bInitializing=false;
        return;
    }

    // Before this line we are not changing the map
    {
        unique_lock<mutex> lock(mpAtlas->GetCurrentMap()->mMutexMapUpdate);
        if ((fabs(mScale - 1.f) > 0.00001) || !mbMonocular) {
            Sophus::SE3f Twg(mRwg.cast<float>().transpose(), Eigen::Vector3f::Zero());
            mpAtlas->GetCurrentMap()->ApplyScaledRotation(Twg, mScale, true);
            mpTracker->UpdateFrameIMU(mScale, vpKF[0]->GetImuBias(), mpCurrentKeyFrame);
        }

        // Check if initialization OK
        if (!mpAtlas->isImuInitialized())
            for (int i = 0; i < N; i++) {
                KeyFrame *pKF2 = vpKF[i];
                pKF2->bImu = true;
            }
    }

    mpTracker->UpdateFrameIMU(1.0,vpKF[0]->GetImuBias(),mpCurrentKeyFrame);
    if (!mpAtlas->isImuInitialized())
    {
        mpAtlas->SetImuInitialized();
        mpTracker->t0IMU = mpTracker->mCurrentFrame.mTimeStamp;
        mpCurrentKeyFrame->bImu = true;
    }

    std::chrono::steady_clock::time_point t4 = std::chrono::steady_clock::now();
    if (bFIBA)
    {
        if (priorA!=0.f)
            Optimizer::FullInertialBA(mpAtlas->GetCurrentMap(), 100, false, mpCurrentKeyFrame->mnId, NULL, true, priorG, priorA);
        else
            Optimizer::FullInertialBA(mpAtlas->GetCurrentMap(), 100, false, mpCurrentKeyFrame->mnId, NULL, false);
    }

    std::chrono::steady_clock::time_point t5 = std::chrono::steady_clock::now();

    Verbose::PrintMess("Global Bundle Adjustment finished\nUpdating map ...", Verbose::VERBOSITY_NORMAL);

    // Get Map Mutex
    unique_lock<mutex> lock(mpAtlas->GetCurrentMap()->mMutexMapUpdate);

    unsigned long GBAid = mpCurrentKeyFrame->mnId;

    // Process keyframes in the queue
    while(CheckNewKeyFrames())
    {
        ProcessNewKeyFrame();
        vpKF.push_back(mpCurrentKeyFrame);
        lpKF.push_back(mpCurrentKeyFrame);
    }

    // Correct keyframes starting at map first keyframe
    list<KeyFrame*> lpKFtoCheck(mpAtlas->GetCurrentMap()->mvpKeyFrameOrigins.begin(),mpAtlas->GetCurrentMap()->mvpKeyFrameOrigins.end());

    while(!lpKFtoCheck.empty())
    {
        KeyFrame* pKF = lpKFtoCheck.front();
        const set<KeyFrame*> sChilds = pKF->GetChilds();
        Sophus::SE3f Twc = pKF->GetPoseInverse();
        for(set<KeyFrame*>::const_iterator sit=sChilds.begin();sit!=sChilds.end();sit++)
        {
            KeyFrame* pChild = *sit;
            if(!pChild || pChild->isBad())
                continue;

            if(pChild->mnBAGlobalForKF!=GBAid)
            {
                Sophus::SE3f Tchildc = pChild->GetPose() * Twc;
                pChild->mTcwGBA = Tchildc * pKF->mTcwGBA;

                Sophus::SO3f Rcor = pChild->mTcwGBA.so3().inverse() * pChild->GetPose().so3();
                if(pChild->isVelocitySet()){
                    pChild->mVwbGBA = Rcor * pChild->GetVelocity();
                }
                else {
                    Verbose::PrintMess("Child velocity empty!! ", Verbose::VERBOSITY_NORMAL);
                }

                pChild->mBiasGBA = pChild->GetImuBias();
                pChild->mnBAGlobalForKF = GBAid;

            }
            lpKFtoCheck.push_back(pChild);
        }

        pKF->mTcwBefGBA = pKF->GetPose();
        pKF->SetPose(pKF->mTcwGBA);

        if(pKF->bImu)
        {
            pKF->mVwbBefGBA = pKF->GetVelocity();
            pKF->SetVelocity(pKF->mVwbGBA);
            pKF->SetNewBias(pKF->mBiasGBA);
        } else {
            cout << "KF " << pKF->mnId << " not set to inertial!! \n";
        }

        lpKFtoCheck.pop_front();
    }

    // Correct MapPoints
    const vector<MapPoint*> vpMPs = mpAtlas->GetCurrentMap()->GetAllMapPoints();

    for(size_t i=0; i<vpMPs.size(); i++)
    {
        MapPoint* pMP = vpMPs[i];

        if(pMP->isBad())
            continue;

        if(pMP->mnBAGlobalForKF==GBAid)
        {
            // If optimized by Global BA, just update
            pMP->SetWorldPos(pMP->mPosGBA);
        }
        else
        {
            // Update according to the correction of its reference keyframe
            KeyFrame* pRefKF = pMP->GetReferenceKeyFrame();

            if(pRefKF->mnBAGlobalForKF!=GBAid)
                continue;

            // Map to non-corrected camera
            Eigen::Vector3f Xc = pRefKF->mTcwBefGBA * pMP->GetWorldPos();

            // Backproject using corrected camera
            pMP->SetWorldPos(pRefKF->GetPoseInverse() * Xc);
        }
    }

    Verbose::PrintMess("Map updated!", Verbose::VERBOSITY_NORMAL);

    mnKFs=vpKF.size();
    mIdxInit++;

    for(list<KeyFrame*>::iterator lit = mlNewKeyFrames.begin(), lend=mlNewKeyFrames.end(); lit!=lend; lit++)
    {
        (*lit)->SetBadFlag();
        delete *lit;
    }
    mlNewKeyFrames.clear();

    mpTracker->mState=Tracking::OK;
    bInitializing = false;

    mpCurrentKeyFrame->GetMap()->IncreaseChangeIndex();

    return;
}

三、总结

双目相机加入imu模块的的好处,是解决了高速运动中间数据处理的问题。相机运动过程中间出现模糊,两帧之间重叠区域太少,因此很难做特征点匹配。相机自己又能在慢速运动中间解决 imu的漂移问题,这两者是互补的。

<think>嗯,用户让我介绍一下ORB-SLAM2的学习笔记。我需要先回忆一下ORB-SLAM2的基本框架核心部分。ORB-SLAM2是一个比较经典的视觉SLAM系统,分为几个主要模块:特征提取、跟踪、局部建图、回环检测地图管理。首先,我应该从系统架构开始介绍,说明各个模块的作用相互关系。 接下来,用户可能需要了解ORB特征的相关内容。ORB特征结合了FAST关键点检测BRIEF描述子,具有旋转不变性一定的尺度不变性。这部分需要详细解释ORB特征的提取过程,包括如何构建图像金字塔,计算方向,生成描述子等步骤。 然后是跟踪线程,也就是Tracking部分。这里需要说明如何通过帧间匹配、运动模型或重定来估计相机姿。可能需要提到初始化的过程,比如使用单应矩阵或基础矩阵来恢复运动,以及如何三角化初始地图点。 局部建图模块负责处理新的关键帧,剔除冗余点,进行局部BA优化。这部分需要解释BA(Bundle Adjustment)的概念,以及如何优化相机姿地图点。同时,还要提到关键帧的选择策略,比如根据共视点数量或时间间隔来决定是否插入新的关键帧。 回环检测地图融合部分也是重点。这里需要讲DBoW2的词袋模型,如何通过视觉词典检测回环,然后进行Sim3优化来校正漂移。此外,还要说明全局BA优化在回环校正后的作用,以及如何优化整个地图的一致性。 ORB-SLAM2支持单目、双目RGB-D相机,需要比较不同传感器下的系统表现。比如单目初始化困难,存在尺度不确定性,而双目RGB-D可以直接获取深度信息,初始化更简单。 在优缺点分析方面,优点包括实时性高、支持多传感器、具备回环检测重定能力。缺点可能有对动态环境敏感,单目模式下尺度问题,以及特征提取在纹理缺失区域效果差。可以提到后续的ORB-SLAM3在IMU融合多地图系统上的改进。 最后,学习建议部分应该推荐一些资源,比如论文、代码库、书籍课程。同时建议从基础开始,动手实践,并参与社区讨论。 需要注意的是,用户可能希望了解实际应用中的问题,比如如何调整参数,处理特定场景的挑战,或者如何扩展ORB-SLAM2的功能。不过根据用户的问题,主要是介绍学习笔记,所以需要结构清晰,分点说明,确保覆盖主要模块关键点。 可能还需要检查LaTeX格式是否正确,比如公式是否用$或$$包裹,变量是否正确。例如,提到BA优化时,可以写目标函数是$$\min \sum \| \pi(X, T) - x \|^2$$,其中$T$是姿,$X$是地图点。确保这些公式的格式正确。 总之,我需要按照系统架构、特征提取、跟踪、局部建图、回环检测、多传感器支持、优缺点、学习建议的结构来组织内容,使用清晰的中文,正确插入公式,并确保技术细节准确。</think>以下为ORB-SLAM2学习笔记的整理,按模块分阶段说明: --- ### 一、系统架构 $$ \text{ORB-SLAM2} = \text{Tracking} + \text{Local Mapping} + \text{Loop Closing} + \text{Map} $$ 三大线程并行运行: 1. **Tracking线程**:实时估计相机姿($T_{cw} \in SE(3)$) 2. **Local Mapping线程**:局部BA优化与地图点维护 3. **Loop Closing线程**:回环检测与全局优化 --- ### 二、ORB特征提取 - **FAST关键点检测**:阈值化像素差异($|I_p - I_{center}| > \delta$) - **BRIEF描述子**:二进制编码(256-bit,汉明距离匹配) - **改进特性**: - 图像金字塔实现尺度不变性($n$层缩放,比例因子$s=1.2$) - 灰度质心法计算方向($m_{10}=\sum xI(x,y), m_{01}=\sum yI(x,y)$) --- ### 三、跟踪线程(Tracking) 1. **初始姿估计**: - 通过上一帧运动模型预测:$T_{k} = T_{k-1} \cdot T_{k-1}^{k}$ - 或通过特征匹配与PnP求解:$\min_T \sum \| \pi(T \cdot X_i) - x_i \|^2$ 2. **关键帧判定**: - 跟踪质量下降(匹配点 < 阈值) - 距离上一关键帧超过20帧 --- ### 四、局部建图(Local Mapping) 1. **局部BA优化**: $$ \min_{T_i, X_j} \sum \| \pi(T_i \cdot X_j) - x_{ij} \|^2 + \rho(\| T_i^{-1}T_j \|) $$ - 仅优化当前关键帧及其共视关键帧 2. **地图点剔除**: - 观测次数 < 2次 - 重投影误差 > $\chi^2$阈值($\chi^2_{0.95}=5.991$) --- ### 五、回环检测(Loop Closing) 1. **词袋模型(DBoW2)**: - 离线训练视觉词典(k-means树结构) - 计算相似性得分:$s = \frac{\langle v_q, v_t \rangle}{\|v_q\| \|v_t\|}$ 2. **Sim3优化**: $$ \min_{S} \sum \| \pi(S \cdot X_i) - x_i \|^2 + \| \pi(S^{-1} \cdot X_j') - x_j' \|^2 $$ - 解决尺度漂移问题($S \in Sim(3)$) --- ### 六、多传感器支持 | 传感器类型 | 初始化方式 | 深度获取方法 | |------------|---------------------|-----------------------| | 单目 | 分解H/F矩阵 | 三角化(需运动) | | 双目/RGB-D | 直接构造深度图 | 视差/深度传感器 | --- ### 七、优缺点分析 - **优点**: - 实时性(30fps,GTX 1070) - 支持重定与动态场景处理 - **缺点**: - 依赖特征匹配(纹理缺失易失效) - 单目模式需10秒以上初始化 --- ### 八、学习建议 1. **必读文献**: - 原论文《ORB-SLAM2: An Open-Source SLAM System for Monocular, Stereo and RGB-D Cameras》 - 《Multiple View Geometry in Computer Vision》 2. **代码实践**: ```bash git clone https://github.com/raulmur/ORB_SLAM2 cd ORB_SLAM2 && chmod +x build.sh && ./build.sh ``` 3. **调试工具**: - 使用g2o_viewer可视化优化结果 - 分析`KeyFrame::EraseMapPointMatch()`等关键函数 --- 以上内容需结合代码阅读(推荐使用CLion或VSCode调试),重点关注`Tracking.cc``Optimizer.cc`模块。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

极客范儿

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

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

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

打赏作者

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

抵扣说明:

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

余额充值