使用ORB-SLAM构建octomap要注意的一些问题
以ORB-SLAM系统做为基本框架,RGB-D相机作为基本输入。然后在此基础上,新建一个点云建图线程,将普通帧或关键帧对应的深度图像转换为三维点云,再发布至octomap_server功能包(或者直接仿照此功能包在线程中直接重写八叉树地图构建代码)是目前诸多开源代码的主要流程。
结构点云与非结构点云、深度值合法性、坐标参考系转换
ORB-SLAM与ROS系统的坐标系模式转换
octomap_server建图时部分方格无故消失问题
在复现此类仓库代码时,有一些问题需要注意,在此总结如下。
1.
结构点云与非结构点云、深度值合法性、坐标参考系转换
如上述代码,根据关键帧pkf的深度图想构建了一个位于相机坐标系下的三维点云团camera_pc_和一个位于世界坐标系下的三维点云团world_pc_。需要注意的问题,已在注释中说明。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 void PointCloudMapping::generatePointCloud (KeyFrame* pkf ,pcl::PointCloud<pcl::PointXYZRGB>::Ptr camera_pc_ , pcl::PointCloud<pcl::PointXYZRGB>::Ptr world_pc_) { camera_pc_->resize (pkf->mImDep.rows * pkf->mImDep.cols); camera_pc_->width = pkf->mImDep.cols; camera_pc_->height = pkf->mImDep.rows; camera_pc_->is_dense = false ; for ( int m=0 ; m<pkf->mImDep.rows; m+=1 ) { for ( int n=0 ; n<pkf->mImDep.cols; n+=1 ) { float d = pkf->mImDep.ptr <float >(m)[n]; if (d < camera_valid_depth_Min || d > camera_valid_depth_Max || isnan (d)) continue ; size_t index = m * pkf->mImDep.cols + n; camera_pc_->points[index].z = d; camera_pc_->points[index].x = ( n - pkf->cx) * d / pkf->fx; camera_pc_->points[index].y = ( m - pkf->cy) * d / pkf->fy; camera_pc_->points[index].r = pkf->mImRGB.ptr <uchar>(m)[n*3 +2 ]; camera_pc_->points[index].g = pkf->mImRGB.ptr <uchar>(m)[n*3 +1 ]; camera_pc_->points[index].b = pkf->mImRGB.ptr <uchar>(m)[n*3 +0 ]; } } Eigen::Isometry3d Twc = ORB_SLAM2::Converter::toSE3Quat ( pkf->GetPoseInverse () ); pcl::transformPointCloud (*camera_pc_, *world_pc_, Twc.matrix ()); if (pkf->mvObjects2D.size () > 0 && is_octo_semantic_map_construction) mpDetector3D->Detect (pkf->mvObjects2D,pkf->mImDep,world_pc_); if (is_global_pc_reconstruction) slam_to_ros_mode_transform (*world_pc_, *world_pc_); }
2.
ORB-SLAM与ROS系统的坐标系模式转换
此处可参考此文章:ORB_SLAM到ROS坐标转换 。用图来说明便是,
SLAM坐标系与ROS坐标系之间的转换
在三维点云重建功能中,一般实现方法是:将连续到来的每张关键帧计算得到的世界系部分点云团world_pc_都累加到一个global_map_pc对象中,然后通过ROS将全图点云团global_map_pc发布至Rviz进行可视化。
问题是,即使从关键帧的深度图中导出了该帧的世界系点云团world_pc_,此世界系位姿也是相对于SLAM系统的初始帧(默认初始帧位姿为世界系单位阵)而言的。它的坐标系表示方式是上图中上方的SLAM(Z超前,X朝右,Y朝下)模式的。如果直接将该点云发布至Rviz进行可视化,那么此点云的显示相对于Rviz的grid网格是有一个错位颠倒的变换的,看起来是“垂直于”网格地面。所以我们发布前需要统一位姿表示模式。
这就是做三维点云地图重建时,代码里要做一个T_slam_to_ros的位姿转换的原因:从SLAM位姿模式的Z、-X、-Y转变为ROS模式下的X、Y、Z,以便在Rviz中正确显示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 if (is_global_pc_reconstruction) slam_to_ros_mode_transform (*world_pc_, *world_pc_); void PointCloudMapping::slam_to_ros_mode_transform (pcl::PointCloud<pcl::PointXYZRGB>& source, pcl::PointCloud<pcl::PointXYZRGB>& out) { Eigen::Matrix4f T_slam_to_ros; T_slam_to_ros<< 0 ,0 ,1 ,0 , -1 ,0 ,0 ,0 , 0 ,-1 ,0 ,0 , 0 ,0 ,0 ,0 ; Eigen::Affine3f transform (T_slam_to_ros) ; pcl::transformPointCloud (source, out, transform); }
同理,向ROS发布当前相机位姿,也是如此处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 Camera_Pose = SLAM.TrackRGBD (imRGB,imD,tframe); Pub_CamPose (Camera_Pose); void Pub_CamPose (cv::Mat &pose) { cv::Mat Rwc (3 ,3 ,CV_32F) ; cv::Mat twc (3 ,1 ,CV_32F) ; Eigen::Matrix<double ,3 ,3 > rotationMat; sg_slam_tf_broadcaster = new tf::TransformBroadcaster; if (pose.dims < 2 || pose.rows < 3 ){ Rwc = Rwc;twc = twc; }else { Rwc = pose.rowRange (0 ,3 ).colRange (0 ,3 ).t (); twc = -Rwc*pose.rowRange (0 ,3 ).col (3 ); rotationMat << Rwc.at <float >(0 ,0 ), Rwc.at <float >(0 ,1 ), Rwc.at <float >(0 ,2 ), Rwc.at <float >(1 ,0 ), Rwc.at <float >(1 ,1 ), Rwc.at <float >(1 ,2 ), Rwc.at <float >(2 ,0 ), Rwc.at <float >(2 ,1 ), Rwc.at <float >(2 ,2 ); Eigen::Quaterniond Q (rotationMat) ; sg_slam_tf.setOrigin (tf::Vector3 (twc.at <float >(2 ), -twc.at <float >(0 ), -twc.at <float >(1 ))); sg_slam_tf.setRotation (tf::Quaternion (Q.z (), -Q.x (), -Q.y (), Q.w ())); Cam_Pose.header.stamp = ros::Time::now (); Cam_Pose.header.frame_id = "/map" ; tf::pointTFToMsg (sg_slam_tf.getOrigin (), Cam_Pose.pose.position); tf::quaternionTFToMsg (sg_slam_tf.getRotation (), Cam_Pose.pose.orientation); CamPose_Pub.publish (Cam_Pose); } }
3.
octomap_server建图时部分方格无故消失问题
通过点云建图线程向ROS发布关键帧的点云话题,然后使用octomap_server功能包订阅点云话题构建八叉树地图(或者可以根据octomap库api自己直接编写相关功能)进行增量地图构建时,地图在扩建的过程中,之前的部分体素格会出现”逐渐消失“情况。如:
DS-SLAM的这部分代码
DS-SLAM发布点云话题相关代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 for ( size_t i=lastKeyframeSize; i<N ; i++ ){ PointCloud::Ptr p = generatePointCloud ( keyframes[i],semanticImgs_color[i], semanticImgs[i],colorImgs[i], depthImgs[i] ); *KfMap += *p; *globalMap += *p; } PointCloud::Ptr tmp1 (new PointCloud()) ; voxel.setInputCloud ( KfMap ); voxel.filter ( *tmp1 ); KfMap->swap ( *tmp1 ); pcl_cloud_kf = *KfMap; Cloud_transform (pcl_cloud_kf,pcl_filter); pcl::toROSMsg (pcl_filter, pcl_point); pcl_point.header.frame_id = "/pointCloud" ; pclPoint_pub.publish (pcl_point);
将点云团由相机系转换为世界系(SLAM模式)后,再由SLAM模式转换为ROS模式(
Cloud_transform(,) )。其点云参考系为“frame_id =
"/pointCloud"”,随后便直接发布至ROS系统。在启动octomap建图时,要先使用roslaunch启动两个节点:
一是octomap_server ,其参考系为/map
1 <param name ="frame_id" type ="string" value ="/map" />
二是transform.launch ,作用是向ROS输出一个全局静态TF变换,即参考系/pointCloud与参考系/map(实际上,在这里map系就是默认的Rviz世界系)的固定变换为"0.195
-0.095 0.9 0 0 0"(x y z r p y表示)。
1 <node pkg ="tf" type ="static_transform\_publisher" name ="map" args ="0.195 -0.095 0.9 0 0 0 /map /pointCloud 70" >
以上便是DS-SLAM发布点云至octomap_server增量建图的基本思路。
接下来,看octomap_server源码中接收点云建图的相关代码。octomap_server节点启动,对象构建时,参数设置完毕后便会启用回调函数进行无限循环(#179) ,回调函数上方是两个话题订阅器,顾名思义,一个订阅点云话题,一个订阅点云位姿tf话题。
1 2 3 4 5 6 m_pointCloudSub = new message_filters::Subscriber <sensor_msgs::PointCloud2> (m_nh, "cloud_in" , 5 ); m_tfPointCloudSub = new tf::MessageFilter <sensor_msgs::PointCloud2> (*m_pointCloudSub, m_tfListener, m_worldFrameId, 5 ); m_tfPointCloudSub->registerCallback (boost::bind (&OctomapServer::insertCloudCallback, this , boost::placeholders::_1));
回调函数 的主要功能在代码中注释如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 void OctomapServer::insertCloudCallback (const sensor_msgs::PointCloud2::ConstPtr& cloud) { ros::WallTime startTime = ros::WallTime::now (); PCLPointCloud pc; pcl::fromROSMsg (*cloud, pc); tf::StampedTransform sensorToWorldTf; try { m_tfListener.lookupTransform (m_worldFrameId, cloud->header.frame_id, cloud->header.stamp, sensorToWorldTf); } catch (tf::TransformException& ex){ ROS_ERROR_STREAM ( "Transform error of sensor data: " << ex.what () << ", quitting callback" ); return ; } Eigen::Matrix4f sensorToWorld; pcl_ros::transformAsMatrix (sensorToWorldTf, sensorToWorld); pcl::PassThrough<PCLPoint> pass_x; …… if (m_filterGroundPlane){ …… } else { pcl::transformPointCloud (pc, pc, sensorToWorld); …… pc_ground.header = pc.header; pc_nonground.header = pc.header; } insertScan (sensorToWorldTf.getOrigin (), pc_ground, pc_nonground); …… }
点云插入增量更新代码如下,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 void OctomapServer::insertScan (const tf::Point& sensorOriginTf, const PCLPointCloud& ground, const PCLPointCloud& nonground) { point3d sensorOrigin = pointTfToOctomap (sensorOriginTf); …… KeySet free_cells, occupied_cells; …… for (PCLPointCloud::const_iterator it = nonground.begin (); it != nonground.end (); ++it){ point3d point (it->x, it->y, it->z) ; if ((m_minRange > 0 ) && (point - sensorOrigin).norm () < m_minRange) continue ; if ((m_maxRange < 0.0 ) || ((point - sensorOrigin).norm () <= m_maxRange) ) { if (m_octree->computeRayKeys (sensorOrigin, point, m_keyRay)){ free_cells.insert (m_keyRay.begin (), m_keyRay.end ()); } OcTreeKey key; if (m_octree->coordToKeyChecked (point, key)){ occupied_cells.insert (key); …… } else { …… } } } …… …… …… }
从OctomapServer::insertScan()函数中可以看出,每次插入点云时,以传感器在世界系下的原点为起点,以点云(世界系下)坐标为终点,更新octomap。即,从原点到终点光线ray上的所有点设置为空闲free,点云所在点(终点)设置为占据occupied状态(free
on ray, occupied on endpoint),如下图所示。
https://www.arminhornung.de/Research/pub/hornung13roscon.pdf
综上,那么DS-SLAM以及一些类似的问题[1] [2] 出现体素格莫名其妙消失(清除cleared)的原因可以分析如下:
在SLAM系统生成点云时,在SLAM中便将3D点云转换为了世界系(此时均为SLAM位姿模式),然后又将SLAM模式转换为ROS模式(见本文第二节),最后将点云消息进行发布。
octomap_server功能包的建图逻辑是订阅
【相机系下的3D点云信息】、【与该点云信息有关的传感器系(本文即相机系)到世界系的位姿变换
tf 】两种数据(octomap_server默认所有消息的位姿类型均为ROS模式)。
DS-SLAM并没有在SLAM系统中发布相机系到世界系的位姿变换 tf
,而且它将相机系下的3D点云(SLAM模式)手动转换为了世界系下的3D点云(ROS模式)并发布出去。
为了使得octomap_server可以处理DS-SLAM发布的点云数据,DS-SLAM直接roslaunch了一个tf静态变换文件,持续向ROS发布相机系/pointCloud到世界系/map的位姿变换tf,只不过这个tf除了一些xyz的单纯位置变换外,并没有其他的旋转操作。
1 <node pkg ="tf" type ="static_transform_publisher" name ="map" args ="0.195 -0.095 0.9 0 0 0 /map /pointCloud 70" >
两种数据齐备,那么octomap_server此时确实已经可以运行。不过这里有一个小小的问题,那便是:
由于每次octomap_server接收到的相机系到世界系的位姿变换tf是DS-SLAM手动发布的静态信息,因此octomap_server每次处理3D点云并对octomap进行增量更新时,它所参考的传感器原点(上文图中的sensor
origin)一直是"0.195 -0.095 0.9 0 0
0"这个相对于世界系的静态坐标,从来不会发生变化。
再由八叉树更新原理可知,每次的空闲和占据状态是根据传感器真正原点到点云坐标点的“光线”(下图中以true
sensor origin起始的黑色箭头)来进行处理的。本来每次的原点sensor
origin都应该是传感器实时相对于世界系的真实原点,而点云则是基于传感器系下的坐标,然后回调函数insertCloudCallback()会自己对这些数据进行坐标系变换处理。
正确的传感器原点、错误的传感器原点设置
然而,如上图所示,现在DS-SLAM的部分代码一边对点云的坐标系和模式进行了手动处理(点云变为了ROS模式的世界系坐标),一边发布了一个静态tf变换对octomap_server进行“蒙蔽”。这就导致一旦相机拍摄到已经占据的体素格的点云,新产生的光线线路(图中绿色箭头)上所有的体素格(无论占据与否)都将更新为空闲free。那么便会有一部分实际为占据状态的体素(如end
point),被更新为空闲状态。这就导致了部分体素“消失”的现象。
解决的方法很简单,按照octomap_server的正常使用方式使用即可:
1. generatePointCloud( )直接输出相机系下的点云团, 只将SLAM位姿模式改为ROS模式即可 , 并将点云团参考系设为/camera .
2. 求解相机系到世界系的位姿变换tf(ROS模式的) ,
3. 发布点云团和tf变换,标记tf参考系变换为/camera_sensor至/map
那么,octomap_server现在接收到的信息便是:
每个关键帧的位于相机系下的ROS模式点云团数据,参考系为/camera_sensor;
每个关键帧的自相机系/camera_sensor至世界系/map的ROS模式位姿变换tf;
然后octomap_server便可正确处理这些数据了。
还可参考代码或文章
高翔老师octomap
tutor
本文部分代码摘自仓库
[^](#ref_1_0)https://answers.ros.org/question/224488/octomap-decreasing-probabilities-when-obstacle-is-not-there-anymore/
[^](#ref_2_0)https://answers.ros.org/question/51837/octomap_server-globally-referenced-pointcloud-and-transform/