问题:在二维平面上,给定一些带有坐标位置的点,如何获取包含所有这些点的最小凸边形?
该问题就是在二维平面上求凸包(Convex Hull)。
如下图,图中给定了一些点,外围连线所围成的凸边形刚好将所有给定的点包围起来。
如果我们将这些点和凸边形放到一个二维平面坐标系中观察。
我们可以发现,在这些点中,沿着横坐标方向上,x坐标值最大和最小的点一定在凸包上,同样的,沿着纵坐标方向上,y坐标值最大和最小的点也一定在凸包上。因为这本身就是凸包所定义的,凸包是最外围的一圈点,所以最外围的点必然是各个方向上的极值。
接着,从A点上观察,所给定集合的点都在AD边的一侧,也都在AB边一侧。同样的,凸包上的点都满足这个要求。这个特点,我们可以怎么利用呢?
我们先通过角度来利用。
以A点为例,边AB与x轴正方向所形成的角是集合中所有点与A所形成的边与x轴正方向所形成的的所有角中最小的,边AE与x轴正方向所形成的角是集合中所有点与A所形成的边与x轴正方向所形成的的所有角中最大的。
通俗一点描述,从A点出发,沿着平行与x轴的方向画一条射线,该射线围着点A逆时针转动,那么点B就是该条射线第一个碰到的点,点E是这条射线最后一个碰到的点。如下图:
再以B点为例,如果从B点出发,沿着边AB方向画一条射线,该射线围绕着点B逆时针转动,那么点C就是该条射线第一个碰到的点(角最小),点A是这条射线最后一个碰到的点(角最大)。如下图:
依次分析凸包上其它的点,都满足该特性。
我们利用这个特点来求得凸包。步骤:
1)第一步,找到最左下角的点(即y轴坐标值最小且x轴坐标值最小的点)P0,该点一定是凸包上的一个点;
2)点集合中其它点与点P0连成边,计算这些边与x轴正方向所形成的夹角,取其中夹角最小的点,作为凸包上的下一个点P1;
3)接着以点P0、点P1形成向量,点集合中除点P0、点P1外其它所有点与点P1组成向量,并计算这些向量与向量的夹角,取其中夹角最小的点,作为凸包上的下一个点P2;
4)依次类推,重复执行第3步骤中的操作,每次取其中夹角最小的点,作为凸包的下一个点。直到一个点求得的下一个凸包点等于点P0,回到起点,则停止计算。
流程图如下:
该方法其实就是 Jarvis步进法的基本思路。
那么如何求两个向量之间的夹角呢?
实际上,我们这里并不需要去求出真正的夹角,我们只需要获取夹角最小的点,那么也就是说是一个相对比较的结果。先看下图:
向量与x轴正方向形成的夹角,向量 与x轴正方向形成的夹角,向量与x轴正方向形成的夹角,<<.
从线段OP1的角度来看,<<是因为点P2与点P3都在线段OP1的左上方。
转换成向量的角度,从向量的角度来看,则是向量的值>0,的值>0,且的值>0.
我们再回到一开始的图
当我们在B点开始计算下一个凸包节点的时候,点集合中其它点与B点组成的向量叉乘向量的结果都是<0的,反过来就是说向量叉乘其它向量都是>0的。
转换成代码的执行流程
1)开始以B点作为向量尾,点集合中其它任一点为凸包下一节点,并作为向量头,组成向量BP(next);
2)然后遍历剩余点集合中的其它点,和B点组成向量BP(i)
向量BP(next) 叉乘 向量BP(i)
3)如果叉乘结果>0时,不做处理;
如果叉乘结果=0时,说明点P(next)和点P(i)共线,则取两点中与点B距离远的点,替换一开始选中的点;
如果叉乘结果<0时,将点P(i)替换点P(next),作为凸包的下一个节点。
至此,我们有了一个点的执行流程之后,我么就可以推广到全部点的全局流程上来。
我们再重新整理一下整个流程图,如下:
关键代码如下:
1)共线情况找出距离远的点
#define SEGMENTLEN(x0,y0,x1,y1) (sqrt(pow(((x1)-(x0)), 2.0) + pow(((y1)-(y0)), 2.0)))
2)判断点的位置
qreal Convex::comparePointClock(const QPointF &point_0, const QPointF &point_c, const QPointF &point_i)
{
return ((point_i.x() - point_0.x())*(point_c.y() - point_0.y()) - (point_i.y() - point_0.y())*(point_c.x() - point_0.x()));
}
3)获取最小坐标
QPointF Convex::getMinimumPoint(const QVector<QPointF> &vecPoints)
{
if (vecPoints.isEmpty())
return QPointF();
QPointF minPoint = vecPoints.at(0);
quint16 point_x = vecPoints.at(0).x(), point_y = vecPoints.at(0).y();
for (QVector<QPointF>::const_iterator it = vecPoints.constBegin(); it != vecPoints.constEnd(); it++)
{
//比较Y坐标,找Y坐标最小的
if (it->y() < minPoint.y())
{
minPoint = (*it);
}
else
{
//Y坐标相同,找X坐标小的
if (it->y() == minPoint.y() && it->x() < minPoint.x())
{
minPoint = (*it);
}
}
}
return minPoint;
}
4)核心处理业务
//Jarvis's march 算法,O(nH),H为点的个数。
qint8 Convex::getConvexHullJarvis(const QVector<QPointF> &vecSourPoints, QVector<QPointF> &vecTarPoints)
{
if (vecSourPoints.isEmpty())
return -1;
QPointF minPoint;
QPointF lowPoint, point_0, point_i, point_c;
qreal count = 0,z = 0;
qreal length_1, length_2;
QVector<QPointF> tempVecPoint(vecSourPoints);
vecTarPoints.clear();
//删除重复坐标
if (removeRepeatPoints(tempVecPoint) <= 0)
return -1;
//查找最小坐标
minPoint = getMinimumPoint(tempVecPoint);
lowPoint = minPoint;
point_0 = lowPoint;
do {
//起始点point_0压入凸包点集中
vecTarPoints.push_back(point_0);
count = 0;
for (QVector<QPointF>::iterator it = tempVecPoint.begin(); it != tempVecPoint.end(); it++)
{
//跳过起始坐标
if ((*it) == point_0)
continue;
count++;
if (count == 1) //把第一个遍历的点作为point_c
{
point_c = (*it);
continue;
}
//如果z>0则point在point_i和point_c连线的下方,z<0则point_i在连线的上方,z=0则point_i共线
z = comparePointClock(point_0,point_c,(*it));//((it->x() - point_0.x())*(point_c.y() - point_0.y()) - (it->y() - point_0.y())*(point_c.x() - point_0.x()));
if (z > 0)
{
point_c = (*it);
}
else if (z == 0)
{
//共线情况找出距离point_0较远的那个点作为point_c
length_1 = SEGMENTLEN(point_0.x(),point_0.y(),it->x(),it->y());
length_2 = SEGMENTLEN(point_0.x(), point_0.y(), point_c.x(), point_c.y());
if (length_1 > length_2)
{
point_c = (*it);
}
}
}
point_0 = point_c;
} while (point_0 != lowPoint);
vecTarPoints.push_back(lowPoint);
if (vecTarPoints.isEmpty())
return -1;
return 0;
}