How can I fit a curve to a 3d point cloud?
Asked Answered
T

2

3

My goal is to fit a line through a point cloud. The point cloud is approximately cylindrical but can be curved, which is why the fitted line should not be straight. I’ve tried several things, but this is my current approach :

I use PyTorch to optimise on a surface equation and compute the loss for each point. However, this does not seem to lead to “good” results, since the plane/surface does not cut the point cloud vertically, which I would expect to lead to the least error. Since I’m new to PyTorch I don’t know whether it’s a mistake in my code or whether there’s a mathematical problem with the idea. This approach is in the code below.

Additionally, once I’ve fitted this surface, I would like to obtain a line on it, but I’m unsure on the best way to do this. My questions are : Why is the fit of the plane/surface not more centred ? How could I then obtain a curved line of best fit on the resulting surface ?

Other approaches I've tried :

  • SVD, but like I said, im looking for a line that is not straight
  • Computing the mean of each z-slice (i.e. each horizontal slice) in the y and x direction and using the resulting points to fit a spline (using python’s splrep). The problem with this approach is that a couple of top or bottom slices can consist of very few points that are not necessarily in the “center” of the point cloud (e.g. are on the left extreme of the top slice), which forces the spline in that direction. It’s really important for the project that the line is “central”.

The data in this code example is just toy data, the real data would have a more complex shape. To check the idea I'm fitting a plane, but in the end I'd like a more complex surface. The code results in this figure.

import numpy as np
import torch
import matplotlib.pyplot as plt
from torch import nn
from torch.functional import F


ix = np.random.uniform(0,2, 1000)
iy = np.random.uniform(0,2, 1000)
iz = np.random.uniform(0,100, 1000)


x = torch.tensor(np.array([  ix, iy ]).T).float()
y = torch.tensor(iz).float()
degree =1


class Model(nn.Module):
    def __init__(self):
        super().__init__()
        if degree == 1 : 
            weights = torch.distributions.Uniform(-1, 1).sample((4,))
        if degree == 3 : 
            weights = torch.distributions.Uniform(-1, 1).sample((8,))

        self.weights = nn.Parameter(weights)        
        
    def forward(self, X):
        if degree ==1 : 
            a_1, a_2, a_3, a_4 = self.weights
            return (a_1 *X[:,0] + a_2*X[:,1] +a_3) 
        if degree == 3 : 
            a_1, a_2, a_3, a_4, a_5, a_6, a_7, a_8 = self.weights
            return (a_1 * X[:,0]**3 + a_2*X[:,1]**3  + a_3*X[:,0]**2 + a_4 *X[:,1] **2  + a_5 *X[:,0] + a_6 *X[:,1] + a_7)


def training_loop(model, optimizer):
    losses = []
    loss = 10000
    it = 0
    if degree == 3 : 
        lim = 0.1
    if degree == 1 : 
        lim = 0.1
    while loss > lim:
        it +=1
        if it > 5000:
            break
        preds1= model(x).float()
        l1 = torch.nn.L1Loss()
        loss = l1(preds1, y)
        loss.backward()
        optimizer.step()
        optimizer.zero_grad()
        losses.append(loss.detach().numpy())
        print(loss)  
    return losses


m = Model()
if degree == 1 :
    opt = torch.optim.Adam(m.parameters(), lr=0.01)
    losses = np.array(training_loop(m, opt))
if degree == 3 : 
    opt= torch.optim.Adam(m.parameters(), lr=0.001)
    losses = np.array(training_loop(m, opt))

params=list(m.parameters())[0].detach().numpy()

X = np.arange(0, 2, 0.1)
Y = np.arange(0, 2, 0.1)
X, Y = np.meshgrid(X, Y)

if degree == 1 : 
    Z = (params[0] * X + params[1]*Y + params[2])
if degree == 3: 
    Z = (params[0] * X**3 + params[1]*Y**3 + params[2]*X**2 + params[3]*Y**2 + params[4]*X + params[5]*Y + params[6])

fig = plt.figure()
ax = plt.axes(projection='3d')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
surf = ax.plot_surface(X, Y, Z, color='tab:orange', alpha = 0.5,linewidth=0, antialiased=False)
ax.scatter3D(ix,iy,iz, alpha = 0.3, s=2)
plt.show()

Edit : I tried the approach in this post : Fit Curve-Spline to 3D Point Cloud However this forces me to specify a source and target for the shortest path. Simply choosing the centers of the lowest and highest slice results in this :

enter image description here

Thermochemistry answered 27/9, 2021 at 17:45 Comment(0)
R
2

You can use Delaunay/Voronoi methods to get an approximation of the medial axis of the point cloud and pass a spline curve through it. See my previous answer here, which does exactly that for points sampled on a cylindrical surface. The figure below was taken from that answer. If your point cloud also has points from inside the bounding surface (and not just samples from the outer surface), you can compute the 3D alpha-shape using the code from this answer and then just take the points on the outer surface and approximate the medial axis as I describe in the answer (or use a different method to extract the medial curve from the triangulated boundary surface).

enter image description here

Ragged answered 11/10, 2021 at 6:33 Comment(2)
thanks for the answer, that's a cool approach! However I just tried this, and I saw that to find the shortest path this still requires me to choose a top and bottom point for the source and target of the path. My problem is that I have no way of getting these points, since choosing the "highest" point forces the line through it, which I don't want to do. I'll include an image of the result of this in the post. Do you have any idea how to use this approach without having to specify source and target points ?Thermochemistry
The choice of source and target points of the path, as you note, requires some kind of apriori knowledge of the application and/or the underlying surface. If your surface is cylindrical, for example, you can use projections on the largest PCA axis (e.g. with sklearn.decomposition.PCA) and take some average of the points at the ends. Probably other such heuristics can be thought of for other types of surfaces as well.Ragged
C
1

If you get an accurate equation defining the cylinder, that should allow you to perform a cartesian (x,y,z) to cylindrical(r,theta,z) space transform. The curved surface of the cylinder should resolve as a flat plane in the cylindrical space. Whatever features and or critical locations from the cylinder can similarly be transformed into this space and be used to draw a line or spline or any other shape that you fancy. Then simply transform the resulting line/spline/shape backwards (cylindrical to cartesian) and you have your curved thing that tracks the surface of the cylinder.

Keep in mind that to perform the cartesian to cylindrical transform, you only really need the center of mass and the primary axis of variation from the cylinder in question (obtained by pca or equivalent).

I also think a devoted 3d processing library such as pcl or open3d is generally better for this type of thing. (where you could run an optimized cylindrical ransac)

Ca answered 27/9, 2021 at 18:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.