Click here to Skip to main content
15,885,141 members
Articles / Programming Languages / Python

Point Inside 3D Convex Polygon in Python

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
20 Jan 2016CPOL2 min read 14.6K   263   1  
An algorithm to determine if a point is inside a 3D convex polygon for a given polygon vertices in Python

Introduction

This is a Python version of the original C++ algorithm which can be found here.

Background

Please refer to the original C++ algorithm here.

Using the Code

The algorithm is wrapped into a Python class library folder GeoProc. The main test program PythonLASProc reads point cloud data from a LAS file, then writes all inside points into a new LAS file. A LAS file is able to be displayed with a freeware FugroViewer.

To consume the GeoProc library, prepare a GeoPolygonProc instance first like this:

Python
gp = [p1, p2, p3, p4, p5, p6, p7, p8];
gpInst = GeoPolygon(gp);
procInst = GeoPolygonProc(gpInst);

In the above code, gp is a GeoPoint array; gpInst is a GeoPolygon instance created from a GeoPoint array as its vertices; procInst is a GeoPolygonProc instance created from a GeoPolygon instance.

In the main test program PythonLASProc.py, the pronInst is used to check the 3D polygon boundary first to pre-filter the inside points for coarse screening, then use its main public method PointInside3DPolygon to get the actual inside points as below:

Python
if (x > procInst.x0 and x < procInst.x1 and
    y > procInst.y0 and y < procInst.y1 and
    z > procInst.z0 and z < procInst.z1):
    if (procInst.PointInside3DPolygon(x, y, z)):

Here are all Python GeoProc classes:

The codes are almost self-evident with comments.

GeoPoint.py

Python has operator overloading, public function __add__ is a operator + overloading, which is referenced in GeoVector.py.

Python
class GeoPoint(object):
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z
    def __add__(self, p):
        return GeoPoint(self.x + p.x, self.y + p.y, self.z + p.z)

GeoVector.py

This class declares two read-write properties and three read-only properties, public function __mul__ is a multiple operator * overloading, which is referenced in GeoPlane.py.

Python
from GeoPoint import *

class GeoVector(object):

    def __init__(self, p0, p1): # GeoPoint p0, p1        
        self.__p0 = p0 # read write (get set)
        self.__p1 = p1 # read write (get set)
        self.__x = p1.x - p0.x # read only
        self.__y = p1.y - p0.y # read only       
        self.__z = p1.z - p0.z # read only
        
    @property
    def p0(self):        
        return self.__p0
    @p0.setter
    def p0(self, p0):        
        self.__p0 = p0
        self.__x = self.p1.x - p0.x
        self.__y = self.p1.y - p0.y
        self.__z = self.p1.z - p0.z
        
    @property    
    def p1(self):        
        return self.__p1
    @p1.setter
    def p1(self, p1):        
        self.__p1 = p1
        self.__x = p1.x - self.p0.x
        self.__y = p1.y - self.p0.y
        self.__z = p1.z - self.p0.z

    @property     
    def x(self):                
        return self.__x
    
    @property
    def y(self):
        return self.__y
    
    @property
    def z(self):
        return self.__z
    
    def __mul__(self, v): # v: GeoVector        
        x = self.y * v.z - self.z * v.y
        y = self.z * v.x - self.x * v.z
        z = self.x * v.y - self.y * v.x
        p0 = v.p0
        p1 = p0 + GeoPoint(x, y, z)
        return GeoVector(p0, p1)

GeoPlane.py

The class declares 4 public data members, one public static method Create, one unary operator - overloading __neg__, one multiple operator overloading __mul__, these methods are referenced in GeoPolygonProc.py.

Python
from GeoVector import *

class GeoPlane(object):
    
    def __init__(self, a, b, c, d):
        self.a = a
        self.b = b
        self.c = c
        self.d = d    

    @staticmethod
    def Create(p0, p1, p2): # p0, p1, p2: GeoPoint
        
        v = GeoVector(p0, p1)
        u = GeoVector(p0, p2)

        # normal vector
        n = u * v;

        a = n.x
        b = n.y
        c = n.z
        d = - (a * p0.x + b * p0.y + c * p0.z)

        return GeoPlane(a, b, c, d)

    def __neg__(self):
        return GeoPlane(-self.a, -self.b, -self.c, -self.d)
    
    def __mul__(self, pt): # pt: GeoPoint
        return (self.a * pt.x + self.b * pt.y + self.c * pt.z + self.d)

GeoFace.py

The class declares two read-only array properties.

Python
class GeoFace(object):

    # ptList: vertice GeoPoint Array
    # idxList: vertice index Integer Array
    def __init__(self, ptList, idxList):

        #alloc instance array memory
        self.__v = []   # vertice point array
        self.__idx = [] # vertice index array

        self.__n = len(ptList) # number of vertices
                        
        for i in range(0, self.__n):
            self.__v.append(ptList[i])
            self.__idx.append(idxList[i])

    @property
    def v(self):
        return self.__v

    @property
    def idx(self):
        return self.__idx

    @property
    def n(self):
        return self.__n        

GeoPolgyon.py

The class declares two read-only array properties.

Python
class GeoPolygon(object):

    def __init__(self, ptList): # ptList: vertice GeoPoint Array

        #alloc instance array memory
        self.__v = []   # vertice point array
        self.__idx = [] # vertice index array

        self.__n = len(ptList) # number of vertices
                        
        for pt in ptList:
            self.__v.append(pt)
            self.__idx.append(ptList.index(pt))

    @property
    def v(self):
        return self.__v

    @property
    def idx(self):
        return self.__idx

    @property
    def n(self):
        return self.__n

Utility.py

The class delcares a static method to check if a 2d array constains a 1d array as its item. This method is used to avoid adding duplicate faces in GeoPolygonProc.py.

Python
class Utility(object):
    @staticmethod
    def ContainsList(listList, listItem):
        listItem.sort()
        for i in range(0, len(listList)):
            temp = listList[i]
            if(len(temp) == len(listItem)):
                temp.sort()
                itemEqual = True
                for j in range(0, len(listItem)):
                    if(temp[j] != listItem[j]):
                        itemEqual = False
                        break
                if(itemEqual):
                   return True
            else:
                return False
        return False

GeoPolygonProc.py

Please refer to the same section in original C++ version for the explanation.

Python
from Utility import *
from GeoPlane import *
from GeoPoint import *
from GeoFace import *

class GeoPolygonProc(object):

    def __init__(self, polygonInst): # polygonInst: GeoPolygon

        self.__MaxUnitMeasureError = 0.001;

        # Set boundary
        self.__Set3DPolygonBoundary(polygonInst)

        # Set maximum point to face plane distance error,
        self.__Set3DPolygonUnitError()

        # Set faces and face planes
        self.__SetConvex3DFaces(polygonInst)

    @property
    def x0(self):
        return self.__x0
    @property
    def x1(self):
        return self.__x1
    @property
    def y0(self):
        return self.__y0
    @property
    def y1(self):
        return self.__y1
    @property
    def z0(self):
        return self.__z0
    @property
    def z1(self):
        return self.__z1
    @property
    def NumberOfFaces(self):
        return self.__NumberOfFaces
    @property
    def FacePlanes(self):
        return self.__FacePlanes
    @property
    def Faces(self):
        return self.__Faces

    def __Set3DPolygonBoundary(self, polygon): # polygon: GeoPolygon
    
        # List<GeoPoint>
        vertices = polygon.v;

        n = polygon.n;

        xmin = vertices[0].x
        xmax = vertices[0].x
        ymin = vertices[0].y
        ymax = vertices[0].y
        zmin = vertices[0].z
        zmax = vertices[0].z

        for i in range(1, n):
        
            if (vertices[i].x < xmin): xmin = vertices[i].x
            if (vertices[i].y < ymin): ymin = vertices[i].y
            if (vertices[i].z < zmin): zmin = vertices[i].z
            if (vertices[i].x > xmax): xmax = vertices[i].x
            if (vertices[i].y > ymax): ymax = vertices[i].y
            if (vertices[i].z > zmax): zmax = vertices[i].z
                
        self.__x0 = xmin
        self.__x1 = xmax
        self.__y0 = ymin
        self.__y1 = ymax
        self.__z0 = zmin
        self.__z1 = zmax

    def __Set3DPolygonUnitError(self):
        self.MaxDisError = ((abs(self.__x0) + abs(self.__x1) +
                             abs(self.__y0) + abs(self.__y1) +
                             abs(self.__z0) + abs(self.__z1)) / 6 * self.__MaxUnitMeasureError)

    def __SetConvex3DFaces(self, polygon): # polygon: GeoPolygon                             
        
        # vertices indexes for all faces, 2d list                
        faceVerticeIndex = []         

        # face planes for all faces, 1d list       
        fpOutward = []
        
        vertices = polygon.v  
        n = polygon.n
        
        for i in range(0, n):
                    
            # triangle point 1
            
            p0 = vertices[i]
          
            for  j in range(i + 1, n):
                  
                # triangle p             
                p1 = vertices[j]
             
                for k in range(j + 1, n):
                     
                    # triangle point 3
                    
                    p2 = vertices[k]
                    
                    trianglePlane = GeoPlane.Create(p0, p1, p2)
            
                    onLeftCount = 0       
                    onRightCount = 0
                                
                    # indexes of points that is in same plane with face triangle plane           
                    pointInSamePlaneIndex = []           
               
                    for l in range(0, n):
                    
                        # any point other than the 3 triangle points
                        if(l != i and l != j and l != k):
                                               
                            pt = vertices[l]
                                                 
                            dis = trianglePlane * pt
                          
                            # next point is in the triangle plane
                            if(abs(dis) < self.MaxDisError):                                
                                pointInSamePlaneIndex.append(l)                          
                            else:                          
                                if(dis < 0):                             
                                    onLeftCount = onLeftCount + 1                             
                                else:                        
                                    onRightCount = onRightCount + 1

                    # This is a face for a CONVEX 3d polygon.
                    # For a CONCAVE 3d polygon, this maybe not a face.
                    if(onLeftCount == 0 or onRightCount == 0):
                                        
                        verticeIndexInOneFace = [];                                       

                        # add 3 triangle plane vertices
                        verticeIndexInOneFace.append(i);
                        verticeIndexInOneFace.append(j);
                        verticeIndexInOneFace.append(k);

                        m = len(pointInSamePlaneIndex);
                        
                        # add other vertices in the same triangle plane
                        if(m > 0):
                            for p in range(0, m):                      
                                verticeIndexInOneFace.append(pointInSamePlaneIndex[p])
                                                                     
                        # if verticeIndexInOneFace is a new face               
                        if ( not Utility.ContainsList
                           (faceVerticeIndex, verticeIndexInOneFace)):

                            #print(verticeIndexInOneFace)
                            
                            # add it in the faceVerticeIndex list
                            faceVerticeIndex.append(verticeIndexInOneFace)

                            # add the trianglePlane in the face plane list fpOutward.
                            if (onRightCount == 0):                      
                                fpOutward.append(trianglePlane)                  
                            else:
                                if (onLeftCount == 0):                      
                                    fpOutward.append(-trianglePlane)
                        #else:

                            # possible reasons :
                            # 1. the plane is not a face of a convex 3d polygon,
                            #    it is a plane crossing the convex 3d polygon.
                            # 2. the plane is a face of a concave 3d polygon

        # number of faces           
        self.__NumberOfFaces = len(faceVerticeIndex)
        # face list
        self.__Faces = []
        # face planes list
        self.__FacePlanes = []

        for  i in range(0, self.__NumberOfFaces):
        
            self.__FacePlanes.append(GeoPlane(fpOutward[i].a, fpOutward[i].b,
                                              fpOutward[i].c, fpOutward[i].d))

            # face vertices
            v = []
            # face vertices indexes
            idx = []     

            # number of vertices of the face
            count = len(faceVerticeIndex[i])

            for j in range(0, count):
          
                idx.append(faceVerticeIndex[i][j])
                v.append(GeoPoint(vertices[ idx[j] ].x,
                                  vertices[ idx[j] ].y,
                                  vertices[ idx[j] ].z))
          
            self.__Faces.append(GeoFace(v, idx))

    def PointInside3DPolygon(self, x, y, z):
        
        pt = GeoPoint(x, y, z)
        
        for  i in range(0, self.__NumberOfFaces):
            dis = self.__FacePlanes[i] * pt
            # If the point is in the same half space 
            # with normal vector for any face of the cube,
            # then it is outside of the 3D polygon
            if(dis > 0):
                return False
            
        # If the point is in the opposite half space with normal vector for all 6 faces,
        # then it is inside of the 3D polygon            
        return True

PythonLASProc.py

The unpack and pack are used in binary file access which is similar with PHP.

Python
import struct
import sys
sys.path.append('../GeoProc')

from GeoPoint import *
from GeoPolygon import *
from GeoPolygonProc import *

print('Start Processing ...')

try:

    # prepare GeoPolygonProc instance
    p1 = GeoPoint( - 27.28046,  37.11775,  - 39.03485)
    p2 = GeoPoint( - 44.40014,  38.50727,  - 28.78860)
    p3 = GeoPoint( - 49.63065,  20.24757,  - 35.05160)
    p4 = GeoPoint( - 32.51096,  18.85805,  - 45.29785)
    p5 = GeoPoint( - 23.59142,  10.81737,  - 29.30445)
    p6 = GeoPoint( - 18.36091,  29.07707,  - 23.04144)
    p7 = GeoPoint( - 35.48060,  30.46659,  - 12.79519)
    p8 = GeoPoint( - 40.71110,  12.20689,  - 19.05819)
    gp = [p1, p2, p3, p4, p5, p6, p7, p8];        
    gpInst = GeoPolygon(gp)    
    procInst = GeoPolygonProc(gpInst)    

    # open files
    rbFile = open('../LASInput/cube.las', 'rb')    
    wbFile = open('../LASOutput/cube_inside.las', 'wb')    
    wtFile = open('../LASOutput/cube_inside.txt', 'w')
    
    # read LAS public header
    rbFile.seek(96)

    r_point_offset = rbFile.read(4)
    r_variable_len_num = rbFile.read(4)
    r_record_type = rbFile.read(1)
    r_record_len = rbFile.read(2)
    r_record_num = rbFile.read(4)

    rbFile.seek(131)
    r_x_scale = rbFile.read(8)
    r_y_scale = rbFile.read(8)
    r_z_scale = rbFile.read(8)
    r_x_offset = rbFile.read(8)
    r_y_offset = rbFile.read(8)
    r_z_offset = rbFile.read(8)

    point_offset = struct.unpack('I', r_point_offset)
    record_len = struct.unpack('H', r_record_len)
    record_num = struct.unpack('L', r_record_num)    
    x_scale = struct.unpack('d', r_x_scale)
    y_scale = struct.unpack('d', r_y_scale)
    z_scale = struct.unpack('d', r_z_scale)
    x_offset = struct.unpack('d', r_x_offset)
    y_offset = struct.unpack('d', r_y_offset)
    z_offset = struct.unpack('d', r_z_offset)
    
    # read LAS header
    rbFile.seek(0)
    buf = rbFile.read(point_offset[0])

    # write LAS header
    wbFile.write(buf)
   
    insideCount = 0
 
    # read points
    for i in range(0, record_num[0]):
                            
        record_loc = int(point_offset[0]) + int(record_len[0]) * int(i)
            
        rbFile.seek(record_loc)
                
        xi = struct.unpack('l', rbFile.read(4))
        yi = struct.unpack('l', rbFile.read(4))
        zi = struct.unpack('l', rbFile.read(4))
       
        x = (int(xi[0]) * x_scale[0]) + x_offset[0]
        y = (int(yi[0]) * y_scale[0]) + x_offset[0]
        z = (int(zi[0]) * z_scale[0]) + z_offset[0]

        if (x > procInst.x0 and x < procInst.x1 and
            y > procInst.y0 and y < procInst.y1 and
            z > procInst.z0 and z < procInst.z1):
            if (procInst.PointInside3DPolygon(x, y, z)):                

                # read point record
                rbFile.seek(record_loc)
                buf = rbFile.read(record_len[0])

                # write binary point record
                wbFile.write(buf)

                #write text point record
                wtFile.write("%15.6f %15.6f %15.6f\n" % ( x, y, z) )

                insideCount = insideCount + 1
            
    # update point number of ground LAS
    wbFile.seek(107)
    wbFile.write(struct.pack('i', insideCount))
    
finally:
    wbFile.close()
    wtFile.close()
    rbFile.close()
    print('Completed.');

Points of Interest

Python syntax is very special, i.e., indentation is used as code block boundary.

History

  • 20th January, 2016: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Canada Canada
My name is Jiyang Hou (or John Hou). I was born in HeiLongJiang province in north east of China. I got all my educations in China. My university major is Geophysics, but my main professional role is software developer. My biggest accomplishment so far is quit smoking about 5 years ago after almost 20 years smoking history. I am still interested on programming beside making living with it like many other developers. I immigrated to Canada in 2003 and became a permanent resident till now. I live in Calgary, Alberta, Canada. You can reach me by jyhou69@gmail.com regarding to any questions, comments, advice, etc.

Comments and Discussions

 
-- There are no messages in this forum --