2

I have a bunch of points in 3d space (x,y and z) and want to find their perpendicular projection on a surface in python. My surface is created by four points using the following function:

PRECISION = 1e-8    # Arbitrary zero for real-world purposes
def plane_from_points(points):
    centroid = np.mean(points, axis=0)
    _, eigenvalues, eigenvectors = np.linalg.svd(points - centroid)
    if eigenvalues[1] < PRECISION:
        raise ValueError("Points are aligned, can't define a plane")
    normal = eigenvectors[2]
    d = -np.dot(centroid, normal)
    plane = np.append(normal, d)
    thickness = eigenvalues[2]
    return plane, thickness

These are my input points stored as list for creating the surface:

surface_maker=[np.array([[44., 5., 25.],
                        [44., 25., 25.],
                        [59., 5., 75.],
                        [59., 25., 75.]])]

I want to ind the perpedicular projection of the following points in the created surface:

projection_point=[np.array([[49.9,  5., 65.],
                        [48., 17., 69.],
                        [47., 25., 71.9],
                        [60., 5., 39.],
                        [55., 15., 42.1],
                        [57., 25., 40.1]])]

I tried the following code to do so, but it is giving me the horizontal projection while i want to find the perpendilar projection:

pls=[]
for i in surface_maker:
    i=i.tolist()
    pl, thickness= plane_from_points(i)
    pls.append(pl)
point_on_surf=[]
n_iter=1
for i in range (n_iter):
    a, b, c, d = pls[i][0], pls[i][1], pls[i][2], pls[i][3]
    def fun(row):
        row[0] = -(d + b * row[1] + c * row[2]) / a # calculates new x
        return row[0], row[1], row[2] # new x and old y and z
    to_be_projected=[copy.copy(projection_point[i])]
    new_points = np.asarray(list(map(fun, [x for point in to_be_projected for x in point])))
    point_on_surf.append(new_points)

In fig I visualized what I want. I just used different colurs for points to make the figure more readable. For the upper thre point I showed arrows to visualize the projection. My code is giving me the points which are at the end of red arrows but I want to find the projection point that are perpendicular to the surface. In fact, my code is only calculating a new x for the projection_point. In the fig green arrows show the direction I want. I want to do so for all the points existing in projection_point. In advance, I do appreciate any help.

enter image description here

Ali_d
  • 1,089
  • 9
  • 24
  • 1
    Does this answer your question? [How to project a point onto a plane in 3D?](https://stackoverflow.com/questions/9605556/how-to-project-a-point-onto-a-plane-in-3d) –  Apr 07 '21 at 13:43
  • Dear @Paul, Thanks for the link. It is similar but not the same. The first thing is that I want to do it in Python. he second difference is that I just want the projection but in the link it is looking for something more than projection. – Ali_d Apr 07 '21 at 13:58
  • 2
    The linked post gives you the projection of a point to a plane, just as you asked. You have to translate the formulas to Python, which shouldn't be that hard. The only thing you'll also need is the unit normal vector of your plane, which you can get from three points in your plane via a cross product. – M Oehm Apr 07 '21 at 14:29
  • Dear @M Oehm, the point is that I am not that much experience in coding and math and I read the answers in the link but still I cannot figure out how to solve my problem. I do appreciate if you give me more hints. – Ali_d Apr 07 '21 at 14:33

1 Answers1

2

One way to define a plane is by three points P, Q and R. (Four points do not necesarrily lie in the same plane, but yout four points do.) Altenatively, you can define a plane by one point P in the plane and a normal vector n, which you can determine via the cross product.

    n = (Q − P) × (R - P)

Normalize the norml vector, so that you have a unit vector u of length 1:

    u = n   ∕   | n |

You can get the distance d of a point S to the plane from the dot product of the unit normal u with the difference vector from the point in the plane P and S:

    d = (S − P) · u

The distance is signed: It is positive when S is on the side of the plane where u faces and negative when it is on the other side. (It is zero, it S is in the plane, of course.)

You can get the point S′, which is S pojected to the plane, by subtracting d · u from S:

    S′ = S − d · u = S − ((S − P) · u) · u

So, now let's put that into Python. First, Point and Vector classes. (Aren't they the sameß You can see it that way, but I find distingishig between them useful: The difference of two Points is a Vector; a Point plus a Vector is a Point; Dot and cross products make sense for Vectors, but nor for Points. In any case I prefer to have a class with x, y and z members over tuples for spatial Vectors.)

Anyway, here goes:

class Point:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z
        
    def __repr__(self):
        return "Point(%g, %g, %g)" % (self.x, self.y, self.z)
        
    def __sub__(self, other):
        """P - Q"""        
        if isinstance(other, Vector):
            return Point(self.x - other.x,
                         self.y - other.y,
                         self.z - other.z)

        return Vector(self.x - other.x,
                      self.y - other.y,
                      self.z - other.z)
       

class Vector:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z
        
    def __repr__(self):
        return "Vector(%g, %g, %g)" % (self.x, self.y, self.z)
        
    def norm(self):
        """u / |u|"""        
        d = math.sqrt(self.x**2 + self.y**2 + self.z**2)
        
        return Vector(self.x / d, self.y / d, self.z / d)
        
    def __mul__(self, other):
        """dot product u · v or scaling x · u""" 
        if isinstance(other, Vector):        
            return (self.x * other.x
                  + self.y * other.y
                  + self.z * other.z)
            
        return Vector(self.x * other,
                      self.y * other,
                      self.z * other)
        

def cross(a, b):
    return Vector(a.y * b.z - a.z * b.y,
                  a.z * b.x - a.x * b.z,
                  a.x * b.y - a.y * b.x)

These are just the operations we need for your case. Multiplication does double duty: Multiplying two Vectors yields a scalar dot product; multiplying a Vector and a scalar number yields a scaled Vector.

Your plane, reduced to three points:

plane = (
    Point(44.0,  5.0, 25.0),
    Point(44.0, 25.0, 25.0),
    Point(59.0,  5.0, 75.0)
)

The points you want to project:

points = [
    Point(49.9,  5.0, 65.0),
    Point(48.0, 17.0, 69.0),
    Point(47.0, 25.0, 71.9),
    Point(60.0,  5.0, 39.0),
    Point(55.0, 15.0, 42.1),
    Point(57.0, 25.0, 40.1)
]

And the projection code:

x = plane[1] - plane[0]
y = plane[2] - plane[0]
u = cross(x, y).norm()
    
for p in points:
    d = (p - plane[0]) * u
    pp = p - u * d
    
    print(pp)

This will yield the points projected into the plane:

Point(55.4963, 5, 63.3211)
Point(56.4404, 17, 66.4679)
Point(57.156, 25, 68.8532)
Point(49.1743, 5, 42.2477)
Point(49.6147, 15, 43.7156)
Point(49.2294, 25, 42.4312)

That plane is infinitely large, so that the projected points don't necessarily lie between the four points of your surface.

M Oehm
  • 28,726
  • 3
  • 31
  • 42
  • Dear @M Oehm, firstly I do appreciate your help. I have a question. How can I export the values? I mean I do not want them any more as a class. When I try `type(pp)` it says it is a `__main__.Point` but I want to have each `pp` as a single row of a list. I also tried your method for some complicated data sets and In some cases I am facing problems. For example, it is giving some negative values whicle I expect to have positive ones. Thanks in advance for any feedback. – Ali_d Apr 08 '21 at 09:43
  • 1
    Your code uses tuples instead of a Vector or Point class, so `(pp.x, pp.y, pp.z)`. You can use tuples throughout if you like, if you replace every `.x`, `.y` and `.z` with `[0]`, `[1]` and `[2]` respectively. I cannot say anything about the problems you face without the data. – M Oehm Apr 08 '21 at 09:51
  • Dear @M Oehm, You solved the export issue. For negative points lets say `plane = ( Point(18.40923786, 1.5 , 1.66666667), Point(28.04941654, 28.5 , 1.66666667), Point(73.39366277, 28.5 , 98.33333333) ) points = [ Point(41.66666667, 1.5 , 67.57634481), ]`. Then, the projected point has a negative `y` value and I expected to have a positive value – Ali_d Apr 08 '21 at 10:01
  • The projected point is (47.3507, −0.529464, 64.9101). It lies in the plane, but outside the defining triangle. Why do you expect it to have a positive y? Can you visualize the data? Then you will see that the resulting point is the projection of the original point. – M Oehm Apr 08 '21 at 10:16
  • Dear @M Oehm, I expect to have y values for my projections which are in the range min and max of the three points of my plane. I want projected points to be orthogonal to the surface. Points are some times exactly in the edge of plane but still I want their projection to be on the plane. For example, the edge of my plane in the example shown in my comment is `1.5` and I expected that the point with `1.5` `y` value, to be projected exactly on the edge (expected the `y` value of projection to be also `1.5`). – Ali_d Apr 08 '21 at 10:23
  • 1
    My answer treats the plane as infinite plane and it says so. In your case, the direct projection lies outside of the triangle. If you want to move the projected point to the triangle, you must make additional calculations. Perhaps the algorithm above isn't the best approach in that case. Do you want to find the nearest point to P in the surface instead of a "perpendicular projection"? – M Oehm Apr 08 '21 at 10:46
  • Yes, I meant that nearest point in the surface. You could apply an affine transformaton to all points so that the plane is in the X′Y′ plane, set the Z′ coordinates to zero and then move it into the original surface if it is outside. Such an anlgorithm is more complex than what your original question was about. – M Oehm Apr 08 '21 at 11:00
  • Dear @M Oehm, Is it possible to do such modification on the algorithm you wrote me? I really have no idea how to do such things in python or any other programming language. – Ali_d Apr 08 '21 at 11:07
  • Dear @M Oehm, Sorry for bothering in your schedule. Have you any idea how I can modify your method to be useable for tilted surfaces? When my surfaces are completely perpendicular to x direction, it is fine but when the surface is tiled, rojected points are not what I want. In advance, I do appreciate all the help from your side. – Ali_d Apr 09 '21 at 07:14