栅格地图、障碍物地图与膨胀地图(障碍物地图(二))

上一篇大致看完了障碍物地图的初始化内容以及对于传感器数据的处理,我们知道在该部分算法维护了一个ObservationBuffer,其中存储了一段时间内的点云数据。每次新的数据进来后,还会根据设定的时间参数observation_keep_time抛弃比较久远的障碍物点云。但是在看的过程中,我们也产生了一些疑惑,比如ObservationBuffer本身维护的是一系列世界坐标系下的pointcloud,它跟栅格地图之间是如何结合的?又比如在维护ObservationBuffer时,每一帧点云都是通过Observation类函数完成的。在其中传入了当时点云产生时其传感器在世界坐标系下的位置origin,以及还有两个似乎一直没用过的参数obstacle_range与raytrace_range。带着这些疑惑我们重新回到obstacle_layer这个类函数中,继续学习其中剩下的内容。

1、updateBounds

在obstacle_layer类函数中,我们会发现它除了初始化与点云函数的订阅外,还有一个很重要的函数updateBounds。这个函数首先确定了是否需要更新地图的原点:

if (rolling_window_)
    updateOrigin(robot_x - getSizeInMetersX() / 2, robot_y - getSizeInMetersY() / 2);

这行代码比较有意思,感觉它相当于是一个用来决定当前地图是否锁定机器人视角的参数,如果是的话地图的中心会始终随着机器人的位置变化。正常来说它应该是false,静态地图本身也不会随着机器人的位置变化而变化。
然后是获取了下地图的上下界:

  useExtraBounds(min_x, min_y, max_x, max_y);

接下来的函数是获取了两个vector:

  bool current = true;
  std::vector<Observation> observations, clearing_observations;
  // get the marking observations
  current = current && getMarkingObservations(observations);
  // get the clearing observations
  current = current && getClearingObservations(clearing_observations);
  // update the global current status
  current_ = current;

这里维护的是两个Observation类的数据。这两个之间咋一看似乎是互斥的,但是实际上它们的数据来源是一样的。为什么这么说呢,首先看一下getMarkingObservations这个函数,它的作用呢是返回marking_buffers_的值,这个buffer的值本身是在上一篇中初始化函数时执行的这么一个步骤:

// create an observation buffer
    observation_buffers_.push_back(
        boost::shared_ptr < ObservationBuffer
            > (new ObservationBuffer(topic, observation_keep_time, expected_update_rate, min_obstacle_height,
                                     max_obstacle_height, obstacle_range, raytrace_range, *tf_, global_frame_,
                                     sensor_frame, transform_tolerance)));

    // check if we'll add this buffer to our marking observation buffers
    if (marking)
      marking_buffers_.push_back(observation_buffers_.back());

    // check if we'll also add this buffer to our clearing observation buffers
    if (clearing)
      clearing_buffers_.push_back(observation_buffers_.back());

在代码创建完成ObservationBuffer指针后,marking_buffers_实际上存储了该指针,所以操作marking_buffers_其实约等于操作了ObservationBuffer。因为marking_buffers_和 observation_buffers_共享了相同的ObservationBuffer 对象。注意到下面的clearing_buffers_也进行了相同的操作。

然后我们再回到上面的代码中,再看一下getClearingObservations这个函数,而这个函数其实也就是返回了clearing_buffers_中的所有点云值。所以说这两个函数其实殊涂同归,最终都是执行的ObservationBuffer::getObservations这个函数。那么为什么这里要获取两个相同的东西呢?继续往下看。

下面一个函数跟我们解释了clearing_observations中数据的使用方式

  // raytrace freespace
  for (unsigned int i = 0; i < clearing_observations.size(); ++i)
  {
    raytraceFreespace(clearing_observations[i], min_x, min_y, max_x, max_y);
  }

这个地方调用了一个raytraceFreespace函数,这个函数的详细解释在第三部分,它的作用是清理出传感器到被测物体之间的栅格,设置为空闲。

在根据clearing_observations将传感器到点云之间的栅格清理完毕后,下一步自然是要操作这些障碍物点云了,所以这里就用到了observations的点云,算法在后面对其进行了遍历,取出每一帧点云,首先判断它到传感器之间的距离,注意到这里用到了一个参数obstacle_range_,这个上一章中提到Observations类时提到了,但是当时没有使用到,后面在这里我们发现它被使用了,它的作用是过滤掉那些点云距离传感器原点超过该值的点。然后对剩下的点求取其在地图下的栅格位置index,最后将该栅格的值设置为LETHAL_OBSTACLE(254:障碍物)。

// place the new obstacles into a priority queue... each with a priority of zero to begin with
  for (std::vector<Observation>::const_iterator it = observations.begin(); it != observations.end(); ++it)
  {
    const Observation& obs = *it;

    const sensor_msgs::PointCloud2& cloud = *(obs.cloud_);

    double sq_obstacle_range = obs.obstacle_range_ * obs.obstacle_range_;

    sensor_msgs::PointCloud2ConstIterator<float> iter_x(cloud, "x");
    sensor_msgs::PointCloud2ConstIterator<float> iter_y(cloud, "y");
    sensor_msgs::PointCloud2ConstIterator<float> iter_z(cloud, "z");

    for (; iter_x !=iter_x.end(); ++iter_x, ++iter_y, ++iter_z)
    {
      double px = *iter_x, py = *iter_y, pz = *iter_z;

      // if the obstacle is too high or too far away from the robot we won't add it
      if (pz > max_obstacle_height_)
      {
        ROS_DEBUG("The point is too high");
        continue;
      }

      // compute the squared distance from the hitpoint to the pointcloud's origin
      double sq_dist = (px - obs.origin_.x) * (px - obs.origin_.x) + (py - obs.origin_.y) * (py - obs.origin_.y)
          + (pz - obs.origin_.z) * (pz - obs.origin_.z);

      // if the point is far enough away... we won't consider it
      if (sq_dist >= sq_obstacle_range)
      {
        ROS_DEBUG("The point is too far away");
        continue;
      }

      // now we need to compute the map coordinates for the observation
      unsigned int mx, my;
      if (!worldToMap(px, py, mx, my))
      {
        ROS_DEBUG("Computing map coords failed");
        continue;
      }

      unsigned int index = getIndex(mx, my);
      costmap_[index] = LETHAL_OBSTACLE;
      touch(px, py, min_x, min_y, max_x, max_y);
    }
  }

注意到后面使用到的

costmap_[index] = LETHAL_OBSTACLE;

这里已经是在处理地图层面了,本身updateBounds这个函数就是在LayeredCostmap::updateMap中被调用,而这个函数的作用也就是为了更新地图。因此,这里就将障碍物点云与地图信息结合起来了。

这个函数的最后一步,是计算了机器人所占的栅格位置:

updateFootprint(robot_x, robot_y, robot_yaw, min_x, min_y, max_x, max_y);

函数本身不长,但是涉及到一些细节问题,具体函数实现参看第四部分。自此该函数结束。

2、updateCosts

这个函数是障碍物地图中除了updateBounds外最重要的一个函数了,它的作用是将机器人足迹范围内设置为FREE_SPACE,并且在bound范围内将本层障碍地图的内容合并到主地图上。这个函数一共就两部分,首先看一下它如何将机器人足迹范围内设置为FREE_SPACE:

if (footprint_clearing_enabled_)
  {
    setConvexPolygonCost(transformed_footprint_, costmap_2d::FREE_SPACE);
  }

它本身调用的是Costmap2D下的setConvexPolygonCost函数,这个函数需要传递两个参数:足迹所在的位置以及空闲的栅格值。首先,函数对传入的机器人多边形进行位置判断,判断它是否在地图上:

  std::vector<MapLocation> map_polygon;
  for (unsigned int i = 0; i < polygon.size(); ++i)
  {
    MapLocation loc;
    if (!worldToMap(polygon[i].x, polygon[i].y, loc.x, loc.y))
    {
      // ("Polygon lies outside map bounds, so we can't fill it");
      return false;
    }
    map_polygon.push_back(loc);
  }

对于在地图上的点,它们会被存储到一个map_polygon这么一个容器中,然后下一步调用convexFillCells函数,该函数通过机器人顶点坐标数组map_polygon得到多边形边缘及内部的全部cell,存放在polygon_cells中:

  // get the cells that fill the polygon
  convexFillCells(map_polygon, polygon_cells);

碍于篇幅就不展开了,知道是这么个功能就行,然后下一步则是对这些栅格进行赋值:

  // set the cost of those cells
  for (unsigned int i = 0; i < polygon_cells.size(); ++i)
  {
    unsigned int index = getIndex(polygon_cells[i].x, polygon_cells[i].y);
    costmap_[index] = cost_value;
  }

可以看到这里对于所有栅格在机器人多边形包含范围内的数据,其概率值都被设置为0了,也就是空闲。这个就是这个函数的功能与具体执行过程。
然后回到该节开始部分,我们提到updateCosts执行了两个功能,一个是将机器人足迹范围内设置为FREE_SPACE,剩下的是将本层障碍地图的内容合并到主地图上,第一个已经讲完,看一下第二部分:

switch (combination_method_)
  {
    case 0:  // Overwrite
      updateWithOverwrite(master_grid, min_i, min_j, max_i, max_j);
      break;
    case 1:  // Maximum
      updateWithMax(master_grid, min_i, min_j, max_i, max_j);
      break;
    default:  // Nothing
      break;
  }

这边是一个switch语句,根据combination_method_决定使用的是哪个函数,这个参数默认是0,所以这里我们简单看一下updateWithOverwrite函数。这个函数本身是定义在CostmapLayer类中的,它的功能其实很简单,就是赋值:

void CostmapLayer::updateWithOverwrite(costmap_2d::Costmap2D& master_grid, int min_i, int min_j, int max_i, int max_j)
{
  if (!enabled_)
    return;
  unsigned char* master = master_grid.getCharMap();
  unsigned int span = master_grid.getSizeInCellsX();

  for (int j = min_j; j < max_j; j++)
  {
    unsigned int it = span*j+min_i;
    for (int i = min_i; i < max_i; i++)
    {
      if (costmap_[it] != NO_INFORMATION)
        master[it] = costmap_[it];
      it++;
    }
  }
}

这个函数相当于是对地图所有栅格执行了一次遍历,然后对于costmap_这张地图中栅格状态已知的点进行赋值给master_grid。这里的master_grid是updateCosts函数的传参,来自于LayeredCostmap这个类的私有变量:

private:
  //这是一个普通的`Costmap2D`类型的对象定义。它是通过值传递的方式进行操作,即在创建副本时对原始对象进行复制。因此,对该变量的修改不会影响到原始对象。
  Costmap2D costmap_;
  {
    if((*plugin)->isEnabled())
      (*plugin)->updateCosts(costmap_, x0, y0, xn, yn);
  }

所以这张地图应该是维护的全局地图?而函数中的costmap_则是来自于Costmap2D类:

//`const Costmap2D& costmap_`:这是一个引用类型的常量变量定义。它引用了一个`Costmap2D`类型的对象,并且该对象在定义时被声明为常量,即不可修改。
//通过引用,对该变量的任何修改都会直接影响到原始对象。
unsigned char* costmap_;

这里涉及到一些非三张地图函数的调用,仅看着一块感觉不是很清楚。我的理解是:Costmap2D类中维护的是一张临时性的地图,操作障碍物点云时存储的是这里,而LayeredCostmap这里则是实际对外使用的地图,例如全局路径规划,所以在LayeredCostmap中会需要调用到ObstacleLayer下的updateCosts函数,将局部的障碍物添加到全局地图中。

3、raytraceFreespace

这个函数的作用是清理出传感器到被测物体之间的栅格,设置为空闲。具体过程如下:

首先该函数获取了一帧数据中的origin参数,这个参数在上一篇中我们知道它代表的是当时记录数据时传感器在地图坐标系下的位置,以及设置了一个指针指向了点云:

  double ox = clearing_observation.origin_.x;
  double oy = clearing_observation.origin_.y;
  const sensor_msgs::PointCloud2 &cloud = *(clearing_observation.cloud_);

然后计算了这个坐标点是否在地图上,对于静态地图而言,其实应该用不着这步,能记录下来的基本肯定在地图里面:

  // get the map coordinates of the origin of the sensor
  unsigned int x0, y0;
  if (!worldToMap(ox, oy, x0, y0))
  {
    ROS_WARN_THROTTLE(
        1.0, "The origin for the sensor at (%.2f, %.2f) is out of map bounds. So, the costmap cannot raytrace for it.",
        ox, oy);
    return;
  }

然后还是一个范围限定函数:

// 保证传感器原点在bound范围内
  touch(ox, oy, min_x, min_y, max_x, max_y);

这个函数的作用感觉不是很明确,它同样是限定了传感器原点,但是又没有给出超过这个范围该怎么处理,感觉可有可无。

接下来,正餐开始!

前面一些乱七八糟的前奏处理完了,接下来自然是开始处理实际的点云数据。我们首先从点云中遍历每一个点,拿到它的世界坐标系下的坐标,同时拿到传感器的位置,然后XY轴的距离:

//对于点云中的每个点,我们要追踪原点和clear obstacles之间的一条线
  for (; iter_x != iter_x.end(); ++iter_x, ++iter_y)
  {
    //wx wy是当前点云中的点的坐标
    double wx = *iter_x;
    double wy = *iter_y;

    // now we also need to make sure that the enpoint we're raytracing
    // to isn't off the costmap and scale if necessary
    //a、b是该点跟传感器原点的距离
    double a = wx - ox;
    double b = wy - oy;

接下来的这一步,是为了对那些超出地图的点云的处理:

	// the minimum value to raytrace from is the origin
    //如果当前点x方向比地图原点还小
    if (wx < origin_x)
    {
      //t(比例)=(地图原点-传感器原点)/(点云中的该点-传感器原点)
      double t = (origin_x - ox) / a;
      //当前点x = 地图原点x
      wx = origin_x;
      //当前点y = 
      //实际上还是把点云点和传感器连线之间清空,只是通过相似三角形丢弃了超出地图原点范围外的部分,下面三个判断结构同理
      wy = oy + b * t;
    }
    if (wy < origin_y)
    {
      double t = (origin_y - oy) / b;
      wx = ox + a * t;
      wy = origin_y;
    }

这部分该怎么理解呢,在看栅格地图这一章节的时候,我们知道了地图的原点实际上是在地图的左下角的。所以对于一个栅格而言,它的XY的值其实应该是一定大于地图原点的。所以对于任何一个方向小于地图原点的点,它本身其实都是超出了地图的。而对于这些点,这里并没有直接丢弃掉,而是根据相似三角形原理截取了它在地图范围内的一部分,这就是上述部分代码的原理。
当然,判断了XY很小的值,同样也需要判断一下超过上限的部分,原理相同,不重复叙述:

	// the maximum value to raytrace to is the end of the map
    if (wx > map_end_x)
    {
      double t = (map_end_x - ox) / a;
      wx = map_end_x - .001;
      wy = oy + b * t;
    }
    if (wy > map_end_y)
    {
      double t = (map_end_y - oy) / b;
      wx = ox + a * t;
      wy = map_end_y - .001;
    }

然后,我们在这里设置了一个参数:

unsigned int cell_raytrace_range = cellDistance(clearing_observation.raytrace_range_);

这个参数的来源是最开始我们初始化Observation时传递的raytrace_range参数,这里根据分辨率转换出来了一个栅格距离,也就是它大概经过了多少个栅格。

这个距离是干嘛的呢?它其实是用于清理传感器原点和障碍物点之间的cell的:

MarkCell marker(costmap_, FREE_SPACE);
raytraceLine(marker, x0, y0, x1, y1, cell_raytrace_range);
updateRaytraceBounds(ox, oy, wx, wy, clearing_observation.raytrace_range_, min_x, min_y, max_x, max_y);

这里首先调用了一个MarkCell,它需要两个参数,一个是地图的原始数据costmap,一个是某一个栅格的值value。同时MarkCell维护了一个operator重载函数,它只有一个作用,对某一个栅格赋值:

    inline void operator()(unsigned int offset)
    {
      costmap_[offset] = value_;
    }

然后是调用了两个不同的函数,第一个函数是画出了每一个点云到传感器原点之间的连线所经过的栅格,底层使用的是bresenham算法,关于这个算法的原理可以参考《【计算机图形学】画线算法——Bresenham算法(任意斜率)》一文,这里不展开详细叙述了。重要的是在这里面对每一个起点与终点之间的栅格调用了重载函数,也就是经过这种方式将所有传感器原点到障碍物之间的点云都转化成了空闲的状态。而updateRaytraceBounds函数跟前面提到的touch函数比较类似,只是上面限定的是传感器的原点,这里限制的是画线的点。没太看懂有啥用。到此raytraceFreespace函数结束。

4、updateFootprint

这个函数内容不多,就是几行代码:

//它的作用是基于机器人当前位置确定该位置下的足迹
void ObstacleLayer::updateFootprint(double robot_x, double robot_y, double robot_yaw, double* min_x, double* min_y,
                                    double* max_x, double* max_y)
{
    if (!footprint_clearing_enabled_) return;
    transformFootprint(robot_x, robot_y, robot_yaw, getFootprint(), transformed_footprint_);

    for (unsigned int i = 0; i < transformed_footprint_.size(); i++)
    {
      touch(transformed_footprint_[i].x, transformed_footprint_[i].y, min_x, min_y, max_x, max_y);
    }
}

单独摘出来讲主要是为了理一下其中使用到的transformFootprint函数与getFootprint函数。首先我们可以先看一下getFootprint这个函数,它本身是返回的一组点:


const std::vector<geometry_msgs::Point>& Layer::getFootprint() const
{
  return layered_costmap_->getFootprint();
}
  /** @brief Returns the latest footprint stored with setFootprint(). */
  const std::vector<geometry_msgs::Point>& getFootprint() { return footprint_; }

在layered_costmap_中footprint_定义为:

  std::vector<geometry_msgs::Point> footprint_;

可以看到它是一组点。其实这组点代表的是机器人的外轮廓。对于多边形机器人而言,自然是好理解的,主要问题在于如何处理圆形呢?这部分主要在Costmap2DROS这个类中进行了处理,在函数初始化时,调用了一个setUnpaddedRobotFootprint函数,这个函数负责存储footprint_多边形,在机器人类型为圆形时,其传参时调用了makeFootprintFromRadius函数:

setUnpaddedRobotFootprint(makeFootprintFromRadius(new_config.robot_radius));

而makeFootprintFromRadius函数的主要工作就是根据圆的半径生成出16个圆上的点:

std::vector<geometry_msgs::Point> makeFootprintFromRadius(double radius)
{
  std::vector<geometry_msgs::Point> points;

  // Loop over 16 angles around a circle making a point each time
  int N = 16;
  geometry_msgs::Point pt;
  for (int i = 0; i < N; ++i)
  {
    double angle = i * 2 * M_PI / N;
    pt.x = cos(angle) * radius;
    pt.y = sin(angle) * radius;

    points.push_back(pt);
  }
  return points;
}

因此,对于圆形的机器人,其footprint_是使用的16个圆周上的点表示的。这就是getFootprint函数中足迹的来源。

然后我们再看一下transformFootprint函数,它对于这16个点做了一个操作:

void transformFootprint(double x, double y, double theta, const std::vector<geometry_msgs::Point>& footprint_spec,
                        std::vector<geometry_msgs::Point>& oriented_footprint)
{
  // build the oriented footprint at a given location
  oriented_footprint.clear();
  double cos_th = cos(theta);
  double sin_th = sin(theta);
  for (unsigned int i = 0; i < footprint_spec.size(); ++i)
  {
    geometry_msgs::Point new_pt;
    new_pt.x = x + (footprint_spec[i].x * cos_th - footprint_spec[i].y * sin_th);
    new_pt.y = y + (footprint_spec[i].x * sin_th + footprint_spec[i].y * cos_th);
    oriented_footprint.push_back(new_pt);
  }
}

对于这块比较敏感的人其实应该一眼能看出来了哈,这个函数其实本身就是实现了点的转换。将原来的16个点投影到了地图上。因为最原始的足迹我们可以看到它是以机器人为中心的16个坐标点,但是通过根据机器人在世界坐标系下的位置与角度关系,我们就将这16个点转换到了世界坐标系下。而这个,就是updateFootprint函数的主要工作。

总结

通过对上述几个函数的简单翻阅,我们大致理解了障碍物地图中上一节遇到的一些问题,例如ObservationBuffer本身维护的是一系列世界坐标系下的pointcloud,它跟栅格地图之间是如何结合的?又比如在维护ObservationBuffer时,每一帧点云都是通过Observation类函数完成的。在其中传入了当时点云产生时其传感器在世界坐标系下的位置origin,以及还有两个似乎一直没用过的参数obstacle_range与raytrace_range。这些在这部分函数中都得到了一定的解答,也因此对算法本身有了一个更加深刻的认知。总体来说这部分代码大概实现的所有东西就在这两节内容中了,唯一一个让我目前还比较疑惑的地方是:当时静态地图初始化时获取的全局地图属于LayeredCostmap类,与这里的第二节更新的地图似乎是同一个?但总感觉应该不是,这里有个地方应该漏掉了,做个笔记,后面如果看到了再补充。

参考:
《【ROS-Navigation】Costmap2D代价地图源码解读-障碍层ObstacleLayer》
《障碍层ObstacleLayer》

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

生成一个好故事!StoryDiffusion:一致自注意力和语义运动预测器必不可少(南开字节)

文章链接&#xff1a;https://arxiv.org/pdf/2405.01434 主页&#xff1a;https://storydiffusion.github.io/ 对于最近基于扩散的生成模型来说&#xff0c;在一系列生成的图像中保持一致的内容&#xff0c;尤其是那些包含主题和复杂细节的图像&#xff0c;是一个重大挑战。本…

C/C++ BM32 合并二叉树

文章目录 前言题目解决方案一1.1 思路阐述1.2 源码 解决方案二2.1 思路阐述2.2 源码 总结 前言 树的题目大概率是要用到递归的&#xff0c;将一个树的问题拆分成子树的问题&#xff0c;不断拆分。 这题也用到了递归的思想。 题目 已知两颗二叉树&#xff0c;将它们合并成一颗…

腾讯地图商业授权说明一篇文章讲清楚如何操作

最近在使用腾讯地图&#xff0c;发现我要上架应用商店APP需要我有地图的授权书。 认真研究了一下原来腾讯地图现在要收费了&#xff0c;如果你打算以商业目的使用它&#xff0c;比如对第三方用户收费或者进行项目投标等&#xff0c;就需要先获取腾讯位置服务的商业授权许可。申…

网络演进技术演进:裸纤专线、SDH、MSTP+、OTN、PTN、IP-RAN

前言 文章主要介绍常见名词以及其在各自领域实现的功能价值。 01 裸纤 裸光纤&#xff08;裸光纤&#xff09;由运营商提供&#xff0c;是无中继的光纤线路&#xff0c;仅通过配线架连接。相比传统光纤&#xff0c;裸光纤提供纯粹的物理传输路径&#xff0c;无需额外网…

win2012磁盘空间不足,c盘正常,d盘无法写入,如何解决?

&#x1f3c6;本文收录于「Bug调优」专栏&#xff0c;主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&收藏&&…

人工智能概述与入门基础简述

人工智能&#xff08;AI&#xff09;是计算机科学的一个分支&#xff0c;它致力于创建能够执行通常需要人类智能的任务的机器。这篇科普文章将全面介绍人工智能的基本概念、发展历程、主要技术、实际应用以及如何入门这一领域。 一、人工智能的定义与发展历程 人工智能的概念…

vue2实现生成二维码和复制保存图片功能(复制的同时会给图片加文字)

<template><divstyle"display: flex;justify-content: center;align-items: center;width: 100vw;height: 100vh;"><div><!-- 生成二维码按钮和输入二维码的输入框 --><input v-model"url" placeholder"输入链接" ty…

第四篇:记忆的迷宫:探索计算机存储结构的奥秘与创新

记忆的迷宫&#xff1a;探索计算机存储结构的奥秘与创新 1 引言 1.1 计算机存储系统的发展与重要性 在现代计算技术中&#xff0c;存储系统承担着非常关键的角色&#xff0c;它不仅负责信息的持久保存&#xff0c;同时确保高效的数据访问速度&#xff0c;影响着整体系统性能的…

[redis] redis为什么快

1. Redis与Memcached的区别 两者都是非关系型内存键值数据库&#xff0c;现在公司一般都是用 Redis 来实现缓存&#xff0c;而且 Redis 自身也越来越强大了&#xff01;Redis 与 Memcached 主要有以下不同&#xff1a; (1) memcached所有的值均是简单的字符串&#xff0c;red…

electron 通信总结

默认开启上下文隔离的情况下 渲染进程调用主进程方法&#xff1a; 主进程 在 main.js 中&#xff0c; 使用 ipcMain.handle&#xff0c;添加要处理的主进程方法 const { ipcMain } require("electron"); 在 electron 中创建 preload.ts 文件&#xff0c;从 ele…

getchar和putchar函数详解

getchar和putchar函数详解 1.getchar函数1.1函数概述1.2函数返回值1.3函数注意事项1.4函数的使用 2.putchar函数2.1函数概述2.2函数返回值2.3函数使用实例 1.getchar函数 1.1函数概述 从一个流中读取一个字符&#xff0c;或者从标准输入中获得一个字符 函数原型&#xff1a; …

HFSS学习-day1-T形波导的内场分析和优化设计

入门实例--T形波导的内场分析和优化设计 HFSS--此实例详细步骤1.创建项目2.设置求解类型3.设置与建模相关的一些信息设置默认的建模长度单位 4.创建T形模型的三个臂基本参数端口激励进行复制 5.创建被挖去的部分设置正确的边界条件和端口激励方式添加求解设置添加扫频项检查一下…

基于EWT联合SVD去噪

一、代码原理 &#xff08;1&#xff09;基于EWT-SVD的信号去噪算法原理 经验小波变换&#xff08;Empirical Wavelet Transform&#xff0c;EWT&#xff09;&#xff1a;EWT是一种基于信号局部特征的小波变换方法&#xff0c;能够更好地适应非线性和非平稳信号的特性。奇异值…

寻找最佳App分发平台:小猪APP分发脱颖而出

在当今移动应用市场日益饱和的环境下&#xff0c;选择一个合适的App分发平台对于开发者来说至关重要。这不仅关系到应用能否快速触达目标用户&#xff0c;还直接影响到品牌的塑造与市场份额的争夺。本文将深入探讨几大关键因素&#xff0c;帮助开发者判断哪个App分发平台最适合…

pyside6的调色板QPalette的简单应用

使用调色板需要先导入:from PySide6.QtGui import QPalette 调色板QPalette的源代码&#xff1a; class QPalette(Shiboken.Object):class ColorGroup(enum.Enum):Active : QPalette.ColorGroup ... # 0x0Normal : QPalette.ColorGrou…

权益商城系统源码 现支持多种支付方式

简介&#xff1a; 权益商城系统源码&#xff0c;支持多种支付方式&#xff0c;后台商品管理&#xff0c;订单管理&#xff0c;串货管理&#xff0c;分站管理&#xff0c;会员列表&#xff0c;分销日志&#xff0c;应用配置。 上传到服务器&#xff0c;修改数据库信息&#xff…

裁员为什么先裁技术人员?

最近这个问题比较火&#xff0c;我分享一个印象深刻的答案&#xff1a;楼盖完了&#xff0c;还需要搬砖的吗&#xff1f; 这个答案让我对互联网/程序员这个行业/职业有了新的认识。 房地产是在现实世界里盖房子&#xff0c;互联网是在虚拟世界里盖房子&#xff0c;只不过互联网…

【CTF Web】XCTF GFSJ0485 simple_php Writeup(代码审计+GET请求+PHP弱类型漏洞)

simple_php 小宁听说php是最好的语言,于是她简单学习之后写了几行php代码。 解法 &#xfeff;<?php show_source(__FILE__); include("config.php"); $a$_GET[a]; $b$_GET[b]; if($a0 and $a){echo $flag1; } if(is_numeric($b)){exit(); } if($b>1234){ech…

【氮化镓】GaN HEMTs 在金星及恶劣环境下的应用

文章是关于GaN增强模式晶体管(enhancement-mode p-GaN-gate AlGaN/GaN HEMTs)在金星探索和其它恶劣环境下的应用研究。文章由Qingyun Xie等人撰写,发表在《Applied Physics Letters》上,属于(Ultra)Wide-bandgap Semiconductors for Extreme Environment Electronics特刊。…

基于Springboot的果蔬作物疾病防治系统(有报告)。Javaee项目,springboot项目。

演示视频&#xff1a; 基于Springboot的果蔬作物疾病防治系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;springboot项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系…
最新文章