LIDAR ODOMETRY
Jack Mo (mobangjack@whu.edu.cn)
在用激光雷达(LIDAR)做实时定位与地图构建(SLAM)时,不仅需要LIDAR的点云数据,还需要LIDAR的里程计(Odometry)信息。里程计数据可以从其他传感器获得,如IMU的二阶状态估计、电机编码器(Encoder)的角度换算值、其他视觉里程计等。当然,LIDAR本身也可以作为里程计使用。下面我们将以上海思岚科技有限公司(slamtec.com)的RPLIDAR A2为例,讨论用2D LIDAR来获取里程计数据的方法。
LIDAR是Light Detection and Range的简写,即“激光探测与测量”。现在常说的LIDAR传感器,大多指激光雷达这一类。图1为RPLIDAR的产品实物图。
(注:本文中所有有关RPLIDAR产品的图表均摘自RPLIDAR的官方数据手册)

图1 RPLIDAR 实物图
如图2,RPLIDAR主要包含激光测距核心和底座两部分。测距核心包含激光发射器、接收器和微处理器,底座负责供电并驱动激光测距核心绕轴心以一定速率运转。

图2 RPLIDAR系统部件
图3为RPLIDAR的测量原理图。
LIDAR的一个测距周期大致包含以下步骤:
1)发射器发射一束调制过的激光光束;
2)开启内部高精度定时器;
3)光束在遇到障碍物后部分反射到接收器;
4)放大、解调接收到的激光回波;
5)确认是本系统发射出的探测激光,停止内部计时;
6)解算数据。

图3 RPLIDAR工作原理
解算后的数据以LIDAR坐标系为参考,包含角度
 及距离
 。图3为RPLIDAR坐标系示意。

图4 RPLIDAR坐标系
当然,由于系统设计和构造上的原因,
 和
 也有一定限制。表1为RPLIDAR的性能参数表。由该表可见,RPLIDAR的测距范围最大为6米,最小为0.15米,精度为0.5毫米;角度范围为0~360度,分辨率为0.45度;测量频率2000~4100Hz,扫描频率为5~15Hz。
表1 RPLIDAR性能参数表

LIDAR的每个测量数据包含两个维度的信息,一个是该点与激光雷达的相对方向
 ,另一个是该点与激光雷达的径向距离
 ,即雷达坐标系采用的是极坐标
 。把
 当作自变量,
 为应变量,雷达一周的扫描数据可以描述为一个映射:
 (1)
由于激光雷达具有一定的角度、距离测量范围和分辨率,所以实际的
 并非连续。设雷达角度测量范围为
 ,分辨率为
 ,则
 (2)
 (3)
 (4)
 (5)
设雷达距离测量范围为
 ,分辨率为
 ,则
 (6)
 (7)
 (8)
 (9)
所以
 (10)
记
 (11)
 (12)
则激光雷达一个周期的测量数据可以描述为一个集合
 (13)
记
 (14)
则
 (15)
下标k可以表示角度
 ,我们把集合中的
 去掉,这样,激光雷达一个周期的扫描数据可以描述为一个n维向量x,
 (16)
以上面介绍的RPLIDAR为例,
 (17)
 (18)
 (19)
问题:平面内有一点集
 ,已知t时刻雷达对X的观测值为
 ,t+1时刻雷达对X的观测值为
 。求从t时刻到t+1时刻雷达坐标系的旋转
 和位移
 。(在本文中我们用和t有关的右上标表示数据的时间戳,如
 表示t时刻的一组观测数据)

图5 坐标系变换
设t时刻雷达坐标系为
 ,t+1时刻雷达坐标系为
 ,则
 :
 (20)
根据此变换,用
 来匹配
 ,并计算误差

 (21)
上式隐含了从极坐标到直角坐标的转换,并计算所有待匹配点的欧氏距离。
令
 (22)
 (23)
得
 (24)
最小化匹配误差,从而得到最优变换参数
 (25)
将优化后的参数k,l回代(23)(24)可以得到在最小匹配误差下,
 的雷达坐标系旋转、平移参数
 、
 。在直角坐标系下,
 分解为
 和
 
 (26)
 (27)
逐次迭代上面的算法,可以获得LIDAR在任意时刻与初始位姿的相对关系。
总里程
 (28)
总偏航
 (29)
此外,用上面计算得到的一阶状态量对时间求导还可以得到LIDAR运动的角速度和线速度等信息。
上面虽然假设所使用的的观测数据有时间关系,但式(21)(24)可以脱离该假设使用。对任意两组观测数据
 ,定义最小匹配误差
 (30)
式(30)可以用来匹配LIDAR扫描过的点,并完善SLAM中的构图。例如剔除不稳定的点或新增之前没扫描到的点。
#include "ros/ros.h"
#include "sensor_msgs/LaserScan.h"
#include 
#include 
#include 
#include <sys/time.h>
#define RAD2DEG(x) ((x)*180./M_PI)
#define DEG2RAD(x) ((x)*M_PI/180.)
#define SQUARE(x) ((x)*(x))
int ms()
{
    struct timeval tv;
    gettimeofday( &tv, NULL );
    return tv.tv_sec * 1000 + tv.tv_usec / 1000;
}
void procCallback(const sensor_msgs::LaserScan::ConstPtr& scan)
{
    //int t = ms();
    const int N = 360;
    static float cacher[N] = {0};
    static float stheta = 0.f;
    static float srho = 0.f;
    //static float cachex[N] = {0};
    //static float cachey[N] = {0};
    static int cnt = 0;
    const int CNT = 20;
    const float eps = 1.0f;
    const float lam = 0.03f;
    const float dtheta_max = 50.0f;
    const float dtheta_min = -dtheta_max;
    const float drho_max = 0.2f;
    const float drho_min = -drho_max;
    const int kmax = dtheta_max / eps;
    const int kmin = dtheta_min / eps;
    const int lmax = drho_max / lam;
    const int lmin = drho_min / lam;
    //const int loop = (kmax - kmin) * (lmax - lmin);
    int count = scan->scan_time / scan->time_increment;
    if(cnt < CNT) {
        for(int i = 0; i < count; i++) {
            //float rad = scan->angle_min + scan->angle_increment * i;
            //float range = scan->ranges[i];
            cacher[i] = scan->ranges[i];
            //cachex[i] = range  * cos(rad);
            //cachey[i] = range  * sin(rad);
        }
	cnt++;
    } else {
        float error = FLT_MAX;
        int ke = 0, le = 0;
        for(int k = kmin; k < kmax; k++) {
            for(int l = lmin; l < lmax; l++) {
                float err = 0;
                for(int i = 0; i < count; i++) {
                    int ze = i + k;
                    if(ze < 0) {
                        ze += N;
                    } else if(ze > N - 1) {
                        ze -= N;
                    }
                    if(!std::isnormal(cacher[ze]))
                        continue;
                    float rad = scan->angle_min + scan->angle_increment * i;
                    float range = scan->ranges[i];
                    if(!std::isnormal(range))
                        continue;
                    float xi = range * cos(rad);
                    float yi = range * sin(rad);
                    float re = cacher[ze] + l * lam;
                    float xe = re * cos(rad);
                    float ye = re * sin(rad);
                    float dx = xi - xe;
                    float dy = yi - ye;
                    if(std::isnormal(dx) && std::isnormal(dy))
                        err += SQUARE(dx) + SQUARE(dy);
                }
                if(err < error) {
                    error = err;
                    ke = k;
                    le = l;
                }
            }
        }
        float dtheta = ke * eps;
        float drho = le * lam;
        /*
        float dz = DEG2RAD(dtheta);
        float dx = drho * cos(dz);
        float dy = drho * sin(dz);
        float vx = dx / scan->scan_time;
        float vy = dy / scan->scan_time;
        float vz = dz / scan->scan_time;
        */
        stheta += dtheta;
        srho += drho;
        if(srho < 0)
            srho = 0;
        /*
        float pz = DEG2RAD(stheta);
        float px = srho * cos(pz);
        float py = srho * sin(pz);
        */
        for(int i = 0; i < count; i++) {
            cacher[i] = scan->ranges[i];
        }
        //t = ms() - t;
        ROS_INFO("a=%.2f,r=%.2f,e=%.2f", stheta, srho, error);
        //ROS_INFO("a=%.2f,r=%.2f,da=%.2f,dr=%.2f,dx=%.2f,dy=%.2f,dz=%.2f,px=%.2f,py=%.2f,pz=%.2f,vx=%.2f,vy=%.2f,vz=%.2f,l=%d,t=%d", stheta, srho, dtheta, drho, dx, dy, dz, px, py, pz, vx, vy, vz, loop, t);
    }
}
int main(int argc, char **argv)
{
    ros::init(argc, argv, "rplidar_node_client");
    ros::NodeHandle n;
    ros::Subscriber sub = n.subscribe("/scan", 1000, procCallback);
    ros::spin();
    return 0;
}
 
RPLIDAR官方直接给出了RPLIDAR SDK的ROS集成,为代码编写提供了极大的便利。原理已经在前面叙述,此处代码比较简单,没有添加注释。需要注意的是,为加快求解速度,代码中缩小了参数空间。
下面为上述算法在ROS下的实测效果,控制台中a为LIDAR坐标系旋转角度,r为LIDAR坐标系位移,e为相邻兩帧LIDAR扫描数据的sum of error,其他性能参数未给出。测试过程中目测了一番,角度解算比较理想,位移解算与实际情况有较大偏差。
本章我们从LIDAR相邻兩帧数据与坐标系变换的关系出发,通过逐次迭代的方法来获取LIDAR的里程计信息。值得思考的是,在该算法中我们只关注了这兩帧数据之间的关系,并没有考虑一帧数据本身的结构或特征,下一章我们将讨论如何将更多的信息利用起来,让我们的算法更加robust。

帮杰
疯狂于web和智能设备开发,专注人机互联。
HOW DO I DIY MY OWN REMOTE CONTROL SWITCH IN MY DORMITORY
THE SIMPLEST KALMAN FILTER IN THE WORLD
COMPUTING MEAN AND VARIANCE RECURSIVELY
LEARNING GOOGLE TENSORFLOW [L1: MAKE THE FIRST ACUAINTANCE]