costmap_2d包介绍

文章目录

    • 一. costmap_2d包介绍
    • 二. Costmap包的执行入口-- move_base中调用
    • 三. Costmap包的初始化以及维护
      • 3.1 Costmap2DROS类
        • 3.1.1 构造函数 Costmap2DROS::Costmap2DROS
        • 3.1.2 地图更新线程 Costmap2DROS::mapUpdateLoop
        • 3.1.3 地图更新 Costmap2DROS::updateMap()
        • 3.1.4 激活各层地图 Costmap2DROS::start()
      • 3.2 Costmap2D类
      • 3.3 LayeredCostmap类
      • 3.4 CostmapLayer类

关于Move_base的框架梳理参考: 此链接

一. costmap_2d包介绍

costmap_2d包提供了一种可配置框架来维护机器人在代价地图上应该如何导航的信息。实际使用的时候,可以分为global_costmap和local_costmap两部分,每一种都可以配置多个图层,包括以下几种:

  • static_layer :静态地图层,基本不变的地图层,通常都是SLAM建图完成的静态地图,用于路径规划
  • obstacle_layer:障碍地图层,用于动态的记录传感器感知的障碍物信息,用于路径规划和避障。
  • inflation_layer: 膨胀层,在以上两层地图进行膨胀,以避免机器人的撞上障碍物,用于路径规划。
  • Other Layers:可以通过插件的形式自己实现costmap,目前已有Social Costmap Layer、Range Sensor Layer等开源插件。

costmap_2d包提供的ROS化功能接口:costmap_2d::Costmap2DROS。它使用costmap_2d::LayeredCostmap 来跟踪每一层。 每一层在Costmap2DROS中以插件方式被实例化,并被添加到LayeredCostmap。 每一层可以独立编译,且可使用C++接口实现对代价地图的随意修改。costmap_2d::Costmap2D 类中实现了用来存储和访问2D代价地图的的基本数据结构。其中包含或者涉及到的类如下:

  • Costmap2DROS类

  是对整个代价地图内容的封装。

  • LayeredCostmap类

  是Costmap2DROS的类成员,它是“主地图”,也能管理各层地图,因为它含有指向各层子地图的指针,能够调用子地图的类方法,开启子地图的更新。并且各层子地图最后都会合并到主地图上,提供给规划器的使用。它含有Costmap2D类成员,这个类就是底层地图,用于记录地图成员。

  • Layer类

  含有子地图层用到的一些函数,如更新size、更新bound、和主地图合并等;

  • Costmap2D类

  存储该层维护的地图数据,包括地图的x,y方向的尺寸,地图的分辨率,地图原点位姿。

  • CostmapLayer类

  派生自Layer类和Costmap2D类,里面提供了 用主地图尺寸设置该层地图尺寸matchSize、用当前子地图数据更新对应区域updateWithMax等函数,相当于函数模版,在相关层进行继承和调用。

  • Staticlayer类和Obsaclelayer类

  由CostmapLayer类派生出Staticlayer类和Obsaclelayer类,即静态层和障碍层,前者获取静态地图,后者通过传感器数据不断更新,获得能反应障碍物信息的子地图。

  • InflationLayer类

  由Layer类单独派生InflationLayer类,即膨胀层,用它来反映障碍物在地图上向周边的膨胀。由于它的父类中不含有Costmap2D类,所以其实膨胀层自身没有栅格地图要维护,这一点和另外两层有区别。

类与类的继承关系如下图:

在这里插入图片描述

二. Costmap包的执行入口-- move_base中调用

代码路径:move_base/src/move_base.cpp

首先获取地图信息,内切圆半径inscribed_radius/外接圆形半径circumscribed_radius等信息。

private_nh.param("local_costmap/inscribed_radius", inscribed_radius_, 0.325);
private_nh.param("local_costmap/circumscribed_radius", circumscribed_radius_, 0.46);
private_nh.param("clearing_radius", clearing_radius_, circumscribed_radius_);
private_nh.param("conservative_reset_dist", conservative_reset_dist_, 3.0);private_nh.param("shutdown_costmaps", shutdown_costmaps_, false);
private_nh.param("clearing_rotation_allowed", clearing_rotation_allowed_, true);
private_nh.param("recovery_behavior_enabled", recovery_behavior_enabled_, true);

下面通过模块costmap_2d构建了全局代价地图对象,并通过其成员函数pause()暂停运行,将在必要的功能模块初始化结束之后通过成员接口start()开启。

planner_costmap_ros_ = new costmap_2d::Costmap2DROS("global_costmap", tf_);
planner_costmap_ros_->pause();

加载的全局代价地图是由静态地图层、 障碍层和膨胀层构成,如下面下边的代码片段所示。而局部代价地图只有障碍层和膨胀层。

# global_costmap_params.yaml- {name: static_layer,type: "costmap_2d::StaticLayer"}- {name: obstacle_layer,type: "costmap_2d::VoxelLayer"}- {name: inflation_layer,type: "costmap_2d::InflationLayer"}
# lobal_costmap_params.yaml- {name: obstacle_layer,type: "costmap_2d::VoxelLayer"}- {name: inflation_layer,type: "costmap_2d::InflationLayer"}

构建了必要的功能模块之后,就可以开启代价地图的计算了。

planner_costmap_ros_->start();

三. Costmap包的初始化以及维护

3.1 Costmap2DROS类

代码路径:costmap_2d/src/costmap_2d_ros.cpp

大致的运行关系如下图:

在这里插入图片描述

3.1.1 构造函数 Costmap2DROS::Costmap2DROS

首先是一些参数的获取:

  • 循环等待直到获得机器人底盘坐标系和global系之间的坐标转换;
  • 获取rolling_window、track_unknown_space、always_send_full_costmap的参数,默认为false;
Costmap2DROS::Costmap2DROS(std::string name, tf::TransformListener& tf) :layered_costmap_(NULL),name_(name),tf_(tf),transform_tolerance_(0.3),map_update_thread_shutdown_(false),stop_updates_(false),initialized_(true),stopped_(false),robot_stopped_(false),map_update_thread_(NULL),last_publish_(0),plugin_loader_("costmap_2d", "costmap_2d::Layer"),publisher_(NULL),dsrv_(NULL),footprint_padding_(0.0)
{//初始化old poseold_pose_.setIdentity();old_pose_.setOrigin(tf::Vector3(1e30, 1e30, 1e30));ros::NodeHandle private_nh("~/" + name);ros::NodeHandle g_nh;//获取tf前缀ros::NodeHandle prefix_nh;std::string tf_prefix = tf::getPrefixParam(prefix_nh);//两个坐标系private_nh.param("global_frame", global_frame_, std::string("/map"));private_nh.param("robot_base_frame", robot_base_frame_, std::string("base_link"));//确保基于tf前缀正确设置了坐标系//注:tf::resolve或者TransformListener::resolve方法的作用就是使用tf_prefix参数将frame_name解析为frame_idglobal_frame_ = tf::resolve(tf_prefix, global_frame_);robot_base_frame_ = tf::resolve(tf_prefix, robot_base_frame_);ros::Time last_error = ros::Time::now();std::string tf_error;//确保机器人底盘坐标系和世界坐标系之间的有效转换//否则一直停留在这里阻塞while (ros::ok()&& !tf_.waitForTransform(global_frame_, robot_base_frame_, ros::Time(), ros::Duration(0.1), ros::Duration(0.01),&tf_error)){ros::spinOnce();if (last_error + ros::Duration(5.0) < ros::Time::now()){ROS_WARN("Timed out waiting for transform from %s to %s to become available before running costmap, tf error: %s",robot_base_frame_.c_str(), global_frame_.c_str(), tf_error.c_str());last_error = ros::Time::now();}// The error string will accumulate and errors will typically be the same, so the last// will do for the warning above. Reset the string here to avoid accumulation.tf_error.clear();}//检测是否需要“窗口滚动”,从参数服务器获取以下参数bool rolling_window, track_unknown_space, always_send_full_costmap;private_nh.param("rolling_window", rolling_window, false);private_nh.param("track_unknown_space", track_unknown_space, false);private_nh.param("always_send_full_costmap", always_send_full_costmap, false);

接下来创建一个LayeredCostmap类实例layered_costmap_,作为规划器使用的主地图,并通过它管理各层地图

通过LayeredCostmap的对象创造了一个由指向plugin的共享指针组成的容器,通过配置文件,向里逐个添加插件层。

//初始化一个layered_costmap
layered_costmap_ = new LayeredCostmap(global_frame_, rolling_window, track_unknown_space);

接着,在参数服务器上获取“plugins”参数,这里得到的插件即为各层子地图。每层子地图使用Pluginlib(ROS插件机制)来实例化,各个层可以被独立的编译,且允许使用C++接口对Costmap做出任意的改变。循环将每个plugin即每层子地图添加进layered_costmap_类的指针组对象中,并对每层地图调用其initialize类方法,初始化各层地图。这个函数定义在Layer类中,它向各层地图“通知”主地图的存在,并调用oninitialize类方法(Layer类中的虚函数,被定义在各层地图中)。

  //如果没有这一项参数,重新设置旧参数if (!private_nh.hasParam("plugins")){resetOldParameters(private_nh);}//加入各个层次的地图if (private_nh.hasParam("plugins")){XmlRpc::XmlRpcValue my_list;private_nh.getParam("plugins", my_list);for (int32_t i = 0; i < my_list.size(); ++i){ //从my_list里获取地图名称和种类std::string pname = static_cast<std::string>(my_list[i]["name"]);std::string type = static_cast<std::string>(my_list[i]["type"]);ROS_INFO("Using plugin "%s"", pname.c_str());//创建一个以type为类类型的实例变量,然后让plugin这个指针指向这个实例boost::shared_ptr<Layer> plugin = plugin_loader_.createInstance(type);layered_costmap_->addPlugin(plugin);//实际执行的是plugin调用的父类Layer的方法initializeplugin->initialize(layered_costmap_, name + "/" + pname, &tf_);}}

订阅footprint话题,回调函数为setUnpaddedRobotFootprintPolygon。当话题上收到footprint时,回调函数会将接收到的footprint根据参数footprint_padding的值进行“膨胀”,得到“膨胀”后的padded_footprint,传递给各级地图。

创建另一个话题,该话题上将发布的内容是根据机器人当前位置计算出来的实时footprint的位置。

  std::string topic_param, topic;if (!private_nh.searchParam("footprint_topic", topic_param)){topic_param = "footprint_topic";}private_nh.param(topic_param, topic, std::string("footprint"));footprint_sub_ = private_nh.subscribe(topic, 1, &Costmap2DROS::setUnpaddedRobotFootprintPolygon, this);if (!private_nh.searchParam("published_footprint_topic", topic_param)){topic_param = "published_footprint";}private_nh.param(topic_param, topic, std::string("oriented_footprint"));footprint_pub_ = private_nh.advertise<geometry_msgs::PolygonStamped>("footprint", 1);setUnpaddedRobotFootprint(makeFootprintFromParams(private_nh));

创建地图的发布器实例,Costmap2DPublisher类是用于地图发布的封装类。

//发布Costmap2Dpublisher_ = new Costmap2DPublisher(&private_nh, layered_costmap_->getCostmap(), global_frame_, "costmap",always_send_full_costmap);//创建地图更新线程的控制量stop_updates_ = false;initialized_ = true;stopped_ = false;//创建一个timer去检测机器人是否在移动robot_stopped_ = false;//回调函数movementCB通过比较前后两个pose的差来判断机器人是否在移动timer_ = private_nh.createTimer(ros::Duration(.1), &Costmap2DROS::movementCB, this);

开启动态参数配置,它的回调函数Costmap2DROS::reconfigureCB会在节点启动时运行一次,加载.cfg文件的配置参数。这个函数给对应参数赋值,并创建了一个Costmap2DROS::mapUpdateLoop函数的线程,即地图更新线程。

//开启参数动态配置dsrv_ = new dynamic_reconfigure::Server<Costmap2DConfig>(ros::NodeHandle("~/" + name));//回调函数reconfigureCB 除了对一些类成员的配置值做赋值以外,还会开启一个更新map的线程 dynamic_reconfigure::Server<Costmap2DConfig>::CallbackType cb = boost::bind(&Costmap2DROS::reconfigureCB, this, _1,_2);dsrv_->setCallback(cb);
}
3.1.2 地图更新线程 Costmap2DROS::mapUpdateLoop

这个函数循环调用UpdateMap函数,更新地图,并以publish_cycle为周期,发布更新后的地图。它分为两步:

  • 第一步:更新bound,即确定地图更新的范围;
  • 第二步:更新cost,更新每层地图cell对应的cost值后整合到主地图上。
void Costmap2DROS::mapUpdateLoop(double frequency)
{// the user might not want to run the loop every cycleif (frequency == 0.0)return;ros::NodeHandle nh;ros::Rate r(frequency);while (nh.ok() && !map_update_thread_shutdown_){struct timeval start, end;double start_t, end_t, t_diff;gettimeofday(&start, NULL);updateMap();gettimeofday(&end, NULL);start_t = start.tv_sec + double(start.tv_usec) / 1e6;end_t = end.tv_sec + double(end.tv_usec) / 1e6;t_diff = end_t - start_t;//计算地图更新时间ROS_DEBUG("Map update time: %.9f", t_diff);//更新地图边界及发布if (publish_cycle.toSec() > 0 && layered_costmap_->isInitialized()){unsigned int x0, y0, xn, yn;layered_costmap_->getBounds(&x0, &xn, &y0, &yn);publisher_->updateBounds(x0, xn, y0, yn);ros::Time now = ros::Time::now();if (last_publish_ + publish_cycle < now){publisher_->publishCostmap();last_publish_ = now;}}r.sleep();// make sure to sleep for the remainder of our cycle timeif (r.cycleTime() > ros::Duration(1 / frequency))ROS_WARN("Map update loop missed its desired rate of %.4fHz... the loop actually took %.4f seconds", frequency,r.cycleTime().toSec());}
}
3.1.3 地图更新 Costmap2DROS::updateMap()

这个函数首先调用类内getRobotPose函数,通过tf转换,将机器人底盘系的坐标转换到global系,得到机器人的位姿。然后通过layered_costmap_调用LayeredCostmap类的updateMap函数,更新地图。

void Costmap2DROS::updateMap()
{if (!stop_updates_){//获取机器人在地图上的位置和姿态tf::Stamped < tf::Pose > pose;if (getRobotPose (pose)){double x = pose.getOrigin().x(),y = pose.getOrigin().y(),yaw = tf::getYaw(pose.getRotation());//调用layered_costmap_的updateMap函数,参数是机器人位姿layered_costmap_->updateMap(x, y, yaw);

这里更新机器人的实时足迹,通过footprint_pub_发布。

//更新机器人足迹geometry_msgs::PolygonStamped footprint;footprint.header.frame_id = global_frame_;footprint.header.stamp = ros::Time::now();transformFootprint(x, y, yaw, padded_footprint_, footprint);footprint_pub_.publish(footprint);initialized_ = true;}}
}
3.1.4 激活各层地图 Costmap2DROS::start()

start函数在Movebase中被调用,这个函数对各层地图插件调用activate函数,激活各层地图。

void Costmap2DROS::start()
{std::vector < boost::shared_ptr<Layer> > *plugins = layered_costmap_->getPlugins();//检查地图是否暂停或者停止if (stopped_){//如果停止,需要重新订阅话题//“Layer”是一个类,active是其中一个类方法for (vector<boost::shared_ptr<Layer> >::iterator plugin = plugins->begin(); plugin != plugins->end();++plugin){(*plugin)->activate();}stopped_ = false;}stop_updates_ = false;//在costmap被重新初始化前阻塞..meaning one update cycle has runros::Rate r(100.0);while (ros::ok() && !initialized_)r.sleep();
}

3.2 Costmap2D类

Costmap2D类是记录地图数据的底层,它记录地图的x、y方向的尺寸,地图的分辨率,地图原点位置,以及用unsigned char类型记录地图内容。并且,Costmap2D类提供了一些对地图进行基本操作的函数,如:地图复制、用index/点坐标来设置/获取地图上该点的cost值、地图坐标和世界坐标之间的转换、获取地图大小/分辨率/原点、设置多边形边缘及内部点的cost值。

它充当一种以模版形式存在的类,Layer类则是调用它的工具人形式。

代码路径:costmap_2d/src/costmap_2d.cpp

大致的运行关系如下图:

在这里插入图片描述

3.3 LayeredCostmap类

LayeredCostmap类是Costmap2DROS的成员,含有主地图,并能通过它操作各层子地图。LayeredCostmap类的地图更新函数主要分为两步,先更新bound,再更新cost,它调用Layer类方法,它在各层子地图中被重载。

代码路径:costmap_2d/src/layered_costmap.cpp

大致的运行关系如下图:

在这里插入图片描述

3.4 CostmapLayer类

这个类继承自Layer类和Costmap2D类,它是地图插件(静态层和障碍层)的基类。它的类方法主要用于处理bound和用几种不同的策略合并子地图和主地图。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/239419.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

openssl3.2 - 在VS2019下源码调试openssl.exe

文章目录 openssl3.2 - 在VS2019下源码调试openssl.exe概述笔记先看一个用.bat调用openssl干活的实例VS2019调试参数设置设置 - 命令参数设置 - 工作目录设置 - 环境变量将命令行中需要的文件拷贝到exe目录单步调试备注END openssl3.2 - 在VS2019下源码调试openssl.exe 概述 …

多租户体系实现

文章目录 核心思路方案选择设计考量安全性扩展性通用性易用性 具体实现租户信息透传透传变量名命名规范应用内透传应用间透传 数据层租户隔离MySQL存储方案&#xff1a;多租户Mybatis插件Mybatis插件特点使用多租户Mybatis插件的优势参考文档 应用场景 经过工作中的一处场景启发…

PLC编程中ST语言操作符的使用方法

ST&#xff08;Structured Text&#xff09;语言操作符主要用于PLC编程&#xff0c;主要包括算术运算符、比较运算符和逻辑运算符等。 算术运算符包括加&#xff08;&#xff09;、减&#xff08;-&#xff09;、乘&#xff08;*&#xff09;、除&#xff08;/&#xff09;和指…

中国1981-2023年逐年每15天8km植被指数数据集

摘要 中国1981-2023年逐年每15天8km植被指数数据集来源于GIMMS NDVI数据&#xff0c;包括了1981年7月&#xff0d;2023年12月的长时间序列逐年每15天植被指数变化&#xff0c;格式为arcgis grid格式&#xff0c;投影为WGS84&#xff0c;其时间分辨率是15天&#xff0c;空间分辨…

什么是云服务器,阿里云优势如何?

阿里云服务器ECS英文全程Elastic Compute Service&#xff0c;云服务器ECS是一种安全可靠、弹性可伸缩的云计算服务&#xff0c;阿里云提供多种云服务器ECS实例规格&#xff0c;如经济型e实例、通用算力型u1、ECS计算型c7、通用型g7、GPU实例等&#xff0c;阿里云百科aliyunbai…

C/C++--ProtoBuf使用

一.什么是ProtoBuf 1.序列化和反序列化概念 序列化&#xff1a;把对象转变为字节序列的过程&#xff0c;称为系列化。 反序列化&#xff1a;把字节序列的内容恢复为对象的过程&#xff0c;称为反序列化。 2.什么情况下需要序列化和反序列化 存储数据&#xff1a;将内存中的对象…

Vulnhub靶机:driftingblues 6

一、介绍 运行环境&#xff1a;Virtualbox 攻击机&#xff1a;kali&#xff08;10.0.2.15&#xff09; 靶机&#xff1a;driftingblues6&#xff08;10.0.2.22&#xff09; 目标&#xff1a;获取靶机root权限和flag 靶机下载地址&#xff1a;https://www.vulnhub.com/entr…

洛谷 P9868 [NOIP2023] 词典

原文链接&#xff1a;NOIP真题第四讲&#xff1a;词典 题目来源&#xff1a;2023 年 NOIP T1 本题考察点&#xff1a;【贪心、枚举、模拟】 前置知识 字典序&#xff1a;指按照a、b、c、...、z的顺序&#xff0c;即a<b<c<...<z&#xff1b; 一、题目及链接 题…

如何用ChatGPT写教案设计?以“沁园春雪”为例

1. 引言 随着人工智能技术的飞速发展&#xff0c;ChatGPT已成为教育领域的一大创新工具。ChatGPT不仅能够模拟人类对话&#xff0c;还可以帮助设计互动丰富、内容丰富的教案。本文将探索如何利用ChatGPT进行教案教学设计&#xff0c;特别是通过“沁园春雪”这一案例&#xff0…

项目压测优化实践思路

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱吃芝士的土豆倪&#xff0c;24届校招生Java选手&#xff0c;很高兴认识大家&#x1f4d5;系列专栏&#xff1a;Spring原理、JUC原理、Kafka原理、分布式技术原理、数据库技术&#x1f525;如果感觉博主的文章还不错的…

RuntimeError: CUDA error: device-side assert triggered

授人以鱼不如授人以渔 解决步骤 记录下解决步骤…cuda报错真要人命 首先根据终端的提示 他说让你加这个来定位具体的python代码错哪了&#xff0c;所以咱们就加。 我这里启动命令是&#xff1a; accelerate launch --config_file "utils/acc_configs/accelerate_con…

官方认可!360荣获“科技产业高质量发展突出贡献企业”称号

近日&#xff0c;2023年度朝阳区高质量发展突出贡献企业表彰大会在北京成功召开。会上&#xff0c;朝阳区管委会&#xff08;区科信局&#xff09;对朝阳区做出积极贡献的企业单位进行表彰&#xff0c;360数字安全集团作为数字安全的领导者&#xff0c;在技术能力、研发创新和实…

windows系统下docker软件中使用ubuntu发行版本的linux系统

1.软件下载 官网下载地址 下载安装之后&#xff0c;再去微软商店下载wsl软件&#xff0c;可以直接用&#xff0c;或者也可以使用命令行拉取&#xff08;下面会讲&#xff09; 2.在docker里面创建容器的两种方法 2.1.命令行创建 前言&#xff1a;输入 winr 打开命令行进行下面…

WordPress企业模板

首页大图wordpress外贸企业模板 橙色的wordpress企业模板 演示 https://www.zhanyes.com/waimao/6250.html

i18n多国语言Internationalization的实现

i18n 是"Internationalization”的缩写&#xff0c;这个术语来源于英文单词中首尾字母“”和“n”以及中间的字符数(共计18个字符) 当我们需要开发不同语言版本时&#xff0c;就可以使用i18n多国语言的一个操作处理&#xff0c;i18n主要实现那一方面的内容呢&#xff1f;…

RHCE9学习指南 第19章 网络时间服务器

19.1 时间同步的必要性 对于一些服务来说对时间要求非常严格&#xff0c;例如&#xff0c;图19-1所示由三台服务器搭建的ceph集群。 图19-1 三台机器搭建的集群对时间要求比较高 这三台服务器的时间必须要保持一样&#xff0c;如果不一样&#xff0c;就会显示报警信息。那么…

基于SSM的戏剧推广网站的设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;Vue、HTML 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#xff1a;是…

试用清华Chatglm智能体

清华AI平台&#xff0c;感觉在见过的国内AI平台中做的是比较优秀的&#xff0c;目前该平台提供的智能体功能感觉更智能或者说更傻瓜式一些。定义可以定义专属智能体&#xff0c;这些智能体是自己想要的网络上的汇集处理后的信息&#xff0c;或者是绘画或者是编写某个方面的代码…

23111 网络编程 day3

思维导图 tip协作服务 程序如下&#xff1a; #include<myhead.h> #define SER_PORT 69 #define SER_IP "192.168.125.180"int do_upload(int cfd,struct sockaddr_in sin) {//向服务器发送上传请求char buf[512]"";//组装请求数据short *p1(short*…

程序员接私活还不知道这几个平台?那你真的亏了!

程序员接私活现在已经是一个老生常谈的话题了&#xff0c;现在市面上各种程序员接单平台层出不穷&#xff0c;也参差不齐&#xff0c;有比较老牌的知名平台&#xff0c;也有比较好的新兴平台&#xff0c;如此多的平台就容易让人眼花缭乱&#xff0c;不知道该如何选择。 这期文…