1. 问题引出
凸包是计算几何的一个概念,所谓的凸包问题就是:给定点集,求构成凸包的点.所谓凸,用数学上的描述是,凸集中的任意两点连线,该线上的任意点均在凸集内,与之相对就是凹包.
凸集
凹集
目前的算法有:
- 穷举法:
- 思路:逐条线检测是否所有其它边都在它的一侧
- 时间复杂度:O(n^3)
- 分治法:
- 思路:将大问题分成结构相同的小问题,再用递归的方法解决.简单来说,对集合求距离最远的点,连接该点后将集合分上下包,再将子集重复上面步骤,参考[1];
- 时间复杂度:O(nLogn)
- Jarvis步进法:
- 思路:先找到几何某一个方向的最小点(例如纵坐标),然后逆时针方向找下一个顶点,判断是否为顶点的依据是前后两点组成的边的夹角;
- 时间复杂度:O(nH)
- Graham算法:
- 思路:与Jarvis步进法类似,不同点事不直接使用夹角,而是两边的叉积;
- 时间复杂度:O(nLogn)
下文介绍以2维的Graham算法为例介绍凸包检测.
2. Graham算法(2维)
2.1 算法流程
步骤:
- 从集合中找到纵坐标最小的点,设为P0
- 计算其它所有点与P0组成的向量的模长和角度,利用模长和角度对点集排序,原则如下:
- 向量与x轴(0,1)的夹角 α \alpha α 越小则越靠前
- 当夹角 α \alpha α 相等时,模长越短越靠前
- 将P0和排序后的第一个点放入栈中
- while(取出当前点Pi != end):
- while(将栈顶[top]和栈顶下一个点[top-1]连线L1,Pi和[top]连线L2,通过L1和L2叉积判断L2是否满足逆时针的关系,如果不满足):
栈顶出栈; - 将当前点放入栈.
- while(将栈顶[top]和栈顶下一个点[top-1]连线L1,Pi和[top]连线L2,通过L1和L2叉积判断L2是否满足逆时针的关系,如果不满足):
分析:为什么排序?
我认为:由于我们是按照逆时针的顺序使用边进行逐一"包裹",那么最后出来的顶点一定也同样沿着逆时针顺序出现,既然如此,事先对点集排序,可以有效避免盲目地进行顶点搜索.
2.2 C++实现
#define EPS 1e-15
typedef std::vector<Eigen::Vector2f> VecPoint;
float Cross(const Eigen::Vector2f& p1, const Eigen::Vector2f& p2)
{
return (p1.x()*p2.y() - p1.y()*p2.x());
}
bool Compare(Eigen::Vector2f& p1, Eigen::Vector2f& p2)
{
float tmp = Cross(p1, p2);
if(tmp > EPS)
return true;
else if(tmp < -EPS)
return false;
else {
if(p1.norm() < p2.norm())
return true;
else
return false;
}
}
void GrahamSlove(VecPoint vPoints, VecPoint& vTops, VecPoint& vInliers)
{
if(vPoints.size() <= 3)
return;
// 1. 找到最左下角的点
size_t first = 0;
for(size_t i = 1; i < vPoints.size(); i++) {
float tmp = vPoints[i].y() - vPoints[first].y();
if(tmp < -EPS) {
first = i;
} else if(fabs(tmp) < EPS && vPoints[i].x() < vPoints[first].x()) {
if(vPoints[i].x() < vPoints[first].x())
first = i;
}
}
vPoints[0].swap(vPoints[first]);
Eigen::Vector2f origin = vPoints[0];
// 2. 排序
for(auto& p: vPoints) {
p -= origin;
}
std::sort(vPoints.begin()+1, vPoints.end(), Compare);
// 3. 如符合要求入栈
std::vector<size_t> stack(vPoints.size(), 0);
stack[0] = 0;
stack[1] = 1;
size_t top = 2;
for(size_t i = 2; i < vPoints.size(); i++) {
while((top > 1) &&
(Cross(vPoints[stack[top-1]] - vPoints[stack[top-2]], vPoints[i]-vPoints[stack[top-2]]) < 0.) ) {
top--;
}
stack[top++] = i;
}
vTops.resize(top);
vInliers.resize(vPoints.size() - top);
for(size_t i = 0, j = 0, k = 0; i < vPoints.size(); i++) {
if(stack[j] == i) {
vTops[j++] = vPoints[i] + origin;
} else {
vInliers[k++] = vPoints[i] + origin;
}
}
}
参考文档
- [1] pcl库中的CropHull滤波和凸包算法
- [2] 凸包之graham算法
- [3] 凸包算法(Graham扫描法)