2

If I have a square based pyramid of which I know every point (all 5), what is the algorithm to determine if a given point is inside that pyramid or not? I saw a similar question here Check if a point is inside a rectangular shaped area (3D)? and here How do I find if a point exists in a3D solid?. I tried to change the math a bit but didn't work out for me (maybe because I don't understand some parts correctly).

I have seen approaches with creating a normal to each plane (4 triangles + 1 square) and checking on which side of the plane the point lies... however I do not know the correct math for that.

visualization

If someone could help me with the exact math for that (preferably with explanation), it would really help.

Python "Example":

Input:

# Pyarmid Points
A = np.array([2, 2, 4]) # pyramid top
B = np.array([1, 1, 1]) 
C = np.array([1, 3, 3])
D = np.array([3, 3, 1])
E = np.array([3, 1, 1])

Example points

point_inside = np.array([2, 2, 2]) point_outside = np.array([3, 3, 3])

Expectation:

def check_point_in_pyramid(A, B, C, D, E, Point):
# math...

if point_inside_pyramid:

    return true


return false

So for point_inside the function return would be true and for the point_outside it would be false.

I do not need the python code, I just added it for clarification purposes what I need. The math is enough.

Edit after I read the answers: I posted this question on stackoverflow too. Below are more deep level explanations and as far as I could tell a different approach to some extent. Definitely interesting. On stackoverflow are more lightweight answer for those looking. https://stackoverflow.com/questions/68641598/check-if-a-3d-point-is-in-a-square-based-pyramid-or-not

Nils
  • 23

3 Answers3

2

All convex polyhedra can be described as the intersection of four or more half-spaces. We describe a half-space using a plane.

If $\vec{n} = (n_x, n_y, n_z)$ is the normal of the plane, and $d$ is the signed distance from the plane to origin, then point $\vec{p} = (x, y, z)$ is within the half-space if and only if $$\vec{p} \cdot \vec{n} \ge d \tag{1a}\label{BtV1a}$$ which is equivalent to $$x n_x + y n_y + z n_z \ge d \tag{1b}\label{BtV1b}$$ in Cartesian coordinates.

To define a plane or a half-space ($\vec{n}$ and $d$), you can use three points (three consecutive vertices of a polyhedron side polygon): $\vec{v}_1 = (x_1, y_1, z_1)$, $\vec{v}_2 = (x_2, y_2, z_2)$, $\vec{v}_3 = (x_3, y_3, z_3)$: $$\left\lbrace \, \begin{aligned} \vec{n} & = (v_1 - v_2) \times (v_3 - v_2) \\ d & = \vec{n} \cdot \vec{v}_1 = \vec{n} \cdot \vec{v}_2 = \vec{n} \cdot \vec{v}_3 \\ \end{aligned} \right. \tag{2a}\label{BtV2a}$$ where $\times$ is the vector cross product, and you can choose any of the three right sides for $d$ (because they are equivalent). This is equivalent to $$\left\lbrace \, \begin{aligned} n_x &= (y_1 - y_2)(z_3 - z_2) - (z_1 - z_2)(y_3 - y_2) \\ n_y &= (z_1 - z_2)(x_3 - x_2) - (x_1 - x_2)(z_3 - z_2) \\ n_z &= (x_1 - x_2)(y_3 - y_2) - (y_1 - y_2)(x_3 - x_2) \\ d &= n_x x_1 + n_y y_1 + n_z z_1 = n_x x_2 + n_y y_2 + n_z z_2 = n_x x_3 + n_y y_3 + n_z z_3 \\ \end{aligned} \right . \tag{2b}\label{BtV2b}$$ in Cartesian coordinates.

When working with floating-point numbers, you should use maximum of the three right sides for the signed distance $d$, $$d = \max\bigl( n_x x_1 + n_y y_1 + n_z z_1 ,\, n_x x_2 + n_y y_2 + n_z z_2 ,\, n_x x_3 + n_y y_3 + n_z z_3 \bigr)$$ to minimize rounding errors. (For the base of the pyramid, you'll want to use the fourth planar vertex here also, to ensure it too is considered "inside" the pyramid.)

Using the notation in $\eqref{BtV1a}$ and $\eqref{BtV1b}$, the plane normal points to "inside". Depending on the order of the three points above, you can get $\vec{n}$ that points "inside" or "outside". If you know you have point $\vec{p} = (x, y, z)$ "inside" the half-space, just calculate $$\vec{p} \cdot \vec{n} - d = x n_x + y n_y + z n_z - d \tag{3}\label{BtV3}$$ If this is negative, you need to negate both $\vec{n}$ and $d$ (i.e, negate all four scalars $n_x$, $n_y$, $n_z$, and $d$). Then, you can be sure $\vec{n}$ points "inside" the half-space.

In your program, you need to calculate the plane/half-space for each side of the rectangular pyramid; five in total. You can use either the centroid of the vertices (average of the five vertex coordinates) –– because the pyramid is convex, this is always inside it ––, or one of the vertices to verify your normals all point "inside".

Here is an example Python 3 implementation, pyramid.py:

# SPDX-License-Identifier: CC0-1.0
# -*- encoding: utf8 -*-

from math import sqrt

class Halfspace(tuple): """Simple 3D half-space/plane"""

def __new__(cls, x, y, z, d):
    return tuple.__new__(cls, (float(x), float(y), float(z), float(d)))

@property
def normal(self):
    """Normal vector as a tuple"""
    return self[0:3]

@property
def x(self):
    """Normal vector x coordinate"""
    return self[0]

@property
def y(self):
    """Normal vector y coordinate"""
    return self[1]

@property
def z(self):
    """Normal vector z coordinate"""
    return self[2]

@property
def d(self):
    """Signed distance from origin"""
    return self[3]

def contains(self, point):
    """Test if specified point is inside this halfspace"""
    return self[0]*point[0] + self[1]*point[1] + self[2]*point[2] >= self[3]

def __contains__(self, point):
    """Returns True if specified point is inside this halfspace"""
    return self[0]*point[0] + self[1]*point[1] + self[2]*point[2] >= self[3]

def distance(self, point):
    """Signed distance to plane, in units of normal vector length.
       This is positive inside the half-space, negative outside."""
    return self[0]*point[0] + self[1]*point[1] + self[2]*point[2] - self[3]

@classmethod
def fourpoint(cls, p, v1, v2, v3, *vmore):
    """Construct a halfspace from points v1, v2, v3, such that p is inside it"""

    ux, uy, uz = v1[0]-v2[0], v1[1]-v2[1], v1[2]-v2[2]
    vx, vy, vz = v3[0]-v2[0], v3[1]-v2[1], v3[2]-v2[2]
    nx, ny, nz = uy*vz-uz*vy, uz*vx-ux*vz, ux*vy-uy*vx

    # Use the max of the signed distance from origin, to minimize rounding errors
    d = max(nx*v1[0] + ny*v1[1] + nz*v1[2],
            nx*v2[0] + ny*v2[1] + nz*v2[2],
            nx*v3[0] + ny*v3[1] + nz*v3[2])

    # If additional plane vertices are specified, verify they are "inside" also.
    for v in vmore:
        d = max(d, nx*v[0] + ny*v[1] + nz*v[2])

    # Optional: we normalize (nx, ny, nz) to unit length here.
    #           This is only necessary if the .distance() method is used/needed.
    nn = sqrt(nx*nx + ny*ny + nz*nz)
    nx, ny, nz, d = nx/nn, ny/nn, nz/nn, d/nn

    # Check the normal vector direction, and correct (swap) it if necessary.
    if nx*p[0] + ny*p[1] + nz*p[2] < d:
        nx, ny, nz, d = -nx, -ny, -nz, -d

    return cls(nx, ny, nz, d)

class Pyramid(tuple): """Simple rectangular 3D pyramid"""

def __new__(cls, p0, p1, p2, p3, p4):
    """Create a new pyramid with apex at p0, and base p1-p2-p3-p4"""

    # h0 is the base of the pyramid.  We assume it is planar.
    h0 = Halfspace.fourpoint(p0, p1,p2,p3,p4)

    # h1-h4 are the sides of the pyramid.
    h1 = Halfspace.fourpoint(p3, p1,p2,p0)
    h2 = Halfspace.fourpoint(p4, p2,p3,p0)
    h3 = Halfspace.fourpoint(p1, p3,p4,p0)
    h4 = Halfspace.fourpoint(p2, p4,p1,p0)

    return tuple.__new__(cls, (h0, h1, h2, h3, h4))

def contains(self, point):
    """Check if point is inside the pyramid"""
    return (self[0].contains(point) and self[1].contains(point) and
            self[2].contains(point) and self[3].contains(point) and
            self[4].contains(point))

def __contains__(self, point):
    """Returns True if point is inside this pyramid"""
    return (self[0].contains(point) and self[1].contains(point) and
            self[2].contains(point) and self[3].contains(point) and
            self[4].contains(point))

def distance(self, point):
    """Return the distance between point and the surface of the pyramid,
       if the point is inside the pyramid.  Otherwise return -1 (outside)."""
    # Note: This requires Halfspace.fourpoint() normalizes the normal to unit length.
    d = min(self[0].distance(point), self[1].distance(point),
            self[2].distance(point), self[3].distance(point),
            self[4].distance(point))
    if d >= 0:
        return d
    else:
        return -1

Note that the vertices for the base must be listed in (cyclic) order, i.e. tracing the vertices in order as one travels the perimeter of the face. If you specify them in "butterfly" order, you'll get very odd results.

For example, to construct a pyramid P with base $(-1,0,0)$, $(0,-1,0)$, $(1,0,0)$, $(0,1,0)$, and apex at $(0,0,1)$, and to check if point $(0,0,0.5)$ is inside the pyramid, use (in the same directory as above pyramid.py)

import pyramid

P = Pyramid((0,0,1), (-1,0,0), (0,-1,0), (1,0,0), (0,1,0)) if (0,0,0.5) in P: print("Point (0, 0, 0.5) is inside pyramid P.")

In general, this half-space based test tends to be the most efficient test for convex polyhedra. (When there are many faces, it is often useful to define one or more spheres that are completely inside the polyhedron, and/or one (or more) that contain the polyhedron. This is because point-in-sphere test is even more trivial; the former allow shortcuts to "inside", and the latter to "outside".)

For this pyramid case, the test involves 15 multiplications, 10 additions, and 5 comparisons (between floating-point scalars), which is quite efficient.

0

Here's one way to do it. Let the vertices of the pyramid be $v_0,v_1,v_2,v_3$. The set of points inside the pyramid are precisely those points $x$ that can be expressed as a convex combination of the vertices:$$x=av_0+bv_1+cv_2+dv_3,\tag1$$ where $$a,b,c,d\geq0,\ a+b+c+d=1$$ This assumes that "inside" includes points on the surface of the pyramid, including the edges and vertices. If you want to exclude these points, change $a,b,c,d\geq0$ to $a,b,c,d>0$.

To make $(1)$ easier to deal with, subtract $v_0$ from both sides: $$\begin{align} x-v_0&=av_0+bv_1+cv_2+dv_3-(a+b+c+d)v_0\\ &=b(v_1-v_0)+c(v_2-v_0)+d(v_3-v_0) \end{align}$$ which we rewrite as $$x'=bv_1'+cv_2'+dv_3'$$ If you write this out in terms of coordinates, you'll see that it's a system of $3$ linear equations in the $3$ unknown $b,c,d$. Solve for $b,c,d$. $x$ is inside the pyramid if and only if $b,c,d\geq0$ and $b+c+d\leq1$. If you want to exclude the faces, edges, and vertices from the "inside", change both these inequalities to be strict.

Since you are using python, you could use sympy.linsolve or numpy.linalg.solve to solve the system. If the point is very near the surface, or on the surface, roundoff error might be a problem, as is usual with floating point calculations. If the vertices have rational coordinates, then I believe that linsolve will give an exact solution.

I hope you can follow this. Let me know if something isn't clear.

EDIT

I wrote a script to test that linsolve would give exact solutions for rational input, and as expected, this is true. Here's the script, in case it's of use to you.

import sympy as sp

b,c,d = sp.symbols('b,c,d') rat = sp.Rational

vertices = sp.Matrix([ [rat(3,5), rat(2,7), rat(0,1)], [rat(9,11), rat(-7,2), rat(0,1)], [rat(12,7),rat(4,5), rat(0,1)], [rat(9,1), rat(3,2), rat(3,1)]])

x = sp.Matrix([[rat(3,2), rat(5,4), rat(2,3)]])

def inside(vert, x): v0 = vert.row(0) M = sp.zeros(3,3) for k in range(3): M[k,:] = vert.row(k+1)-v0 x = x - v0
system = (M.transpose(), x.transpose()) y = list(sp.linsolve(system))[0] y = (1-sum(y),)+y answer = all(z >=0 for z in y) return answer, y

answer, y = inside(vertices, x) print(answer, y)

For testing

rows = [vertices.row(k) for k in range(4)] total = sp.zeros(1,3) for k in range(4): total += y[k]*rows[k] assert total == x

saulspatz
  • 53,131
0

Given $q$, a test point and the pyramid vertexes $p_k,\ k=1,\cdots,5$ take one of then, for instance $p_5$ and calculate:

$$ \delta_k = p_k-p_5,\ \ k=1,\cdots,4 $$

and then solve the linear programming problem

$$ \min\sum_{k=1}^4\lambda_k\ \ \text{s. t.}\ \ \cases{q-p_5=\sum_{k=1}^4\lambda_k\delta_k\\ \lambda_k\ge 0,\ \ k = 1,\cdots,4} $$

now if $0\le \min\sum_{k=1}^4\lambda_k\le 1$ then $q$ is in the pyramid otherwise not.

Attached a python script which implements this procedure.

from scipy.optimize import linprog
import numpy as np
from random import uniform

pts = [[2, 2, 4],[1, 1, 1],[1, 3, 3],[3, 3, 1],[3, 1, 1]] g = [uniform(1.,3.),uniform(1.,3.),uniform(1.,3.)] print(g)

def inside(pts, g): p5 = pts[4] error = 0.000000001 dp = [] for i in range(4): dp.append(list(np.subtract(np.array(pts[i]),np.array(p5)))) b = list(np.subtract(np.array(g),np.array(p5))) c = [1,1,1,1] A = np.transpose(dp) x0 = (0,0,0,0) x1 = (None,None,None,None) res=linprog(c,A_eq=A,b_eq=b,bounds=list(zip(x0,x1))) ##print(res) if (-error <= res.fun) & (res.fun <= 1+error): return True return False

print(inside(pts, g))

Cesareo
  • 33,252