最近有项目要用激光建图与定位(SLAM),花了一周把aloam源码读了一遍;后来开了几次项目会议,才发现用不到激光slam,于是对激光slam的研究暂时搁浅。
参考了其他aloam源码的理解的文章,这里是我理解的部分。
主要的逻辑:
- 订阅激光点云数据;
- 剔除无效点与离传感器过近的点;
- 确定点云的起始方位角和结束方位角,令结束方位角与开始方位角差值控制在(PI,3*PI)范围
- 计算每个点的仰角,根据仰角为每个点找到了对应的扫描线;
- 每条扫瞄线分成6段扇区,每个扇区选出2个曲率极大点和20个曲率次极大点,同时防止特征点过于密集;
- 以同样的方法选出4个曲率极小点;剩余的点全都归于曲率次极小点,因次极小点过多,做一个体素滤波;
- 将上述曲率极大点、次极大点、极小点、次极小点发布出去;
// This is an advanced implementation of the algorithm described in the following paper:
// J. Zhang and S. Singh. LOAM: Lidar Odometry and Mapping in Real-time.
// Robotics: Science and Systems Conference (RSS). Berkeley, CA, July 2014.
// Modifier: Tong Qin qintonguav@gmail.com
// Shaozu Cao saozu.cao@connect.ust.hk
// Copyright 2013, Ji Zhang, Carnegie Mellon University
// Further contributions copyright (c) 2016, Southwest Research Institute
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
// 3. Neither the name of the copyright holder nor the names of its
// contributors may be used to endorse or promote products derived from this
// software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
#include <cmath>
#include <vector>
#include <string>
#include "aloam_velodyne/common.h"
#include "aloam_velodyne/tic_toc.h"
#include <nav_msgs/Odometry.h>
#include <opencv/cv.h>
#include <pcl_conversions/pcl_conversions.h>
#include <pcl/point_cloud.h>
#include <pcl/point_types.h>
#include <pcl/filters/voxel_grid.h>
#include <pcl/kdtree/kdtree_flann.h>
#include <ros/ros.h>
#include <sensor_msgs/Imu.h>
#include <sensor_msgs/PointCloud2.h>
#include <tf/transform_datatypes.h>
#include <tf/transform_broadcaster.h>
using std::atan2;
using std::cos;
using std::sin;
const double scanPeriod = 0.1; // 激光扫描周期0.1s
const int systemDelay = 0; // 初始化时,丢弃掉前n帧
int systemInitCount = 0;
bool systemInited = false;
int N_SCANS = 0;
float cloudCurvature[400000]; // 点云曲率
int cloudSortInd[400000]; // 曲率点对应的序号
int cloudNeighborPicked[400000];// 点是否被选过;0-未筛选,1-已筛选
int cloudLabel[400000]; // 点分类标号:2-代表曲率很大,1-代表曲率比较大,-1-代表曲率很小,0-曲率比较小(其中1包含了2,0包含了1,0和1构成了点云全部的点)
bool comp (int i,int j) { return (cloudCurvature[i]<cloudCurvature[j]); }
ros::Publisher pubLaserCloud;
ros::Publisher pubCornerPointsSharp;
ros::Publisher pubCornerPointsLessSharp;
ros::Publisher pubSurfPointsFlat;
ros::Publisher pubSurfPointsLessFlat;
ros::Publisher pubRemovePoints;
std::vector<ros::Publisher> pubEachScan;
bool PUB_EACH_LINE = false;
double MINIMUM_RANGE = 0.1;
/**
* @brief 删除距离传感器过近的点
* @tparam PointT
* @param cloud_in
* @param cloud_out
* @param thres
*/
template <typename PointT>
void removeClosedPointCloud(const pcl::PointCloud<PointT> &cloud_in,
pcl::PointCloud<PointT> &cloud_out, float thres)
{
// 假如输入输出点云不使用同一个变量,则需要将输出点云的时间戳和容器大小与输入点云同步
if (&cloud_in != &cloud_out)
{
cloud_out.header = cloud_in.header;
cloud_out.points.resize(cloud_in.points.size());
}
size_t j = 0;
for (size_t i = 0; i < cloud_in.points.size(); ++i)
{
if (cloud_in.points[i].x * cloud_in.points[i].x + cloud_in.points[i].y * cloud_in.points[i].y + cloud_in.points[i].z * cloud_in.points[i].z < thres * thres)
continue;
cloud_out.points[j] = cloud_in.points[i];
j++;
}
// 重新调整输出容器大小
if (j != cloud_in.points.size())
{
cloud_out.points.resize(j);
}
cloud_out.height = 1;
cloud_out.width = static_cast<uint32_t>(j);
cloud_out.is_dense = true;
}
/**
* @brief 点云回调函数
* @param laserCloudMsg
*/
void laserCloudHandler(const sensor_msgs::PointCloud2ConstPtr &laserCloudMsg)
{
// 作用就是延时,为了确保有IMU数据后, 再进行点云数据的处理(这里没有imu数据)
if (!systemInited)
{
systemInitCount++;
if (systemInitCount >= systemDelay)
{
systemInited = true;
}
else
return;
}
TicToc t_whole;
TicToc t_prepare;
std::vector<int> scanStartInd(N_SCANS, 0); // 每条线的起始索引
std::vector<int> scanEndInd(N_SCANS, 0); // 末尾索引
pcl::PointCloud<pcl::PointXYZ> laserCloudIn;
pcl::fromROSMsg(*laserCloudMsg, laserCloudIn); // sensor_msgs::PointCloud2--->pcl::PointCloud<pcl::PointXYZ>
std::vector<int> indices;
// 删除无效点
pcl::removeNaNFromPointCloud(laserCloudIn, laserCloudIn, indices);
removeClosedPointCloud(laserCloudIn, laserCloudIn, MINIMUM_RANGE);
// 激光雷达顺时针旋转,所以取负号(atan默认以逆时针为正)
// velodyne是顺时针旋转的,角度取负值相当于将逆时针转换成顺时针运动
// startOir [-pi, pi]; endOri [pi, 3pi];
int cloudSize = laserCloudIn.points.size();
float startOri = -atan2(laserCloudIn.points[0].y, laserCloudIn.points[0].x);
float endOri = -atan2(laserCloudIn.points[cloudSize - 1].y,
laserCloudIn.points[cloudSize - 1].x) +
2 * M_PI;
// 结束方位角与开始方位角差值控制在(PI,3*PI)范围
// lidar有可能扫0.5圈,也有可能扫1.5圈,在0.5-1.5范围内
// 正常情况下在这个范围内:pi < endOri - startOri < 3*pi,异常则修正
if (endOri - startOri > 3 * M_PI)
{
endOri -= 2 * M_PI;
}
else if (endOri - startOri < M_PI)
{
endOri += 2 * M_PI;
}
//printf("end Ori %f\n", endOri);
bool halfPassed = false;
int count = cloudSize; // 有效点计数
PointType point;
std::vector<pcl::PointCloud<PointType>> laserCloudScans(N_SCANS); // 每条线上的点集合
for (int i = 0; i < cloudSize; i++)
{
point.x = laserCloudIn.points[i].x;
point.y = laserCloudIn.points[i].y;
point.z = laserCloudIn.points[i].z;
// 仰角
float angle = atan(point.z / sqrt(point.x * point.x + point.y * point.y)) * 180 / M_PI;
int scanID = 0;
// 根据仰角范围,丢弃无效点
if (N_SCANS == 16)
{
// 参数:velodyne lidar ±15°的垂直视场, 水平分辨率2度
// 根据仰角排列激光线号(0.5是为了方便四舍五入)
// 过滤点,只挑选[-15度,+15度]范围内的点,scanID属于[0,15]
scanID = int((angle + 15) / 2 + 0.5);
if (scanID > (N_SCANS - 1) || scanID < 0)
{
count--;
continue;
}
}
else if (N_SCANS == 32)
{
scanID = int((angle + 92.0/3.0) * 3.0 / 4.0);
if (scanID > (N_SCANS - 1) || scanID < 0)
{
count--;
continue;
}
}
else if (N_SCANS == 64)
{
if (angle >= -8.83)
scanID = int((2 - angle) * 3.0 + 0.5);
else
scanID = N_SCANS / 2 + int((-8.83 - angle) * 2.0 + 0.5);
// use [0 50] > 50 remove outlies
if (angle > 2 || angle < -24.33 || scanID > 50 || scanID < 0)
{
count--;
continue;
}
}
else
{
printf("wrong scan number\n");
ROS_BREAK();
}
// printf("angle %f scanID %d \n", angle, scanID);
// 现在已经为每个点云点找到了对应的扫描线,并且确定了角度范围,可以按照角度分配时间戳了。
// 根据扫描线是否旋转过半选择与起始位置还是终止位置进行差值计算,从而进行补偿
float ori = -atan2(point.y, point.x);
if (!halfPassed)
{
//确保-pi/2 < ori - startOri < 3*pi/2
if (ori < startOri - M_PI / 2)
{
ori += 2 * M_PI;
}
else if (ori > startOri + M_PI * 3 / 2)
{
ori -= 2 * M_PI;
}
if (ori - startOri > M_PI)
{
halfPassed = true;
}
}
else
{
ori += 2 * M_PI;
//确保-3*pi/2 < ori - endOri < pi/2
if (ori < endOri - M_PI * 3 / 2)
{
ori += 2 * M_PI;
}
else if (ori > endOri + M_PI / 2)
{
ori -= 2 * M_PI;
}
}
//0 < relTime < 1(点旋转的角度与整个周期旋转角度的比率, 即点云中点的相对时间)
float relTime = (ori - startOri) / (endOri - startOri);
// 点强度=线号+点相对时间(即一个整数+一个小数,整数部分是线号,小数部分是该点的相对时间),
// 匀速扫描:根据当前扫描的角度和扫描周期计算相对扫描起始位置的时间
point.intensity = scanID + scanPeriod * relTime;
laserCloudScans[scanID].push_back(point);
}
cloudSize = count;
printf("points size %d \n", cloudSize);
// 全部集合到一个点云里面去,使用两个数组标记开始和结果,这里分别+5和-6是为了计算曲率方便
pcl::PointCloud<PointType>::Ptr laserCloud(new pcl::PointCloud<PointType>());
for (int i = 0; i < N_SCANS; i++)
{
// 前5个点和后5个点都无法计算曲率,因为他们不满足左右两侧各有5个点
scanStartInd[i] = laserCloud->size() + 5; // 5
*laserCloud += laserCloudScans[i];
scanEndInd[i] = laserCloud->size() - 6; // n-6
}
// 将一帧无序点云转换成有序点云消耗的时间
printf("prepare time %f \n", t_prepare.toc());
// 计算每一个点的曲率,这里的laserCloud是有序的点云,故可以直接这样计算(论文中说对每条线扫scan计算曲率)
// 但是在每条scan的交界处 计算得到的曲率是不准确的,这可通过scanStartInd[i]、scanEndInd[i]来选取
/*
* 表面上除了前后五个点都应该有曲率,但是由于临近点都在扫描上选取,实际上每条扫描线上的前后五个点也不太准确,
* 应该由scanStartInd[i]、scanEndInd[i]来确定范围
*/
for (int i = 5; i < cloudSize - 5; i++)
{
float diffX = laserCloud->points[i - 5].x + laserCloud->points[i - 4].x + laserCloud->points[i - 3].x + laserCloud->points[i - 2].x + laserCloud->points[i - 1].x - 10 * laserCloud->points[i].x + laserCloud->points[i + 1].x + laserCloud->points[i + 2].x + laserCloud->points[i + 3].x + laserCloud->points[i + 4].x + laserCloud->points[i + 5].x;
float diffY = laserCloud->points[i - 5].y + laserCloud->points[i - 4].y + laserCloud->points[i - 3].y + laserCloud->points[i - 2].y + laserCloud->points[i - 1].y - 10 * laserCloud->points[i].y + laserCloud->points[i + 1].y + laserCloud->points[i + 2].y + laserCloud->points[i + 3].y + laserCloud->points[i + 4].y + laserCloud->points[i + 5].y;
float diffZ = laserCloud->points[i - 5].z + laserCloud->points[i - 4].z + laserCloud->points[i - 3].z + laserCloud->points[i - 2].z + laserCloud->points[i - 1].z - 10 * laserCloud->points[i].z + laserCloud->points[i + 1].z + laserCloud->points[i + 2].z + laserCloud->points[i + 3].z + laserCloud->points[i + 4].z + laserCloud->points[i + 5].z;
cloudCurvature[i] = diffX * diffX + diffY * diffY + diffZ * diffZ;
cloudSortInd[i] = i; // 当前点序号
cloudNeighborPicked[i] = 0; // 初始化0
cloudLabel[i] = 0; // 初始化0
}
TicToc t_pts;
pcl::PointCloud<PointType> cornerPointsSharp; // 曲率极大的点集合
pcl::PointCloud<PointType> cornerPointsLessSharp; // 曲率次极大+曲率大的点
pcl::PointCloud<PointType> surfPointsFlat; // 平坦点集合
pcl::PointCloud<PointType> surfPointsLessFlat; // 存储次平坦的点(体素滤波)
float t_q_sort = 0;
for (int i = 0; i < N_SCANS; i++)
{
// 如果最后一个可算曲率的点与第一个的差小于6,说明该条线无法分成6个扇区,跳过
if( scanEndInd[i] - scanStartInd[i] < 6)
continue;
pcl::PointCloud<PointType>::Ptr surfPointsLessFlatScan(new pcl::PointCloud<PointType>);
// 一条线分成6个扇区
// 假设有100个点(实际远大于100个点),每个扇区15点,则sp[5,20,35,50,65,80], ep[19,34,49,64,79,94]
for (int j = 0; j < 6; j++)
{
int sp = scanStartInd[i] + (scanEndInd[i] - scanStartInd[i]) * j / 6; // 6等分起点
int ep = scanStartInd[i] + (scanEndInd[i] - scanStartInd[i]) * (j + 1) / 6 - 1; // 6等分终点
// 每个扇区按曲率从小到大排序
TicToc t_tmp;
std::sort (cloudSortInd + sp, cloudSortInd + ep + 1, comp);
// t_q_sort累计每个扇区曲率排序时间总和
t_q_sort += t_tmp.toc();
int largestPickedNum = 0;
// 曲率从大到小遍历
// 选取极大曲率点(2个)和次极大曲率点(20个,包含2个极大曲率点)
for (int k = ep; k >= sp; k--)
{
// 未排序前的序号
int ind = cloudSortInd[k];
// 未筛选 且 曲率>0.1
if (cloudNeighborPicked[ind] == 0 &&
cloudCurvature[ind] > 0.1)
{
largestPickedNum++;
if (largestPickedNum <= 2) // 每段选2个曲率大的点
{
cloudLabel[ind] = 2; // label为2是曲率大的标记
cornerPointsSharp.push_back(laserCloud->points[ind]);
cornerPointsLessSharp.push_back(laserCloud->points[ind]);
}
else if (largestPickedNum <= 20) // 以及20个曲率稍微大一些的点
{
cloudLabel[ind] = 1; // label置1表示曲率稍微大
cornerPointsLessSharp.push_back(laserCloud->points[ind]);
}
else // 超过20个就算了
{
break;
}
// 这个点被选中后 pick标志位置1
cloudNeighborPicked[ind] = 1;
// 为了保证特征点不过度集中,将选中的点周围10个点都置1,避免后续会选到
// 防止特征点聚集,使得特征点在每个方向上尽量分布均匀
for (int l = 1; l <= 5; l++)
{
// 查看相邻点距离是否差异过大,如果差异过大说明点云在此不连续,是特征边缘,就会是新的特征,因此就不置位了
float diffX = laserCloud->points[ind + l].x - laserCloud->points[ind + l - 1].x;
float diffY = laserCloud->points[ind + l].y - laserCloud->points[ind + l - 1].y;
float diffZ = laserCloud->points[ind + l].z - laserCloud->points[ind + l - 1].z;
if (diffX * diffX + diffY * diffY + diffZ * diffZ > 0.05)
{
break;
}
cloudNeighborPicked[ind + l] = 1;
}
for (int l = -1; l >= -5; l--)
{
float diffX = laserCloud->points[ind + l].x - laserCloud->points[ind + l + 1].x;
float diffY = laserCloud->points[ind + l].y - laserCloud->points[ind + l + 1].y;
float diffZ = laserCloud->points[ind + l].z - laserCloud->points[ind + l + 1].z;
if (diffX * diffX + diffY * diffY + diffZ * diffZ > 0.05)
{
break;
}
cloudNeighborPicked[ind + l] = 1;
}
}
}
// 下面开始挑选面点
// 曲率从小到大遍历
int smallestPickedNum = 0;
for (int k = sp; k <= ep; k++)
{
int ind = cloudSortInd[k];
// 确保这个点没有被pick且曲率小于阈值
if (cloudNeighborPicked[ind] == 0 &&
cloudCurvature[ind] < 0.1)
{
cloudLabel[ind] = -1; // -1认为是平坦的点
surfPointsFlat.push_back(laserCloud->points[ind]);
smallestPickedNum++;
// 这里不区分平坦和比较平坦,因为剩下的点label默认是0,就是比较平坦
// 只选最小的四个,剩下的Label==0,就都是比较平坦
if (smallestPickedNum >= 4)
{
break;
}
// 前四个平坦的点被选中后 pick标志位置1
cloudNeighborPicked[ind] = 1;
for (int l = 1; l <= 5; l++)
{
float diffX = laserCloud->points[ind + l].x - laserCloud->points[ind + l - 1].x;
float diffY = laserCloud->points[ind + l].y - laserCloud->points[ind + l - 1].y;
float diffZ = laserCloud->points[ind + l].z - laserCloud->points[ind + l - 1].z;
if (diffX * diffX + diffY * diffY + diffZ * diffZ > 0.05)
{
break;
}
cloudNeighborPicked[ind + l] = 1;
}
for (int l = -1; l >= -5; l--)
{
float diffX = laserCloud->points[ind + l].x - laserCloud->points[ind + l + 1].x;
float diffY = laserCloud->points[ind + l].y - laserCloud->points[ind + l + 1].y;
float diffZ = laserCloud->points[ind + l].z - laserCloud->points[ind + l + 1].z;
if (diffX * diffX + diffY * diffY + diffZ * diffZ > 0.05)
{
break;
}
cloudNeighborPicked[ind + l] = 1;
}
}
}
for (int k = sp; k <= ep; k++)
{
// 剩下来的点都是次平坦,这个也符合实际
if (cloudLabel[k] <= 0)
{
surfPointsLessFlatScan->push_back(laserCloud->points[k]);
}
}
}
// 次平坦的点比较多,所以这里做一个体素滤波
pcl::PointCloud<PointType> surfPointsLessFlatScanDS;
pcl::VoxelGrid<PointType> downSizeFilter;
downSizeFilter.setInputCloud(surfPointsLessFlatScan);
downSizeFilter.setLeafSize(0.2, 0.2, 0.2);
downSizeFilter.filter(surfPointsLessFlatScanDS);
surfPointsLessFlat += surfPointsLessFlatScanDS;
}
printf("sort q time %f \n", t_q_sort);
printf("seperate points time %f \n", t_pts.toc());
// 分别将当前点云、四种特征的点云发布出去
sensor_msgs::PointCloud2 laserCloudOutMsg;
pcl::toROSMsg(*laserCloud, laserCloudOutMsg);
laserCloudOutMsg.header.stamp = laserCloudMsg->header.stamp;
laserCloudOutMsg.header.frame_id = "/camera_init";
pubLaserCloud.publish(laserCloudOutMsg);
sensor_msgs::PointCloud2 cornerPointsSharpMsg;
pcl::toROSMsg(cornerPointsSharp, cornerPointsSharpMsg);
cornerPointsSharpMsg.header.stamp = laserCloudMsg->header.stamp;
cornerPointsSharpMsg.header.frame_id = "/camera_init";
pubCornerPointsSharp.publish(cornerPointsSharpMsg);
sensor_msgs::PointCloud2 cornerPointsLessSharpMsg;
pcl::toROSMsg(cornerPointsLessSharp, cornerPointsLessSharpMsg);
cornerPointsLessSharpMsg.header.stamp = laserCloudMsg->header.stamp;
cornerPointsLessSharpMsg.header.frame_id = "/camera_init";
pubCornerPointsLessSharp.publish(cornerPointsLessSharpMsg);
sensor_msgs::PointCloud2 surfPointsFlat2;
pcl::toROSMsg(surfPointsFlat, surfPointsFlat2);
surfPointsFlat2.header.stamp = laserCloudMsg->header.stamp;
surfPointsFlat2.header.frame_id = "/camera_init";
pubSurfPointsFlat.publish(surfPointsFlat2);
sensor_msgs::PointCloud2 surfPointsLessFlat2;
pcl::toROSMsg(surfPointsLessFlat, surfPointsLessFlat2);
surfPointsLessFlat2.header.stamp = laserCloudMsg->header.stamp;
surfPointsLessFlat2.header.frame_id = "/camera_init";
pubSurfPointsLessFlat.publish(surfPointsLessFlat2);
// pub each scam
if(PUB_EACH_LINE)
{
for(int i = 0; i< N_SCANS; i++)
{
sensor_msgs::PointCloud2 scanMsg;
pcl::toROSMsg(laserCloudScans[i], scanMsg);
scanMsg.header.stamp = laserCloudMsg->header.stamp;
scanMsg.header.frame_id = "/camera_init";
pubEachScan[i].publish(scanMsg);
}
}
printf("scan registration time %f ms *************\n", t_whole.toc());
if(t_whole.toc() > 100)
ROS_WARN("scan registration process over 100ms");
}
int main(int argc, char **argv)
{
ros::init(argc, argv, "scanRegistration");
ros::NodeHandle nh;
nh.param<int>("scan_line", N_SCANS, 16);
nh.param<double>("minimum_range", MINIMUM_RANGE, 0.1);
printf("scan line number %d \n", N_SCANS);
if(N_SCANS != 16 && N_SCANS != 32 && N_SCANS != 64)
{
printf("only support velodyne with 16, 32 or 64 scan line!");
return 0;
}
ros::Subscriber subLaserCloud = nh.subscribe<sensor_msgs::PointCloud2>("/velodyne_points", 100, laserCloudHandler);
pubLaserCloud = nh.advertise<sensor_msgs::PointCloud2>("/velodyne_cloud_2", 100);
pubCornerPointsSharp = nh.advertise<sensor_msgs::PointCloud2>("/laser_cloud_sharp", 100);
pubCornerPointsLessSharp = nh.advertise<sensor_msgs::PointCloud2>("/laser_cloud_less_sharp", 100);
pubSurfPointsFlat = nh.advertise<sensor_msgs::PointCloud2>("/laser_cloud_flat", 100);
pubSurfPointsLessFlat = nh.advertise<sensor_msgs::PointCloud2>("/laser_cloud_less_flat", 100);
pubRemovePoints = nh.advertise<sensor_msgs::PointCloud2>("/laser_remove_points", 100);
if(PUB_EACH_LINE)
{
for(int i = 0; i < N_SCANS; i++)
{
ros::Publisher tmp = nh.advertise<sensor_msgs::PointCloud2>("/laser_scanid_" + std::to_string(i), 100);
pubEachScan.push_back(tmp);
}
}
ros::spin();
return 0;
}