サービス指向のモバイルロボットはどのようにして屋内パスの完全なカバレッジとクリーニングを実現しますか?さわやかで清潔な家を提供します

0.本文初心:

スイープロボット(エリアカバレッジアルゴリズムに焦点を当てる)とロジスティクスロボット(輸送スケジューリングアルゴリズムに焦点を当てる)についての私のブログ投稿にはいつも友達がいましたが、時間の制約のために完全な応答はありませんでした。この期間中、少しの空き時間があり、残業は7127から変更されました997、ついに記事が書けるようになりました。

32倍速スイープロボットエリアカバレッジの例-トップ

 

屋内環境の3Dシミュレーション-中

 

32倍速スイープロボットエリアカバレッジの例-次へ

1.予備知識:

一日忙しいプログラマーが帰宅したときに地面がきれいになるようにする方法、スイープロボットを作ってみましょう。難しい?もちろん、難しくはありません、超シンプルです、信じられませんか?

サービス指向のモバイルロボットは、どのようにして屋内パスの完全なカバレッジとスイープを実現しますか?さわやかで清潔な家を提供します(フルバージョンの記録を委託)

1.1ロボットモデル

スイープロボットには2つの主要なモデルがあります。1つは通常の家庭用の2つの車輪を備え、もう1つはスタジアムやスーパーマーケットなどの広いスペース用の4つの車輪を備えています。

ここでは主にキネマティクスモデルの紹介ですが、ダイナミクスは少し難しいです...

スイープロボットモデル

 注目してください!この種のロボットは横に歩くことも、横に歩くことも、横に歩くこともできません!

このモデルを説明する

二輪スイープロボットは前後に動き、左右に曲がることはできますが、横方向に移動することはできません。なぜ、この機能をより専門的に説明するのか、その数学モデルは非常に明確な答えを与えます!左右のホイールがどのように回転し\ nu _ {y}ても、0になります。

ロボットによる正確な数学的モデルの確立は、このシステムを理解、分析、および制御する上で非常に重要な役割を果たします。

このタイプのロボットの運動モデルには、次の2つのタイプがあります。

I型

これは、左右のホイールの速度に基づいて確立されたモデルです。通常、線形速度と角速度でロボットを分析するために使用されます。

II型

設計時にタイプIIを使用し、ロボットを制御するためにタイプIを使用するのは非常に簡単ですが、これだけでは不十分です。環境マップも知っておく必要があります。次のセクションを参照してください->

1.2環境マップ

不思議な場所にたどり着いたときは地図なしではいられません。慣れ親しんだ場所に戻ったときは地図が記憶に残っており、ポジショニング、ナビゲーション、ルート、ミッションプランニングなどに欠かせない地図であることがわかります。とても重要です。一般的なフラットマップは次のとおりです。

大きな環境:

ショッピングモールの概略図
パストポロジ図

小さな環境:

オフィススケッチ
パストポロジ図

では、スイープロボットにはどのようなマップが必要ですか?これらのマップを作成するにはどうすればよいですか?シミュレーションと物理オブジェクトにはそれぞれ2つの方法があります。ここではシミュレーションを紹介し、物理オブジェクトは記事の最後にあります。

「InitiallyMind」の場合の典型的な屋内環境が示されています。これは別のライブラリの場合です。

ライブラリシーン

この環境用にロボットSLAMによって作成された環境マップは次のとおりです。詳細については、対応する詳細なブログ投稿を参照してください。

シーンの2次元マップ

このマップを使用すると、環境内のロボットのさまざまなパスプランニング関連タスクの設計を実現できます。

パスプランニングとは何ですか?? 

地域カバレッジパス計画

この写真はどのようにして生まれたのですか?

1.3経路計画アルゴリズム

ナビゲーション

ポイントAからポイントBへのナビゲーションパスの計画:

ポイントSからポイントTに移動する方法
技術的準備金はしっかりしている必要があります

 

アルゴリズムのデバッグを改善する必要があります
最終的に目標を達成することができます
シミュレーションはまったく同じです

 カバー

簡単に言えば、カバレッジとは、ナビゲーションポイントが、ロボットがマップエリア内で移動できるすべてのポイントをカバーすることを意味し、ロボットはすべてを移動する必要があります。

この場所への訪問はなんとつらいことでしょう。

ロボットエリアカバレッジをスイープするアルゴリズム

2016年のブログ投稿への具体的な参照:スイープロボットアルゴリズムのいくつかのアイデアとテスト

2.技術分析:

スイープロボットのコアは、エリアカバレッジアルゴリズムがロボットモデルと一致する必要があることです。次に例を示します。

ロボット半径とクリーニング半径が異なるエリアカバレッジマップは、次のように設定されます。

ロボットの長さと幅は60cm * 60cm、または半径は30cmです。

このとき、ジャムなどの極端な状況を防ぐために、小さなドアのある部屋は清掃計画の範囲外であることが発見されました。

ロボットの長さと幅は20cm * 20cm、または半径は10cmです。

清掃は非常に徹底的ですが、経路計画は非常に集中的です。

単純または複雑な環境のマップの場合、同じアルゴリズムを適用できる場合、そのアルゴリズムは非常に適応性があります。

簡単な地図:

シンプルな構造マップ
エリアカバレッジパス

すべてのアルゴリズムテストは、単純なものから複雑なものまでのプロセスを経る必要があり、成功のために急いではいけません。

複雑なマップ:

これが「最初の心」の環境から構築された地図です:

物理モデルから2次元マップへ

特定の洗浄効果は何ですか?色を区別するために、見てみましょう:

クリーニング時間1のレンダリング
クリーニング時間2のレンダリング

なに、掃引速度が遅すぎる?このように、ロボットは724でしか動作できません。「電気」が十分である限り、ロボットは24時間動作できます。

もっと速くなりたいのなら?このブログ投稿を参照してください:https//zhangrelay.blog.csdn.net/article/details/76850690

マルチロボットコラボレーション

3台のロボットが一緒に掃除するのはどうですか?

それでも満足できない場合は、実際のロボットを使用して、スイープロボットのパスプランニングアルゴリズムをデバッグする必要がありますか?これを見てください、多分それはあなたを助けることができます:

3.参考文献

  • フルカバレッジパスプランナー(FCPP)

4.コアコード

  • full_coverage_path_planner.cpp
//
// Copyright [2020] Nobleo Technology"  [legal/copyright]
//
#include <list>
#include <vector>

#include "full_coverage_path_planner/full_coverage_path_planner.h"

/*  *** Note the coordinate system ***
 *  grid[][] is a 2D-vector:
 *  where ix is column-index and x-coordinate in map,
 *  iy is row-index and y-coordinate in map.
 *
 *            Cols  [ix]
 *        _______________________
 *       |__|__|__|__|__|__|__|__|
 *       |__|__|__|__|__|__|__|__|
 * Rows  |__|__|__|__|__|__|__|__|
 * [iy]  |__|__|__|__|__|__|__|__|
 *       |__|__|__|__|__|__|__|__|
 *y-axis |__|__|__|__|__|__|__|__|
 *   ^   |__|__|__|__|__|__|__|__|
 *   ^   |__|__|__|__|__|__|__|__|
 *   |   |__|__|__|__|__|__|__|__|
 *   |   |__|__|__|__|__|__|__|__|
 *
 *   O   --->> x-axis
 */

// #define DEBUG_PLOT

// Default Constructor
namespace full_coverage_path_planner
{
FullCoveragePathPlanner::FullCoveragePathPlanner() : initialized_(false)
{
}

void FullCoveragePathPlanner::publishPlan(const std::vector<geometry_msgs::PoseStamped>& path)
{
  if (!initialized_)
  {
    ROS_ERROR("This planner has not been initialized yet, but it is being used, please call initialize() before use");
    return;
  }

  // create a message for the plan
  nav_msgs::Path gui_path;
  gui_path.poses.resize(path.size());

  if (!path.empty())
  {
    gui_path.header.frame_id = path[0].header.frame_id;
    gui_path.header.stamp = path[0].header.stamp;
  }

  // Extract the plan in world co-ordinates, we assume the path is all in the same frame
  for (unsigned int i = 0; i < path.size(); i++)
  {
    gui_path.poses[i] = path[i];
  }

  plan_pub_.publish(gui_path);
}

void FullCoveragePathPlanner::parsePointlist2Plan(const geometry_msgs::PoseStamped& start,
    std::list<Point_t> const& goalpoints,
    std::vector<geometry_msgs::PoseStamped>& plan)
{
  geometry_msgs::PoseStamped new_goal;
  std::list<Point_t>::const_iterator it, it_next, it_prev;
  int dx_now, dy_now, dx_next, dy_next, move_dir_now = 0, move_dir_prev = 0, move_dir_next = 0;
  bool do_publish = false;
  float orientation = eDirNone;
  ROS_INFO("Received goalpoints with length: %lu", goalpoints.size());
  if (goalpoints.size() > 1)
  {
    for (it = goalpoints.begin(); it != goalpoints.end(); ++it)
    {
      it_next = it;
      it_next++;
      it_prev = it;
      it_prev--;

      // Check for the direction of movement
      if (it == goalpoints.begin())
      {
        dx_now = it_next->x - it->x;
        dy_now = it_next->y - it->y;
      }
      else
      {
        dx_now = it->x - it_prev->x;
        dy_now = it->y - it_prev->y;
        dx_next = it_next->x - it->x;
        dy_next = it_next->y - it->y;
      }

      // Calculate direction enum: dx + dy*2 will give a unique number for each of the four possible directions because
      // of their signs:
      //  1 +  0*2 =  1
      //  0 +  1*2 =  2
      // -1 +  0*2 = -1
      //  0 + -1*2 = -2
      move_dir_now = dx_now + dy_now * 2;
      move_dir_next = dx_next + dy_next * 2;

      // Check if this points needs to be published (i.e. a change of direction or first or last point in list)
      do_publish = move_dir_next != move_dir_now || it == goalpoints.begin() ||
                   (it != goalpoints.end() && it == --goalpoints.end());
      move_dir_prev = move_dir_now;

      // Add to vector if required
      if (do_publish)
      {
        new_goal.header.frame_id = "map";
        new_goal.pose.position.x = (it->x) * tile_size_ + grid_origin_.x + tile_size_ * 0.5;
        new_goal.pose.position.y = (it->y) * tile_size_ + grid_origin_.y + tile_size_ * 0.5;
        // Calculate desired orientation to be in line with movement direction
        switch (move_dir_now)
        {
        case eDirNone:
          // Keep orientation
          break;
        case eDirRight:
          orientation = 0;
          break;
        case eDirUp:
          orientation = M_PI / 2;
          break;
        case eDirLeft:
          orientation = M_PI;
          break;
        case eDirDown:
          orientation = M_PI * 1.5;
          break;
        }
        new_goal.pose.orientation = tf::createQuaternionMsgFromYaw(orientation);
        if (it != goalpoints.begin())
        {
          previous_goal_.pose.orientation = new_goal.pose.orientation;
          // republish previous goal but with new orientation to indicate change of direction
          // useful when the plan is strictly followed with base_link
          plan.push_back(previous_goal_);
        }
        ROS_DEBUG("Voila new point: x=%f, y=%f, o=%f,%f,%f,%f", new_goal.pose.position.x, new_goal.pose.position.y,
                  new_goal.pose.orientation.x, new_goal.pose.orientation.y, new_goal.pose.orientation.z,
                  new_goal.pose.orientation.w);
        plan.push_back(new_goal);
        previous_goal_ = new_goal;
      }
    }
  }
  else
  {
    new_goal.header.frame_id = "map";
    new_goal.pose.position.x = (goalpoints.begin()->x) * tile_size_ + grid_origin_.x + tile_size_ * 0.5;
    new_goal.pose.position.y = (goalpoints.begin()->y) * tile_size_ + grid_origin_.y + tile_size_ * 0.5;
    new_goal.pose.orientation = tf::createQuaternionMsgFromYaw(0);
    plan.push_back(new_goal);
  }
  /* Add poses from current position to start of plan */

  // Compute angle between current pose and first plan point
  double dy = plan.begin()->pose.position.y - start.pose.position.y;
  double dx = plan.begin()->pose.position.x - start.pose.position.x;
  // Arbitrary choice of 100.0*FLT_EPSILON to determine minimum angle precision of 1%
  if (!(fabs(dy) < 100.0 * FLT_EPSILON && fabs(dx) < 100.0 * FLT_EPSILON))
  {
    // Add extra translation waypoint
    double yaw = std::atan2(dy, dx);
    geometry_msgs::Quaternion quat_temp = tf::createQuaternionMsgFromYaw(yaw);
    geometry_msgs::PoseStamped extra_pose;
    extra_pose = *plan.begin();
    extra_pose.pose.orientation = quat_temp;
    plan.insert(plan.begin(), extra_pose);
    extra_pose = start;
    extra_pose.pose.orientation = quat_temp;
    plan.insert(plan.begin(), extra_pose);
  }

  // Insert current pose
  plan.insert(plan.begin(), start);

  ROS_INFO("Plan ready containing %lu goals!", plan.size());
}

bool FullCoveragePathPlanner::parseGrid(nav_msgs::OccupancyGrid const& cpp_grid_,
                                        std::vector<std::vector<bool> >& grid,
                                        float robotRadius,
                                        float toolRadius,
                                        geometry_msgs::PoseStamped const& realStart,
                                        Point_t& scaledStart)
{
  int ix, iy, nodeRow, nodeColl;
  uint32_t nodeSize = dmax(floor(toolRadius / cpp_grid_.info.resolution), 1);  // Size of node in pixels/units
  uint32_t robotNodeSize = dmax(floor(robotRadius / cpp_grid_.info.resolution), 1);  // RobotRadius in pixels/units
  uint32_t nRows = cpp_grid_.info.height, nCols = cpp_grid_.info.width;
  ROS_INFO("nRows: %u nCols: %u nodeSize: %d", nRows, nCols, nodeSize);

  if (nRows == 0 || nCols == 0)
  {
    return false;
  }

  // Save map origin and scaling
  tile_size_ = nodeSize * cpp_grid_.info.resolution;  // Size of a tile in meters
  grid_origin_.x = cpp_grid_.info.origin.position.x;  // x-origin in meters
  grid_origin_.y = cpp_grid_.info.origin.position.y;  // y-origin in meters

  // Scale starting point
  scaledStart.x = static_cast<unsigned int>(clamp((realStart.pose.position.x - grid_origin_.x) / tile_size_, 0.0,
                             floor(cpp_grid_.info.width / tile_size_)));
  scaledStart.y = static_cast<unsigned int>(clamp((realStart.pose.position.y - grid_origin_.y) / tile_size_, 0.0,
                             floor(cpp_grid_.info.height / tile_size_)));

  // Scale grid
  for (iy = 0; iy < nRows; iy = iy + nodeSize)
  {
    std::vector<bool> gridRow;
    for (ix = 0; ix < nCols; ix = ix + nodeSize)
    {
      bool nodeOccupied = false;
      for (nodeRow = 0; (nodeRow < robotNodeSize) && ((iy + nodeRow) < nRows) && (nodeOccupied == false); ++nodeRow)
      {
        for (nodeColl = 0; (nodeColl < robotNodeSize) && ((ix + nodeColl) < nCols); ++nodeColl)
        {
          int index_grid = dmax((iy + nodeRow - ceil(static_cast<float>(robotNodeSize - nodeSize) / 2.0))
                            * nCols + (ix + nodeColl - ceil(static_cast<float>(robotNodeSize - nodeSize) / 2.0)), 0);
          if (cpp_grid_.data[index_grid] > 65)
          {
            nodeOccupied = true;
            break;
          }
        }
      }
      gridRow.push_back(nodeOccupied);
    }
    grid.push_back(gridRow);
  }
  return true;
}
}  // namespace full_coverage_path_planner

 

 

おすすめ

転載: blog.csdn.net/ZhangRelay/article/details/109249378