3D空间中距离多条直线最近的三维点

3

我正在寻找一种非迭代、闭合形式的算法,以找到最接近一组3D直线的点的最小二乘解。它类似于3D点三角测量(用于最小化重新投影),但似乎更简单、更快速?

直线可以用任何形式描述,比如2个点、一个点和单位方向等。


1
你最好在math.stackexchange.com上询问这个问题,如果需要编码帮助再回到这里。 - corsiKa
您可以使用完整的线集测试每个点。如果您对点和线进行排序,则可以实现一些改进,因此许多点和线可以快速丢弃。对于点-线3D距离,您可以例如使用这个 - Ripi2
我的答案对你有效吗?我认为它效果不错,但你没有留言。 - Gene
抱歉,这个任务已经延迟了很长时间。我回到它时,会写下评论和结果。对于这个暂停,我感到非常抱歉。 - minorlogic
3个回答

9

假设第 i 行由点 ai 和单位方向向量 di 给出。我们需要找到使点到直线距离的平方和最小的单个点。这是梯度为零向量的地方:

unsimplified gradient of sum of squared distances

扩展渐变,

rewritten gradient set to zero

代数产生一个规范的3x3线性系统,

3x3 linear system

矩阵M的第k行(一个三元素行向量)是

matrix M

使用向量ek作为相应的单位基向量,以及

vector b

把这个转化为代码并不难。我从Rosettacode借鉴了一个高斯消元函数来解决这个系统方程组,并修复了其中的一个小错误。感谢作者!

#include <stdio.h>
#include <math.h>

typedef double VEC[3];
typedef VEC MAT[3];

void solve(double *a, double *b, double *x, int n);  // linear solver

double dot(VEC a, VEC b) { return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]; }

void find_nearest_point(VEC p, VEC a[], VEC d[], int n) {
  MAT m = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}};
  VEC b = {0, 0, 0};
  for (int i = 0; i < n; ++i) {
    double d2 = dot(d[i], d[i]), da = dot(d[i], a[i]);
    for (int ii = 0; ii < 3; ++ii) {
      for (int jj = 0; jj < 3; ++jj) m[ii][jj] += d[i][ii] * d[i][jj];
      m[ii][ii] -= d2;
      b[ii] += d[i][ii] * da - a[i][ii] * d2;
    }
  }
  solve(&m[0][0], b, p, 3);
}

// Debug printing.
void pp(VEC v, char *l, char *r) {
  printf("%s%.3lf, %.3lf, %.3lf%s", l, v[0], v[1], v[2], r);
} 

void pv(VEC v) { pp(v, "(", ")"); } 

void pm(MAT m) { for (int i = 0; i < 3; ++i) pp(m[i], "\n[", "]"); } 

// A simple verifier.
double dist2(VEC p, VEC a, VEC d) {
  VEC pa = { a[0]-p[0], a[1]-p[1], a[2]-p[2] };
  double dpa = dot(d, pa);
  return dot(d, d) * dot(pa, pa) - dpa * dpa;
}

double sum_dist2(VEC p, VEC a[], VEC d[], int n) {
  double sum = 0;
  for (int i = 0; i < n; ++i) sum += dist2(p, a[i], d[i]);
  return sum;
}

// Check 26 nearby points and verify the provided one is nearest.
int is_nearest(VEC p, VEC a[], VEC d[], int n) {
  double min_d2 = 1e100;
  int ii = 2, jj = 2, kk = 2;
#define D 0.01
  for (int i = -1; i <= 1; ++i) 
    for (int j = -1; j <= 1; ++j)
      for (int k = -1; k <= 1; ++k) {
        VEC pp = { p[0] + D * i, p[1] + D * j, p[2] + D * k };
        double d2 = sum_dist2(pp, a, d, n);
        // Prefer provided point among equals.
        if (d2 < min_d2 || i == 0 && j == 0 && k == 0 && d2 == min_d2) {
          min_d2 = d2;
          ii = i; jj = j; kk = k;
        }
      }
  return ii == 0 && jj == 0 && kk == 0;
}

void normalize(VEC v) {
  double len = sqrt(dot(v, v));
  v[0] /= len;
  v[1] /= len;
  v[2] /= len;
}

int main(void) {
  VEC a[] = {{-14.2, 17, -1}, {1, 1, 1}, {2.3, 4.1, 9.8}, {1,2,3}};
  VEC d[] = {{1.3, 1.3, -10}, {12.1, -17.2, 1.1}, {19.2, 31.8, 3.5}, {4,5,6}};
  int n = 4;
  for (int i = 0; i < n; ++i) normalize(d[i]);
  VEC p;
  find_nearest_point(p, a, d, n);
  pv(p);
  printf("\n");
  if (!is_nearest(p, a, d, n)) printf("Woops. Not nearest.\n");
  return 0;
}

// A linear solver from rosettacode (with bug fix: added a missing fabs())
#define mat_elem(a, y, x, n) (a + ((y) * (n) + (x)))

void swap_row(double *a, double *b, int r1, int r2, int n)
{
  double tmp, *p1, *p2;
  int i;

  if (r1 == r2) return;
  for (i = 0; i < n; i++) {
    p1 = mat_elem(a, r1, i, n);
    p2 = mat_elem(a, r2, i, n);
    tmp = *p1, *p1 = *p2, *p2 = tmp;
  }
  tmp = b[r1], b[r1] = b[r2], b[r2] = tmp;
}

void solve(double *a, double *b, double *x, int n)
{
#define A(y, x) (*mat_elem(a, y, x, n))
  int i, j, col, row, max_row, dia;
  double max, tmp;

  for (dia = 0; dia < n; dia++) {
    max_row = dia, max = fabs(A(dia, dia));
    for (row = dia + 1; row < n; row++)
      if ((tmp = fabs(A(row, dia))) > max) max_row = row, max = tmp; 
    swap_row(a, b, dia, max_row, n);
    for (row = dia + 1; row < n; row++) {
      tmp = A(row, dia) / A(dia, dia);
      for (col = dia+1; col < n; col++)
        A(row, col) -= tmp * A(dia, col);
      A(row, dia) = 0;
      b[row] -= tmp * b[dia];
    }
  }
  for (row = n - 1; row >= 0; row--) {
    tmp = b[row];
    for (j = n - 1; j > row; j--) tmp -= x[j] * A(row, j);
    x[row] = tmp / A(row, row);
  }
#undef A
}

这个还没有经过广泛测试,但似乎工作正常。

1

假设线的基点为p,单位方向向量为d。 那么从点v到该直线的距离可以通过使用叉积计算。

SquaredDist = ((v -  p) x d)^2

使用Maple包进行符号计算,我们可以得到

d := <dx, dy, dz>;
v := <vx, vy, vz>;
p := <px, py, pz>;
w := v - p;
cp := CrossProduct(d, w);
nrm := BilinearForm(cp, cp, conjugate=false);  //squared dist
nr := expand(nrm);       

//now partial derivatives
nrx := diff(nr, vx);     
//results:
nrx := -2*dz^2*px-2*dy^2*px+2*dz^2*vx+2*dy^2*vx
       +2*dx*py*dy-2*dx*vy*dy+2*dz*dx*pz-2*dz*dx*vz
nry := -2*dx^2*py-2*dz^2*py-2*dy*vz*dz+2*dx^2*vy
       +2*dz^2*vy+2*dy*pz*dz+2*dx*dy*px-2*dx*dy*vx
nrz := -2*dy^2*pz+2*dy^2*vz-2*dy*dz*vy+2*dx^2*vz
       -2*dx^2*pz-2*dz*vx*dx+2*dy*dz*py+2*dz*px*dx

为了最小化平方距离的总和,我们必须制定零偏导数的线性方程组,如下所示:
  vx*2*(Sum(dz^2)+Sum(dy^2)) + vy * (-2*Sum(dx*dy)) + vz *(-2*Sum(dz*dx)) = 
     2*Sum(dz^2*px)-2*Sum(dy^2*px) -2*Sum(dx*py*dy)-2*Sum(dz*dx*pz)

where 
  Sum(dz^2) = Sum{over all i in line indexes} {dz[i] * dz[i]}

并解决vx、vy、vz的未知数

编辑:旧的错误答案是针对平面而不是直线的,仅供参考

如果我们使用直线的一般方程

 A * x + B * y + C * z + D = 0

那么从点(x,y,z)到此直线的距离是

Dist = Abs(A * x + B * y + C * z + D) / Sqrt(A^2 + B^2 + C^2)

简单来说,只需通过除以Norm的值来规范化所有线性方程。
Norm = Sqrt(A^2 + B^2 + C^2)
a = A / Norm
b = B / Norm
c = C / Norm
d = D / Norm

现在的方程式是

 a * x + b * y + c * z  + d = 0

和距离

Dist = Abs(a * x + b * y + c * z + d)

我们可以使用平方距离,就像最小二乘法一样(ai、bi、ci、di是第i条线的系数)

F = Sum(ai*x + bi*y + ci * z + d)^2 = 
Sum(ai^2*x^2 + bi^2*y^2 + ci^2*z^2 + d^2 +
2 * (ai*bi*x*y + ai*ci*x*z + bi*y*ci*z + ai*x*di + bi*y*di + ci*z*di))

  partial derivatives
dF/dx = 2*Sum(ai^2*x + ai*bi*y + ai*ci*z + ai*di) = 0
dF/dy = 2*Sum(bi^2*y + ai*bi*x + bi*ci*z + bi*di) = 0
dF/dz = 2*Sum(ci^2*z + ai*ci*x + bi*ci*y + ci*di) = 0
  so we have system of linear equation 
x * Sum(ai^2) + y * Sum(ai*bi) + z * Sum(ai*ci)= - Sum(ai*di)
y * Sum(bi^2) + x * Sum(ai*bi) + z * Sum(bi*ci)= - Sum(bi*di)
z * Sum(ci^2) + x * Sum(ai*ci) + y * Sum(bi*ci)= - Sum(ci*di)

x * Saa + y * Sab + z * Sac = - Sad
x * Sab + y * Sbb + z * Sbc = - Sbd
x * Sac + y * Sbc + z * Scc = - Scd

where S** are corresponding sums

并且可以解决未知数 x,y,z


2
“A * x + B * y + C * z + D = 0” 不是三维空间中的一条直线,而是一个平面。因此,下面的计算可能也不符合提问者的问题。 - Ralf Kleberhoff
1
稍微思考了一下:到一条直线的平方距离可以通过将到两个正交平面(在线段相交处)的平方距离相加来计算。因此,如果您为每条直线创建两个平面,则您的方法可能很有用。 - Ralf Kleberhoff
@Ralf Kleberhoff 哦天啊,是的 :( 我为2D情况做了答案,然后愚蠢地将其扩展到3D情况。 - MBo
我不明白你的最终解决方案是如何工作的。可能有任意数量的点,所以你写的是任意数量的方程式。但只有3个未知数。正如我在评论中所说(根据你的变量名进行调整),你需要解决 gradient( sum_i (d_i x (p_i - v))^2 ) = 0,这是三个未知数的三个方程式。 - Gene
@Gene,我考虑了计算点坐标的问题,使其到给定线集的平方距离之和最小化。这实际上是解3个未知数的方程(我只显式地写出了一个)。你是在谈论从点集中选择最佳点吗? - MBo
@MBo 好的。抱歉我误解了你的符号。现在明白了。我们得到了相同的答案吗?看起来是,但不确定。 - Gene

0
我需要这个在Processing中的草图,所以我移植了Gene的答案。它运行得很好,我认为这可能会为其他人节省一些时间。不幸的是,PVector/PMatrix没有向量或矩阵的数组访问器,因此我不得不添加这些作为本地函数。
float getv(PVector v, int i) {
  if(i == 0) return v.x;
  if(i == 1) return v.y;
  return v.z;
}

void setv(PVector v, int i, float value) {
  if      (i == 0) v.x = value;
  else if (i == 1) v.y = value;
  else             v.z = value;
}

void incv(PVector v, int i, float value) {
  setv(v,i,getv(v,i) + value);
}

float getm(float[] mm, int r, int c)              { return mm[c + r*4]; }
void  setm(float[] mm, int r, int c, float value) { mm[c + r*4] = value; }
void  incm(float[] mm, int r, int c, float value) { mm[c + r*4] += value; }

PVector findNearestPoint(PVector a[], PVector d[]) {
  var mm = new float[16];
  var b  = new PVector();
  var n  = a.length;

  for (int i = 0; i < n; ++i) {
    var d2 = d[i].dot(d[i]);
    var da = d[i].dot(a[i]);
    
    for (int ii = 0; ii < 3; ++ii) {
      for (int jj = 0; jj < 3; ++jj) {
        incm(mm,ii,jj, getv(d[i],ii) * getv(d[i],jj));
      }
      
      incm(mm, ii,ii, -d2);
      incv(b, ii, getv(d[i], ii) * da - getv(a[i], ii) * d2);
    }
  }
  
  var p = solve(mm, new float[] {b.x, b.y, b.z});
  return new PVector(p[0],p[1],p[2]);
}


// Verifier
float dist2(PVector p, PVector a, PVector d) {
  PVector pa  = new PVector( a.x-p.x, a.y-p.y, a.z-p.z );
  float   dpa = d.dot(pa);
  return  d.dot(d) * pa.dot(pa) - dpa * dpa;
}

//double sum_dist2(VEC p, VEC a[], VEC d[], int n) {
float sum_dist2(PVector p, PVector a[], PVector d[]) {
  int n = a.length;
  float sum = 0;
  for (int i = 0; i < n; ++i) { 
    sum += dist2(p, a[i], d[i]);
  }
  return sum;
}

// Check 26 nearby points and verify the provided one is nearest.
boolean isNearest(PVector p, PVector a[], PVector d[]) {
  float min_d2 = 3.4028235E38;
  int   ii = 2, jj = 2, kk = 2;
  final float D  = 0.1f;
  
  for (int i = -1; i <= 1; ++i) 
    for (int j = -1; j <= 1; ++j)
      for (int k = -1; k <= 1; ++k) {
        PVector pp = new PVector( p.x + D * i, p.y + D * j, p.z + D * k );
        float d2 = sum_dist2(pp, a, d);
        // Prefer provided point among equals.
        if (d2 < min_d2 || i == 0 && j == 0 && k == 0 && d2 == min_d2) {
          min_d2 = d2;
          ii = i; jj = j; kk = k;
        }
      }
      
  return ii == 0 && jj == 0 && kk == 0;
}



void setup() {
  PVector a[] = {
    new PVector(-14.2, 17, -1), 
    new PVector(1, 1, 1),
    new PVector(2.3, 4.1, 9.8),
    new PVector(1,2,3)
  };
  
  PVector d[] = {
    new PVector(1.3, 1.3, -10),
    new PVector(12.1, -17.2, 1.1),
    new PVector(19.2, 31.8, 3.5),
    new PVector(4,5,6)
  };
  
  int n = 4;
  for (int i = 0; i < n; ++i)
    d[i].normalize();
    
  PVector p = findNearestPoint(a, d);
  println(p);
  
  if (!isNearest(p, a, d))
    println("Woops. Not nearest.\n");
}



// From rosettacode (with bug fix: added a missing fabs())
int mat_elem(int y, int x) { return y*4+x; }

void swap_row(float[] a, float[] b, int r1, int r2, int n)
{
  float tmp;
  int p1, p2;
  int i;

  if (r1 == r2) return;
  
  for (i = 0; i < n; i++) {
    p1 = mat_elem(r1, i);
    p2 = mat_elem(r2, i);
    
    tmp = a[p1];
    a[p1] = a[p2];
    a[p2] = tmp;
  }
  
  tmp = b[r1];
  b[r1] = b[r2];
  b[r2] = tmp;
}

float[] solve(float[] a, float[] b)
{
  float[] x = new float[] {0,0,0};
  int n = x.length;
  int i, j, col, row, max_row, dia;
  float max, tmp;

  for (dia = 0; dia < n; dia++) {
    max_row = dia;
    max = abs(getm(a, dia, dia));
    for (row = dia + 1; row < n; row++) {
      if ((tmp = abs(getm(a, row, dia))) > max) { 
        max_row = row;
        max = tmp;
      }
    }
    swap_row(a, b, dia, max_row, n);
    for (row = dia + 1; row < n; row++) {
      tmp = getm(a, row, dia) / getm(a, dia, dia);
      for (col = dia+1; col < n; col++) {
        incm(a, row, col, -tmp * getm(a, dia, col));
      }
      setm(a,row,dia, 0);
      b[row] -= tmp * b[dia];
    }
  }
  for (row = n - 1; row >= 0; row--) {
    tmp = b[row];
    for (j = n - 1; j > row; j--) {
      tmp -= x[j] * getm(a, row, j);
    }
    x[row] = tmp / getm(a, row, row);
  }
  
  return x;
}

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