如何在Numpy/Scipy中获取向量到平面的正交距离?

6 投票
3 回答
7744 浏览
提问于 2025-04-17 14:21

我有一组向量,存储在一个numpy数组里。我需要计算这些向量到一个由两个向量v1和v2定义的平面的垂直距离。对于单个向量,我可以很简单地使用Gram-Schmidt过程来计算。但是,我想知道有没有办法能快速地对很多向量进行这个计算,而不需要一个一个地循环处理,也不想用np.vectorize。

谢谢!

3 个回答

2

编辑 我之前写的代码运行不正常,所以我把它删掉了。不过,按照下面解释的思路,如果你花点时间思考一下,其实不需要用到克拉默法则,代码可以简化得很多,如下所示:

def distance(v1, v2, u) :
    u = np.array(u, ndmin=2)
    v = np.vstack((v1, v2))
    vv = np.dot(v, v.T) # shape (2, 2)
    uv = np.dot(u, v.T) # shape (n ,2)
    ab = np.dot(np.linalg.inv(vv), uv.T) # shape(2, n)
    w = u - np.dot(ab.T, v)
    return np.sqrt(np.sum(w**2, axis=1)) # shape (n,)

为了确保它能正常工作,我把Dave的代码封装成了一个叫 distance_3d 的函数,并尝试了以下内容:

>>> d, n = 3, 1000
>>> v1, v2, u = np.random.rand(d), np.random.rand(d), np.random.rand(n, d)
>>> np.testing.assert_almost_equal(distance_3d(v1, v2, u), distance(v1, v2, u))

当然,现在它可以适用于任何 d 了:

>>> d, n = 1000, 3
>>> v1, v2, u = np.random.rand(d), np.random.rand(d), np.random.rand(n, d)
>>> distance(v1, v2, u)
array([ 10.57891286,  10.89765779,  10.75935644])

你需要把你的向量,假设叫 u,分解成两个向量的和,形式是 u = v + w,其中 v 在平面内,所以可以进一步分解为 v = a * v1 + b * v2,而 w 是垂直于平面的,因此有 np.dot(w, v1) = np.dot(w, v2) = 0

如果你写出 u = a * v1 + b * v2 + w,然后把这个表达式和 v1 以及 v2 做点积,你会得到两个方程和两个未知数:

np.dot(u, v1) = a * np.dot(v1, v1) + b * np.dot(v2, v1)
np.dot(u, v2) = a * np.dot(v1, v2) + b * np.dot(v2, v2)

因为这是一个2x2的系统,我们可以用 克拉默法则 来解:

uv1 = np.dot(u, v1)
uv2 = np.dot(u, v2)
v11 = np.dot(v1, v2)
v22 = np.dot(v2, v2)
v12 = np.dot(v1, v2)
v21 = np.dot(v2, v1)
det = v11 * v22 - v21 * v12
a = (uv1 * v22 - v21 * uv2) / det
b = (v11 * uv2 - uv1 * v12) / det

从这里,你可以得到:

w = u - v = u - a * v1 - b * v2

而到平面的距离就是 w 的模。

4

你需要构建一个平面的单位法向量:

在三维空间中,这个操作很简单:

nhat=np.cross( v1, v2 )
nhat=nhat/np.sqrt( np.dot( nhat,nhat) )

然后将这个法向量和你每个向量进行点乘;我假设你的向量是一个 Nx3 的矩阵 M

result=np.zeros( M.shape[0], dtype=M.dtype )
for idx in xrange( M.shape[0] ):
    result[idx]=np.abs(np.dot( nhat, M[idx,:] ))

这样 result[idx] 就是第 idx'th 个向量到平面的距离。

5

更明确地实现@Jaime的答案,你可以清楚地说明投影算子的构造方式:

def normalized(v):
    return v/np.sqrt( np.dot(v,v))
def ortho_proj_vec(v, uhat ):
    '''Returns the projection of the vector v  on the subspace
    orthogonal to uhat (which must be a unit vector) by subtracting off
    the appropriate multiple of uhat.
    i.e. dot( retval, uhat )==0
    '''
    return v-np.dot(v,uhat)*uhat

def ortho_proj_array( Varray, uhat ):
     ''' Compute the orhogonal projection for an entire array of vectors.
     @arg Varray:  is an array of vectors, each row is one vector
          (i.e. Varray.shape[1]==len(uhat)).
     @arg uhat: a unit vector
     @retval : an array (same shape as Varray), where each vector
               has had the component parallel to uhat removed.
               postcondition: np.dot( retval[i,:], uhat) ==0.0
               for all i. 
    ''' 
    return Varray-np.outer( np.dot( Varray, uhat), uhat )




# We need to ensure that the vectors defining the subspace are unit norm
v1hat=normalized( v1 )

# now to deal with v2, we need to project it into the subspace
# orhogonal to v1, and normalize it
v2hat=normalized( ortho_proj(v2, v1hat ) )
# TODO: if np.dot( normalized(v2), v1hat) ) is close to 1.0, we probably
# have an ill-conditioned system (the vectors are close to parallel)



# Act on each of your data vectors with the projection matrix,
# take the norm of the resulting vector.
result=np.zeros( M.shape[0], dtype=M.dtype )
for idx in xrange( M.shape[0] ):
    tmp=ortho_proj_vec( ortho_proj_vec(M[idx,:], v1hat), v2hat )             
    result[idx]=np.sqrt(np.dot(tmp,tmp))

 # The preceeding loop could be avoided via
 #tmp=orhto_proj_array( ortho_proj_array( M, v1hat), v2hat )
 #result=np.sum( tmp**2, axis=-1 )
 # but this results in many copies of matrices that are the same 
 # size as M, so, initially, I prefer the loop approach just on
 # a memory usage basis.

这实际上只是Gram-Schmidt正交化过程的一种推广。注意,在这个过程的最后,我们会得到 np.dot(v1hat, v1hat.T)==1np.dot(v2hat,v2hat.T)==1np.dot(v1hat, v2hat)==0(在数值精度范围内)

撰写回答