【基础知识】服务型移动机器人如何实现室内路径全覆盖清扫给你一个清爽干净的家,tianbot_mini机器人上手ROS/SLAM/Navigation究竟有多简单???

153 0

0.本文初心:

一直有朋友在我博文中关于扫地机器人(侧重区域覆盖算法)和物流机器人(侧重运输调度算法)留言交流,受限于时间一直没有完整回复,这段时间稍稍有空,加班由7127变成了997,终于可以写一篇了。

32倍速扫地机器人区域覆盖示例-上

 

室内环境三维仿真-中

 

32倍速扫地机器人区域覆盖示例-下

1.预备知识:

如何让忙碌了一天的程序员到家后发现地面一尘不染,做一个扫地机器人吧。很难吗?当然不难,超简单的,不信?

服务型移动机器人如何实现室内路径全覆盖清扫给你一个清爽干净的家(调试完整版记录)

1.1机器人模型

扫地机器人主要有两种模型哦,一种两个轮子适合普通家用,还有一种四个轮子适合体育馆超市等大型空间使用。

这里以运动学模型介绍为主,动力学有点难……

扫地机器人模型

 请务必注意!!!这种机器人不能横着走,不能横着走,不能横着走!!!

解释一下这个模型

我们都知道两轮的扫地机器人可以前进,后退,左转和右转,但是不能侧向平移,为啥,如何更专业的描述这一特性,其数学模型给出了非常明确的答案!无论左轮和右轮如何旋转,都是等于0的啊!

机器人建立准确的数学模型,对于理解,分析和控制此系统有着非常重要的作用。

这类机器人的运动学模型有两类,分别如下所示:

I型

这是依据左右轮速度建立的模型,我们通常习惯用线速度和角速度去分析机器人:

II型

设计时候使用II型,控制机器人用I型,很简单吧,但是这还不够啊,还需要知道我们的环境地图,看下节->

1.2环境地图

到了陌生的地方自然离不开地图,回到熟悉的地方,我们有记忆中的地图,可见地图对于定位,导航,路径,任务规划等必不可少,非常重要。常见的平面地图有如下:

大环境:

商场类场所示意图 路径拓扑图

小环境:

办公室示意图 路径拓扑图

那么我们扫地机器人需要啥地图呢???如何建立这些地图呢???分别由仿真和实物两种方式,这里介绍仿真,实物放在文末。

一个典型室内环境如“初心”中的案例所示,这里再举一个图书馆的案例:

图书馆场景

对这个环境用机器人SLAM建立的环境地图如下,详细内容参考对应详细介绍的博文:

场景二维地图

有了这个地图,就可以实现机器人在环境下的各种路径规划类相关任务设计啦。

啥是路径规划??? 

区域覆盖路径规划

这张图是怎么来的???

1.3路径规划算法

导航

由点A到点B的导航路径规划:

从S点如何移动到T点呢 技术储备要扎实

 

算法调试要提升 目标最终能实现 仿真实物完全一致

 覆盖

简单说,覆盖就是导航点覆盖了地图区域范围内所有机器人可以运动到的点,并且机器人必须全部走一遍!

多么痛苦的到此一游啊!

扫地机器人区域覆盖算法

具体参考2016年博文:扫地机器人算法的一些想法和测试

2.技术解析:

扫地机器人核心是区域覆盖算法要和机器人模型相匹配,例如:

设置了不同机器人自身半径和清扫半径的区域覆盖图如下:

机器人长宽为60cm*60cm或半径为30cm

这时候发现为了防止出现卡住等极端情况,门狭小的房间并未在清扫规划范围呢。

机器人长宽为20cm*20cm或半径为10cm

清扫非常彻底,但是路径规划很密集。

对于环境简单或复杂的地图,如果同一个算法都能适用,那说明算法的适应性非常好!!!

简单地图:

简单结构地图 区域覆盖路径

所有算法测试都需要经过从简单到复杂的过程,不要急于求成啊。

复杂地图:

这里选用“初心”中的环境构建出的地图:

从实物模型到二维地图

具体的清扫效果如何呢?看看,为了区别色彩调了一下:

清扫时间1效果图 清扫时间2效果图

啥,扫地速度太慢了???这样机器人工作只能724,没关系,机器人可以24小时工作的,只要“电”管够!

如果想快一点?参考这篇博文:https://zhangrelay.blog.csdn.net/article/details/76850690

多机器人协作

三个机器人一起清扫如何?

如果还不满意,要用真实机器人自己调试扫地机器人路径规划算法?看看这一篇,也许能帮到你:

  • tianbot_mini机器人上手ROS/SLAM/Navigation究竟有多简单???

3.参考文献

  • Full Coverage Path Planner (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

 

 

0

上一篇: 美国服务机器人技术路线图 下一篇: 实现机器人自主定位导航必解决的三大问题

教程资料来源于网络,如有侵权,请及时联系平台进行删除

其他

课程目录
搜索
基础知识
乔布斯《遗失的访谈》全文:尘封16年的预见
stm32使用HAL库快速编写智能寻迹避障小车(附代码)
计算机科学技术发展史的缩影
嵌入式软件工程师杂谈之一 ----- BSP工程师
嵌入式底层软件开发学习系列之三开发与就业方向
智能车的转弯部分_10个极品智能车方案合辑,夏日避暑进阶两不误
到底什么是嵌入式?
乔布斯-遗失的访谈中英双文版-尘封十余年的伟大遇见!
转:乔布斯《遗失的访谈》全文:尘封16年的预见
正在崛起的高薪岗位—嵌入式开发工程师
无人驾驶硬件平台
【无人驾驶系列十】无人驾驶硬件平台设计
无人驾驶之硬件平台详解
VR的理想与现实
从iPad Pro的ToF摄像头说起,ToF前途未卜还是一片光明?
全球及中国机器视觉产业十四五投资动态及未来竞争态势研究报告2021-2027年
全球及中国增强现实产业战略布局及运营前景决策分析报告2021-2027年
搜索算法案例分析
前辈们的话--大疆技术总监的金玉良言
激光雷达与毫米波雷达对比, 
传感器小结
一线工程师告诉你嵌入式真实现状与发展前景
优劣几何?三角法和TOF激光雷达大解析!
10个激光、超声波测距方案带你玩转测距传感器
SLAM刚刚开始的未来之“工程细节”(张哲的ICRA 2017 的一些整理
101 从一个错误开始讲场效应管的应用
2017嵌入式软件行业现状及概述
人工智能概述
电子信息工程专业/单片机毕设题目推荐
SLAM刚刚开始的未来之“工程细节”
为什么别人可以这么牛
第879期机器学习日报(2017-02-13)
推箱子自动寻路的实现(未完)
人工智能技术与现代应用
二 树莓派3+ROS-kinetic+mbed-二轮差分模型
自动驾驶中激光雷达和高精度地图的关系
平台采用小米1代扫地机源码,stm32f103真实项目程序源码,代码注释清晰、代码规范好、每个函数必有输入输出范围参数解释。
无人驾驶硬件平台
激光雷达—无人驾驶汽车的眼睛
最适合男生的十大高薪工科类专业!
(三)LiDAR的测距原理(师弟师妹)简单科普
VIO_FUSION
3D视觉传感技术:时间飞行法 (ToF) 技术分析
设计一个AOA蓝牙精准室内定位系统
嵌入式软件岗位就业指导建议!!!
激光雷达类型分类,知名激光雷达公司介绍,三角测距激光雷达与TOF激光雷达原理
《人工智能狂潮》读后感——什么是人工智能?(一)
STM32(10):超声波模块的使用
计算机控制技术课程解释与问题答疑
相位式激光测距法中相位产生原理
【 學習心得 笔记 1】大疆技术总监:如何用六年成为一个全能的机器人工程师
机器人工程师学习计划(新工科自学方案)------杨硕
机器人工程师之路——从大一到研究生,YY硕经验谈
激光雷达类型分类,知名激光雷达公司介绍,三角测距激光雷达与TOF激光雷达原理
单线激光雷达在自动驾驶中的原理?
卷王指南,大学计算机专业,面临分专业,计科,软工,大数据,物联网,网络工程,该选什么?
一家非典型机器人公司的27年成长史丨独家探访 iRobot
STM32项目设计:基于stm32f4的智能门锁(附项目视频全套教程、源码资料)
大疆技术总监:如何用六年成为一个全能的机器人工程师(转载)
大疆工程师《机器人工程师学习计划》
如何成为一名很酷的机器人工程师?
ROS机器人开发概述,需要掌握的知识
SLAM中的EKF,UKF,PF原理简介 [转高博]
物联网定位技术超全解析!定位正在从室外走向室内
【概述】基于SLAM的机器人的自主定位导航
服务机器人是如何实现自主定位导航的?
收藏 | 基于深度学习SLAM的机器人的自主定位导航解析
物联网定位技术超全解析!定位正在从室外走向室内~
移动机器人技术(6)-- 机器人控制策略
ROS的优势与不足(除了ROS 机器人自主定位导航还能怎么做?)
SLAM技术大解析:它是如何帮助机器人实现智能行走的?
美国服务机器人技术路线图
服务型移动机器人如何实现室内路径全覆盖清扫给你一个清爽干净的家,tianbot_mini机器人上手ROS/SLAM/Navigation究竟有多简单???
实现机器人自主定位导航必解决的三大问题
室内定位技术方案---Wifi、RFID、bluetooth、Zigbee
常用室内定位技术总结
让机器人告别乱碰乱撞,激光导航让扫地机“睁开双眼”
机器人
SLAM技术是什么?它是如何帮助机器人实现智能行走?
服务机器人其最大的问题:定位导航
基于优化方法的机器人同步定位与地图创建(SLAM)后端(Back-end)设计技术收集
基于SLAM的机器人的自主定位导航
物联网定位技术超全解析
从理论到实践,机器人SLAM技术详解
360扫地机原理大揭秘,竟还有无人驾驶技术?——浅析家用机器人SLAM方案
SLAM导航技术赋能机器人智能行走
浅析服务机器人自主定位导航技术(三)
SLAM≠机器人自主定位导航
除了ROS, 机器人定位导航还有其他方案吗?
浅析服务机器人自主定位导航技术(一)
服务机器人常用的定位导航技术及优缺点分析
移动机器人定位导航方式的演进
服务机器人技术 —— 自主定位导航
机器人学习--移动机器人定位导航性能评估规范
自主移动机器人常用的导航定位技术及原理
扫地机器人如何聪明地干活? | iRobot解读智慧家庭的正确打开方式
寿命长性价比高的室内扫地机器人SLAM导航方案
扫地机器人能有多硬核?好家伙自动驾驶、激光扫描、NLP这些硬科技全上了,科沃斯:技术创新才能打破行业内卷...
扫地机器人有这些路径规划方法
智能扫地机器人软硬件开发笔记(1)-规格需求书
智能扫地机器人陀螺仪导航
【Robot】扫地机器人实现方案
自研扫地机器人激光雷达,Camsense有何胜算?
自动集尘系统会成为扫地机器人标配吗
干货|自动驾驶 vs 机器人定位技术
扫地机器人会否抛弃激光雷达这位原配?
智能扫地机器人陀螺仪导航模块
革了激光的命?双目视觉能否推动扫地机器人再次迭代
扫地机器人系统,主要划分为哪几个模块?
什么是激光导航扫地机器人?
扫地机器人是如何实现路径规划的 揭秘扫地机的定位导航原理
扫地机器人有这些路径规划方法
扫地机器人导航原理解读
机器视觉在扫地机器人领域的应用
深度解读扫地机器人的导航原理
扫地机器人智能化升级之路 智能决策成为关键
扫地机器人的回充方法实现
扫地机器人自动回冲工作原理
智能扫地机器人 陀螺仪导航系统
扫地机器人的路径规划方法
【室内定位】常用的机器人定位导航技术及优缺点
服务机器人常用的定位导航技术及优缺点分析