这一篇的建图是后面功能实现的基础,图建不好,会严重影响后面的导航和抓取,所以建图尽量准确,能够省去很多麻烦事。

一、 使用gmapping 算法构建地图

这里的gmapping算法只做了解,可以不用去深入学习算法本身的知识,有兴趣或者专业的可以深入学习,因为已经写好了launch文件,可以直接调用完成建图工作。

Gmapping 是一种高效的 Rao-Blakwellized 粒子滤波器,用于根据激光雷达测距数据来生成 2D栅格地图,ROS gmapping 是 OpenSLAM 下的 GMapping 的再封装移植版本。

Slam 算法介绍

SLAM 的英文全称是 Simultaneous Localization and Mapping,中文称作同时定位与地图创
建,它主要用于解决机器人在未知环境运动时的定位和地图构建问题。SLAM 有很多种分类方法。按照传感器的不同,可以分为基于激光雷达的 2D/3D SLAM、基于深度相机的 RGBD SLAM、基于视觉传感器的 visual SLAM(以下简称 vSLAM),我们主要使用的是基于激光雷达的 2D SLAM,根据算法的不同,激光 2D SLAM 又可以分为:
基于粒子滤波的 slam 算法,常用的有 HectorSLAM,Gmapping 算法
基于图优化的 slam 算法,常用的有 KartoSLAM,Cartographer 算法

HectorSLAM

HectorSLAM 是一种结合了鲁棒性较好的扫描匹方法 2D SLAM 方法和使用惯性传感系统的导航技术。
优点:不依赖里程计,适应无人机及地面不平坦的区域
缺点:对传感器性能要求非常高,激光雷达必须是高频率(40HZ 以上)且测量噪声很小的

Gmapping

Gmapping 是一种基于激光的 SLAM 算法,它已经集成在 ROS 中,是移动机器人中使用最多的SLAM 算法。这个算法是由 Grisetti 等人提出的一种基于 Rao-Blackwellized 的粒子滤波 SLAM 方法。
优点:对激光雷达频率要求低、鲁棒性高,能有效利用了车轮里程计信息,在构建小场景地图时,速度快,计算量小
缺点:严重依赖里程计,无法适应无人机及地面不平坦的区域,无回环(激光 SLAM 很难做回环检测),大的场景,粒子较多的情况下,特别消耗资源。

KartoSLAM

KartoSLAM 是基于图优化的方法,用高度优化和非迭代 cholesky 矩阵进行稀疏系统解耦作为解.图优化方法利用图的均值表示地图,每个节点表示机器人轨迹的一个位置点和传感器测量数据集,箭头的指向的连接表示连续机器人位置点的运动,每个新节点加入,地图就会依据空间中的节点箭头的约束进行计算更新.

Cartographer

cartographer 是 Google 的实时室内建图项目,它也是基于图优化的 SLAM 方法,可以生成分辨率为 5cm 的 2D 格网地图。获得的每一帧 laser scan 数据,利用 scan match 在最佳估计位置处插入子图(submap)中,且 scan matching 只跟当前 submap 有关。在生成一个 submap 后,会进行一次局部的回环(loop close),利用分支定位和预先计算的网格,所有 submap 完成后,会进行全局的回环,相对 KartoSLAM,Cartographer 采取优化库的是 google 的 ceres 构建 problem 优化。
优点:适用大场景建图,能进行闭环检测,稳定性好
缺点:计算量大,内存需求很大

步骤如下(参考官方给出的步骤,我会注释需要注意的地方):

第一步:
参考第三天那篇博文,实现虚拟机和小车的远程连接工作;

第二步:
使用远程连接工具xshell或者直接虚拟机ssh,进入小车的命令行中,输入如下指令,启动小车的建图:
roslaunch dashgo_nav gmapping_imu.launch #启动建图 launch

第三步:
在电脑中直接打开 rviz,观察地图:
export ROS_MASTER_URI=http://192.168.31.200:11311 #这个IP和端口是小车固定的,根据相应的官方文件进行修改
roslaunch dashgo_rviz view_navigation.launch #启动rviz

第四步
再在xshell中打开一个连接或者虚拟机中打开新的命令行,连接上小车,输入如下指令,就可以开始对小车控制
rosrun dashgo_tools teleop_twist_keyboard.py #启动键盘控制移动
更加提示可以看到可以控制八个方向
u	i	o
j	k	l
m	,	.
然后就控制小车四面八方的跑动,开始建图,建图实时效果可以根据rviz进行监控,如果看懂有些地方效果不好,可以多跑一下。

第五步
建图完成,就需要保存当前地图,再次打开一个窗口,输入以下指令:
roscd dashgo_nav/maps 		#进入地图目录
rosrun map_server map_saver –f eai_map_imu 	#保存地图为 eai_map_imu

这样一张地图就建出来了,不需要我们写什么,相关功能已经写好封装在lanunch文件中了,
可以查找原文件进行学习优化,我们所要做的就是根据实际情况修改参数值。具体操作如下。

二、gmmaping算法相关参数解释

<launch>
 <arg name="scan_topic" default="scan" /> //指定雷达消息主题为/scan
 <arg name="base_frame" default="base_footprint"/> //指定底盘坐标系
 <arg name="odom_frame" default="odom_combined"/> //指定里程计坐标系
 <node pkg="gmapping" type="slam_gmapping" name="slam_gmapping" output="screen" respawn="true" >
 <param name="base_frame" value="$(arg base_frame)"/>
 <param name="odom_frame" value="$(arg odom_frame)"/>
 <param name="map_update_interval" value="1"/> //地图更新的时间间隔
 <param name="maxUrange" value="8.0"/> //雷达可用的最大有效测距值,
//一般情况下设置 maxUrange < 雷达的现实实际测距值 <= maxRange
 <param name="maxRange" value="10.0"/> //maxRange 是雷达的理论最大测距值
120
 <param name="sigma" value="0.05"/> //endpoint 匹配标准差
 <param name="kernelSize" value="3"/> //最多使用内核数
 <param name="lstep" value="0.05"/> //优化机器人移动的初始值(距离)
 <param name="astep" value="0.05"/> //优化 optimize 机器人移动的初始值(角度)
 <param name="iterations" value="5"/> //扫描匹配器的迭代次数
 <param name="lsigma" value="0.075"/> //用于计算的波束可能性的 sigma
 <param name="ogain" value="3.0"/> //在评估可能性时使用的增益,用于平滑重采样效果
 <param name="lskip" value="0"/> //为 0,表示所有的激光都处理
 <param name="minimumScore" value="30"/> //最小匹配得分,这个参数很重要,它决定了对激光的一个置信度,
//越高说明对激光匹配算法的要求越高,激光的匹配也越容易失败而
//转去使用里程计数据,而设的太低又会使地图中出现大量噪声,
//所以需要权衡调整
 <param name="srr" value="0.01"/> //运动模型的噪声参数
 <param name="srt" value="0.02"/> //运动模型的噪声参数
 <param name="str" value="0.01"/> //运动模型的噪声参数
 <param name="stt" value="0.02"/> //运动模型的噪声参数
 <param name="linearUpdate" value="0.05"/> //机器人移动 linearUpdate 距离,进行 scanmatch
 <param name="angularUpdate" value="0.0436"/> //机器人选装 angularUpdate 角度,进行 scanmatch 
 <param name="temporalUpdate" value="-1.0"/> //如果上次扫描处理的时间早于更新时间(秒),则处理扫描。
//小于零的值将关闭基于时间的更新
 <param name="resampleThreshold" value="0.5"/> //基于 Neff 的重采样阈值
 <param name="particles" value="50"/> //粒子个数,用于粒子滤波算法
 <param name="xmin" value="-1.0"/> //map 初始化的大小
 <param name="ymin" value="-1.0"/>
 <param name="xmax" value="1.0"/>
 <param name="ymax" value="1.0"/>
 <param name="delta" value="0.05"/> //地图的分辨率
 <param name="llsamplerange" value="0.01"/> //可能性的平移采样范围
 <param name="llsamplestep" value="0.01"/> //可能性的平移采样步骤
 <param name="lasamplerange" value="0.005"/> //角度采样范围的可能性
 <param name="lasamplestep" value="0.005"/> //角度采样步骤的可能性
 <remap from="scan" to="$(arg scan_topic)"/>
 </node>
</launch>

主要可以修改以下两个参数,优化我们的建图,至于其它参数的额详解可以自行网上搜索学习。
name=“minimumScore” value=“30”//最小匹配得分,这个参数很重要,它决定了对激光的一个置信度,
name=“particles” value=“50” //粒子个数,用于粒子滤波算法

三、navigation 自主导航

这里就不做详细解释了,直接上代码,具体可以参看相关注释。

先简单说一下这里的思路。
首先我们要明白要去哪些点,根据比赛要求,是15个快递的,然后是10个投递目标点,以及1个起始点。
所以需要确定的是26个点。但是这个点属实有点多,不可能一个个去测,所以首先可以确定10个投递点,
这10个点事一定要测试的,根据前面的博文,测得10个点以后,只需要修改程序中对应的数组中10个省份。
加上起始点,11个点是一定要测试的。其次是邮件的抓取点,由于要求是邮件基本均匀摆放,
根据场地和数量可以算出邮件中心之间间距15CM,所以这11个点我们只需要改变Y轴坐标,
进行累加,其它的值包括X轴,Z轴,以及另外四个位姿值不变。
但是这里可能会出现误差,所以我们导航到下一个点是,可能邮件并不在我们的镜头中,所以需要进行调整。
我的调整方案是,左右依次旋转一定角度,在导航误差不大的情况下,就可以正常找到邮件并识别,
当然我也提到是误差不大,误差大了相当与直接放弃抓取,去往下一个,由于赛题是对抗赛,所以放弃抓取未尝不可,
可以更快的进入下一个邮件的识别抓取。
其次,我这个方案还有一个比较大的问题就是有点浪费时间,我是左右分别等待了10~15S左右,
要等待镜头和识别稳定下来,所以万一这一次完全没识别失败了,那么可能就会浪费30s。
这里我也简单的做了一个假设方案,我们可以采取识别,使用颜色分割,上面提到邮件不显示完全,
就无法正常识别,往往我们可以在图像中找到对应的黄色区域(赛题中做了要求,背景为蓝色,邮件为黄色)
然后我们可以根据黄色区域在我们画面的位置,来控制机械臂往对应方向移动。
但是我们最终没有去实现,一个是比较麻烦,得去做图像这一块的处理,加上机械臂的运动范围有限,可能目的范围会超出机械臂的范围,
而且这个范围是不好确定的,具体的可以参考越疆机械臂具体资料。
其次是再进行图像处理可能又会增加硬件的压力,耗时会大大加大,毕竟是部署在单片机上,性能远不比电脑(文字识别用的最小模型,帧率上都比较低,具体后面说)
所以综上所述,还是采取左右固定移动的方式,希望建图和导航不会出现较大的误差!

正式上代码(我不列出全部代码,以防影响大家,有疑问可交流,主要介绍一下各个功能函数)

'''
-------------------------------------------------------------------------------------------------
功能:导航停止
参数:无
讲解:
导航结束停止或者出现异常停止
作者:瓜洲
-------------------------------------------------------------------------------------------------
'''
def shutdown():
    rospy.loginfo("Stopping the robot...")
    # Cancel any active goals
    move_base.cancel_goal()
    rospy.sleep(2)
    # Stop the robot
    cmd_vel_pub.publish(Twist())
    rospy.sleep(1)
'''
-------------------------------------------------------------------------------------------------
功能:读取导航点
参数:txt文件名
讲解:
将10个导航点存储在txt中,第一个是起始点,剩余十个对应邮递箱的点,其顺序和
代码数组中的10个省份对应,这样方便修改和写入,不改动代码
作者:瓜洲
-------------------------------------------------------------------------------------------------
'''
from geometry_msgs.msg import Pose, Point, Quaternion, Twist
def read_goals(filename):
    # 定义导航点信息
    global nav_pointcnt
    quaternions = []		#坐标点
    Position = []			#位姿点
    data = []
    # 读入txt数据
    with open(filename) as f:
        for line in f.readlines():
            temp = line.split()
            data.append(temp)
    # 把数据解析存入目标点列表
    for n in data:
        x, y, z = float(n[0]), float(n[1]), float(n[2])
        x1, y1, z1, w = float(n[3]), float(n[4]), float(n[5]), float(n[6])
        data1 = Point(x, y, z)
        data2 = Quaternion(x1, y1, z1, w)
        Position.append(data1)
        quaternions.append(data2)
    nav_pointcnt = len(Position)
    for i in range(0, nav_pointcnt):
        waypoints.append(Pose(Position[i], quaternions[i])) #pose功能函数自带,使用即可
    print"read goals finish!!"
    return nav_pointcnt



'''
-------------------------------------------------------------------------------------------------
功能:导航移动
参数:目标点
讲解:
使用move_base.send_goal(goal)函数,实现导航功能,这里的自动导航已经用此函数进行封装了,所以我们直接使用就行
至于实现方法有兴趣可以找到源文件学习,这里不做讲解
作者:瓜洲
-------------------------------------------------------------------------------------------------
'''
def move(goal):
    # 发送移动目标点
    move_base.send_goal(goal)
    #等待超时,单位 秒
    finished_within_time = move_base.wait_for_result(rospy.Duration(70))
    # 如果超时取消本目标点的导航,进入下一步
    if not finished_within_time:
        move_base.cancel_goal()
        rospy.loginfo("Timed out achieving goal")
    else:
        # 否则导航完成,获取状态
        state = move_base.get_state()
        # 如果返回GoalStatus.SUCCEEDED,则导航成功
        if state == GoalStatus.SUCCEEDED:
            state = True
            rospy.loginfo("Goal succeeded!")
            return state


'''
-------------------------------------------------------------------------------------------------
功能:机械臂状态回调函数
参数:状态值
讲解:
本函数主要接收抓取结束时和投递结束时的状态,以此来判断是移动向投递箱还是分拣台
作者:瓜洲
-------------------------------------------------------------------------------------------------
'''
def dobot_statecallback(data):
    global dobot_grap_state, dobot_deliver_state, move_state, box_index
    try:
        dobot_grap_state = data.dobot_grap_flag
        dobot_deliver_state = data.dobot_deliver_flag
        box_index = data.deliver_box_index
        move_state = True
        #print("dobot_grap_state: ", dobot_grap_state,"deliver_state: ", dobot_deliver_state)
        print("box_index: ", box_index, "move_state:", move_state)
        return dobot_deliver_state, dobot_deliver_state, move_state, box_index
    except:
        print("receive dobot_state error")

'''
-------------------------------------------------------------------------------------------------
功能:导航主函数
参数:无
讲解:
只展示初始化和话题的接收和创建,只有后面的导航逻辑这里省略,主要是更加上面的回调函数判断过程,然后执行相应的操作
作者:瓜洲
-------------------------------------------------------------------------------------------------
'''
def main():
    global dobot_grap_state, dobot_deliver_state ,move_state, box_index, \
        nav_grap_state, nav_deliver_state, nav_pointcnt
    pointcnt = 0
    para_move = 0.13			#邮件移动的常数值
    rospy.init_node('send_goal', anonymous=False)
    rospy.on_shutdown(shutdown)
    rospy.loginfo("Waiting for move_base action server...")
    pub_nav_state = rospy.Publisher('/navigational_state', navigational_state, queue_size=10)  # 发布数据
    rospy.Subscriber('/dobot_state', dobot_state, dobot_statecallback)
    # 等待服务器
    move_base.wait_for_server(rospy.Duration(20))
    rospy.loginfo("Connected to move base server")
    nav_pointcnt = read_goals("/home/eaibot/moveit_ws/goals.txt")

    rate = rospy.Rate(10)

以下代码省略,主要就是调用上面的函数,判断小车的移动逻辑。

四、总结

这一篇的核心在导航这一块,但是建图却是关键,图建不好就会严重影响导航的准确率,也就对全过程产生影响,所以一点要注意建图的方式方法。其次是检查小车的硬件,因为我遇到了小伙伴,他们建图的时候始终有缺陷,地图建出来严重变形,有比较大的发散,应该是激光雷达上翘了,最终连续厂家换了一台就好了。
其次就是虽然涉及到建图算法和导航算法,但是根据上面的内容,我们并没有去深究它,因为已经做了封装,直接拿来用就可以。当然要改进其性能,那肯定要熟读学习;
最后是可以参考我上面提到的一些问题,自己做一些改进,毕竟本人所学尚浅,还有很大的进步学习空间。下一章分享文字识别相关知识。