点集的凸包算法

问题:在二维平面上,给定一些带有坐标位置的点,如何获取包含所有这些点的最小凸边形?

该问题就是在二维平面上求凸包(Convex Hull)。

如下图,图中给定了一些点,外围连线所围成的凸边形刚好将所有给定的点包围起来。

如果我们将这些点和凸边形放到一个二维平面坐标系中观察。

我们可以发现,在这些点中,沿着横坐标方向上,x坐标值最大和最小的点一定在凸包上,同样的,沿着纵坐标方向上,y坐标值最大和最小的点也一定在凸包上。因为这本身就是凸包所定义的,凸包是最外围的一圈点,所以最外围的点必然是各个方向上的极值。

接着,从A点上观察,所给定集合的点都在AD边的一侧,也都在AB边一侧。同样的,凸包上的点都满足这个要求。这个特点,我们可以怎么利用呢?

我们先通过角度来利用。

以A点为例,边AB与x轴正方向所形成的角是集合中所有点与A所形成的边与x轴正方向所形成的的所有角中最小的,边AE与x轴正方向所形成的角是集合中所有点与A所形成的边与x轴正方向所形成的的所有角中最大的。

通俗一点描述,从A点出发,沿着平行与x轴的方向画一条射线,该射线围着点A逆时针转动,那么点B就是该条射线第一个碰到的点,点E是这条射线最后一个碰到的点。如下图:

扫描二维码关注公众号,回复: 14909542 查看本文章

 再以B点为例,如果从B点出发,沿着边AB方向画一条射线,该射线围绕着点B逆时针转动,那么点C就是该条射线第一个碰到的点(角最小),点A是这条射线最后一个碰到的点(角最大)。如下图:

依次分析凸包上其它的点,都满足该特性。

我们利用这个特点来求得凸包。步骤:

1)第一步,找到最左下角的点(即y轴坐标值最小且x轴坐标值最小的点)P0,该点一定是凸包上的一个点;

2)点集合中其它点与点P0连成边,计算这些边与x轴正方向所形成的夹角,取其中夹角最小的点,作为凸包上的下一个点P1;

3)接着以点P0、点P1形成向量\overrightarrow{P0P1},点集合中除点P0、点P1外其它所有点与点P1组成向量,并计算这些向量与向量\overrightarrow{P0P1}的夹角,取其中夹角最小的点,作为凸包上的下一个点P2;

4)依次类推,重复执行第3步骤中的操作,每次取其中夹角最小的点,作为凸包的下一个点。直到一个点求得的下一个凸包点等于点P0,回到起点,则停止计算。

流程图如下:

该方法其实就是 Jarvis步进法的基本思路。

 那么如何求两个向量之间的夹角呢?

实际上,我们这里并不需要去求出真正的夹角,我们只需要获取夹角最小的点,那么也就是说是一个相对比较的结果。先看下图:

向量\overrightarrow{OP1}与x轴正方向形成的夹角\alpha,向量 \overrightarrow{OP2}与x轴正方向形成的夹角\beta,向量\overrightarrow{OP3}与x轴正方向形成的夹角\theta\angle \alpha<\angle \beta<\angle \theta.

从线段OP1的角度来看,\angle \alpha<\angle \beta<\angle \theta是因为点P2与点P3都在线段OP1的左上方。

转换成向量的角度,从向量\overrightarrow{OP1}的角度来看,则是向量\overrightarrow{OP1}\times\overrightarrow{OP2}的值>0,\overrightarrow{OP1}\times\overrightarrow{OP3}的值>0,且\overrightarrow{OP2}\times\overrightarrow{OP3}的值>0.

我们再回到一开始的图

 当我们在B点开始计算下一个凸包节点的时候,点集合中其它点与B点组成的向量叉乘向量\overrightarrow{BC}的结果都是<0的,反过来就是说向量\overrightarrow{BC}叉乘其它向量都是>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;
}

猜你喜欢

转载自blog.csdn.net/m0_74178120/article/details/128753677
今日推荐