Introduction
This is a PHP 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 PHP class library folder GeoProc
. The main test program PHPLASProc
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:
$gp = [$p1, $p2, $p3, $p4, $p5, $p6, $p7, $p8];
$gpInst = new GeoPolygon($gp);
$procInst = new 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 PHPLASProc.php, 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:
if($x > $procInst->getX0() && $x < $procInst->getX1() &&
$y > $procInst->getY0() && $x < $procInst->getY1() &&
$z > $procInst->getZ0() && $x < $procInst->getZ1() )
{
if($procInst->PointInside3DPolygon($x, $y, $z))
Here are all the GeoProc
classes:
GeoPoint.php
PHP has no operator overloading, a static
function Add
is used to simulate Add
operator for two GeoPoint
. It is referenced in the Multiple
function in GeoVector.php.
The three coordinates member $x
, $y
, $z
are declared as public
to simply its referencing.
<?php
class GeoPoint
{
public $x;
public $y;
public $z;
public function __construct($x, $y, $z)
{
$this->x = $x;
$this->y = $y;
$this->z = $z;
}
public static function Add(GeoPoint $p0, GeoPoint $p1)
{
return new GeoPoint($p0->x + $p1->x, $p0->y + $p1->y, $p0->z + $p1->z);
}
}
?>
GeoVector.php
The static
function Multiple
is to simulate multiple operator for two GeoVector
object. It is referenced in the Create
function in GeoPlane.php.
The GeoVector
member $p0
, $p1
, $x
, $y
, $z
are declared as private
and use five public get
methods to simulate its getter.
<?php
require_once ('GeoPoint.php');
class GeoVector
{
private $p0;
private $p1;
private $x;
private $y;
private $z;
public function getP0() {return $this->p0;}
public function getP1() {return $this->p1;}
public function getX() {return $this->x;}
public function getY() {return $this->y;}
public function getZ() {return $this->z;}
public function __construct(GeoPoint $p0, GeoPoint $p1)
{
$this->p0 = $p0;
$this->p1 = $p1;
$this->x = $p1->x - $p0->x;
$this->y = $p1->y - $p0->y;
$this->z = $p1->z - $p0->z;
}
public static function Multiple(GeoVector $u, GeoVector $v)
{
$x = $u->y * $v->z - $u->z * $v->y;
$y = $u->z * $v->getX() - $u->x * $v->getZ();
$z = $u->x * $v->getY() - $u->y * $v->getX();
$p0 = $v->getP0();
$p1 = GeoPoint::Add($p0, new GeoPoint($x, $y, $z));
return new GeoVector($p0, $p1);
}
}
?>
GeoPlane.php
This is a generic declaration for a geometry plane, it is used to describe a face plane where a face of the 3D polygon lies in. It is referenced in a GeoPlane
array $FacePlanes
in GeoPolygonProc.php.
<?php
require_once ('GeoPoint.php');
require_once ('GeoVector.php');
class GeoPlane
{
private $a;
private $b;
private $c;
private $d;
public function getA() {return $this->a;}
public function getB() {return $this->b;}
public function getC() {return $this->c;}
public function getD() {return $this->d;}
public function __construct($a, $b, $c, $d)
{
$this->a = $a;
$this->b = $b;
$this->c = $c;
$this->d = $d;
}
public static function Create
(GeoPoint $p0, GeoPoint $p1, GeoPoint $p2)
{
$v = new GeoVector($p0, $p1);
$u = new GeoVector($p0, $p2);
$n = GeoVector::Multiple($u, $v);
$a = $n->getX();
$b = $n->getY();
$c = $n->getZ();
$d = - ($a * $p0->x + $b * $p0->y + $c * $p0->z);
return new GeoPlane($a, $b, $c, $d);
}
public static function Negative(GeoPlane $pl)
{
return new GeoPlane( - $pl->getA(), - $pl->getB(), - $pl->getC(), - $pl->getD());
}
public static function Multiple(GeoPlane $pl, GeoPoint $pt)
{
return ($pl->getA() * $pt->x + $pl->getB() *
$pt->y + $pl->getC() * $pt->z + $pl->getD());
}
}
?>
GeoPolygon.php
The class declared the 3D polgyon with its all vertices. It is used to construct GeoPolygonProc
instance with initializing its boundary, face planes and faces. Its references are in GeoPolygonProc
.
<?php
class GeoPolygon
{
private $v;
private $idx;
private $n;
public function getV(){return $this->v;}
public function getIdx(){return $this->idx;}
public function getN(){return $this->n;}
public function __construct($v)
{
$this->v = [];
$this->idx = [];
$this->n = count($v);
for($i=0; $i< $this->n; $i++)
{
array_push($this->v, $v[$i]);
array_push($this->idx, $i);
}
}
public function __destruct()
{
unset($this->v);
unset($this->idx);
}
}
?>
GeoFace.php
The class declares a 3D polygon face with the face vertices. The face vertices indexes are from a GeoPolygon
vertice indexes which the face belongs to.
<?php
class GeoFace
{
private $v;
private $idx;
private $n;
public function getV(){return $this->v;}
public function getIdx(){return $this->idx;}
public function getN(){return $this->n;}
public function __construct(array $v, array $idx)
{
$this->v = [];
$this->idx = [];
$this->n = count($v);
for($i=0; $i< $this->n; $i++)
{
array_push($this->v, $v[$i]);
array_push($this->idx, $idx[$i]);
}
}
public function __destruct()
{
unset($this->v);
unset($this->idx);
}
}
?>
Utility.php
The class has only one function to check if a 2d array contains a 1d array. The function is used to prevent the 2d face index array $faceVerticeIndex
from adding the duplicate new face $verticeIndexInOneFace
in GeoPolygonProc.php.
<?php
class Utility
{
public static function ContainsList
($listList, $listItem)
{
if($listList == null || $listItem == null)
{
return false;
}
sort($listItem);
for ($i = 0; $i < count($listList); $i ++ )
{
$temp = $listList[$i];
if (count($temp) == count($listItem))
{
sort($temp);
$itemEqual = true;
for($j = 0; $j < count($listItem); $j ++ )
{
if($temp[$j] != $listItem[$j])
{
$itemEqual = false;
break;
}
}
if($itemEqual)
{
return true;
}
}
else
{
return false;
}
}
return false;
}
}
?>
GeoPolygonProc.php
This is the main class for referencing the GeoProc
library. It is capable of extending its functions to do more complex 3D polygon geometry processing based on the GeoProc
library. The current GeoPolygonProc
provides basic settings for 3D polygon boundary, face planes and faces. The particular public
function PointInside3DPolygon
is a role model for adding other functions to solve 3D polygon problems.
SetConvex3DFaces
is the essential method, it extracts all faces information from the simple vertices. The procedure is:
- Declare a 2d array
$faceVerticeIndex
, first dimension is face index, second dimension is face vertice index. - Go through all given vertices, construct a triangle plane with any 3 vertices combination.
- Determine if the triangle plane is a face through checking if all other vertices in same half space.
- Declare a 1d array
$verticeIndexInOneFace
for a complete vertices indexes in one face. - Find other vertices in the same plane with the triangle plane, put them in 1d array
$pointInSamePlaneIndex
. - Add unique face to
$faceVerticeIndex
with adding the 3 triangle vertices and the same plane vertices. - Get all face planes and faces.
<?php
require_once ('GeoPoint.php');
require_once ('GeoVector.php');
require_once ('GeoPlane.php');
require_once ('GeoPolygon.php');
require_once ('GeoFace.php');
require_once ('Utility.php');
class GeoPolygonProc
{
private $MaxUnitMeasureError = 0.001;
private $Faces;
private $FacePlanes;
private $NumberOfFaces;
private $x0, $y0, $x1, $y1, $z0, $z1;
public $MaxDisError;
public function getFaces(){return $this->Faces;}
public function getFacePlanes(){return $this->FacePlanes;}
public function getNumberOfFaces(){return $this->NumberOfFaces;}
public function getX0(){return $this->x0;}
public function getX1(){return $this->x1;}
public function getY0(){return $this->y0;}
public function getY1(){return $this->y1;}
public function getZ0(){return $this->z0;}
public function getZ1(){return $this->z1;}
public function __construct($polygonInst)
{
$this->Set3DPolygonBoundary($polygonInst);
$this->Set3DPolygonUnitError();
$this->SetConvex3DFaces($polygonInst);
}
public function __destruct()
{
unset($this->Faces);
unset($this->FaceFacePlaness);
}
private function Set3DPolygonBoundary($polygon)
{
$vertices = $polygon->getV();
$n = $polygon->getN();
$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 = 1; $i < $n; $i ++ )
{
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;
}
$this->x0 = $xmin;
$this->x1 = $xmax;
$this->y0 = $ymin;
$this->y1 = $ymax;
$this->z0 = $zmin;
$this->z1 = $zmax;
}
private function Set3DPolygonUnitError()
{
$avarageError = (abs($this->x0) + abs($this->x1) +
abs($this->y0) + abs($this->y1) +
abs($this->z0) + abs($this->z1)) / 6;
$this->MaxDisError = $avarageError * $this->MaxUnitMeasureError;
}
private function SetConvex3DFaces($polygon)
{
$this->Faces = [];
$this->FacePlanes = [];
$faceVerticeIndex = [];
$fpOutward = [];
$vertices = $polygon->getV();
$n = $polygon->getN();
for($i = 0; $i < $n; $i ++ )
{
$p0 = $vertices[$i];
for($j = $i + 1; $j < $n; $j ++ )
{
$p1 = $vertices[$j];
for($k = $j + 1; $k < $n; $k ++ )
{
$p2 = $vertices[$k];
$trianglePlane = GeoPlane::Create($p0, $p1, $p2);
$onLeftCount = 0;
$onRightCount = 0;
$pointInSamePlaneIndex = [];
for($l = 0; $l < $n ; $l ++ )
{
if($l != $i && $l != $j && $l != $k)
{
$pt = new GeoPoint($vertices[$l]->x,
$vertices[$l]->y, $vertices[$l]->z);
$dis = GeoPlane::Multiple($trianglePlane, $pt);
if(abs($dis) < $this->MaxDisError)
{
array_push($pointInSamePlaneIndex, $l);
}
else
{
if($dis < 0)
{
$onLeftCount ++ ;
}
else
{
$onRightCount ++ ;
}
}
}
}
if($onLeftCount == 0 || $onRightCount == 0)
{
$verticeIndexInOneFace = [];
array_push($verticeIndexInOneFace, $i);
array_push($verticeIndexInOneFace, $j);
array_push($verticeIndexInOneFace, $k);
$m = count($pointInSamePlaneIndex);
if($m > 0)
{
for($p = 0; $p < $m; $p ++ )
{
array_push($verticeIndexInOneFace, $pointInSamePlaneIndex[$p]);
}
}
if ( ! Utility::ContainsList($faceVerticeIndex, $verticeIndexInOneFace))
{
array_push($faceVerticeIndex, $verticeIndexInOneFace);
if ($onRightCount == 0)
{
array_push($fpOutward, $trianglePlane);
}
else if ($onLeftCount == 0)
{
array_push($fpOutward, GeoPlane::Negative($trianglePlane));
}
}
}
else
{
}
}
}
}
$this->NumberOfFaces = count($faceVerticeIndex);
for ($i = 0; $i < $this->NumberOfFaces; $i ++ )
{
array_push($this->FacePlanes, new GeoPlane(
$fpOutward[$i]->getA(), $fpOutward[$i]->getB(),
$fpOutward[$i]->getC(), $fpOutward[$i]->getD() ));
$gp = [];
$vi = [];
$count = count($faceVerticeIndex[$i]);
for ($j = 0; $j < $count; $j ++ )
{
array_push($vi, $faceVerticeIndex[$i][$j]);
array_push($gp, new GeoPoint($vertices[ $vi[$j] ]->x,
$vertices[ $vi[$j] ]->y,
$vertices[ $vi[$j] ]->z));
}
array_push($this->Faces, new GeoFace($gp, $vi));
}
unset($faceVerticeIndex);
unset($fpOutward);
unset($pointInSamePlaneIndex);
unset($verticeIndexInOneFace);
unset($gp);
unset($vi);
}
public function PointInside3DPolygon($x, $y, $z)
{
$pt = new GeoPoint($x, $y, $z);
for ($i = 0; $i < $this->NumberOfFaces; $i ++ )
{
$dis = GeoPlane::Multiple($this->FacePlanes[$i], $pt);
if ($dis > 0)
{
return false;
}
}
return true;
}
}
?>
PHPLASProc.php
This is main test program to consume the GeoProc
library, it reads original LAS file cube.las, then gets all inside points against the given 8 3D polygon vertices while writing them to a new LAS file and a coordinate plain text file.
<?php
require_once ('../GeoProc/GeoPoint.php');
require_once ('../GeoProc/GeoVector.php');
require_once ('../GeoProc/GeoPlane.php');
require_once ('../GeoProc/GeoPolygon.php');
require_once ('../GeoProc/GeoFace.php');
require_once ('../GeoProc/GeoPolygonProc.php');
$p1 = new GeoPoint( - 27.28046, 37.11775, - 39.03485);
$p2 = new GeoPoint( - 44.40014, 38.50727, - 28.78860);
$p3 = new GeoPoint( - 49.63065, 20.24757, - 35.05160);
$p4 = new GeoPoint( - 32.51096, 18.85805, - 45.29785);
$p5 = new GeoPoint( - 23.59142, 10.81737, - 29.30445);
$p6 = new GeoPoint( - 18.36091, 29.07707, - 23.04144);
$p7 = new GeoPoint( - 35.48060, 30.46659, - 12.79519);
$p8 = new GeoPoint( - 40.71110, 12.20689, - 19.05819);
$gp = [$p1, $p2, $p3, $p4, $p5, $p6, $p7, $p8];
$gpInst = new GeoPolygon($gp);
$procInst = new GeoPolygonProc($gpInst);
$lasFile = "../LASInput/cube.las";
$rbFile = fopen($lasFile, "rb");
$wbFile = fopen("../LASOutput/cube_inside.las", "wb");
$wtFile = fopen("../LASOutput/cube_inside.txt", "w");
fseek($rbFile, 96);
$offset_to_point_data_value = unpack('V', fread($rbFile, 4))[1];
$variable_len_num = unpack('V', fread($rbFile, 4))[1];
$record_type = unpack('C', fread($rbFile, 1))[1];
$record_len = unpack('v', fread($rbFile, 2))[1];
$record_num = unpack('V', fread($rbFile, 4))[1];
fseek($rbFile, 131);
$x_scale = unpack('d', fread($rbFile, 8))[1];
$y_scale = unpack('d', fread($rbFile, 8))[1];
$z_scale = unpack('d', fread($rbFile, 8))[1];
$x_offset = unpack('d', fread($rbFile, 8))[1];
$y_offset = unpack('d', fread($rbFile, 8))[1];
$z_offset = unpack('d', fread($rbFile, 8))[1];
fseek($rbFile, 0);
$headerBuffer = fread($rbFile, $offset_to_point_data_value);
fwrite($wbFile, $headerBuffer);
$insideCount = 0;
for($i = 0; $i < $record_num; $i++)
{
$record_loc = $offset_to_point_data_value + $record_len * $i;
fseek($rbFile, $record_loc);
$xi = unpack('l', fread($rbFile, 4))[1];
$yi = unpack('l', fread($rbFile, 4))[1];
$zi = unpack('l', fread($rbFile, 4))[1];
$x = $xi * $x_scale + $x_offset;
$y = $yi * $y_scale + $y_offset;
$z = $zi * $z_scale + $z_offset;
if($x > $procInst->getX0() && $x < $procInst->getX1() &&
$y > $procInst->getY0() && $x < $procInst->getY1() &&
$z > $procInst->getZ0() && $x < $procInst->getZ1() )
{
if($procInst->PointInside3DPolygon($x, $y, $z))
{
fprintf($wtFile, "%15.6f %15.6f %15.6f \n", $x, $y, $z);
fseek($rbFile, $record_loc);
$recordBuffer = fread($rbFile, $record_len);
fwrite($wbFile, $recordBuffer);
$insideCount++;
}
}
}
fseek($wbFile, 107);
fwrite($wbFile, pack('V', $insideCount), 4);
fclose($rbFile);
fclose($wbFile);
fclose($wtFile);
?>
Points of Interest
The PHP binary file process provides fopen
, fread
, fseek
, fwrite
and fclose
methods, this looks like very similar with C style, but its distinctive "pack
" and "unpack
" methods must be used, and the Big Endian or Little Endian bytes order must be considered, these features are also used in Python.
History
- 17th January, 2016: Initial version