You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Python中拟合带指定法向量的3D点集曲线?

3D空间带法向量的三点曲面拟合问题

问题背景

我在3D空间中有三个点,每个点都带有对应的法向量:A、B、C为点的位置,N_A、N_B、N_C为各自的法向量:

A = np.array([  348.92065834, -1402.3305998,     32.69313966])
N_A = np.array([-0.86925426,  0.02836434, -0.49355091])

B =  np.array([282.19332067,  82.52027998,  -5.92595371])
N_B = np.array([-0.82339849,  0.43041935,  0.3698028])

C = np.array([247.37475615,  -3.70129865, -22.10494737])
N_C = np.array([-0.83989222, 0.23796899, 0.48780305])

这三个点几乎共面,但距离最近的B和C之间存在轻微方向变化。从B、C到A点,我推测X-Y坐标平面上存在曲率,而X-Z坐标平面上呈抛物线形态。

若曲率更明显,圆柱面可拟合这三个点;结合X、Z坐标的法向量来看,B和C的法向量朝向下方,A的法向量朝向上方,因此整体可能为抛物面。问题在于如何结合法向量进行拟合;若无法实现,如何拟合出带有X、Y方向曲率的曲面?

以下是绘图代码:

import numpy as np
import matplotlib.pyplot as plt
def set_axes_radius(ax, origin, radius):

    ax.set_xlim3d([origin[0] - radius, origin[0] + radius])
    ax.set_ylim3d([origin[1] - radius, origin[1] + radius])
    ax.set_zlim3d([origin[2] - radius, origin[2] + radius])


def set_axes_equal(ax, zoom=1.):
    '''
        Make axes of 3D plot have equal scale so that spheres appear as spheres,
        cubes as cubes, etc..  This is one possible solution to Matplotlib's
        ax.set_aspect("equal") and ax.axis("equal") not working for 3D.
        input:
          ax:   a matplotlib axis, e.g., as output from plt.gca().

    '''

    limits = np.array([
        ax.get_xlim3d(),
        ax.get_ylim3d(),
        ax.get_zlim3d(),
    ])

    origin = np.mean(limits, axis=1)
    radius = 0.5 * np.max(np.abs(limits[:, 1] - limits[:, 0])) / zoom
    set_axes_radius(ax, origin, radius)
%matplotlib qt 


# positions and their respective normal vectors

A = np.array([  348.92065834, -1402.3305998,     32.69313966])
N_A = np.array([-0.86925426,  0.02836434, -0.49355091])

B =  np.array([282.19332067,  82.52027998,  -5.92595371])
N_B = np.array([-0.82339849,  0.43041935,  0.3698028])

C = np.array([247.37475615,  -3.70129865, -22.10494737])
N_C = np.array([-0.83989222, 0.23796899, 0.48780305])

# A plane is given by
# a*x + b*y + c*z + d = 0
# where (a, b, c) is the normal.
# If the point (x, y, z) lies on the plane, then solving for d yield:
# d = -(a*x + b*y + c*z)
d_A = -np.sum(N_A * A)
d_B = -np.sum(N_B * B)
d_C = -np.sum(N_C * C)

# Create a meshgrid:
delta = 200

xlim_A = A[0] - delta, A[0] + delta
ylim_A = A[1] - delta, A[1] + delta
xx_A, yy_A = np.meshgrid(np.arange(*xlim_A), np.arange(*ylim_A))

xlim_B = B[0] - delta, B[0] + delta
ylim_B = B[1] - delta, B[1] + delta
xx_B, yy_B = np.meshgrid(np.arange(*xlim_B), np.arange(*ylim_B))

xlim_C = C[0] - delta, C[0] + delta
ylim_C = C[1] - delta, C[1] + delta
xx_C, yy_C = np.meshgrid(np.arange(*xlim_C), np.arange(*ylim_C))

# Solving the equation above for z:
# z = -(a*x + b*y +d) / c
zz_A = -(N_A[0] * xx_A + N_A[1] * yy_A + d_A) / N_A[2]
zz_B = -(N_B[0] * xx_B + N_B[1] * yy_B + d_B) / N_B[2]
zz_C = -(N_C[0] * xx_C + N_C[1] * yy_C + d_C) / N_C[2]


fig = plt.figure()
ax = fig.add_subplot(projection='3d')
ax.plot_surface(xx_A, yy_A, zz_A, alpha=0.5, color='g')
ax.plot_surface(xx_B, yy_B, zz_B, alpha=0.5, color='cyan')
ax.plot_surface(xx_C, yy_C, zz_C, alpha=0.5, color='crimson')





# Plot point.


x_A, y_A, z_A = A
x_B, y_B, z_B = B
x_C, y_C, z_C = C

Plane_A, = ax.plot(x_A, y_A, z_A, marker='o', markersize=5, color='g')
Plane_A.set_label('A position')
ax.legend()

Plane_B, = ax.plot(x_B, y_B, z_B, marker='o', markersize=5, color='cyan')
Plane_B.set_label('B position')
ax.legend()

Plane_C, = ax.plot(x_C, y_C, z_C, marker='o', markersize=5, color='crimson')
Plane_C.set_label('C position')
ax.legend()

# Plot normal.


dx_A, dy_A, dz_A = delta * N_A
ax.quiver(x_A, y_A, z_A, dx_A, dy_A, dz_A, arrow_length_ratio=0.15, linewidth=3, color='g')

dx_B, dy_B, dz_B = delta * N_B
ax.quiver(x_B, y_B, z_B, dx_B, dy_B, dz_B, arrow_length_ratio=0.15, linewidth=3, color='cyan')

dx_C, dy_C, dz_C = delta * N_C
ax.quiver(x_B, y_C, z_C, dx_C, dy_C, dz_C, arrow_length_ratio=0.15, linewidth=3, color='crimson')

# Enforce equal axis aspects so that the normal also appears to be normal.



ax.set_xlim(xmax=1500,xmin=-1500)
ax.set_ylim(ymax=400, ymin=-400)

zlim = max(A[2], B[2], C[2]) - delta, max(A[2], B[2], C[2]) + delta
ax.set_zlim(*zlim)

ax = plt.gca()

#ax.set_box_aspect([1,1,1])
set_axes_equal(ax)
ax.set_xlabel('X', fontsize=20)
ax.set_ylabel('Y', fontsize=20)
ax.set_zlabel('Z', fontsize=20)

plt.show()

解决方案

一、结合法向量拟合抛物面

针对3个点+对应法向量的场景,优先选择二次抛物面模型,贴合你推测的X-Z抛物线、X-Y曲率特征,简化后的模型形式为:
$$z = ax^2 + by^2 + dx + ey + f$$

约束条件构建

每个点需满足两个核心约束:

  1. 点在曲面上:将点坐标代入曲面方程,得到第一个等式;
  2. 法向量与曲面梯度垂直:曲面在该点的梯度为$(\frac{\partial z}{\partial x}, \frac{\partial z}{\partial y}, -1)$,需与法向量$N=(n_x,n_y,n_z)$点积为0,即:
    $$n_x \cdot (2ax + d) + n_y \cdot (2by + e) - n_z = 0$$

三个点共生成6个方程,模型有5个待求参数,属于超定方程组,用最小二乘法求解即可。

代码实现示例

import numpy as np

# 点和法向量数据
points = np.array([A, B, C])
normals = np.array([N_A, N_B, N_C])

# 构建方程组矩阵M和向量b
M = []
b = []
for p, n in zip(points, normals):
    x, y, z = p
    nx, ny, nz = n
    # 曲面约束方程
    M.append([x**2, y**2, x, y, 1])
    b.append(z)
    # 法向量约束方程
    M.append([2 * nx * x, 2 * ny * y, nx, ny, 0])
    b.append(nz)

M = np.array(M)
b = np.array(b)

# 最小二乘求解参数
params, residuals, _, _ = np.linalg.lstsq(M, b, rcond=None)
a, b_coeff, d, e, f = params

# 定义拟合的曲面函数
def fit_surface(x, y):
    return a * x**2 + b_coeff * y**2 + d * x + e * y + f

二、拟合带双曲率的曲面(不考虑法向量)

如果暂时不需要结合法向量,仅基于三个点拟合带X、Y方向曲率的曲面,依然可以用上述二次曲面模型。此时三个点仅能生成3个方程,参数自由度较高,可加入正则化项避免过拟合,或选择圆柱面模型。

圆柱面拟合思路

圆柱面的一般方程为:
$$(x - x_0)^2 + (y - y_0)^2 + (z - z_0)^2 - ( (x - x_0)u + (y - y_0)v + (z - z_0)w )^2 = r^2$$
其中$(u,v,w)$是圆柱轴线的单位方向向量,$(x_0,y_0,z_0)$是轴线上一点,$r$是半径。

可先通过PCA分析点集的主成分确定轴线方向,再代入点坐标用最小二乘法求解半径和轴线位置,降低计算复杂度。


内容的提问来源于stack exchange,提问作者The first man

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 07:15:44