寻找两条线段之间的距离?

4
给定两条线段,找到这两条线段之间距离为d的两个点。
这类似于“两条线段之间最短距离问题”,但我们要解决的是线段上相隔一定距离d的两个点。
每条线段由两个三维点组成。
我通过谷歌搜索找到的数学公式既让我害怕又令我困惑。我是一名程序员,但我很难理解解决这种问题背后的证明和分析。 输入:2条线段和一个距离d 输出:每条线段上距离为d的2个点,如果不存在这样的两个点,则输出

通常情况下,会有无限多对点的距离是d。应该返回哪一对? 无所谓吗? - Rory Daulton
在这种情况下,“两条直线之间的距离”是什么定义?标准定义通常意味着连接这两条直线的最小向量垂直于这两条直线。这样的向量是唯一的(除非直线平行),因此是最小距离。因此,如果您需要距离“d”的两个点,而不是最近的点,则必须更多地限制距离定义,因为有多个满足该条件的点。例如,您可以要求距离“d”,使连接点的向量与一条直线正交。 - Mauricio Cele Lopez Belon
5个回答

4
这里是一种非迭代的解决方案。我担心数学会让你感到烦恼,但这里没有什么复杂的内容。
首先,最好始终使用距离的平方来进行计算。
如果一个三维线由点P和Q描述,另一个由点R和S描述,则表述问题的一种方法是,我们要找到标量m和n,使得第一条线上分数为m的点与第二条线上分数为n的点之间的距离平方为给定的dsq。
我们必须限制m和n在0到1(含)之间,以确保我们的点实际上在线段上。
如果有了m和n,那么这些点就是:
X = P + m*(Q-P)
Y = R + n*(S-R)

假设我们首先找到Dsq的最小值和最大值,这将告诉我们是否有解:如果给定的dsq值小于最小值或大于最大值,则没有解决方案,我们可以停止。

设最小值出现的点的m和n值为m_min和n_min,最大值出现的点的m和n值为m_max和n_max。如果我们引入一个新变量z(在[0,1]范围内),那么我们可以考虑一条由m、n值组成的“线”:

m(z) = m_min + z*(m_max-m_min)
n(z) = n_min + z*(n_max-n_min)

当z为0时,这些是最小Dsq的值,而当z为1时,它们是最大Dsq的值。因此,随着我们将z从0增加到1,Dsq的值必须通过我们想要的值!也就是说,我们只需要搜索使Dsq成为我们想要的值的z值。
问题(相对)简单的原因是X和Y之间的distanceSquared是m和n的二次多项式。具体来说,一些繁琐的代数运算表明,如果Dsq是X和Y之间的距离平方,则:
Dsq = a + 2*b*m + 2*c*m + d*m*m + 2*e*m*n + f*m*m
where, in terms of dot products
a = (P-R).(P-R)
b = (P-R).(Q-P)
c =-(P-R).(S-R)
d = (Q-P).(Q-P)
e =-(Q-P).(S-R)
f = (S-R).(S-R)

最大值和最小值必须出现在角落之一((m,n)=(0,0) 或 (0,1) 或 (1,0) 或 (1,1)),或沿着边缘之一(在 (0,n) 或 (1,n) 处,对于某些 n,或在 (m,0) 或 (m,1) 处,对于某些 m),或在中间的一个点上,在该点处,Dsq 的导数(相对于 m 和 n)均为 0。 例如,请注意在边缘 (0,n) 上我们得到了 Dsq 中 n 的二次项式,因此很容易找到其最大值。
此外,当我们沿着最小值和最大值之间的“线”寻找时,如果将 m(z) 和 n(z) 替换为我们的 Dsq 公式,则经过更多繁琐的代数运算,我们得到 z 的二次函数,因此很容易找到会给出所需 Dsq 值的 z 值。
好了,这篇文章已经相当长了,所以这里是实现这些想法的 C 代码。我随机尝试了一百万个点,当距离始终在最大值和最小值之间时,它总是能找到合适的三维点。在我的(相当普通的)Linux 桌面上,这需要几秒钟。
   //   3d vectors
static  void    v3_sub( double* P, double* Q, double* D)
{   D[0] = P[0]-Q[0];
    D[1] = P[1]-Q[1];
    D[2] = P[2]-Q[2];
}
static  double  v3_dot( double* P, double* Q)
{   return P[0]*Q[0] + P[1]*Q[1] + P[2]*Q[2];
}

//  quadratic in one variable
// return *x so X -> r[0] + 2*r[1]*X + r[2]*X*X has minumum at *x
static  int quad_min( const double*r, double* x)
{   if ( r[2] <= 0.0)
    {   return 0;
    }
    *x = -r[1]/r[2];
    return 1;
}

// return x so r[0] + 2*r[1]*x + r[2]*x*x == d, and whether 0<=x<=1
static  int solve_quad( const double* r, double d, double* x)
{
double  ap = r[0] - d;
    if ( r[1] > 0.0)
    {
    double  root1 = -(r[1] + sqrt( r[1]*r[1] - ap*r[2]));   // < 0 
        *x = ap/root1;
    }
    else
    {
    double  root1 = (-r[1] + sqrt( r[1]*r[1] - ap*r[2]));   // >= 0
        if ( root1 < r[2])
        {   *x = root1/r[2];
        }
        else
        {   *x = ap/root1;
        }
    }
    return 0.0 <= *x && *x <= 1.0;
}

//  quadratic in 2 variables
typedef struct
{   double  a,b,c,d,e,f;
}   quad2T;

static  double  eval_quad2( const quad2T* q, double m, double n)
{
    return  q->a
    +   2.0*(m*q->b + n*q->c)
    +   m*m*q->d + 2.0*m*n*q->e + n*n*q->f
    ;
}

// eval coeffs of quad2 so that quad2(m,n) = distsq( P+m*(Q-P), R+n*(S-R))
static  quad2T  set_quad2( double* P, double* Q, double* R, double* S)
{
double  D[3];   v3_sub( P, R, D);
double  U[3];   v3_sub( Q, P, U);
double  V[3];   v3_sub( S, R, V);
quad2T  q;
    // expansion of lengthSq( D+m*U-n*V)
    q.a = v3_dot( D, D);
    q.b = v3_dot( D, U);
    q.c = -v3_dot( D, V);
    q.d = v3_dot( U, U);
    q.e = -v3_dot( U, V);
    q.f = v3_dot( V, V);
    return q;
}

// if gradient of q is 0 in [0,1]x[0,1], return (m,n) where it is zero
// gradient of q is 2*( q->b + m*q->d + n*q->e, q->c + m*q->e + n*q->f)
// so must solve    ( q->d  q->e ) * (m) = -(q->b)
//          ( q->e  q->f )   (n)    (q->c)
static  int dq_zero( const quad2T* q, double* m, double* n)
{
double  det = q->d*q->f - q->e*q->e;
    if ( det <= 0.0)
    {   // note matrix be semi-positive definite, so negative determinant is rounding error
        return 0;
    }
    *m  = -( q->f*q->b - q->e*q->c)/det;
    *n  = -(-q->e*q->b + q->d*q->c)/det;

    return  0.0 <= *m && *m <= 1.0
    &&  0.0 <= *n && *n <= 1.0
    ;
}

// fill *n with minimising value, if any in [0,1], of n -> q(m0,n)
static  int m_edge_min( const quad2T* q, double m0, double* n)
{
double  r[3];   // coeffs of poly in n when m == m0
    r[0] = q->a + 2.0*m0*q->b + m0*m0*q->d;
    r[1] = q->c + m0*q->e;
    r[2] = q->f;
    return  ( quad_min( r, n)
        && *n > 0.0 && *n < 1.0
        );
}

// fill *m with minimising value, if any in [0,1], of m -> q(m,n0)
static  int n_edge_min( const quad2T* q, double* m, double n0)
{
double  r[3];   // coeffs of poly in m when n == n0
    r[0] = q->a + 2.0*n0*q->c + n0*n0*q->f;
    r[1] = q->b + n0*q->e;
    r[2] = q->d;
    return  ( quad_min( r, m)
        && *m > 0.0 && *m < 1.0
        );
}

// candidates for min, man
typedef struct
{   double  m,n;    // steps along lines
    double  d;  // distance squared between points
}   candT;

static  int find_cands( const quad2T* q, candT* c)
{
int nc = 0;
double  x, y;
    // the corners
    c[nc++] = (candT){ 0.0,0.0, eval_quad2( q, 0.0, 0.0)};
    c[nc++] = (candT){ 0.0,1.0, eval_quad2( q, 0.0, 1.0)};
    c[nc++] = (candT){ 1.0,1.0, eval_quad2( q, 1.0, 1.0)};
    c[nc++] = (candT){ 1.0,0.0, eval_quad2( q, 1.0, 0.0)};
    // the edges
    if ( m_edge_min( q, 0.0, &x))
    {   c[nc++] = (candT){ 0.0,x, eval_quad2( q, 0.0, x)};
    }
    if ( m_edge_min( q, 1.0, &x))
    {   c[nc++] = (candT){ 1.0,x, eval_quad2( q, 1.0, x)};
    }
    if ( n_edge_min( q, &x, 0.0))
    {   c[nc++] = (candT){ x, 0.0, eval_quad2( q, x, 0.0)};
    }
    if ( n_edge_min( q, &x, 1.0))
    {   c[nc++] = (candT){ x, 1.0, eval_quad2( q, x, 1.0)};
    }
    // where the derivatives are 0
    if ( dq_zero( q, &x, &y))
    {   c[nc++] = (candT){ x, y, eval_quad2( q, x, y)};
    }
    return nc;
}

// fill in r so that
// r[0] + 2*r[1]*z + r[2]*z*z = q( minm+z*(maxm-minm), minn+x*(maxn-minn))
static  void    form_quad
( const quad2T* q
, double minm, double maxm
, double minn, double maxn
, double* r
)
{
double  a = minm;
double  c = maxm-minm;
double  b = minn;
double  d = maxn-minn;
    r[0] =  q->a + 2.0*q->b*a + 2.0*q->c*b + q->d*a*a + 2.0*q->e*a*b + q->f*b*b;
    r[1] =  q->b*c + q->c*d + q->d*a*c + q->e*(a*d+b*c) + q->f*b*d;
    r[2] =  q->d*c*c + 2.0*q->e*c*d + q->f*d*d;
}

static  int find_points
( double* P, double* Q, double* R, double* S, double dsq, double* X, double* Y
)
{
double  m, n;
quad2T  q = set_quad2( P, Q, R, S);
candT   c[9];
int nc = find_cands( &q, c);    // find candidates for max and min
    // find indices of max and min
int imin = 0;
int imax = 0;
    for( int i=1; i<nc; ++i)
    {   if ( c[i].d < c[imin].d)
        {   imin = i;
        }
        else if ( c[i].d > c[imax].d)
        {   imax = i;
        }
    }
    // check if solution is possible -- should allow some slack here!
    if ( c[imax].d < dsq || c[imin].d > dsq)
    {   return 0;
    }
    // find solution 
double  r[3];
    form_quad( &q, c[imin].m, c[imax].m, c[imin].n, c[imax].n, r);
double  z;
    if ( solve_quad( r, dsq, &z))
    {   // fill in distances along
        m = c[imin].m + z*(c[imax].m - c[imin].m);
        n = c[imin].n + z*(c[imax].n - c[imin].n);
        // compute points
        for( int i=0; i<3; ++i)
        {   X[i] = P[i] + m*(Q[i]-P[i]);
            Y[i] = R[i] + n*(S[i]-R[i]);
        }
        return 1;
    }
    return 0;
}

2
这可以通过解二次多项式的初等代数来解决。看下面的推导:
给定由点P1和P2定义的线段P和由点Q1和Q2定义的线段Q,我们可以将射线P(t)定义为:
P(t) = P1 + t V
其中t是正标量,V是单位向量:
V = (P2 - P1) / |P2 - P1|
以及线段Q(t)定义为:
Q(t) = Q1 + t (Q2 - Q1)
其中t是在区间[0,1]之间的正标量。
任何在线段Q(t)上的点到线段P的最短距离都是该点在线P上的投影。投影沿着线P的法向量进行。
             Q(t)
               |
               |
P1 ------------x------------ P2

所以我们正在寻找线P中的点x,使得矢量(x-Q(t))的长度等于d: |x-Q(t)|^2=d^2
可以使用射线P(t)计算点x,因为t =(Q(t)- P1)•V:
x=P((Q(t)- P1) • V)
x=P1 + ((Q(t)- P1) • V) V
x=P1 - (P1 • V) V + (Q(t) • V) V
x=P1 - (P1 • V) V + (Q1• V) V + t ((Q2 - Q1) • V ) V
其中(•)是点积。
x=C + t D
其中
C=P1 - (P1 • V) V + (Q1• V) V
D=((Q2 - Q1) • V ) V
现在方程看起来像这样: |C + t D - Q1 - t (Q2 - Q1)|^2 = d^2 |C - Q1 + t (D - Q2 + Q1)|^2 = d^2
分组项: |t A + B|^2 = d^2
其中
A=(D - Q2 + Q1)
B=C - Q1
平方后得:
(t A + B) • (t A + B) = d^2
t^2 (A • A) + 2 t (A • B) + (B • B - d^2) = 0
这是一个简单的二次多项式。解出t,如果两个值都是复数,则没有实数解。如果两个值都是实数,则可能有两个答案,由于对称性,我们必须选择区间[0 1]中的一个t。
一旦我们有了t,我们就可以使用Q(t)中的线段和相应点P中的点x计算。
x=P((Q(t)- P1) • V)
如果参数(Q(t)- P1) • V在区间[0 L]内,其中L是矢量(P2- P1)的长度,则x位于线段P的端点之间,否则x在外部,然后没有找到答案。

1
假设线A的两个端点与线B上最近端点的距离不同,我会使用暴力方法。我会选择线A的中心点作为线C的一端,并将线C的另一端在线B上以“插入距离”的步长滑动,直到距离“d”在“插入距离”范围内。
如果我离“d”最近的距离太大,我会将线C的新端点设置为线A的中心点和线B上最近端点之间距离的中点。如果我离“d”最近的距离太小,我会将线A的新端点移动到线A的中心点和线B上最远端点之间距离的中点。
重复这个过程“插入步骤”次,并返回给我距离“d”最接近的端点,如果在达到最大迭代次数之前找不到可接受的值,则可以确定您的算法是否允许足够的步骤或对于接近“d”的值过于严格。
如果线A的两个端点与线B上最近端点的距离相同,则使用线B的最远端点。如果它们都相同,则初始步骤发生的方向是任意的。
此外,在直线B上不仅可以简单地滑动第二个端点,您还可以使用相同的跳跃到越来越小的中点(在正确方向上)的算法来节省计算次数。

1

这个问题是由 Vladimir J. LUMELSKY 1985 年的文章 On fast computation of distance between line segments 的主题。

一般算法如下:

  1. 计算全局 MinD(最小欧几里得距离,全局意味着包含线段的两条无限直线之间的距离)和两个点(基础)的坐标,参见skew lines;如果两个基础都在线段内,则实际 MinD 等于全局 MinD;否则,继续。
  2. 计算两个线段端点之间的距离(总共四个距离)。
  3. 计算一个线段端点到另一个线段上垂线的基点坐标;计算那些基点位于相应线段内的垂线长度(最多四个基点和四个距离)。
  4. 在剩余的距离中,最小的就是所寻找的实际 MinD。总共需要计算六个点和九个距离。
本文进一步描述并证明了如何根据算法初始步骤接收到的数据来减少测试数量,以及如何处理退化情况(例如,线段的等端点)。
Eric Larsen的C语言实现可以在这里找到,查看SegPoints()函数。

1
这里有一个迭代算法,需要一些数学知识但不需要高深的数学优化理解。它很强大,但可能不是特别快。
从高层次上讲,这个算法就像二分查找(技术上是三分查找)。在每一对迭代中,我们砍掉每个段剩余部分的一个恒定比例,注意如果存在有效解则保留。我们可以在数学上证明,随着迭代次数的增加,两个片段都会缩小到点,并且如果存在有效解,则这些点是有效解。实际上,我们在一定数量的迭代后停止(例如100次或者当片段足够短时),并返回每个片段上的任意一点。
这个算法使用了两个数学要素。第一个是一个计算点与线段之间距离的公式。第二个是,随着我们沿着一个片段扫过一个点,到另一个点的距离会先减少再增加。
如果我有时间,我会扩展这个描述。
from __future__ import division


def squared_distance_between_points(p, q):
    """Returns the squared distance between the point p and the point q."""
    px, py, pz = p
    qx, qy, qz = q
    return (px - qx)**2 + (py - qy)**2 + (pz - qz)**2


def squared_distance_between_point_and_segment(p, q, r):
    """Returns the squared distance between the point p and the segment qr."""
    px, py, pz = p
    qx, qy, qz = q
    rx, ry, rz = r
    # Translate the points to move q to the origin (p' = p - q and r' = r - q).
    px -= qx
    py -= qy
    pz -= qz
    rx -= qx
    ry -= qy
    rz -= qz
    # Project p' onto the line 0r'.
    # The point on this line closest to p' is cr'.
    c = (px * rx + py * ry + pz * rz) / (rx * rx + ry * ry + rz * rz)
    # Derive c' by clamping c. The point on the segment 0r' closest to p is c'r'.
    c = min(max(c, 0), 1)
    # Compute the distance between p' and c'r'.
    return squared_distance_between_points((px, py, pz),
                                           (c * rx, c * ry, c * rz))


def trisect(p, q):
    """Returns the point one-third of the way from the point p to the point q."""
    px, py, pz = p
    qx, qy, qz = q
    return ((2 * px + qx) / 3, (2 * py + qy) / 3, (2 * pz + qz) / 3)


def find_points_on_segments_at_distance(p, r, s, u, d, iterations=100):
    """Returns a point q on the segment pr and a point t on the segment su
       such that the distance between q and t is approximately d.
       If this is not possible (or barely possible), returns None."""
    d **= 2
    feasible = False
    for i in range(2 * int(iterations)):
        q1 = trisect(p, r)
        d1 = squared_distance_between_point_and_segment(q1, s, u)
        q2 = trisect(r, p)
        d2 = squared_distance_between_point_and_segment(q2, s, u)
        if d <= min(d1, d2):
            # Use convexity to cut off one third of the search space.
            if d1 <= d2:
                r = q2
            else:
                p = q1
        elif d <= max(d1, d2):
            # There is certainly a solution in the middle third.
            feasible = True
            p = q1
            r = q2
        elif (d <= squared_distance_between_points(p, s)
              or d <= squared_distance_between_points(p, u)):
            # There is certainly a solution in the first third.
            feasible = True
            r = q1
        elif (d <= squared_distance_between_points(r, s)
              or d <= squared_distance_between_points(r, u)):
            # There is certainly a solution in the last third.
            feasible = True
            p = q2
        else:
            # Definitely infeasible.
            return None
        # Swap the segments.
        p, r, s, u = s, u, p, r
    if not feasible:
        return None
    return p, r

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接