[ROS2] 你应该知道Costmap_2d 的这些细节

ROS导航系列(三):costmap2D详解 功能包costmap2D提供一种 阅读详情

Costmap_2d 的插件

Costmap_2d 的插件都是继承于CostmapLayer。具体的关系如下图所示:

StaticLayer

StaticLayer内主要是通过接收map_server发布的地图话题来加载静态地图的。所以StaticLayer内是可以在线更改静态地图的。

ObstacleLayer

ObservationBuffer

ObservationBuffer 是一个障碍物观察数据的buffer。观测到的障碍物数据都将转成sensor_msgs::msg::PointCloud2格式,然后存储到ObservationBuffer 中。

在ObservationBuffer 里存储的历史障碍物数据可以根据想保持的时间来清空。期望保持的时间主要由变量observation_keep_time_来决定。如果设置成rclcpp::Duration(0.0s)则表示每次都只存储最新的,历史障碍物数据都会被清掉。

看到这里,有同学可能会想,既然可以以时间为依据来清除障碍物,是不是也可以以其他条件来清除障碍物呢?答案肯定是可以的。这个就需要根据应用场景来选择了。比如:使用机器人的移动距离来作为判断条件。当观测数据时的机器人位置与现在机器人的位置超过多远就把该数据清掉。

ObstacleLayer

ObstacleLayer内可以加载多种传感器的障碍物观测数据。但是数据类型只支持PointCloud2 和 LaserScan。其中LaserScan类型的数据会被转换成PointCloud2 类型数据。因为ObservationBuffer 只存储PointCloud2 类型数据。

ObstacleLayer内有下面几个层级参数需要关注一下:

declareParameter("enabled", rclcpp::ParameterValue(true));//使能该层

declareParameter("footprint_clearing_enabled", rclcpp::ParameterValue(true));//清楚footprint占用的区域

declareParameter("max_obstacle_height", rclcpp::ParameterValue(2.0));//高于此参数设定的高度的障碍物就忽略

declareParameter("combination_method", rclcpp::ParameterValue(1));//更新cost的方式,0->直接覆盖旧数据,1->取前后最大值

declareParameter("observation_sources", rclcpp::ParameterValue(std::string("")));//观测数据的名称

每种传感器的观测数据都可以独立配置如下参数:

declareParameter(source + "." + "topic", rclcpp::ParameterValue(source));

declareParameter(source + "." + "sensor_frame", rclcpp::ParameterValue(std::string("")));

declareParameter(source + "." + "observation_persistence", rclcpp::ParameterValue(0.0));

declareParameter(source + "." + "expected_update_rate", rclcpp::ParameterValue(0.0));

declareParameter(source + "." + "data_type", rclcpp::ParameterValue(std::string("LaserScan")));

declareParameter(source + "." + "min_obstacle_height", rclcpp::ParameterValue(0.0));

declareParameter(source + "." + "max_obstacle_height", rclcpp::ParameterValue(0.0));

declareParameter(source + "." + "inf_is_valid", rclcpp::ParameterValue(false));

declareParameter(source + "." + "marking", rclcpp::ParameterValue(true));

declareParameter(source + "." + "clearing", rclcpp::ParameterValue(false));

declareParameter(source + "." + "obstacle_max_range", rclcpp::ParameterValue(2.5));

declareParameter(source + "." + "obstacle_min_range", rclcpp::ParameterValue(0.0));

declareParameter(source + "." + "raytrace_max_range", rclcpp::ParameterValue(3.0));

declareParameter(source + "." + "raytrace_min_range", rclcpp::ParameterValue(0.0));

有几个比较重要的参数,这里说明一下:
observation_persistence: 决定障碍物持续时间的参数。
obstacle_max_range,obstacle_min_range:决定了距离传感器安装位置多少距离区间内的障碍物可以被标记到costmap上
raytrace_max_range,raytrace_min_range: 决定了距离传感器安装位置多少距离区间内的障碍物可以被清除掉

这里使用了tf2_ros::MessageFilter来处理障碍物观测数据。主要是因为tf2_ros::MessageFilter可以保证只有在传感器的fram和global_fram的tf关系有效的情况下再执行数据的回调函数。在障碍物数据叠加到costmap层的过程中需要将障碍物数据转换到全局坐标系下。所以需要保证其tf转换是有效的才进行数据处理。

std::shared_ptr<tf2_ros::MessageFilter<sensor_msgs::msg::PointCloud2>> filter(
	new tf2_ros::MessageFilter<sensor_msgs::msg::PointCloud2>(
	*sub, *tf_, global_frame_, 50, rclcpp_node_, 
    tf2::durationFromSec(transform_tolerance)));

filter->registerCallback(
	std::bind(
	&ObstacleLayer::pointCloud2Callback, this, std::placeholders::_1,
	observation_buffers_.back()));

RangeSensorLayer

这是Navigation2中新增加的一个costmap插件层,主要维护超声波的数据。它对超声波数据的模拟是用圆锥体有来表征超声波的检测空间。映射到costmap上时则是三角形。这可能是最接近超声波检测空间的规则图形了吧!但为了直观展示这种模拟方式和实际超声波检测的差异,我在下面放了两张图片。

模拟的超声波检测空间

实际超声波的检测空间

在RangeSensorLayer层中,超声波检测区域中的栅格值是通过概率模型(probabalistic model)进行更新的。

double sensor = 0.0;
if (!clear) {
	sensor = sensor_model(r, phi, theta);
}
double prior = to_prob(getCost(x, y));//得到原来被占用的概率
double prob_occ = sensor * prior;
double prob_not = (1 - sensor) * (1 - prior);
double new_prob = prob_occ / (prob_occ + prob_not);//更新被占用的概率

InflationLayer

InflationLayer中的灵魂操作就是cost的更新函数了(updateCosts)。下面简单梳理一下函数执行的流程:

  1. 提取出障碍物点
  // Start with lethal obstacles: by definition distance is 0.0
  auto & obs_bin = inflation_cells_[0];
  for (int j = min_j; j < max_j; j++) {
    for (int i = min_i; i < max_i; i++) {
      int index = static_cast<int>(master_grid.getIndex(i, j));
      unsigned char cost = master_array[index];
      if (cost == LETHAL_OBSTACLE || (inflate_around_unknown_ && cost == NO_INFORMATION)) {
        obs_bin.emplace_back(index, i, j, i, j);
      }
    }
  }
  1. 迭代障碍物点并更新cost
      // assign the cost associated with the distance from an obstacle to the cell
      unsigned char cost = costLookup(mx, my, sx, sy);
      unsigned char old_cost = master_array[index];
      // In order to avoid artifacts appeared out of boundary areas
      // when some layer is going after inflation_layer,
      // we need to apply inflation_layer only to inside of given bounds
      if (static_cast<int>(mx) >= base_min_i &&
        static_cast<int>(my) >= base_min_j &&
        static_cast<int>(mx) < base_max_i &&
        static_cast<int>(my) < base_max_j)
      {
        if (old_cost == NO_INFORMATION &&
          (inflate_unknown_ ? (cost > FREE_SPACE) : (cost >= INSCRIBED_INFLATED_OBSTACLE)))
        {
          master_array[index] = cost;
        } else {
          master_array[index] = std::max(old_cost, cost);
        }
      }
  1. 将在膨胀半径内的栅格点加入到膨胀队列里
  • 首先向四周扩展栅格
      // attempt to put the neighbors of the current cell onto the inflation list
      if (mx > 0) {
        enqueue(index - 1, mx - 1, my, sx, sy);
      }
      if (my > 0) {
        enqueue(index - size_x, mx, my - 1, sx, sy);
      }
      if (mx < size_x - 1) {
        enqueue(index + 1, mx + 1, my, sx, sy);
      }
      if (my < size_y - 1) {
        enqueue(index + size_x, mx, my + 1, sx, sy);
      }
  • 选取在膨胀半径内的栅格加入到膨胀队列里
void
InflationLayer::enqueue(
  unsigned int index, unsigned int mx, unsigned int my,
  unsigned int src_x, unsigned int src_y)
{
  if (!seen_[index]) {
    // we compute our distance table one cell further than the
    // inflation radius dictates so we can make the check below
    double distance = distanceLookup(mx, my, src_x, src_y);

    // we only want to put the cell in the list if it is within
    // the inflation radius of the obstacle point
    if (distance > cell_inflation_radius_) {
      return;
    }

    const unsigned int r = cell_inflation_radius_ + 2;

    // push the cell data onto the inflation list and mark
    inflation_cells_[distance_matrix_[mx - src_x + r][my - src_y + r]].emplace_back(
      index, mx, my, src_x, src_y);//推到相应的膨胀层的vector内
  }
}

其中inflation_cells_中将按圈层存储栅格。

需要注意的一点是,InflationLayer中并没有包含存储地图数据的costmap_2d层,它唯一的工作就是把之前层上的障碍物信息在组合层里膨胀一下。

关于costmap的插件配置,这里需要注意一下配置的顺序。代码中插件加载的顺序就是按照配置顺序来的。"inflation_layer"一般放在最后面。因为它最终将前面几个层的障碍物信息一起膨胀。如果不想膨胀某个插件层,则可以将其放在"inflation_layer"之后。

plugins: ["static_layer", "obstacle_layer", "voxel_layer", "inflation_layer"] 

Costmap_2d Filters

Costmap_2d Filters 的主要作用是给区域划定不同的功能属性。比如:KeepoutFilter就可以实现虚拟墙的功能。它能限定某些区域机器是不能进去的或者某些区域是不建议通过的。又比如:SpeedFilter限制了在某些区域内机器人的运动速度。

它与CostmapLayer有何不同呢?

  1. Filter调用updateBounds函数基本不干什么事,也不更新cost变动的区域。
void CostmapFilter::updateBounds(
  double robot_x, double robot_y, double robot_yaw,
  double * /*min_x*/, double * /*min_y*/, double * /*max_x*/, double * /*max_y*/)
{
  if (!enabled_) {
    return;
  }

  latest_pose_.x = robot_x;
  latest_pose_.y = robot_y;
  latest_pose_.theta = robot_yaw;
}
  1. Filter可能并不会更改栅格值,比如SpeedFilter只能根据自身维护的栅格值来调整速度限制。而CostmapLayer常常是需要对栅格值进行修改的。

Costmap_2d Filters之间的关系图如下:

Costmap_2d Filters的运行机制如下图所示:

CostmapFilterInfoServer负责加载一些参数发布给filter。MapServer主要是加载地图文件并发布给filter。

你可能发现了。Filter正是利用地图文件来获取区域信息的。而这些地图文件是可以自己定义的。比如你将原来地图文件的某些部分涂黑,那么这些黑色的部分在KeepoutFilter中将被视为禁区或者说虚拟墙。如果你将地图中不建议去的区域加重颜色,加载到KeepoutFilter中时这些颜色比较深但是又没有被标记为障碍物的区域会有比较大的cost值。这样路径规划时就会尽量绕开这些区域。只有在其他区域都完全不可行的情况下才会往这些区域规划路径。

下面是KeepoutFilter的一个例子:

SpeedFilter则会根据标记区域的深浅来约束最大速度。颜色越深,速度约束越大。反之,颜色越浅,速度约束越小。速度约束的方式有两种。一种是通过百分比,即将速度约束为最大速度的多少百分比。一种是绝对速度约束,即直接修改最大可行速度。

SpeedFilter会将计算得到的速度约束发送出来。nav2_controller接受到速度约束话题后将相应的值通过函数setSpeedLimit更新给控制器插件,比如TEB,DWB。

觉得有用就点赞吧!

另外在公众号《首飞》内回复“机器人”获取精心推荐的C/C++,Python,Docker,Qt,ROS1/2等机器人行业常用技术资料。

ROS 进阶学习笔记(16):ROS导航1:关于Costmap_2d Package (代价地图包) === 关于Costmap_2d Package === wiki page: http://wiki.ros.org/costmap_2d === 我学这个包的时候,尽量总结wiki page上的内容如下:=== 所属Stack: navigation Sammary:    1. 实现2D cost map    2. 输入: sensor data    3. 生成: 阅读详情

相关推荐

ROS naviagtion analysis: costmap_2d--Costmap2DROS

在上一篇文章中moveBase就有关于costmap_2d的使用: planner_costmap_ros_是用于全局导航的地图,controller_costmap_ros_是局部导航用的地图,地图类型为经过ROS封装的costmap_2d::Costmap2DROS*。 以下是这个ROS类的UML: 看看这个类的成员变量:LayeredCostmap* layered_costmap_

BjtRob的专栏 7340

ros 导航中出现的costmap

costmap( 代价地图)   costmap是Navigation Stack里的代价地图,它其实也是move_base插件,本质上是C++的动态链接库,用过catkin_make之后生成.so文件,然后move_base在启动时会通过动态加载的方式调用其中的函数。 costmap是一个package是机器人收集传感器信息建立和更新的二维或三维地图,用在move_base的global_...

D学习中- 5432

ROS Navigation Stack之costmap_2d源码分析

草稿

少年的技术积累手册 2978

基于ROS2的costmap中Obstacle Layer中对障碍物信息的增加与删除机制的方案调研。

针对障碍物无法及时清除,主要从两方面下手,一方面是对障碍物数据的预处理,例如给障碍物语义信息,对动态障碍物通过i卡尔曼继续跟踪,或者设置相关的参数,。另一方面是对障碍物清除机制进行优化,目前障碍物是根据光追法对障碍物进行清除,有其有时和弊端。其中最主要的是cloud_和origin_两个变量,origin_类型为Point类型,该位置为/opt/ros/humble/share/geometry_msgs/msg,其数据结构为x,y,z。目前障碍物层主要接收种类型的障碍物信息,分别是。

To be a better man 4745

ROS naviagtion analysis: costmap_2d--Costmap2D

Costmap2D是存储地图数据的父类。真正的地图数据就存储在数据成员unsigned char *costmap_ 。 首先,分析类的构造函数: 默认构造函数:Costmap2D::Costmap2D() :// just initialize everything to NULL by default Costmap2D::Costmap2D() : size_x_(0), siz

BjtRob的专栏 2984

ROS从入门到精通6-1:图文详解代价地图costmap原理与配置

一文总结ROS的地图服务——地图如何加载、发布、编辑;代价地图的核心概念与配置,图文并茂加深理解

FRIGIDWINTER的博客 4829

ROS Melodic中costmap2D详解

0 概述 如果需要深入了解navigation,那costmap2D必不可少。本文章参考了多篇文章以及ros navigation官网最新的资料,并结合自身的理解完成。该软件作为栅格地图建立的函数包,其输入是现实传感器的数据,输出是2D栅格地图的costmap,该软件包还支持基于map_server的costmap初始化(调用map_server节点,以服务的方式读取地图数据),基于滚动窗口的costmap初始化,以及基于参数的订阅和传感器主题的配置。 1 cost值 可以这么说,无论是激光雷达还是如kin

lovely_yoshino的博客 8634

机器人导航算法——Costmap地图ROS源码解析

costmap使用了一种分层地图的技术方案,即分别考虑障碍物层/膨胀层/静态障碍物层更新对应层的地图代价信息,然后再合并到主地图中,具体见我翻译的另一篇文章《costmap文献阅读—Layered Costmaps for Context-Sensitive Navigation以上就是costmap的介绍,后续会持续更新。。。。ReferenceCostmap文献阅读——Layered Costmaps for Context-Sensitive Navigation。

qq_40464599的博客 2982

【ROS-Navigation】Costmap2D代价地图源码解读-1

记录学习阅读ROS Navigation源码的理解,本文为Costmap2D代价地图源码学习记录,以文字总结、绘制结构图说明、代码注释为主。仍在学习过程中,有错误欢迎指正,共同进步。 Costmap通过各层地图订阅话题、接收传感器数据,维护各层地图数据,并最终整合出一张用于路径规划的主地图。 【结构示意图】 【相关文件】 costmap_2d/src/costmap_2d_r...

NEO's NOTEBOOK 5990

IC验证 AHB-RAM 项目(二)——接口与事务代码的编写

本文介绍了准备工作及接口、事务相关代码编写要点。在接口部分,重点说明了lvc_ahb_if.sv、rkv_ahbram_if.sv和rkv_ahbram_tb.sv三个关键文件的功能,其中rkv_ahbram_if.sv通过动态任务实现复位信号控制。事务部分则涉及lvc_ahb_types.sv中的枚举类型定义和lvc_ahb_transaction.sv中的随机化变量设置,用于构建AHB传输激励。整个验证环境采用模块化设计,通过接口实现TB与DUT的通信,为后续验证工作奠定基础。

weixin_45926435的博客 406

Unity中实现FairyGUI弹窗背景模糊的3种方法对比(附完整代码)

本文详细对比了Unity中实现FairyGUI弹窗背景模糊的3种方法,包括FairyGUI原生滤镜方案、多摄像机混合渲染方案和渲染纹理动态混合方案。每种方法均附有完整代码示例,帮助开发者根据项目需求选择最佳方案,实现镜头虚化模糊效果,提升游戏UI的视觉沉浸感。

ff678的博客 233

【ROS-Navigation-costmap_2d】costmap_prohibition_layer障碍物插件

系列文章目录 文章目录系列文章目录前言一、costmap_2d是什么?二、使用步骤1.下载源代码2.安装插件3.在costmap_2d中插入障碍物4.修改launch文件5.设置障碍物坐标参数6.主要事项总结 前言 随着SLAM的不断发展,机器人这门行业也越来越重要,很多人都开启了学习机器人,本文就介绍了ROS机器人操作系统的SLAM导航方向的navigation导航包中的costmap_2d。 以下是添加一个自定义的障碍物插件的流程 一、costmap_2d是什么? costmap_2d是

wesigj的博客 4229

Meta-RL之Meta-Gradient Reinforcement Learning

这篇文章是用元学习算法去学习RL的超参数η={γ,λ}\eta=\{\gamma,\lambda\}η={γ,λ}。当然不仅限于这2个超参数,还可以是和回报相关的超参数。 本文的核心思想:我们之前接触过Meta-Learning参数的初始化(MAML、Reptile)以及Meta-Learning参数的更新优化(L2L-by-gd-by-gd、Opt as a model for few-shot learning),那么这篇就是能否Meta-Learning到η={γ,λ}\eta=\{\gamma,\l

Ton的博客 2521

Nav2代价地图实现和原理--Nav2源码解读之CostMap2D(上)

本文介绍了Costmap2D的几个核心类,包括Costmap2D、CostmapLayer和Layer。包括底下派生的子类StaticLayer和ObstacleLayer

m0_73800387的博客 4495

kettle从入门到精通 第九十二课 ETL之kettle 使用Kettle的Carte对外发布读写接口

场景:使用kettle实现将查询结果返回给客户端,也就是说kettle暴露查询接口供外围系统调用。前提必须是使用carte服务才可以提供接口供外部系统调用。具体实操方法如下:1、设计转换根据具体需求设计转换,主要用到的步骤有获取变量(从请求参数中获取变量)、json输入(解析入参),json 输出将结果转换为json格式输出到客户端。如下图所示:2、接收参数使用获取变量步骤来接收接口请求参数,变量通过${}方法进行获取,如下图${PARAM}则是获取PARAM参数,将参数赋值为req字段。

zhangjin1222的博客 955

C# WPF多页面间的数据共享(经典)

实现页面间的数据共享有很多方法,比如:①事件的发布订阅 ②委托③ioc容器④静态类等,这节主要通过静态的集合实现数据的添加和获取。

sf315418的博客 639

ros-navigation学习笔记(四)costmap代价地图

0x00 什么是costmap代价地图 在机器人进行路径规划时,我们需要明白规划算法是依靠什么在地图上来计算出来一条路径的。依靠的是gmapping扫描构建的一张环境全局地图,但是仅仅依靠一张原始的全局地图是不行的。因为这张地图是静态的,无法随时来更新地图上的障碍物信息。在现实环境中,总会有各种无法预料到的新障碍物出现在当前地图中,或者旧的障碍物现在已经从环境地图中被移除掉了,那么我们就需要来随...

JDWell的博客 1万+

告别城市峡谷定位漂移:手把手教你用PSINS仿真INS/NHC/ODO组合导航(附代码获取)

本文详细解析了INS/NHC/ODO组合导航系统在城市峡谷环境中的应用,通过PSINS平台实现精准定位。文章涵盖核心原理、环境搭建、算法实现及性能优化,帮助开发者解决GNSS信号中断时的定位漂移问题,提升自动驾驶系统在复杂环境中的可靠性。

weixin_30329623的博客 430

【ROS-Navigation】Costmap2D代价地图源码解读-静态层StaticLayer

记录学习阅读ROS Navigation源码的理解,本文为Costmap2D代价地图源码学习记录,以文字总结、绘制结构图说明、代码注释为主。仍在学习过程中,有错误欢迎指正,共同进步。 本篇记录静态地图部分的内容,主要是开启地图的订阅,以及bound和cost的更新。静态地图默认只更新一次,地图的边界也只更新一次,对于rolling地图,需要随着机器人的运动位置不断更新附近的cost,而对于非r...

NEO's NOTEBOOK 3280

Altium Designer应用技巧13: 四层板的拼版方法

本文介绍在Altium Designer中,进行4层板的拼版方法。Altium Designer有多种拼版方法,本文选择最直观的复制、删帖方法来完成,步骤如下: 1、看一下原始文件,如下图所示,我们要对该板进行拼版操作。 2、通过菜单“报告”-“板子信息”得到板子大小,在这里一般切换为mm比较好操作,我们可以使用快捷键"q"。 3、如果是水平拼版,我们需要关心水平长度;如果是垂直拼版...

snmplink的博客 1万+

SAP 系统间 SSO 的真正价值,从 SM59 到可信 RFC 的安全设计

SAP S/4HANA系统间Single Sign-On(SSO)的核心价值在于集中管理通信身份与信任关系,而非仅减少密码输入。传统固定账号密码方式在系统规模扩大后会面临密码管理、审计追踪等挑战。通过SM59配置可信RFC目标系统,结合SSL证书或SAP Trusted System等强认证机制,能有效降低安全凭据暴露风险。SSO配置需配合授权对象、角色管理等建立清晰的信任边界,实现系统间安全通信的同时保持可审计性。这种方案特别适用于Fiori前端与S/4HANA后端等场景,通过可信RFC传递用户身份,避免

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 1065

【ROS-Navigation】Costmap2D代价地图源码解读-膨胀层InflationLayer

记录学习阅读ROS Navigation源码的理解,本文为Costmap2D代价地图源码学习记录,以文字总结、绘制结构图说明、代码注释为主。仍在学习过程中,有错误欢迎指正,共同进步。 InflationLayer没有自身的栅格地图要维护,直接在主地图上进行操作,它根据膨胀参数设置用来膨胀的“参考矩阵”,并在主地图上从障碍物出发,不断传播更新,完成对整个地图障碍的膨胀,等效于完成一个由“将机器人...

NEO's NOTEBOOK 7585

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

ros中的costmap

直到我整理完move_base后才发现costmap也十分重要,才有了这篇文章。 costmap是一个package用于move_base中的global_map和local_map中。用于将2d/3d的传感器数据转化成一个2d的网格地图 1.总览 简单来说,地图中分成几部分 灰色部分:安全区域,robot运动在该区域不会发生碰撞 红色部分:危险区域,robot的footpr

sunyoop的博客 6293

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

costmap_2d: obstacle_layer中关于激光雷达障碍物清除不干净的解决

gvsd

shawfy的博客 1万+

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

costmap_2d包介绍

move_base costmap_2d包整理

gene's blog 2379
上一篇: [ROS2] 使用LifecycleNode管理节点起停等状态
下一篇: 推荐两个C++API参考网站
首飞爱玩机器人
博客等级 码龄14年 6990粉丝 120原创
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

打赏作者

首飞爱玩机器人

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

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

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

打赏作者

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

抵扣说明:

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

余额充值