Octomap采用八叉树数据结构存储三维环境的概率占据地图。关于它的基本介绍,可以移步高翔博客:SLAM拾萃(1):octomap,包括“什么是Octomap”、“Octomap的原理”、“Octomap的安装”以及“以视觉SLAM为背景玩转Octomap系列”。
高翔博客只对github源码版的Octomap做了介绍,并没有给出ROS下使用Octomap的教程,这里做一些补充介绍。
说明一下三种安装方式的区别和联系。
message("${OCTOMAP_INCLUDE_DIRS}")
# /opt/ros/kinetic/include
message("${OCTOMAP_LIBRARIES}")
# /opt/ros/kinetic/lib/liboctomap.so;/opt/ros/kinetic/lib/liboctomath.so
Octomap本身的理论基础并不复杂,概率更新只是一个简单的二值贝叶斯滤波过程,Octomap原文也给出了贝叶斯滤波的最终结果即递推的概率更新公式。对贝叶斯滤波的推导过程感兴趣的同学可以参考弗莱堡大学的课件Lecture 12:Grid Maps and Mapping with Known Poses或者阅读《概率机器人》ch4.2。
这里简单的提一下Octomap工程实现时与八叉树节点概率更新相关的几个重要参数设置函数。
由于log-odd与概率值之间可以相互转化,因此在工程实现时octomap八叉树节点类OcTreeNode存储的数值是log-odd数值,并不是概率值。
octomap自定义的基本数据类型,API文档解释如下:
This class represents a three-dimensional vector.
The three-dimensional vector can be used to represent a translation in three-dimensional space or to represent the attitude of an object using Euler angle.
也就是说,这个数据结构给我们提供了octomap命名空间下DIY的三维向量表达,它可以表达三维空间中的一个平移向量,也可以表达以欧
拉角形式表示的三维物体。另外,这里需要注意到octomath::Vector3的提供了多个与向量运算相关的函数,例如相对于坐标系的变换
实现函数、向量归一化函数、向量点积函数等。
typedef octomath::Vector3 octomap::point3d
显然,point3d是Octomap自定义的类型别名,原始的数据结构还得查看octomath::Vector3
API文档解释如下:
Nodes to be used in OcTree. They represent 3d occupancy grid cells. "value" stores their log-odds occupancy.
octomap自定义的基本数据类型,API文档解释如下:
OcTreeKey is a container class for internal key addressing.The keys count the number of cells (voxels) from the origin as discrete address of a voxel.
也就是说,这个数据结构是一个关键字,它可以实现对八叉树节点OcTreeNode的关键字查询(也就是下面我们即将提到的无序关联容器unordered_set,C++11标准)。
在2D激光SLAM中,我们通常可以生成一张二维栅格地图,依据地图分辨率将一个二维空间点映射成一个二维栅格点,利用Occupancy grid mapping with known poses实现二维栅格地图的概率更新。Octomap其实就是从二维空间扩展到三维空间中了,使用的是八叉树数据结构(回顾下Octomap是什么:采用八叉树数据结构存储三维环境的概率占据地图),OcTreeKey关键字查询的就是离散的三维体元所处的空间地址,OcTreeKey定义的成员变量也说明了这一点。
key_type k [3]
其中key_type是一个类型别名
typedef uint16_t octomap::key_type
typedef unordered_ns::unordered_set octomap::KeySet
API文档解释如下:
Data structure to efficiently compute the nodes to update from a scan insertion using a hash set.
显然,KeySet是Octomap自定义的类型别名!它使用一个无序关联容器(std::unordered_set/boost::unordered_set)来存储一组
关键字集合,由哈希函数来组织。哈希函数也称散列函数,直观上,它是一个映射函数f,实现的功能为:内存中记
录的存储位置=f(关键字),关于哈希函数的更多介绍,可以自行百度。在Octomap中,关键字对应的哈希值由
KeyHash结构体实现。
// OcTreeKey::KeyHash: Provides a hash function on Keys
struct KeyHash
{
size_t operator()(const OcTreeKey& key) const{
// a simple hashing function
// explicit casts to size_t to operate on the complete range
// constanst will be promoted according to C++ standard
return size_t(key.k[0]) + 1447*size_t(key.k[1]) + 345637*size_t(key.k[2]);
}
};
typedef std::vector< OcTreeKey >::const_iterator const_iterator
typedef std::vector< OcTreeKey >::iterator iterator
显然,KeyRay是octomap自定义的类型别名。
实际使用时,KeyRay用于保存单条光束在三维空间中raytracing的结果,KeySet收纳所有光束(也即点云数据)raytracing的结果。
typedef unordered_ns::unordered_map octomap::KeyBoolMap
KeyBoolMap::const_iterator octomap::OccupancyOcTreeBase< NODE >::changedKeysBegin( ) const
KeyBoolMap::const_iterator octomap::OccupancyOcTreeBase< NODE >::changedKeysEnd( )const
API文档解释如下:
Data structrure to efficiently track changed nodes as a combination of OcTreeKeys and a bool flag (to denote newly created nodes)
KeyBoolMap是一个无序关联容器,实现高效的关键字-值的查询。它同时定义了两个迭代器,方便对“changed nodes”
遍历。这个数据结构的设计具有一定的应用背景,这里先不做过多介绍。
官网的解释如下:
A collection of 3D coordinates (point3d), which are regarded as endpoints of a 3D laser scan.
在数据结构的设计上,它包括了两个受保护的成员变量
pose6d current_inv_transform
point3d_collection points
其中
typedef std::vector octomap::point3d_collection
这里,points变量又是一个自定义的类型别名,可以看出octomap::PointCloud使用了vector容器保存点云数据。于是,很自然的在程序中需要从PCL点云数据中提取Octomap点云数据结构时,可以使用push_back压入数据。另外,octomap点云类同时也定义了两个类型别名,方便我们使用迭代器访问vector中的点云数据
typedef point3d_collection::const_iterator octomap::Pointcloud::const_iterator
typedef point3d_collection::iterator octomap::Pointcloud::iterator
在成员函数的实现上,我们使用到了getPoint函数和rotate函数
point3d octomap::Pointcloud::getPoint(unsigned int i) const
void octomap::Pointcloud::rotate (double roll,double pitch,double yaw)
到这里,我们已经在上面将Octomap最基本的数据类型全部列出并加以解释,却唯独还没有解释八叉树类型OcTree的数据结构。其实,可以认为OcTree八叉树类是建立在以上所有数据结构基础上的顶层类,并不是指其它所有类型都派生于它,而是它提供了操作以上所有数据结构的的方法(函数),也就是核心的函数都由八叉树类型OcTree中提供。
通过阅读octomap的github源码,发现核心代码的实现在octomap::OccupancyOcTreeBase、octomap::OcTreeBase两个类中。
其中insertPointCloud函数由OccupancyOcTreeBase类实现。在目前的PC机上,github提供的源码实现是通过OpenMP并行计算完成的。当然,Octomap建图还经常使用在无人机上,受限于计算设备,机载处理器无法像在PC机上实现并行计算,因此ETH ASL实验室的工程人员专门为此开发了适合于无人机应用的Octomap建图程序包volumetric_mapping。
从实现流程上来看,空间点云raycasting到OcTree的过程大致分为三步
bool octomap::OcTreeBaseImpl< OcTreeNode , AbstractOccupancyOcTree >::computeRayKeys(const point3d & origin,const point3d & end,KeyRay & ray ) const
// Traces a ray from origin to end (excluding), returning an OcTreeKey of all nodes traversed by the beam.
virtual OcTreeNode * octomap::OccupancyOcTreeBase< OcTreeNode >::updateNode (const OcTreeKey & key,float log_odds_update,bool lazy_eval = false )
// Manipulate log_odds value of a voxel by changing it by log_odds_update (relative).
从工程实现来看,空间点云八叉树的实现基于C++11标准使用了大量C++ Primer中的数据结构,包括基本的顺序容器如Vector以及关联容器map、set(确切说是无序关联容器unordered_map和unordered_set),对C++ Primer掌握程度要求比较高。
Octomap为我们提供了节点查询函数search、iterator以及光线追迹查询函数castRay,Octomap原作给出的原话:individual octree nodes can be accessed by searching for their coordinate. For efficient batch queries, our implementation provides iterators to traverse the octree analogous to a standard C++ container class. With these iterators, all nodes,leaf nodes, or leaf nodes in a certain bounding box can be queried or they can be filtered according to further criteria.
Ray intersection queries, i.e., casting a ray from an origin into a given direction until it hits an occupied volume,
are an important use-case for a 3D map in robotics. This kind of query is used for visibility checks or to localize with range sensors. Thus, we provide this functionality in the castRay(·) method.
virtual bool octomap::OccupancyOcTreeBase< OcTreeNode >::castRay(const point3d & origin,
const point3d & direction,
point3d & end,
bool ignoreUnknownCells = false,
double maxRange = -1.0
)const
最后,以volumetric_mapping为例,给出串行计算模式下insertPointCloud函数的实现过程代码解析(代码略有改动!)
// T_G_sensor:4*4的齐次变换矩阵,存储相机相对于世界坐标系的实时位姿态
// cloud :PCL点云数据
void NbvPlannerROS::insertPointcloudIntoMapImpl(const Eigen::Matrix4f& T_G_sensor,const pcl::PointCloud::Ptr& cloud)
{
// Remove NaN values, if any.
std::vector indices;
pcl::removeNaNFromPointCloud(*cloud, *cloud, indices);
pcl::transformPointCloud(*cloud, *cloud, T_G_sensor);
const octomap::point3d p_G_sensor = octomap::point3d( T_G_sensor (0,3), T_G_sensor (1,3), T_G_sensor (2,3));
// 关联容器也可以认为是“关联数组”,只是它相比数组可以使用关键字来查询相应的值。
// 关于关联容器的讲述,见C++ Primer 374页和376页最重要!!! 关于关联容器与顺序容器的区别,请见C++ Primer 381页和388页,注意到find函数和insert函数都是关联容器所独有的!!!关联容器并不具备与位置操作相关的push_back等函数
// 我们这里的是无序关联容器,除了哈希管理操作之外,无序容器提供了与有序容器相同的操作,例如insert、find等
octomap::KeySet free_cells, occupied_cells; // 使用哈希函数组织的set API文档对该数据结构的定义:Data structure to efficiently compute the nodes to update from a scan insertion using a hash set.
int i = 0;
for (pcl::PointCloud::const_iterator it = cloud->begin();it != cloud->end(); ++it)
{
const octomap::point3d p_G_point(it->x, it->y, it->z);
// 区别:首先检查endpoint是否已经是occupied_cells的关键字成员了
octomap::OcTreeKey key = m_octomap.coordToKey(p_G_point);
if (occupied_cells.find(key) == occupied_cells.end()) // 如果给定关键字存在于set中则find返回的迭代器指向该关键字,否则返回尾后迭代器。find是关联容器特有的函数
{
castRay(p_G_sensor, p_G_point, &free_cells, &occupied_cells);
i++;
}
}
cout<<"if find, time is: "<::const_iterator it = cloud->begin(); it != cloud->end(); ++it)
{
m_octomap.integrateNodeColor( it->x, it->y, it->z, it->r, it->g, it->b );
}
m_octomap.updateInnerOccupancy();
}
void NbvPlannerROS::castRay(const octomap::point3d& sensor_origin,
const octomap::point3d& point,
octomap::KeySet* free_cells,
octomap::KeySet* occupied_cells) const {
// CHECK_NOTNULL(free_cells);
// CHECK_NOTNULL(occupied_cells);
if ( (point - sensor_origin).norm() <= 4.0 )
{
octomap::KeyRay key_ray;
if (m_octomap.computeRayKeys(sensor_origin, point, key_ray))
{
free_cells->insert(key_ray.begin(), key_ray.end()); // key_ray.begin(), key_ray.end()体现的是顺序容器迭代器的基本用法;
}
octomap::OcTreeKey key;
if (m_octomap.coordToKeyChecked(point, key))
{
occupied_cells->insert(key); //同样还是insert操作,只不过是insert操作的不同实现方式,见C++ Primer 384页
}
}
else
{
octomap::point3d new_end = sensor_origin + (point - sensor_origin).normalized() * 4.0;
octomap::KeyRay key_ray;
if (m_octomap.computeRayKeys(sensor_origin, new_end, key_ray))
{
free_cells->insert(key_ray.begin(), key_ray.end());
}
}
}
void NbvPlannerROS::updateOccupancy(octomap::KeySet* free_cells, octomap::KeySet* occupied_cells)
{
// CHECK_NOTNULL(free_cells);
// CHECK_NOTNULL(occupied_cells);
// Mark occupied cells.
for (octomap::KeySet::iterator it = occupied_cells->begin(),end = occupied_cells->end();it != end; it++) // (无序)关联容器迭代器的使用,见C++ Primer 382页
{
m_octomap.updateNode(*it, true);
// Remove any occupied cells from free cells - assume there are far fewer
// occupied cells than free cells, so this is much faster than checking on
// every free cell.
if (free_cells->find(*it) != free_cells->end()) // find操作,它是(无序)关联容器所特有的函数,访问容器中的指定元素。如果给定关键字存在于set中则find返回的迭代器指向该关键字,否则返回尾后迭代器
{
free_cells->erase(*it); // erase操作,删除容器中的指定元素。
}
}
// Mark free cells.
for (octomap::KeySet::iterator it = free_cells->begin(),end = free_cells->end();it != end; ++it)
{
m_octomap.updateNode(*it, false);
}
// m_octomap.updateInnerOccupancy();
}