文中的点云文件和代码在这里:https://github.com/robinvista/pointcloud-OBB
为什么要计算点云的包围盒,它有什么用?对于无人驾驶和机器人来说,包围盒主要用来做碰撞检测和显示。
最简单的包围盒叫Axis-Aligned Bounding Box (AABB),它的边与坐标轴平行,所以与实际物体相差较大,不是太好用。稍微复杂点的是Orientation Bounding Box(OBB),它与AABB的区别就是考虑了包围盒的方向,不一定与坐标轴平行,可以旋转。本文就是计算OBB的。计算OBB最难的是怎么确定box的方向。计算box的中心很简单,求所有点的平均值或者最大最小点的中间值即可。但是方向却需要计算特征向量。不用担心,现有的库或者软件都提供这样的函数,直接调用即可。以Eigen库为例,它在ROS、百度无人驾驶中都被使用,它里面的函数特点是:
Eigen中的eigenvalues
函数返回的特征值是从小到大排列的,如下:
The eigenvalues are sorted in increasing order.
Mathematica中的Eigenvalues
函数返回的特征值是从大到小排列的,如下。对于数值矩阵,Eigenvectors
函数返回的特征向量是单位向量。
特征值依绝对值递减排列
MATLAB中的eig
函数返回的特征值顺序是未排序的,需要用户自己排序,如下:
默认情况下,eig 并不总是返回已排序的特征值和特征向量。可以使用 sort 函数将特征值按升序排序,并重新排序相应的特征向量。
所需的代码如下:
#include
#include
#include
#include
#include
#include
#include
typedef pcl::PointXYZI PointType;
int main(int argc, char* argv[])
{
// 1. 读入点云文件
pcl::PointCloud::Ptr cloud(new pcl::PointCloud());
pcl::PLYReader reader;
reader.read("pointcloud.ply", *cloud);
//2. 开始欧式聚类
pcl::search::KdTree::Ptr tree(new pcl::search::KdTree);
tree->setInputCloud(cloud);
std::vector local_indices;
pcl::EuclideanClusterExtraction euclid;
euclid.setInputCloud(cloud);
float in_max_cluster_distance = 0.5;
float MIN_CLUSTER_SIZE = 5;
float MAX_CLUSTER_SIZE = 500000;
euclid.setClusterTolerance(in_max_cluster_distance);
euclid.setMinClusterSize(MIN_CLUSTER_SIZE);
euclid.setMaxClusterSize(MAX_CLUSTER_SIZE);
euclid.setSearchMethod(tree);
euclid.extract(local_indices);
//3. 取出第i个聚类,用户输入i的值
std::cout << "Number of clusters = " << local_indices.size() << endl;
size_t i = 0;
pcl::PointCloud::Ptr cloud_cluster(new pcl::PointCloud());
PointType point;
for (auto pit = local_indices[i].indices.begin(); pit != local_indices[i].indices.end(); ++pit)
{
point.x = cloud->points[*pit].x;
point.y = cloud->points[*pit].y;
point.z = cloud->points[*pit].z;
cloud_cluster->points.push_back(point);
}
//5. 计算点云的几何中心和重心,注意这两个概念不一样,重心是所有点的平均值,中心是最大最小点的平均值
Eigen::Vector4f centroid; // 重心
pcl::compute3DCentroid(*cloud_cluster, centroid);
PointType min_pt, max_pt;
Eigen::Vector3f center; // 中心
pcl::getMinMax3D(*cloud_cluster, min_pt, max_pt);
center = (max_pt.getVector3fMap() + min_pt.getVector3fMap())/2;
// 6. 计算协方差矩阵的特征向量
Eigen::Matrix3f covariance;
pcl::computeCovarianceMatrixNormalized(*cloud_cluster, centroid, covariance); // 这里必须用重心
Eigen::SelfAdjointEigenSolver eigen_solver(covariance, Eigen::ComputeEigenvectors);
Eigen::Matrix3f eigenVectorsPCA = eigen_solver.eigenvectors();
Eigen::Vector3f eigenValuesPCA = eigen_solver.eigenvalues();
eigenVectorsPCA.col(2) = eigenVectorsPCA.col(0).cross(eigenVectorsPCA.col(1)); //正交化
eigenVectorsPCA.col(0) = eigenVectorsPCA.col(1).cross(eigenVectorsPCA.col(2));
eigenVectorsPCA.col(1) = eigenVectorsPCA.col(2).cross(eigenVectorsPCA.col(0));
// 7. 计算变换矩阵,只考虑绕全局坐标系Z轴的转动
Eigen::Vector3f ea = (eigenVectorsPCA).eulerAngles(2, 1, 0);
Eigen::AngleAxisf keep_Z_Rot(ea[0], Eigen::Vector3f::UnitZ());
Eigen::Affine3f transform = Eigen::Affine3f::Identity();
transform.translate(center); // translate与rotate书写的顺序不可搞反
transform.rotate(keep_Z_Rot); // radians
// 8. 计算包围盒的尺寸
pcl::PointCloud::Ptr transformedCloud(new pcl::PointCloud);
pcl::transformPointCloud(*cloud_cluster, *transformedCloud, transform.inverse());
// 重新计算变换后的点云的中心,因为变换前的点云的中心不是其包围盒的几何中心
PointType min_pt_T, max_pt_T;
pcl::getMinMax3D(*transformedCloud, min_pt_T, max_pt_T);
Eigen::Vector3f center_new = (max_pt_T.getVector3fMap() + min_pt_T.getVector3fMap()) / 2;
Eigen::Vector3f box_dim;
box_dim = max_pt_T.getVector3fMap() - min_pt_T.getVector3fMap();
Eigen::Affine3f transform2 = Eigen::Affine3f::Identity();
transform2.translate(center_new);
Eigen::Affine3f transform3 = transform * transform2;
//9. 显示
pcl::visualization::PCLVisualizer viewer;
pcl::visualization::PointCloudColorHandlerCustom handler_1(cloud_cluster, 0, 255, 0); //转换到原点的点云
pcl::visualization::PointCloudColorHandlerCustom handler_2(transformedCloud, 255, 0, 0); //输入的初始点云
viewer.addPointCloud(cloud_cluster, handler_1, "cloud1");
viewer.addPointCloud(transformedCloud, handler_2, "cloud2");
viewer.setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 2, "cloud1");
viewer.setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 3, "cloud2");
const Eigen::Quaternionf bboxQ(keep_Z_Rot);
const Eigen::Vector3f bboxT(transform3.translation()); // 这里用新的"中心",因为要显示的包围盒采用几何中心作为参考点
viewer.addCube(bboxT, bboxQ, box_dim(0), box_dim(1), box_dim(2), "bbox");
viewer.setShapeRenderingProperties(pcl::visualization::PCL_VISUALIZER_REPRESENTATION, pcl::visualization::PCL_VISUALIZER_REPRESENTATION_WIREFRAME, "bbox");
viewer.setShapeRenderingProperties(pcl::visualization::PCL_VISUALIZER_COLOR, 1.0, 0.0, 0.0, "bbox");
viewer.addCoordinateSystem(2.0);
viewer.setBackgroundColor(1, 1, 1);
viewer.spin();
return 0;
}
PCLVisualizer中的addCube
函数的输入参数如下图,其中位姿都是用的Eigen的类型,尺寸的顺序就是分别沿XYZ轴(从上往下看,X轴是宽度,Y轴是高度,Z轴是深度)。
其它类似的计算OBB的项目有:
https://github.com/LidarPerception/object_builders_lib
但是,OBB计算出来的包围盒并不完美,我们知道包围盒并没有贴合物体的边缘。要对角度矫正需要知道边缘的方向,这个可以用RANSAC估计出来,但是有些慢。