costmap_2d包介绍

news2024/11/18 10:35:34

文章目录

    • 一. 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 pose
  old_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_id
  global_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的方法initialize
      plugin->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类是用于地图发布的封装类。

//发布Costmap2D
  publisher_ = 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 cycle
  if (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 time
    if (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 run
  ros::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.coloradmin.cn/o/1390134.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【HuggingFace Transformer库学习笔记】基础组件学习:Datasets

基础组件——Datasets datasets基本使用 导入包 from datasets import *加载数据 datasets load_dataset("madao33/new-title-chinese") datasetsDatasetDict({train: Dataset({features: [title, content],num_rows: 5850})validation: Dataset({features: [titl…

高级定时器

本节主要介绍以下内容&#xff1a; 定时器简介 高级定时器功能框图讲解 一、定时器简介 定时器功能 &#xff1a;定时、输出比较、输入捕获、断路输入 定时器分类 &#xff1a;基本定时器、通用定时器、高级定时器 定时器资源 &#xff1a;F103有2个高级定时器、4个通…

C#编程-实现委托

实现委托 委托是可以存储对方法的引用的对象。在C#中,委托允许您动态地改变类中方法的引用。 考虑咖啡售货机的示例,它配置不同口味的咖啡,例如卡布奇诺咖啡和黑咖啡。在选择所需口味的咖啡时,售货机决定混合各种成分,例如奶粉、咖啡粉、热水、卡布奇诺咖啡粉。所有的材…

构建一个最新版本 Maven 项目

文章目录 构建一个最新版本 Maven 项目1. 所用各种软件的版本2. 踩过的坑3. 构建项目过程4. 项目打包方式 构建一个最新版本 Maven 项目 截止 2024 年 1 月 13 日&#xff0c;Apache 官网上 Maven 的最新安全版本为 3.9.6&#xff0c;下载、安装及配置方法见之前的博客&#x…

TIMESAT提取物候信息操作流程

TIMESAT提取物候信息操作流程 软件环境&#xff1a;Matlab R2014aTIMESAT3.2 数据介绍&#xff1a;MODIS A3或Q1的NVI&#xff08;NDVI&#xff09;均测试过这个流程&#xff0c;可行&#xff08;大拇指&#xff09;。 TIMESAT输入n年数据&#xff0c;提取n-1年的物候参数。通…

jmeter--4.参数化的方式

目录 1. 用户定义的变量 2. 用户参数 3. 函数助手 3.1 time获取当前时间 3.2 Random随机数 3.3 随机字符串函数 3.4 字符串变更为大写 4. CSV数据文件设置 5. 接口关联--正则和json等提取 1. 用户定义的变量 线程组->添加->配置元件->用户定义的变量 引用方…

【设计模式-06】Observer观察者模式

简要说明 事件处理模型 场景示例&#xff1a;小朋友睡醒了哭&#xff0c;饿&#xff01; 一、v1版本(披着面向对象的外衣的面向过程) /*** description: 观察者模式-v1版本(披着面向对象的外衣的面向过程)* author: flygo* time: 2022/7/18 16:57*/ public class ObserverMain…

MySQL 从零开始:05 MySQL 数据类型

文章目录 1、数值类型1.1 整形数值1.2 浮点型数值1.3 布尔值 2、日期和时间类型3、字符串类型3.1 CHAR 和 VARCHAR3.2 BINARY 和 VARBINARY3.3 BLOB 和 TEXT3.4 ENUM 类型3.5 SET 类型 4、空间数据类型5、JSON 数据类型5、JSON 数据类型 前面的讲解中已经接触到了表的创建&…

这款软件轻松解决你图片水印问题

随着数字时代的到来&#xff0c;图片已经成为我们生活中不可或缺的一部分。然而&#xff0c;很多时候&#xff0c;我们会遇到带有水印的图片&#xff0c;这不仅影响了图片的视觉效果&#xff0c;还可能遮挡了重要的内容。这时&#xff0c;一款专业的去水印工具就显得尤为重要。…

博途PLC增量式PID和脉冲轴组合控制阀门开度(算法介绍)

这篇博客我们以S7-1200PLC平台来举例,介绍我们的PID闭环控制器如何控制脉冲轴实现阀门角度控制。SMART PLC PID控制器控制伺服驱动器实现关节角度控制详细内容请参考下面文章: https://rxxw-control.blog.csdn.net/article/details/129658364https://rxxw-control.blog.csdn…

Python基础语法(中)—— python列表、字符串、函数

文章目录 5. python中的列表5.1 列表的初始化5.1.1 直接初始化5.1.2 通过append函数初始化5.1.3 通过for语句初始化列表长度和每个位置的数值 5.2访问列表元素5.3使用循环语句遍历列表5.4列表的切片操作5.5列表的复制5.6列表的运算5.7列表的常用操作5.8嵌套列表5.9列表其他小知…

【手撕C语言 第二集】初识C语言

​​ 一、变量的作用域和生命周期 作用域&#xff1a;一个变量在哪里可以使用它&#xff0c;哪里就是它的作用域。 局部变量的作用域&#xff1a;变量所在的局部范围 全局变量的作用域&#xff1a;整个工程 不管整个工程里面有多少源文件&#xff0c;都可以使用全局变量。这样…

力扣电话号码的组合

文章目录 题目说明做题思路代码实现代码解析 题目链接 题目说明 首先我们先分析一下这个题目题目中说呢先给出一个字符串这个字符串其实就是这个九键数字我们要按照要求将数字所代表的字符进行自由组合形成一个字符串并且这个字符串的长度和输入的数字字符串长度相同&#xff0…

《每天一分钟学习C语言·十二》各种指针问题

1、 int arr; int * restrict pt &arr; *pt 100; *arr 10;注&#xff1a;restrict只能修饰指针&#xff0c;被restrict修饰的指针指向一块内存后这块内存就归这个指针管理了&#xff0c;其他任何指针都不能修改这块内存的内容&#xff0c;这是一个约定&#xff0c;当…

备份和容灾讲解

备份和容灾 &#xff08;1&#xff09;容灾&#xff08;容许灾难的发生&#xff09;是一种架构方案&#xff0c;包括了很多方案&#xff0c;如下 本地高可用 双活&#xff08;特指存储&#xff0c;可以理解为两端同时对外提供服务&#xff09;&#xff1a;通过一个双写模块把…

20240115-插入删除 GetRandom O(1)

题目要求 实现 RandomizedSet 类&#xff1a; RandomizedSet() 初始化 RandomizedSet 对象。bool insert(int val) 将不存在的项目 val 插入随机集合。如果项目不存在&#xff0c;则返回 true&#xff0c;否则返回 false。bool remove(int val) 从集合中删除项目 val&#xf…

文理导航期刊投稿方式

《文理导航》杂志系国家新闻出版总署批准&#xff0c;内蒙古自治区文旅厅主管&#xff0c;内蒙古自治区北方文化研究院主办的&#xff0c;面向大中专院校、中小学教育的专业性教育刊物&#xff0c;阅读对象是关心教育事业发展的大中专院校、职业教育、中小学教育的专家、教研员…

FFmpeg连载6-音频重采样

今天我们的实战内容是将音频解码成PCM&#xff0c;并将PCM重采样成特定的采样率&#xff0c;然后输出到本地文件进行播放。 什么是重采样&#xff1f; 所谓重采样&#xff0c;一句话总结就是改变音频的三元素&#xff0c;也就是通过重采样改变音频的采样率、采样格式或者声道数…

PHP项目添加分布式锁,这里是ThinkPHP8框架实现分布式锁

背景&#xff1a;公司旧项目&#xff0c;最初访问量不多&#xff0c;单机部署的。后来&#xff0c;访问量上来了&#xff0c;有阵子很卡&#xff0c;公司决定横向扩展&#xff0c;后端代码部署了三台服务器。部署调整后&#xff0c;有用户反馈&#xff0c;一个订单支付了三次。…

【机器学习入门】机器学习基础概念与原理

*&#xff08;本篇文章旨在帮助新手了解机器学习的基础概念和原理&#xff0c;不深入讨论算法及核心公式&#xff09; 目录 一、机器学习概念 1、什么是机器学习&#xff1f; 2、常见机器学习算法和模型 3、使用Python编程语言进行机器学习实践 4、机器学习的应用领域 二…