Bayesian Generalized Kernel Inference for Terrain Traversability Mapping论文解读
基于贝叶斯广义核推理的地形建模是机器人领域做地形建模的重要文章之一,在实时性和精度方面都取得了较好的效果。在对其代码进行解读之前,先谈几点思考。
围绕机器人环境感知与导航,有很多工作可以做,但是可供选择的方法框架是有限的,当然了在机器人领域很多问题归根到底就是信息的滤波/状态估计/规划与控制问题,最核心的就是优化问题。在机器人领域,如何做的更深,更加具有创新性,需要对场景和问题进行深入的挖掘分析与建模。
代码框架
在工作空间中的src文件下,包含了两个包,一个是LeGO-LOAM,用于提供全局点云和位姿估计,一个就是地形建模的包,包括三个节点。下面进行具体的分析。
traversability_filter
首先看main函数。初始化一个名为“traversability_mapping”的节点,实例化类TraversabilityFilter,接下来我们看下其构造函数。
int main(int argc, char** argv){
ros::init(argc, argv, "traversability_mapping");
TraversabilityFilter TFilter;
ROS_INFO("\033[1;32m---->\033[0m Traversability Filter Started.");
ros::spin();
return 0;
}
TraversabilityFilter类的构造函数。首先订阅sensor_msgs::PointCloud2消息,全部的点云信息。然后发布四种消息。
TraversabilityFilter():
nh("~"){
subCloud = nh.subscribe<sensor_msgs::PointCloud2>("/full_cloud_info", 5, &TraversabilityFilter::cloudHandler, this);
pubCloud = nh.advertise<sensor_msgs::PointCloud2> ("/filtered_pointcloud", 5);
pubCloudVisualHiRes = nh.advertise<sensor_msgs::PointCloud2> ("/filtered_pointcloud_visual_high_res", 5);
pubCloudVisualLowRes = nh.advertise<sensor_msgs::PointCloud2> ("/filtered_pointcloud_visual_low_res", 5);
pubLaserScan = nh.advertise<sensor_msgs::LaserScan> ("/pointcloud_2_laserscan", 5);
allocateMemory();
pointcloud2laserscanInitialization();
}
然后根据订阅消息,进入回调函数
void cloudHandler(const sensor_msgs::PointCloud2ConstPtr& laserCloudMsg){
extractRawCloud(laserCloudMsg);
if (transformCloud() == false) return;
cloud2Matrix();
applyFilter();
extractFilteredCloud();
downsampleCloud();
predictCloudBGK();
publishCloud();
publishLaserScan();
resetParameters();
}
首先将点云消息转化为点云,利用pcl::fromROSMsg即可,然后系统还对rangeMatrix进行了更新。
void extractRawCloud(const sensor_msgs::PointCloud2ConstPtr& laserCloudMsg){
// ROS msg -> PCL cloud
pcl::fromROSMsg(*laserCloudMsg, *laserCloudIn);
// extract range info
for (int i = 0; i < N_SCAN; ++i){
for (int j = 0; j < Horizon_SCAN; ++j){
int index = j + i * Horizon_SCAN;
// skip NaN point
if (laserCloudIn->points[index].intensity == std::numeric_limits<float>::quiet_NaN()) continue;
// save range info
rangeMatrix.at<float>(i, j) = laserCloudIn->points[index].intensity;
// reset obstacle status to 0 - free
obstacleMatrix.at<int>(i, j) = 0;
}
}
}
接下来是对点云进行变换到全局坐标系下。类的私有成员变量tf::TransformListener listener监听全局的坐标变换。
bool transformCloud(){
// Listen to the TF transform and prepare for point cloud transformation
try{listener.lookupTransform("map","base_link", ros::Time(0), transform); }
catch (tf::TransformException ex){ /*ROS_ERROR("Transfrom Failure.");*/ return false; }
robotPoint.x = transform.getOrigin().x();
robotPoint.y = transform.getOrigin().y();
robotPoint.z = transform.getOrigin().z();
laserCloudIn->header.frame_id = "base_link";
laserCloudIn->header.stamp = 0; // don't use the latest time, we don't have that transform in the queue yet
pcl::PointCloud<PointType> laserCloudTemp;
pcl_ros::transformPointCloud("map", *laserCloudIn, laserCloudTemp, listener);
*laserCloudIn = laserCloudTemp;
return true;
}
接下来是将点云转化为矩阵存储的形式。
void cloud2Matrix(){
for (int i = 0; i < N_SCAN; ++i){
for (int j = 0; j < Horizon_SCAN; ++j){
int index = j + i * Horizon_SCAN;
PointType p = laserCloudIn->points[index];
laserCloudMatrix[i][j] = p;
}
}
}
然后根据rangemap对点云进行滤波,主要是对obstacleMatrix进行计算,其存储了每个点是否为障碍物上的点的标志,为0的点即为较为平整的有效的区域。positiveCurbFilter()是对点云进行水平方向的检查,negativeCurbFilter()是对点云进行竖直方向的检查。slopeFilter()是计算局部点云的斜率,进行进一步的地面点的识别。
void applyFilter(){
if (urbanMapping == true){
positiveCurbFilter();
negativeCurbFilter();
}
slopeFilter();
}
接下来是对点云进行提取。根据障碍物的标记矩阵对点进行标记,取出无效点和较远的点,得到laserCloudOut,然后单纯提取出障碍物点laserCloudObstacles。然后将过滤后的当前点云转化成ros消息发布出去。
void extractFilteredCloud(){
for (int i = 0; i < scanNumMax; ++i){
for (int j = 0; j < Horizon_SCAN; ++j){
// invalid points and points too far are skipped
if (rangeMatrix.at<float>(i, j) > sensorRangeLimit ||
rangeMatrix.at<float>(i, j) == -1)
continue;
// update point intensity (occupancy) into
PointType p = laserCloudMatrix[i][j];
p.intensity = obstacleMatrix.at<int>(i,j) == 1 ? 100 : 0;
// save updated points
laserCloudOut->push_back(p);
// extract obstacle points and convert them to laser scan
if (p.intensity == 100)
laserCloudObstacles->push_back(p);
}
}
// Publish laserCloudOut for visualization (before downsample and BGK prediction)
if (pubCloudVisualHiRes.getNumSubscribers() != 0){
sensor_msgs::PointCloud2 laserCloudTemp;
pcl::toROSMsg(*laserCloudOut, laserCloudTemp);
laserCloudTemp.header.stamp = ros::Time::now();
laserCloudTemp.header.frame_id = "map";
pubCloudVisualHiRes.publish(laserCloudTemp);
}
}
接下来是对点云进行滤波,以左下角为坐标原点,构建grid map,形成height map,,每个栅格记录投影到该栅格处的最高点和最低点。然后再将高度地图转化为点云,发布出去。
void downsampleCloud(){
float roundedX = float(int(robotPoint.x * 10.0f)) / 10.0f;
float roundedY = float(int(robotPoint.y * 10.0f)) / 10.0f;
// height map origin
localMapOrigin.x = roundedX - sensorRangeLimit;
localMapOrigin.y = roundedY - sensorRangeLimit;
// convert from point cloud to height map
int cloudSize = laserCloudOut->points.size();
for (int i = 0; i < cloudSize; ++i){
int idx = (laserCloudOut->points[i].x - localMapOrigin.x) / mapResolution;
int idy = (laserCloudOut->points[i].y - localMapOrigin.y) / mapResolution;
// points out of boundry
if (idx < 0 || idy < 0 || idx >= filterHeightMapArrayLength || idy >= filterHeightMapArrayLength)
continue;
// obstacle point (decided by curb or slope filter)
if (laserCloudOut->points[i].intensity == 100)
obstFlag[idx][idy] = true;
// save min and max height of a grid
if (initFlag[idx][idy] == false){
minHeight[idx][idy] = laserCloudOut->points[i].z;
maxHeight[idx][idy] = laserCloudOut->points[i].z;
initFlag[idx][idy] = true;
} else {
minHeight[idx][idy] = std::min(minHeight[idx][idy], laserCloudOut->points[i].z);
maxHeight[idx][idy] = std::max(maxHeight[idx][idy], laserCloudOut->points[i].z);
}
}
// intermediate cloud
pcl::PointCloud<PointType>::Ptr laserCloudTemp(new pcl::PointCloud<PointType>());
// convert from height map to point cloud
for (int i = 0; i < filterHeightMapArrayLength; ++i){
for (int j = 0; j < filterHeightMapArrayLength; ++j){
// no point at this grid
if (initFlag[i][j] == false)
continue;
// convert grid to point
PointType thisPoint;
thisPoint.x = localMapOrigin.x + i * mapResolution + mapResolution / 2.0;
thisPoint.y = localMapOrigin.y + j * mapResolution + mapResolution / 2.0;
thisPoint.z = maxHeight[i][j];
if (obstFlag[i][j] == true || maxHeight[i][j] - minHeight[i][j] > filterHeightLimit){
obstFlag[i][j] = true;
thisPoint.intensity = 100; // obstacle
laserCloudTemp->push_back(thisPoint);
}else{
thisPoint.intensity = 0; // free
laserCloudTemp->push_back(thisPoint);
}
}
}
*laserCloudOut = *laserCloudTemp;
// Publish laserCloudOut for visualization (after downsample but beforeBGK prediction)
if (pubCloudVisualLowRes.getNumSubscribers() != 0){
sensor_msgs::PointCloud2 laserCloudTemp;
pcl::toROSMsg(*laserCloudOut, laserCloudTemp);
laserCloudTemp.header.stamp = ros::Time::now();
laserCloudTemp.header.frame_id = "map";
pubCloudVisualLowRes.publish(laserCloudTemp);
}
}
接下来是利用高斯回归进行一个空白栅格处的信息填充,并将生成的点加入到点云中。具体地,选择一个空白栅格,然后计算其邻域内的非空白栅格作为训练数据,利用高斯回归对当前点进行预测。
void predictCloudBGK(){
if (predictionEnableFlag == false)
return;
int kernelGridLength = int(predictionKernalSize / mapResolution);
for (int i = 0; i < filterHeightMapArrayLength; ++i){
for (int j = 0; j < filterHeightMapArrayLength; ++j){
// skip observed point
if (initFlag[i][j] == true)
continue;
PointType testPoint;
testPoint.x = localMapOrigin.x + i * mapResolution + mapResolution / 2.0;
testPoint.y = localMapOrigin.y + j * mapResolution + mapResolution / 2.0;
testPoint.z = robotPoint.z; // this value is not used except for computing distance with robotPoint
// skip grids too far
if (pointDistance(testPoint, robotPoint) > sensorRangeLimit)
continue;
// Training data
vector<float> xTrainVec; // training data x and y coordinates
vector<float> yTrainVecElev; // training data elevation
vector<float> yTrainVecOccu; // training data occupancy
// Fill trainig data (vector)
for (int m = -kernelGridLength; m <= kernelGridLength; ++m){
for (int n = -kernelGridLength; n <= kernelGridLength; ++n){
// skip grids too far
if (std::sqrt(float(m*m + n*n)) * mapResolution > predictionKernalSize)
continue;
int idx = i + m;
int idy = j + n;
// index out of boundry
if (idx < 0 || idy < 0 || idx >= filterHeightMapArrayLength || idy >= filterHeightMapArrayLength)
continue;
// save only observed grid in this scan
if (initFlag[idx][idy] == true){
xTrainVec.push_back(localMapOrigin.x + idx * mapResolution + mapResolution / 2.0);
xTrainVec.push_back(localMapOrigin.y + idy * mapResolution + mapResolution / 2.0);
yTrainVecElev.push_back(maxHeight[idx][idy]);
yTrainVecOccu.push_back(obstFlag[idx][idy] == true ? 1 : 0);
}
}
}
// no training data available, continue
if (xTrainVec.size() == 0)
continue;
// convert from vector to eigen
Eigen::MatrixXf xTrain = Eigen::Map<const Eigen::Matrix<float, -1, -1, Eigen::RowMajor>>(xTrainVec.data(), xTrainVec.size() / 2, 2);
Eigen::MatrixXf yTrainElev = Eigen::Map<const Eigen::Matrix<float, -1, -1, Eigen::RowMajor>>(yTrainVecElev.data(), yTrainVecElev.size(), 1);
Eigen::MatrixXf yTrainOccu = Eigen::Map<const Eigen::Matrix<float, -1, -1, Eigen::RowMajor>>(yTrainVecOccu.data(), yTrainVecOccu.size(), 1);
// Test data (current grid)
vector<float> xTestVec;
xTestVec.push_back(testPoint.x);
xTestVec.push_back(testPoint.y);
Eigen::MatrixXf xTest = Eigen::Map<const Eigen::Matrix<float, -1, -1, Eigen::RowMajor>>(xTestVec.data(), xTestVec.size() / 2, 2);
// Predict
Eigen::MatrixXf Ks; // covariance matrix
covSparse(xTest, xTrain, Ks); // sparse kernel
Eigen::MatrixXf ybarElev = (Ks * yTrainElev).array();
Eigen::MatrixXf ybarOccu = (Ks * yTrainOccu).array();
Eigen::MatrixXf kbar = Ks.rowwise().sum().array();
// Update Elevation with Prediction
if (std::isnan(ybarElev(0,0)) || std::isnan(ybarOccu(0,0)) || std::isnan(kbar(0,0)))
continue;
if (kbar(0,0) == 0)
continue;
float elevation = ybarElev(0,0) / kbar(0,0);
float occupancy = ybarOccu(0,0) / kbar(0,0);
PointType p;
p.x = xTestVec[0];
p.y = xTestVec[1];
p.z = elevation;
p.intensity = (occupancy > 0.5) ? 100 : 0;
laserCloudOut->push_back(p);
}
接下来就是将补充后的点云发布出去。
void publishCloud(){
sensor_msgs::PointCloud2 laserCloudTemp;
pcl::toROSMsg(*laserCloudOut, laserCloudTemp);
laserCloudTemp.header.stamp = ros::Time::now();
laserCloudTemp.header.frame_id = "map";
pubCloud.publish(laserCloudTemp);
}
然后是对laserscan进行更新和发布,这个存储是机器人周围的最近点。
void publishLaserScan(){
updateLaserScan();
laserScan.header.stamp = ros::Time::now();
pubLaserScan.publish(laserScan);
// initialize laser scan for new scan
std::fill(laserScan.ranges.begin(), laserScan.ranges.end(), laserScan.range_max + 1.0);
}