我在上一篇博客中(详见:计算几何问题汇总–点与线的位置关系)谈到了计算几何最基本的问题:解决点与线(线段or直线)的位置关系判断。那么,更进一步,还需要探讨复杂一点的情况:比如面与线,面与面之间的关系。本文中,我就先说说最常见的两种几何图形:圆与矩形。我将就矩形与圆的碰撞判断问题、线与矩形、线与圆之间的碰撞问题作出分析,以及给出这些解决问题的算法、代码。
在此之前,我默认所有对本文内容感兴趣的读者,都了解基本的计算几何概念,并且对我的上一篇博客:计算几何问题汇总–点与线的位置关系 所讲的内容基本熟悉。因为本文涉及的算法,几乎全部都用到了上一篇博客中所定义的类和函数。
简单将上一篇博客的内容陈列如下,方便读者阅读后面本文的算法:
模块名:PointLine
class Point: 点类,两个属性,分别是横纵坐标x, y
class Segment: 线段类,两个属性,分别是两个端点p1, p2
class Line: 直线类,两个属性,分别是直线上任意两点p1, p2
class Vector: 向量类,以向量的起点和终点为参数,建立一个由横纵坐标为两个属性的类
func: pointDistance(p1, p2): 计算两点之间距离的函数。返回一个浮点型的值
func: pointToLine(C, AB): 计算点C到直线AB的距离。返回一个浮点型的值
func: pointInLine(C, AB): 判断点C是否在直线AB上。在,返回True;不在,返回False
func: pointInSegment(C, AB): 判断点C是否在线段AB上。在,返回True;不在,返回False
func: linesAreParallel(l1, l2): 判断两条直线l1, l2是否平行。平行,返回True;不平行,返回False
func: crossProduct(v1, v2): 计算两个向量叉积
func: segmentsIntersect(s1, s2): 判断两个线段是否相交
func: segmentIntersectsLine(segment, line): 判断一条线段和一条直线是否相交
好了,以上是我在上一篇博客中定义的类和函数,我把他们的基本功能,参数设置等等简要列在了上面,以便理解本文后面将要给出的代码。至于具体的算法及分析,请翻看我的上一篇博客。
需要注意的是,我在本文中,把上一篇博客的工作全部封装成了一个模块:PointLine.py,方便本文的代码调用。
接下来,就进入本文的主题吧。
圆
1. 圆与点的位置关系
简单来分,二维空间内,圆与点的位置关系,只有两种:
(1)点在圆上(包括在圆周上以及在圆内);
(2)点在圆外
而判断的方法很直接,计算点到圆心的距离,再用这个距离和圆的半径比较即可。
先定义一个圆类:两个属性,圆心及半径。
class Circle(object): """Circle has 2 attributes: center and radius""" def __init__(self, center, radius): self.center = center self.radius = radius
然后写出判断点与圆关系的完整代码如下:
from PointLine import * class Circle(object): """Circle has 2 attributes: center and radius""" def __init__(self, center, radius): self.center = center self.radius = radius def pointInCircle(point, circle): """determine whether a point in a circle""" return (pointDistance(point, circle.center) - circle.radius) < 1e-9
其中,pointDistance() 函数在模块 PointLine 已经存在。
最后的距离判断还是要满足浮点值比较的原则,上一篇博客中详细说过,不再赘述。
2. 圆与线的位置关系
(1) 圆与直线的位置关系
简单划分,还是两种:相交(包括相切);不相交
判断的依据是根据圆心到直线的距离与圆半径相比较。判别函数如下:
def lineIntersectsCircle(line, circle): """determine whether a line intersects a circle""" dis = pointToLine(circle.center, line) return dis - circle.radius < 1e-9
其中,pointToLine() 在模块PointLine中已经存在。
(2) 圆与线段的位置关系
线段由于其位置上的限制,导致我们在判断线段和圆的关系的时候会麻烦一点。判断思路如下:
判断线段s所在的直线l与圆心的距离dis和圆的半径r之间的关系。若dis<=r 则进行2、3步的判断;若dis>r ,直接返回False.(也就是说肯定与圆不相交) 计算出圆心到直线l的垂线与直线l的交点(也就是垂足),记为点D. 此处的计算方法参考我在上一篇博客中计算点到直线距离时,所使用的向量法。(详见:计算几何问题汇总–点与线的位置关系) 判断点D是否在线段s上,判断方法是 PointLine 模块中的函数PointInSegment()。 这里直接调用就行。
代码如下:
def segmentIntersectsCircle(AB, circle): """determine whether a segment intersects a circle""" if not lineIntersectsCircle(Line(AB.p1, AB.p2), circle): return False if pointInCircle(AB.p1, circle) or pointInCircle(AB.p2, circle): return True vector_AB = Vector(AB.p1, AB.p2) vector_AO = Vector(AB.p1, circle.center) # two ndarray object tAB = np.array([vector_AB.x, vector_AB.y]) tAO = np.array([vector_AO.x, vector_AO.y]) # vector AD, type: ndarray tAD = ((tAB @ tAO) / (tAB @ tAB)) * tAB # get point D Dx, Dy = tAD[0] + AB.p1.x, tAD[1] + AB.p1.y D = Point(Dx, Dy) return pointInCircle(D, circle) and pointInSegment(D, AB)
运行此函数的前提是还是导入模块PointLine以及库numpy
把上面的所有函数写在一起,方便有需要的读者使用:
from PointLine import * class Circle(object): """Circle has 2 attributes: center and radius""" def __init__(self, center, radius): self.center = center self.radius = radius def pointInCircle(point, circle): """determine whether a point in a circle""" return (pointDistance(point, circle.center) - circle.radius) < 1e-9 def lineIntersectsCircle(line, circle): """determine whether a line intersects a circle""" dis = pointToLine(circle.center, line) return dis - circle.radius < 1e-9 def segmentIntersectsCircle(AB, circle): """determine whether a segment intersects a circle""" if not lineIntersectsCircle(Line(AB.p1, AB.p2), circle): return False if pointInCircle(AB.p1, circle) or pointInCircle(AB.p2, circle): return True vector_AB = Vector(AB.p1, AB.p2) vector_AO = Vector(AB.p1, circle.center) # two ndarray object tAB = np.array([vector_AB.x, vector_AB.y]) tAO = np.array([vector_AO.x, vector_AO.y]) # vector AD, type: ndarray tAD = ((tAB @ tAO) / (tAB @ tAB)) * tAB # get point D Dx, Dy = tAD[0] + AB.p1.x, tAD[1] + AB.p1.y D = Point(Dx, Dy) return pointInCircle(D, circle) and pointInSegment(D, AB)
这就可以构成一个关于圆的相关问题的计算模块了。其中,引入的模块 PointLine 详见我的上一篇博客,链接前面已经给出了。
矩形
了解了圆的问题之后,我们看看矩形。这里,我将圆与矩形的位置关系,也放在矩形中讲解。
在二维平面空间中,若要定义矩形,则至少需要知道矩形的三个顶点,如Fig.1(a)所示。若是有矩形的边一定和坐标轴平行或垂直的条件,则只需要知道两个顶点就行,如Fig.1(b)所示。为使算法具有普遍性,我们研究Fig.1(a)所示的情况:
先给出矩形类:
class Rectangle(object): """four points are defined by clockwise order from upper left corner""" def __init__(self, p1, p2, p3, p4): self.p1, self.p2, self.p3, self.p4 = p1, p2, p3, p4 def getCenter(self): return Point((self.p2.x + self.p4.x) / 2, (self.p2.y + self.p4.y) / 2) def getXline(self): rectangle_center = self.getCenter() x_center = Point((self.p2.x + self.p3.x) / 2, (self.p2.y + self.p3.y) / 2) return Line(x_center, rectangle_center) def getYline(self): rectangle_center = self.getCenter() y_center = Point((self.p1.x + self.p2.x) / 2, (self.p1.y + self.p2.y) / 2) return Line(y_center, rectangle_center)
构造函数中,用矩形的四个顶点构造了一个矩形对象,其实,三个顶点就够了,但在这里,4个还是3个影响不大。
此外,除了初始化对象的构造函数,我还给出了三个矩形类的方法:
getCenter:用来返回矩形的中心点,也就是Fig.2(a)中的点O;
getXline:用来返回经过矩形的中心点O,且平行于矩形一边的轴线,作为针对某个矩形的新的X轴。如Fig.2(a)所示;
getYline:与矩形新的X轴的构造同理,构造一个针对某个矩形的Y轴
显而易见,如果对一个矩形对象执行以上三种方法,我们就可以得到一个以矩形中心点为坐标原点,分别平行于矩形的高和宽的新的坐标系。
1. 点和矩形的位置关系
明确了这一点,再看问题。首先解决点是否在矩形中的问题(这里说的在矩形中,包括在矩形边线上的情况):
想要一个点,比如Fig.2(a)中的点P,在矩形中,那么这个点与上面所说的新建的坐标系一定要同时满足以下两个条件:
点P到新的X轴的距离小于等于矩形高度的1/2 点P到新的Y轴的距离小于等于矩形宽度的1/2
由此,可以写出代码:
from PointLine import * def pointInRectangle(point, rectangle): """determine whether a point in a rectangle""" x_line = rectangle.getXline() y_line = rectangle.getYline() d1 = pointToLine(point, y_line) - pointToLine(rectangle.p2, y_line) d2 = pointToLine(point, x_line) - pointToLine(rectangle.p2, x_line) return d1 < 1e-9 and d2 < 1e-9
函数 PointToLine() 在模块PointLine中给出了。
2. 线和矩形的位置关系
借助点和矩形位置关系的判断方法,我们可以判断线段和矩形的位置关系。若一条线段和矩形相交(包括线段和矩形的边相交以及线段在矩形内的情况),那么线段必须满足以下两个条件之一:
线段的两个端点全部在矩形内 线段和至少一条矩形的边相交
如图Fig.2(b)所示,线段和矩形相交的情况都可以被以上的两个条件涵盖。因此,根据这个思路写出代码即可。
def segmentIntersectsRectangle(s, rectangle): """determine whether a segment intersects a rectangle""" s1 = Segment(rectangle.p1, rectangle.p2) s2 = Segment(rectangle.p2, rectangle.p3) s3 = Segment(rectangle.p3, rectangle.p4) s4 = Segment(rectangle.p4, rectangle.p1) segmentsList = [s1, s2, s3, s4] if pointInRectangle(s.p1, rectangle) and pointInRectangle(s.p2, rectangle): return True for segment in segmentsList: if segmentsIntersect(segment, s): return True return False
s1?s4是矩形的四条边,都是线段。
同理,可以写出直线与矩形位置关系的判别函数。与线段不同的是,直线与矩形任意边相交
是直线与矩形相交的充要条件。给出代码,如下:
def lineIntersectsRectangle(l, rectangle): """determine whether a line intersects a rectangle""" s1 = Segment(rectangle.p1, rectangle.p2) s2 = Segment(rectangle.p2, rectangle.p3) s3 = Segment(rectangle.p3, rectangle.p4) s4 = Segment(rectangle.p4, rectangle.p1) segmentsList = [s1, s2, s3, s4] for segment in segmentsList: if segmentIntersectsLine(segment, l): return True return False
3. 圆与矩形的碰撞检测
现在,已经解决了圆和矩形的基本计算问题,那么如何确定圆跟矩形的位置关系呢,换句话说,如何设计一个检测圆与矩形是否碰撞的算法?
和判断线与矩形,点与矩形的办法一样,还是需要先根据矩形,建立新的坐标系,如图Fig.3所示,然后计算圆心到这个新坐标系X轴和Y轴的距离,根据这个距离与圆的半径、矩形边长的关系可以检测碰撞。
先想一想圆和矩形碰撞的碰撞(相交)的情况,其实一共就三种:
圆只与矩形的一条边相交,并不和矩形的顶点相交。如图Fig.3(a)所示 圆与矩形的一个或多个顶点相交,也包含了矩形完全在圆内的情况。如图Fig.3(b)所示 圆在矩形内。如图Fig.3(c)所示
现在,设dx为圆心到新坐标系X轴的距离,而dy为圆心到新坐标系Y轴的距离,w为矩形的宽度,h为矩形的高度。就像Fig.3中所标出的那样。
可以按如下思路讨论:
判断矩形的四个顶点是否在圆中,只要有一个在,那么矩形与圆碰撞。也就是上面说的情况2; 判断dx<=h2 且 dy<=r+w2是否成立,若成立,则圆一定与矩形的一条高相交,如图Fig.4(a)所示; 判断dy<=w2 且 dy<=r+h2是否成立,若成立,则圆一定与矩形的一条宽相交,如图Fig.4(b)所示; 最后需要说明的是,对于圆在矩形内的情况,2、3步就可以判断了,无需再写代码;
代码如下:
def circleIntersectsRectangle(circle, rectangle): """determine whether a circle intersects a rectangle""" if pointInCircle(rectangle.p1, circle) or pointInCircle(rectangle.p1, circle) or\ pointInCircle(rectangle.p1, circle) or pointInCircle(rectangle.p1, circle): return True x_line = rectangle.getXline() y_line = rectangle.getYline() dx, dy = pointToLine(circle.center, x_line), pointToLine(circle.center, y_line) h, w = pointDistance(rectangle.p1, rectangle.p2), pointDistance(rectangle.p2, rectangle.p3) if dx - h / 2 < 1e-9 and dy - (circle.radius + w / 2) < 1e-9: return True if dy - w / 2 < 1e-9 and dx - (circle.radius + h / 2) < 1e-9: return True return False
关于矩形的相关计算的完整代码我放在这里,方便参考:
from Circle import * from PointLine import * class Rectangle(object): """four points are defined by clockwise order from upper left corner""" def __init__(self, p1, p2, p3, p4): self.p1, self.p2, self.p3, self.p4 = p1, p2, p3, p4 def getCenter(self): return Point((self.p2.x + self.p4.x) / 2, (self.p2.y + self.p4.y) / 2) def getXline(self): rectangle_center = self.getCenter() x_center = Point((self.p2.x + self.p3.x) / 2, (self.p2.y + self.p3.y) / 2) return Line(x_center, rectangle_center) def getYline(self): rectangle_center = self.getCenter() y_center = Point((self.p1.x + self.p2.x) / 2, (self.p1.y + self.p2.y) / 2) return Line(y_center, rectangle_center) def pointInRectangle(point, rectangle): """determine whether a point in a rectangle""" x_line = rectangle.getXline() y_line = rectangle.getYline() d1 = pointToLine(point, y_line) - pointToLine(rectangle.p2, y_line) d2 = pointToLine(point, x_line) - pointToLine(rectangle.p2, x_line) return d1 < 1e-9 and d2 < 1e-9 def segmentIntersectsRectangle(s, rectangle): """determine whether a segment intersects a rectangle""" s1 = Segment(rectangle.p1, rectangle.p2) s2 = Segment(rectangle.p2, rectangle.p3) s3 = Segment(rectangle.p3, rectangle.p4) s4 = Segment(rectangle.p4, rectangle.p1) segmentsList = [s1, s2, s3, s4] if pointInRectangle(s.p1, rectangle) and pointInRectangle(s.p2, rectangle): return True for segment in segmentsList: if segmentsIntersect(segment, s): return True return False def lineIntersectsRectangle(l, rectangle): """determine whether a line intersects a rectangle""" s1 = Segment(rectangle.p1, rectangle.p2) s2 = Segment(rectangle.p2, rectangle.p3) s3 = Segment(rectangle.p3, rectangle.p4) s4 = Segment(rectangle.p4, rectangle.p1) segmentsList = [s1, s2, s3, s4] for segment in segmentsList: if segmentIntersectsLine(segment, l): return True return False def circleIntersectsRectangle(circle, rectangle): """determine whether a circle intersects a rectangle""" if pointInCircle(rectangle.p1, circle) or pointInCircle(rectangle.p1, circle) or\ pointInCircle(rectangle.p1, circle) or pointInCircle(rectangle.p1, circle): return True x_line = rectangle.getXline() y_line = rectangle.getYline() dx, dy = pointToLine(circle.center, x_line), pointToLine(circle.center, y_line) h, w = pointDistance(rectangle.p1, rectangle.p2), pointDistance(rectangle.p2, rectangle.p3) if dx - h / 2 < 1e-9 and dy - (circle.radius + w / 2) < 1e-9: return True if dy - w / 2 < 1e-9 and dx - (circle.radius + h / 2) < 1e-9: return True return False
有关于计算几何问题的完整项目我放在了github主页上,并持续更新:Computational-Geometry. 欢迎访问。