【问题标题】:Test if two segments are roughly collinear (on the same line)测试两个线段是否大致共线(在同一条线上)
【发布时间】:2022-01-18 21:51:25
【问题描述】:

我想使用numpy.cross 测试两个段是否大致共线(在同一条线上)。我有以米为单位的线段坐标。

import numpy as np

segment_A_x1 = -8020537.5158307655
segment_A_y1 = 5674541.918222183
segment_A_x2 = -8020547.42095263
segment_A_y2 = 5674500.781350276

segment_B_x1 = -8020556.569040865
segment_B_y1 = 5674462.788207927
segment_B_x2 = -8020594.740831952
segment_B_y2 = 5674328.095911447

a = np.array([[segment_A_x1, segment_A_y1], [segment_A_x2, segment_A_y2]])
b = np.array([[segment_B_x1, segment_B_y1], [segment_B_x2, segment_B_y2]])
crossproduct = np.cross(a, b)

>>>array([7.42783487e+08, 1.65354844e+09])

crossproduct 的值相当高,即使我会说这两个部分大致共线。为什么?

如何确定这些段是否与crossproduct 结果共线?

是否有可能使用以米为单位的公差来判断线段是否大致共线?

【问题讨论】:

    标签: python numpy


    【解决方案1】:

    您的方法的问题在于叉积值取决于测量尺度。

    也许最直观的共线性度量是线段之间的角度。让我们计算一下:

    import math
    
    def slope(line): 
        """Line slope given two points"""
        p1, p2 = line
        return (p2[1] - p1[1]) / (p2[0] - p1[0])
    
    def angle(s1, s2): 
        """Angle between two lines given their slopes"""
        return math.degrees(math.atan((s2 - s1) / (1 + (s2 * s1))))
    
    ang = angle(slope(b), slope(a))
    print('Angle in degrees = ', ang)
    
    Angle in degrees = 2.2845
    

    我使用了 Anderson Oliveira 的 answer

    【讨论】:

    • 如果直线垂直则不好。
    • 谢谢你的回答,我想我对共线性的定义是错误的。我认为共线性意味着两个向量是对齐的(在同一条线上),但是根据您的回答,我知道它只测试它们是否平行。我说的对吗?
    • 不,你原来对定义的理解是正确的。但是,如果我们知道两条线并不完全对齐而是相交,那么将它们之间的角度视为它们与共线的接近程度的量度是有意义的(当角度为零时)。
    【解决方案2】:

    两个向量abcross-product 可以定义为:||a|| ||b|| sin(θ) 其中θ 是两个向量之间的角度。问题是即使θ 很小,这两个规范的乘积也可能很大,并可能导致很大的交叉乘积。这是你的情况:

    na = np.linalg.norm(a, axis=1)  # array([9824940.10284589, 9824924.42969893])
    nb = np.linalg.norm(b, axis=1)  # array([9824909.95439353, 9824863.32407282])
    nprod = na * nb                 # array([9.65291518e+13, 9.65285397e+13])
    sinθ = crossproduct / nprod     # array([7.69491364e-06, 1.71301508e-05])
    

    确实,sinθ 非常小。这意味着θ 也非常小(实际上,θ 大致等于sinθ 的值,因为sin(θ)θ = 1 的导数为1)。

    如何确定段是否与叉积结果共线?

    您可以根据上述代码计算值np.abs(sinθ),设置任意阈值并检查该值是否小于所选阈值。阈值的最佳值取决于您的实际应用程序/输入(例如统计噪声、输入精度、计算精度等)。如果你不知道用什么,可以以1e-4 开头。

    是否有可能使用以米为单位的公差来判断线段是否大致共线?

    请注意,假设输入向量以米为单位,则根据上述公式,叉积的值以平方米为单位。因此,以米为单位的公差没有多大意义。更一般地说,设置绝对容差肯定是个坏主意。上述解决方案使用相对于输入向量的容差。

    【讨论】:

    • 感谢您的回答,我认为我对共线性的定义有误。我认为共线性意味着两个向量是对齐的(在同一条线上),但是根据您的回答,我知道它只测试它们是否平行。我说的对吗?
    • 好吧,叉积只定义在向量上(在 Numpy 中只有 2D 和 3D 的)。问题中提供的代码不执行由 2 个段形成的 2 个向量的叉积,它对 2 个向量执行两次叉积。如果您想检查段的共线性而不是问题中的 4 个向量,那么您需要“移动”每个段,因此第一个点是原点,因此两个段在应用叉积之前共享一个点(在只有两个向量)。类似:v0, v1 = (a - a[0])[1], (b - b[0])[1]np.cross(v0, v1)
    【解决方案3】:

    共线性有两个方面:线段之间的角度和接近度。您可以分别测试这些方面,并根据其中任何一个短路检查。我建议进行接近检查,完全不需要角度检查。下面的角度检查是出于遗留原因显示的,因为它可能对其他原因有用。

    角度

    交叉产品仅在 3D 中真正定义。点积在任何地方都有定义。两个向量之间的角度定义为它们的单位向量的点积的反余弦值。这适用于 2D 和 10D。

    你可以从原点定义两个向量

    a = np.array([segment_A_x2 - segment_A_x1,
                  segment_A_y2 - segment_A_y1])
    b = np.array([segment_B_x2 - segment_B_x1,
                  segment_B_y2 - segment_B_y1])
    
    a /= np.linalg.norm(a)
    b /= np.linalg.norm(b)
    
    angle = np.arccos(a.dot(b))
    

    angle 将在-np.pinp.pi 之间运行。接近 pi 的角度表示反平行线,而接近零的角度表示平行线。您可以实现类似的测试

    if abs(angle) < angular_threshold or abs(angle) > np.pi - angular_threshold:
       ...
    

    如果您发现自己经常执行此计算,则可以通过完全跳过 arccos 来节省一些周期。当角度变为零或 pi 时,点积分别变为 +1 或 -1。这意味着您只需要预先计算一次dot_threshold = np.cos(angular_threshold)

    if abs(a.dot(b)) >= dot_threshold:
    

    接近度

    要测试接近度,您需要定义距离的测量方式。对于完全平行的线,这是明确的:两条线必须在彼此的distance_threshold 范围内。

    对于不完全平行的线,您可以做类似的事情:一个线段上的任何点都不能比另一线段的线的distance_threshold 更远。使用此定义,您可以将单独角度计算的需要直接吸收到计算中。

    参考下图:

    您必须对照其他行检查所有四个端点。如果您愿意,您可以根据距离的差异计算角度,并可能根据线段的长度将比例因子应用于阈值。

    您也可以使用点积计算距离:

    def dist(p, p1, p2):
        s = p2 - p1
        q = p1 + (p - p1).dot(s) / s.dot(s) * s
        return np.linalg.norm(p - q)
    
    a1 = np.array([segment_A_x1, segment_A_y1])
    a2 = np.array([segment_A_x2, segment_A_y2])
    b1 = np.array([segment_B_x1, segment_B_y1])
    b2 = np.array([segment_B_x2, segment_B_y2])
    
    dists = np.array([[dist(a1, b1, b2), dist(b1, a1, a2)],
                      [dist(a2, b1, b2), dist(b2, a1, a2)]])
    

    我制作的实用程序库中提供了一个更强大的dist 版本,称为haggis。您可以使用haggis.math.segment_distance,如下所示:

    dists = segment_distance([[a1, b1], [a2, b2]], # From point
                             [b1, a1],             # Segment start
                             [b2, a2],             # Segment end
                             axis=-1,              # Axis containing vectors
                             segment=False)        # Distance to entire line
    

    输入一起广播,axis 适用于广播的形状,因此您无需重复两次端点。

    最简单的测试版本是直接约束距离:

    if (dists < distance_threshold).all():
        ...
    

    您可以通过按线段长度缩放来计算角度。长 100 倍的线段可能会偏离其他线段的线 100 倍以上,但仍被认为是共线的。在这种情况下,您将distance_threshold 定义为一个unit 向量可以距另一个向量的线的最远距离。数字必须小于 1 才有意义:

    scale = np.linalg.norm([a2 - a1, b2 - b1], axis=-1)
    if (dists < distance_threshold * scale).all():
        ...
    

    这个版本预设了我们在两个计算版本中对dists 施加的形状,因为scale 的元素数量与dists 的列数一样多。

    也可以考虑线段之间的距离的更复杂的方案。这种接近度的定义留给读者作为练习。

    【讨论】:

    • 我错了,但如果点积接近 0,这两个向量大致平行,但不一定完全对齐?
    • @BelowtheRadar。向量隐含地从零开始。您需要查看线段的各个起点以确定这一点。根据您的需要,这听起来像是一个有趣的附加问题。
    • @BelowtheRadar。我需要更新一下。没想到你的标题很准确
    • @BelowtheRadar。我添加了一个更新。希望它适合您的需求。
    • 感谢您的帮助,它按预期工作!我将 segment_distance 方法添加到我的流程中。令人印象深刻的图书馆!
    猜你喜欢
    • 1970-01-01
    • 2021-11-29
    • 2012-02-21
    • 1970-01-01
    • 2011-04-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多