Slide 1

advertisement
LiDAR simulator
MARV216 project 2012
Ilkka Korpela
Inclined
ground
House
Tree crown
House
LiDAR in a nutshell
LiDAR is flown across the scene
along a trajectory.
tx (X, Y, Z, , , )
tx is a time-point when LiDAR is “fired”
Scan angle 
Target position:
Range
[X,Y,Z] = [X,Y,Z]LiDAR + range ∙ [i, j, k]
==
[X,Y,Z] = p + t * dir
dir = [i,j,k] = f() ∙ g(, , )
Target (X, Y, Z)
(Mirror moves inside the LiDAR, and the
whole LiDAR rotates in 3D).
A real LiDAR can give
raise to and detect
multiple reflections per
pulse transmitted.
Divergence, contains e.g.
1/e2 of the energy
If we send just a single
ray, the divergence is zero
(not 0.3-1.0 mrad as in
real LiDARs).
We can send many per
pulse – call them sub-rays
of a pulse.
Illuminated
area
The ”photon
clump” (pulse).
The leaf (surface)
normal
A sub-ray in a pulse stretches out from LiDAR
position p in direction dir, as far as t says.
[XYZ]hit = p + t ∙ dir
How to define what was hit by the ray?
This is an intersection problem.
A ray – object intersection p.
If the objects comprise 3D
planes, solution for ray-plane
intersection exists and is rather
fast to compute.
3D planes have infinite area. We
will use 3D plane polygons.
They need a class.
For trees, we will use a
function that tells the radius
of the crown envelope, and
another function that tells
the diameter of the stem, for
a given height.
Ray-object intersection is
solved by iteration
(numerically).
Trees will be objects.
How strong reflection?
(How much backscattered
photons to expect at the LiDAR?)
a
Planar (man-made) objects have some
reflectance at the wavelegth of LiDAR (0−1).
It tells (roughly) the proportion light that
scatters.
The indidence angle (a) is important. We assume
that backscatter is reduced by cos(a)**pow. pow
and reflectance are both defined for each
surface material.
Long distance reduces power per unit area
(radiance) at the sensor to the second power.
How strong reflection?
(How much backscattered
photons to expect at the LiDAR?)
A crown has scatters that
potentially produce an echo
(measurable reflection).
Single needle – not enough.
Single leaf – maybe.
Shoot – probably.
Scatterer density [kg/m3]:
- Top-down gradient.
- Clumbs (shoots/branches)
- Foliage at outer hull
Incidence angle – meaningfull?
SKELETON
1. Read the inputs
Scene
1.1. Planar polygons (ground, walls, roof planes)
1.2. Trees
Params 1.3. Flying (Campaign) parameters
1.4. Sensor (LiDAR) parameters
2. Compute the needed trajectory for the whole duration of the flight, for each
moment the LiDAR is fired (transmitted). Let the LiDAR sway.
3. Start the flight, from the moment LiDAR operation begings (time==0). Compute
the mirror angle. Compute the dir-vector of the pulse, and actually dir-vectors of all
N sub-rays. They all start from point p, the position of the LiDAR.
4. Transmit a pulse (made of N sub-rays)
for all sub-rays:
for all planar polygons:
Check intersection
Choose the plane object with shortest distance
for all trees:
Check intersection
Choose tree object with shortest distance
Store the sub-ray result (xyz, distance, reflection strength) to list
Let time grow one unit, and goto 3, unless time is over.
5. Finally write the results (for subrays) to a file.
Wall,0.25,3
10,10,2
10,30,2
10,30,6
10,10,6
10,10,2
Wall,0.25,3
10,10,2
16,10,2
16,10,6
10,10,6
10,10,2
Wall,0.25,3
16,10,2
16,30,2
16,30,6
16,10,6
16,10,2
Wall,0.25,3
10,30,2
10,30,6
16,30,6
16,30,2
10,30,2
Roof,0.18,2
9,9,5.8
9,31,5.8
13,31,10
13,9,10
9,9,5.8
...
1.1 Planar 3D polygons - scene.csv
”name”, reflectance, power
x,y,z
x,y,z
x,y,z,
...
class PolygonSurface:
def __init__(self):
self.Vertices = []
self.refl = None
self.pow = None
self.Box = None
self.A = None
self.B = None
self.C = None
What else?
- Surface normal vector (n = [A,B,C]) is needed
when computing ray-plane intersections
- A bounding box will speed up intersection
calculation calculations
[A,B,C]
Wall,0.25,3
10,10,2
10,30,2
10,30,6
10,10,6
10,10,2
Wall,0.25,3
10,10,2
16,10,2
16,10,6
10,10,6
10,10,2
Wall,0.25,3
16,10,2
16,30,2
16,30,6
16,10,6
16,10,2
Wall,0.25,3
10,30,2
10,30,6
16,30,6
16,30,2
10,30,2
Roof,0.18,2
9,9,5.8
9,31,5.8
13,31,10
13,9,10
9,9,5.8
...
1.1 Planar 3D polygons - reading scene.csv
f = open("scene.csv","r")
Surfaces = [] # To hold the PolygonSurface objects
for line in f:
tinylist = line.split(",")
try:
float(tinylist[0]) # Error for float("Wall")
newpolyline = True
except:
newpolyline = False
if (newpolyline):
Surfaces.append(PolygonSurface())
index = len(Surfaces)-1
Surfaces[index].refl = float(tinylist[1])
Surfaces[index].pow = float(tinylist[2])
else:
x = float(tinylist[0])
y = float(tinylist[1])
z = float(tinylist[2])
Surfaces[index].Vertices.append(Point3D(x,y,z))
f.close()
1.1 Planar 3D polygons - Bounding Box
class BoundingBox:
"""mins and maxs of
def __init__(self,
self.min = mini #
self.max = maxi #
x,y,z that make a non-rotated 3D bounding box"""
mini, maxi):
Point3D object
Point3D object
Include a method in class PolygonSurface:
def SetBox(self):
"""Search for the (min,max) of x,y,z (2 x 3) and store to bounding box"""
mini = Point3D(1E200,1E200, 1E200)
maxi = Point3D(-1E200,-1E200, -1E200)
for i in self.Vertices:
mini.x = min(i.x, mini.x)
mini.y = min(i.y, mini.y)
mini.z = min(i.z, mini.z)
maxi.x = max(i.x, maxi.x)
maxi.y = max(i.y, maxi.y)
maxi.z = max(i.z, maxi.z)
# Make the boundaries of the Box expand 1 m
# in all directions (to be on the safe side)
mini.x -= 1; mini.y -= 1; mini.z -= 1
maxi.x += 1; maxi.y += 1; maxi.z += 1
# Create a BoundingBox object instance and assign to
# attribute Box
self.Box = BoundingBox(mini,maxi)
1.1 Planar 3D polygons - Surface normal vector
Include a method in class PolygonSurface:
def SolveParams(self):
# Solves surface normal vector (A,B,C) from a triangle in the surface.
# See MARV216_project.pdf. Solves linear equations.
# Assign four 3 x 3 matrices. numpy-package is required..
D = numpy.zeros([3,3]); A = numpy.zeros([3,3])
B = numpy.zeros([3,3]); C = numpy.zeros([3,3])
j = -1
for k in range(3):
i = self.Vertices[k]
[A,B,C]
# Take a Point3D in index [k], fill matrices
j += 1
D[j][0] = i.x; D[j][1] = i.y; D[j][2] = i.z
A[j][0] = 1;
A[j][1] = i.y; A[j][2] = i.z
B[j][0] = i.x; B[j][1] = 1;
B[j][2] = i.z
C[j][0] = i.x; C[j][1] = i.y; C[j][2] = 1
d = -1.0
# Compute the determinants of the matrices A-D
D = numpy.linalg.det(D); A = numpy.linalg.det(A)
B = numpy.linalg.det(B); C = numpy.linalg.det(C)
# Normalize the normal vector [A,B,C] to len(one), assign to attributes
self.A = -d/D*A / math.sqrt((-d/D*A)**2+(-d/D*B)**2+(-d/D*C)**2)
self.B = -d/D*B / math.sqrt((-d/D*A)**2+(-d/D*B)**2+(-d/D*C)**2)
self.C = -d/D*C / math.sqrt((-d/D*A)**2+(-d/D*B)**2+(-d/D*C)**2)
print self.A, self.B, self.C
1.1 Planar 3D polygons
Objects (class PolygonSurface)
Stored in list Surfaces
Attributes:
Vertices[i].x (list of Point3D's)
refl
pow
A,B,C
Box (BoundingBox object,
.mini.Point3D, .maxi.Point3D)
Methods:
Surfaces[index].setBox()
Surfaces[index].solveParams()
1.2 Trees in the scene - Trees.csv
id,x,y,z,h,d,hc,sp
1,35,30,1,19,21,6,Conifer
2,37,45,1,24,28,10,Broadleaved
...
class Tree:
# Everything needed to make a tree.
def __init__(self, num, pos, h, dbh, hc, sp):
self.num = num # Some identifier
self.pos = pos # Point3D, tree base
self.h
= h
# Height, m
self.dbh = dbh # Trunk diameter at 1.3-m height, m
self.hc = hc # Height of the base of living crown
self.sp = sp # Species "Conifer" or "Broadleaved"
1.2 Trees in the scene - reading Trees.csv
id,x,y,z,h,d,hc,sp
1,35,30,1,19,21,6,Conifer
2,37,45,1,24,28,10,Broadleaved
...
infile = open("trees.csv",'r')
Trees = []
i = int ; f = float ; s = str
FirstLine = infile.readline()
print FirstLine[0:len(FirstLine)-2]
#
#
#
#
#
Open the file for reading
List will hold the tree objects
Type conversion functions, shortened
Read the header line
Print it, strip the last "\n"
for line in infile:
# Loop the remaining lines
L = line.split(",")
# Split where there is a comma to list L
treepos = Point3D(f(L[1]),f(L[2]),f(L[3]))
# Call the constructor for a Tree(), convert types on-the-fly.
Trees.append(Tree(i(L[0]), treepos ,f(L[4]),f(L[5]),f(L[6]),s(L[7][0:4]) ))
# When done, close the file
infile.close()
1.2 Trees
As objects (class Tree)
Stored in list Trees
Attributes:
.num
.pos
.h
.dbh
.hc
.sp
Methods:
None, but we may create some later....
1.3 Parameters - flying related
class Campaign:
def __init__(self):
self.Startpoint = None
self.Direction = Point3D(0,1,0)
self.Duration = None
self.Flyingspeed = None
Startpoint will be a Point3D that defined the xyz-point when scanning commences.
Direction, we keep this fixed to North (j==1), keeping the altitude (k ==0)
Duration, tells how many seconds we will scan the scene.
Flyingspeed, is groundspeed (though we never know it in advance, due to winds), m/s.
No methods, just a storage class.
1.4 Parameters - Sensor (LiDAR) related
class LiDAR:
def __init__(self):
self.PRF = None
self.Divergence = None
self.ScanFreq = None
self.MaxScanAngle = None
self.NumOfRays = None
#
#
#
#
#
Hz, the pulse repetition frequency
radians, contains 67% energy
Hz, mirror does this many "zigzags" per second
radians, the max zenith angle of the mirror
N, How many sub-rays simulate one pulse
PRF is a crucial parameter in LIDAR, nowadays 25kHz...300kHz.
Divergence, our scene is just 50 x 50 m, so we will fly low, divergence 1...4 mrad?
ScanFreq, typically 20..100 Hz. with PRF, speed, and height define point pattern.
MaxScanAngle, we assume linear (in angle) movement of mirror, swath width!
NumOfRays, The more, the more realistic, and slower...
No methods, just a storage class.
1.3 + 1.4 Parameters - reading from a file
Duration=2.0
Flyingspeed=65.0
Startpoint=25,0,125
Direction=0,1,0
PRF=200000
Divergence=0.001
ScanFreq=50
MaxScanAngle=18
NumOfRays=20
1.3 + 1.4 Parameters - from file
# Parameters - reading from file
c = Campaign() # Construct a Campaign -object
l = LiDAR()
# Construct a LiDAR
-object
Keys =["Duration", "Flyingspeed", "Startpoint", "Direction",\
"PRF","Divergence", "ScanFreq", "MaxScanAngle", "NumOfRays"]
f = open("parameters.txt", "r")
fl = float
for line in f:
a = line.split("=")
if a[0] == Keys[0]: c.Duration = fl(a[1])
if a[0] == Keys[1]: c.Flyingspeed = fl(a[1])
if a[0] == Keys[2]:
b = a[1].split(",")
c.Startpoint = Point3D(fl(b[0]), fl(b[1]), fl(b[2]))
if a[0] == Keys[3]:
b = a[1].split(",")
c.Startpoint = Point3D(fl(b[0]), fl(b[1]), fl(b[2]))
if a[0] == Keys[4]: l.PRF = fl(a[1])
if a[0] == Keys[5]: l.Divergence = fl(a[1])
if a[0] == Keys[6]: l.ScanFreq = fl(a[1])
if a[0] == Keys[7]: l.MaxScanAngle = math.radians(fl(a[1]))
if a[0] == Keys[8]: l.NumOfRays = int(a[1])
Other basic stuff of "input nature"
class Point2D:
# A simple class for storing xy-points/vectors
def __init__(self,x,y):
self.x = float(x)
self.y = float(y)
def norm(self): # The length
return math.sqrt(self.x**2+self.y**2)
class Point3D:
# A simple class for xyz-points/vectors
def __init__(self,x,y,z):
self.x = x
self.y = y
self.z = z
def norm(self):
return math.sqrt(self.x**2+self.y**2+self.z**2)
def normalize(self): # IN PLACE operation, make norm == 1
length = self.norm()
self.x = self.x / length
self.y = self.y / length
self.z = self.z / length
Other basic stuff of "input nature"
Functions
def _3Dnorm(x,y,z):
return math.sqrt(x**2+ y**2 + z**2)
def vector_angle(v1, v2):
"""acute angle between 3Dvectors v1 and v2"""
a = (v1.x*v2.x+v1.y*v2.y+v1.z*v2.z)
b = (v1.norm()*v2.norm())
return math.acos(a/b)
def sign(a):
"""sign of
if a < 0:
if a > 0:
if a == 0:
a"""
return -1
return 1
return 1
def DotProduct(x,y):
"""Gives the dot product (inner product)"""
Dot = 0.0
for i in range(len(x)):
Dot += x[i]*y[i]
return Dot
Other basic stuff of "input nature"
def InsidePolygon(P, Px):
""" Says if Point2D Px is inside P consisting of N Point2D's"""
counter = 0
i = 0
xinters = 0.0
N = len(P)
p1 = Point2D(P[0].x,P[0].y)
for i in range (1, N+1,1):
p2 = Point2D(P[i%N].x,P[i%N].y)
if (Px.y > min(p1.y, p2.y)):
if (Px.y <= max(p1.y, p2.y)):
if (Px.x <= max(p1.x, p2.x)):
if (p1.y != p2.y):
xinters = (Px.y-p1.y)*(p2.x-p1.x)/(p2.y-p1.y)+p1.x
if ((p1.x == p2.x) or (Px.x <= xinters)):
counter = counter + 1
p1 = p2
if (counter%2 == 0):
InsidePolygon = False
else:
InsidePolygon = True
return InsidePolygon
SKELETON
1. Read the inputs
Scene
1.1. Planar polygons (ground, walls, roof planes)
1.2. Trees
Params 1.3. Flying (Campaign) parameters
1.4. Sensor (LiDAR) parameters
2. Compute the needed trajectory for the whole duration of the flight, for each
moment the LiDAR is fired (transmitted). Let the LiDAR sway.
3. Start the flight, from the moment LiDAR operation begings (time==0). Compute
the mirror angle. Compute the dir-vector of the pulse, and actually dir-vectors of all
N sub-rays. They all start from point p, the position of the LiDAR.
4. Transmit a pulse (made of N sub-rays)
for all sub-rays:
for all planar polygons:
Check intersection
Choose the plane object with shortest distance
for all trees:
Check intersection
Choose tree object with shortest distance
Store the sub-ray result (xyz, distance, reflection strength) to list
Let time grow one unit, and goto 3, unless time is over.
5. Finally write the results (for subrays) to a file.
2. Compute the needed trajectory
class Trajectory:
def __init__(self, time, x, y, z, omega, phi, kappa):
self.time = time
# Time runs from 0,1,2... in ticks
self.x = x
# x of LiDAR
self.y = y
# y of LiDAR
self.z = z
# z of LiDAR
self.omega = omega
# LiDAR's rotation over x axis, radians
self.phi = phi
# LiDAR's rotation over y axis, radians
self.kappa = kappa
# LiDAR's rotation over z axis, radians
The pulse-ray is p + t * dir
p = [x,y,z] at time point time
t tells how far the ray goes from p
and dir is the 3D direction [i,j,k].
omega, phi and kappa are needed
as well as mirror angle to compute
dir (we will do that later).
2. The needed trajectory
The needed number of trajectory objects is
l.PRF * c.Duration
One object is perhaps ~ 60 bytes of storage (one int
and 6 floats). So if PRF is 200 kHz and duration is 10
seconds, the storage need is ~133 MegaBytes.
So, for a short flying we can create the trajectory in
advance, store the objects in a list and need not to
create them on-the-fly.
2. The needed trajectory
def ComputeTrajectory(C, L):
# C and L are Campaign and LiDAR objects
t = 0
# time at the beginning, integer 'tick'
TRJ_LIST = [] # List to hold the trajectory objects
Duration = C.Duration * L.PRF # In number of pulses ('ticks')
Tlapse = 1.0/L.PRF
# Time between pulses, seconds
omega = 0; phi = 0; kappa =0 # Aircraft is perfectly aligned
while (t < Duration ):
# The position at time point t.
x = C.Startpoint.x + C.Flyingspeed * Tlapse * t * C.Direction.x
y = C.Startpoint.y + C.Flyingspeed * Tlapse * t * C.Direction.y
z = C.Startpoint.z + C.Flyingspeed * Tlapse * t * C.Direction.z
# Attitude of the plane/sensor, let it float, radians
omega += (random.random()-.5) * 0.0001
phi +=
(random.random()-.5) * 0.0001
# Keeping kappa unchanged.
# If omega and phi wonder too far, limit to +/- 0.05 radians
if abs(omega) > 0.05: omega = sign(omega)*0.05
if abs(phi) > 0.05:
phi
=
sign(phi)*0.05
# Construct a Trajectory object, put it to the list
TRJ_LIST.append(Trajectory(t,x,y,z,omega,phi,kappa))
t +=1 # increment time, by one pulse ('tick')
# Finally, the whole trajectory is known, return the list.
return TRJ_LIST
2. The needed trajectory
Examples of omega(t), phi(t) for a case with t=0...14999 (pulses).
y-axis has the rotation, in radians.
SKELETON
1. Read the inputs
Scene
1.1. Planar polygons (ground, walls, roof planes)
1.2. Trees
Params 1.3. Flying (Campaign) parameters
1.4. Sensor (LiDAR) parameters
2. Compute the needed trajectory for the whole duration of the flight, for each
moment the LiDAR is fired (transmitted). Let the LiDAR sway.
3. Start the flight, from the moment LiDAR operation begings (time==0). Compute
the mirror angle. Compute the dir-vector of the pulse, and actually dir-vectors of all
N sub-rays. They all start from point p, the position of the LiDAR.
4. Transmit a pulse (made of N sub-rays)
for all sub-rays:
for all planar polygons:
Check intersection
Choose the plane object with shortest distance
for all trees:
Check intersection
Choose tree object with shortest distance
Store the sub-ray result (xyz, distance, reflection strength) to list
Let time grow one unit, and goto 3, unless time is over.
5. Finally write the results (for subrays) to a file.
What do we have now?
classes:
Point2D
Point3D
BoundingBox
PolygonSurface
Tree
Campaign
LiDAR
Trajectory
important storage variables:
list with SurfacePolygon_objs
list with Tree_objs
list with Trajectory_objs
identifier with Campaign_obj
identifier with LiDAR_obj
functions:
_3Dnorm((x,y,z))
vector_angle(vectorA,vectorB)
sign(number)
DotProduct(vectorA, vectorB)
InsidePolygon(ListOfPoints2Ds, Point2D)
ComputeTrajectory(Campaign_obj, LiDAR_obj)
3. Start the flight
def main():
# "Houston - Preparing for takeoff".
Echoes = [] # This list will hold the observations
# Read the parameters and construct Campaign and LiDAR objects
c, l = ReadParameterFile("parameters.txt")
TRJ_list = ComputeTrajectory(c, l) # Trajectory - whole flight
S = readsurfaces("Scene.csv")
# Read planar objects to list S
T = readtrees("Trees.csv")
# Trees to list T
# "HOUSTON - PREPARING FOR SCANNING", Mirror angle == Angle
# increment == move mirror this much between pulses, radians
n = 0; Angle = 0.0;
PulsesPerMirrorCycle = l.PRF / l.ScanFreq
increment = (l.MaxScanAngle*4.0)/ PulsesPerMirrorCycle
for TRJ in TRJ_list: # START FLYING WITH LIDAR ON....
n +=1
# Pulse counter
if Angle > 0 and Angle > l.MaxScanAngle: increment = -increment
if Angle < 0 and Angle < -l.MaxScanAngle: increment = -increment
Angle += increment # Update mirror angle, in zigzag pattern
# A. Get a bundle of sub-rays for this Pulse
RayList = computeSubRays(TRJ, Angle,l )
# B. For each sub-ray, find the closest object (range)
# C. Store the closest reflection
# D. Shoot the next Pulse.
# When DONE, Store the observations.
3. Compute dir-vectors for all sub-rays in a pulse
in main(), we had a function call for pulse
RayList = computeSubRays(TRJ, Angle,
l )
Now, TRJ (Trajectory object) has the position (x,y,z) and attitude (omega,
phi, kappa) of the LiDAR for this pulse. Angle holds the mirror angle. l is
the LiDAR object.
We must compute l.NumOfRays sub-rays, filling the angle l.Divergence.
3. Compute dir-vectors for all sub-rays in a pulse
def computeSubRays(TRJ, ScanAngle, LiDAR):
"""Returns a ray pencil of (i,j,k) vectors making the pulse"""
COS = math.cos; SIN = math.sin; TAN = math.tan
RayList = []
for case in range(LiDAR.NumOfRays):
# Assume mirror points down, vector = [0,0,1]
Omega = random.gauss(0,l.Divergence) # shake over x-axis
Phi = random.gauss(0, l.Divergence) # shake over y-axis
# kappa == 0 this means that sin(kappa)==0 and cos(kappa)==1
# 3x3 Rotation matrix becomes
r11= COS(Phi);
r12=0;
r13= SIN(Phi)
r21= SIN(Omega)*SIN(Phi); r22=COS(Omega); r23=-SIN(Omega)*COS(Phi)
r31=-COS(Omega)*SIN(Phi); r32=SIN(Omega); r33= COS(Omega)*COS(Phi)
# Rotate mirror [i,j,k]=R*[0,0,1]
i = r11 * 0 + r12 * 0 + r13 * 1
j = r21 * 0 + r22 * 0 + r23 * 1
k = r31 * 0 + r32 * 0 + r33 * 1
3. Compute dir-vectors for all sub-rays in a pulse
Example:
1000 sub-rays
l.Divergence = 0.0003 (3 milliradians)
[i,j,k] Pattern 100 m away, hits the origin (fig below).
Sdev(x), Sdev(y) = 3 cm.
419 out of 1000 are within radius of 3 cm.
Footprint means 67% of energy (670 s.b. within).
=> Divide Divergence with SQRT(2)
random.gauss(0,l.Divergence/1.4142)
So, we fill the footprint with subrays having the mirror pointing
down - we 'shaked' the (0,0,1)
vector to produce an energy
distribution.
Next we must apply rotation of
the mirror to all sub-rays.
so... Apply rotation of the mirror to all sub-rays.
def compute_SubRays(TRJ, ScanAngle, LiDAR):
...(continues)...
# Mirror is at ScanAngle,
# ScanAngle corrsponds to rotation about the y-axis (Phi)
Phi = ScanAngle
r11 = COS(Phi); r12 = 0; r13 = SIN(Phi)
r21 = 0;
r22 = 1; r23 = 0
r31 = -SIN(Phi); r32 = 0; r33 = COS(Phi)
ii = r11 * i + r12 * j + r13 * k
jj = r21 * i + r22 * j + r23 * k
kk = r31 * i + r32 * j + r33 * k
# Vector [i,j,k] now points to where
# "the mirror says", i.e. to [ii,jj,kk]
3. Compute dir-vectors for all sub-rays in a pulse
The pulse now departs the LiDAR in
known direction [ii,jj,kk], with respect
to the scanangle.
The example below had scanangle of
0.1radians, scanning distance of
distance 100 m. The pattern lies on
the X-axis, ~10 m to the right.
Next, we must rotate the whole
LiDAR that is attached to the
aircraft that rotates in 3D.
omega, phi, kappa rotation
angles are stoted in the
Trajectory object for that pulse.
def compute_SubRays(TRJ, ScanAngle, LiDAR):
...(continues)...
# Next we rotate [ii,jj,kk] to the world coordinate system
# Trajectory object TRJ holds the attitude of the airplane.
Phi = TRJ.phi; Omega = TRJ.omega; Kappa = TRJ.kappa
# Orthogonal 3x3 Rotation matrix
r11 = COS(Phi)*COS(Kappa); r12=-COS(Phi)*SIN(Kappa); r13=SIN(Phi)
r21 = COS(Omega)*SIN(Kappa)+SIN(Omega)*SIN(Phi)*COS(Kappa)
r22 = COS(Omega)*COS(Kappa)-SIN(Omega)*SIN(Phi)*SIN(Kappa)
r23 =-SIN(Omega)*COS(Phi)
r31 = SIN(Omega)*SIN(Kappa)-COS(Omega)*SIN(Phi)*COS(Kappa)
r32 = SIN(Omega)*COS(Kappa)+COS(Omega)*SIN(Phi)*SIN(Kappa)
r33 = COS(Omega) * COS(Phi)
# The [ii,jj,kk] is rotated and assigned back to [i,j,k]
i = r11 * ii + r12 * jj + r13 * kk
j = r21 * ii + r22 * jj + r23 * kk
k = r31 * ii + r32 * jj + r33 * kk
# Normalize it to length one
length = _3Dnorm(i,j,k)
i = i / length ; j = j / length; k = k / length
# Store this sub-ray to RayList as a tuple, negative vector
RayList.append((-i,-j,-k))
3. Compute dir-vectors for all sub-rays in a pulse
Raylist
will hold l.NumOfRays tuples
of floats (i,j,k), i.e. the
direction (dir) vectors of the
sub-rays, making a pulse:
p + t*dir
The list is returned back to
the main().
Now we have for this pulse,
the position p, and the dirvectors of all sub-rays.
It s possible to cast them
down and start to look for
intersections.
4. Transmit a pulse, sub-ray by sub-ray
def main():
# "Houston - Preparing for takeoff".
Echoes = [] # This list will hold the observations
# Read the parameters and construct Campaign and LiDAR objects
c, l = ReadParameterFile("parameters.txt")
TRJ_list = ComputeTrajectory(c, l) # Trajectory - whole flight
S = readsurfaces("Scene.csv")
# Read planar objects to list S
T = readtrees("Trees.csv")
# Trees to list T
# "HOUSTON - PREPARING FOR SCANNING", Mirror angle == Angle
# increment == move mirror this much between pulses, radians
n = 0; Angle = 0.0;
PulsesPerMirrorCycle = l.PRF / l.ScanFreq
increment = (l.MaxScanAngle*4.0)/ PulsesPerMirrorCycle
for TRJ in TRJ_list: # START FLYING WITH LIDAR ON....
n +=1
# Pulse counter
if Angle > 0 and Angle > l.MaxScanAngle: increment = -increment
if Angle < 0 and Angle < -l.MaxScanAngle: increment = -increment
Angle += increment # Update mirror angle, in zigzag pattern
# A. Get a bundle of sub-rays for this Pulse
RayList = compute_SubRays(TRJ, Angle,l )
# B. For each sub-ray, find the closest object (range)
for case in range(len(RayList)):
Object = FindClosest(n, case, RayList[case], c, l, TRJ)
# C. Store the closest reflection
# D. Shoot the next Pulse.
# When DONE, Store the observations.
4. A sub-ray in a pulse - find scane object
def FindClosest(n, case, Ray, S, T, TRJ):
# Pulse-#, Sub-ray-#, Rays' dir vector (tuple), Surfaces, Trees, trajectory
i, j, k = Ray
DistMin = TRJ.z + 1E5
Data = None
# Assign the values from the tuple to i,j,k
# Look for the minimum, candidate to large value
# The return value.
for s in S:
# Loop surfaces S
ValueRet = SurfaceIntersect(n, case, TRJ.x, TRJ.y, TRJ.z, i, j, k, s)
if ValueRet != None:
# We have an intersection for Surface s in S
if ValueRet[4] < DistMin: # Check the distance
DistMin = ValueRet[4]
# New minimum was found
Data = ValueRet
# Surfaces now checked, Data has the minimum case, or None
for t in T: # Now start to check trees in list T
CHit, THit = TreeIntersect(n, case, TRJ.x, TRJ.y, TRJ.z, i,j,k, t)
if CHit[0] < DistMin: # Check the potential crown hit
DistMin = CHit[0] # Set the new minimum
Data = [n, case, "Crown",CHit[4],CHit[0],0,CHit[1], CHit[2], CHit[3],i,j,k]
if THit[0] < DistMin: # There is a potential crown hit
DistMin = THit[0]
Data = [n, case, "Trunk",THit[4],THit[0],0,THit[1], THit[2], THit[3],i,j,k]
return Data
# Trees: [pulse-#, ray-#, Type, distance, reflection, distance, 0, x,y,z, i,j,k]
# Surfaces:[pulse-¤, ray-#, Type, Reflectance, distance, angle, x,y,z, i,j,k]
4. A sub-ray in a pulse - checking intersection
SurfaceIntersect(n, case, TRJ.x, TRJ.y, TRJ.z, i,j,k, s)
TreeIntersect(n, case, TRJ.x, TRJ.y, TRJ.z, i,j,k, t)
ray
s
s
t
t
def SurfaceIntersect(nc, case, x, y, z, i, j, k, Surf):
# nc = pulse #, case = sub-ray #. (x, y, z) = LiDAR position,
# (i,j,k) = direction vector of the sub-ray.
# Surf is a SurfacePolygon -object.
# Ray = (x,y,z) + t * (i,j,k).
# Surface is defined by normal vector n =[A,B,C] and one point, p1.
# Hit is at distance t = ((p1 - (x,y,z)) .dot. n) / ( (i,j,k) .dot. n )
# Note! Here Points/Vectors are tuples [0]==x, [1]==y, [2]==z.
n = (Surf.A, Surf.B, Surf.C)
P1 = (Surf.Pline[0].x,Surf.Pline[0].y, Surf.Pline[0].z)
P = (x,y,z)
Dir = (i,j,k)
Divisor = DotProduct(Dir, n)
P1P = (P1[0]-P[0],P1[1]-P[1],P1[2]-P[2])
Numerator = DotProduct(P1P, n)
t = 0
try :
t = Numerator / Divisor
except:
pass
# We have t solved, make a Point3D (endpoint of ray) at distance t.
test = Point3D(x+i*t, y+j*t, z+k*t)
# BOUNDING-BOX CHECK
if not ((test.x > Surf.Box.min.x and test.x < Surf.Box.max.x) \
and (test.y > Surf.Box.min.y and test.y < Surf.Box.max.y) \
and (test.z > Surf.Box.min.z and test.z < Surf.Box.max.z)):
return None
# TO BE CONTINUED....
... SurfaceIntersect(nc, case, x, y, z, i, j, k, Surf) .. continues
# It is in the Box. Attitude of the surface defines inclusion testing.
# if i ~ 1, surface is ¨aligned with yz-plane. if j ~ 1 it is
# aligned with xz-plane. if k ~ 1 it is aligned with xy-plane.
P = [] # This list holds the 2D test polygon (with Point2D's)
if abs(Surf.A) > 0.9 : # go for the yz-plane option
Px = Point2D(test.y, test.z)
Type = "A"
for m in Surf.Pline:
P.append(Point2D(m.y,m.z))
elif abs(Surf.B) > 0.9: # xz-plane
Px = Point2D(test.x, test.z)
Type = "B"
for m in Surf.Pline:
P.append(Point2D(m.x,m.z))
else: # xy-plane
Px = Point2D(test.x, test.y)
Type = "C"
for m in Surf.Pline:
P.append(Point2D(m.x,m.y))
if (InsidePolygon(P, Px)): # Is Px inside P?
# It is inside, compute angles for both directions of surface normal.
va1 = vector_angle(Point3D(Surf.A,Surf.B,Surf.C), Point3D(i,j,k))
va2 = vector_angle(Point3D(-Surf.A,-Surf.B,-Surf.C), Point3D(i,j,k))
# Return pulse-#, ray-#, Type, reflectance, ditance, angle, hit-point, dir
return [nc,case,Type,Surf.Refl,t,min(va1,va2),test.x,test.y,test.z,i,j,k]
else:
return None # Ray did not hit the surface.
def TreeIntersect(nc, case, x, y, z, i, j, k, t):
# (x,y,z) = LiDAR position (i,j,k) = ray dir-vector, t = tree
treetop = Point3D(t.pos.x, t.pos.y, t.pos.z + t.h)
treebase = Point3D(t.pos.x, t.pos.y, t.pos.z)
crownbase = Point3D(t.pos.x, t.pos.y, t.pos.z + t.hc)
# Ray: (x,y,z) + d * (i,j,k) , solve d for treetop and crownbase z
d_top = (treetop.z-z)/k
# distance | z = treetop.z (apex)
d_base = (treebase.z-z)/k
# distance | z = treebase.z (base)
d_cbase = (crownbase.z-z)/k # distance | z = crownbase.z (base, butt)
# Reflectance of needles
if t.sp == "Coni": refl = 0.35
if t.sp == "Broa": refl = 0.40
# These tuples are the return from this function
CrownHit = (); TrunkHit = ()
# End-point (xt,yt,zt) of the ray, at distance d_top
xt, yt, zt = x + d_top * i, y + d_top * j, z + d_top
# Endpoint's horizontal distance from trunk
d_trunk = math.sqrt((xt-t.pos.x)**2+(yt-t.pos.y)**2)
# Check if we are close enough to continue, a circle
angle = vector_angle(Point3D(0,0,1), Point3D(i,j,k))
limit = math.sin(angle)*t.h + 1.0
if d_trunk > (t.crown_radius(crownbase.z) + limit):
return (1E20,0,0,0,0), (1E20,0,0,0,0)
# The ray
(zt == treetop.z)
* k
around treetop
can not intersect
TreeIntersect() continues...
# The point was inside the test circle, continue with CROWN intersection testing
losses = 0; crownlength = treetop.z-crownbase.z
z_test = zt
while (z_test > crownbase.z): # Loop heights between top and crown base
cr = t.crown_radius(z_test) # Apply the crown model to get crown radius
d_end = (z_test-z)/k
# The end of the ray, solve distance from z_test
xt, yt, zt = x + d_end * i, y + d_end * j, z + d_end * k # the endpoint xyz
d_trunk = math.sqrt((xt-t.pos.x)**2+(yt-t.pos.y)**2)
# distance to trunk
if d_trunk < cr: # if distance is less than crown radius, we are inside
# The new variables (ksii, rc, theta) to test if we get an observation
ksii = (treetop.z-z_test)/crownlength
# Rel. dist. from top
rc = 1 - d_trunk/cr
# Rel. horizontal distance
theta = math.atan2((xt-t.pos.x),(yt-t.pos.y)) # Azimuth -Pi...+Pi
rand = random.random()*TO_RADIANS(60)
# Random rotation 0...60 deg.
thetaf = abs(math.cos(6*phi+rand))
# The branches component
heightf = 1.0
if ksii > .5: heightf = 1 - ksii
# The vertical component
radiusf = 0.2
if rc > 0.25: radiusf = 1-rc+0.05
# Horizontal component
target = heightf*thetaf*radiusf
if (target-losses) > 0.05 :
# Yes!, tuple with dist, x, y, z and strength of backscatter
CrownHit = (d_end, xt, yt, zt,(target-losses)
break # break the while-loop since we have an obs from crown
losses += target
z_test -= 0.30 # Still looping, set the endpoint down 30 cm for next test
TreeIntersect() continues...
# ---- The trunk (cone) below a crown - POOR SOLUTION ---basediam = (1/((t.h-1.3)/t.h)) * t.dbh/100.0
# Parameters for the trunk radius-model (same as the crown model)
tc = 1.0 ; tb = basediam ; ta = 0.0
# The horizontal distances at tree base and crown base
r_base = math.sqrt((x+d_base*i-t.pos.x)**2+(y+d_base*j-t.pos.y)**2)
r_cbase = math.sqrt((x+d_cbase*i-t.pos.x)**2+(y+d_cbase*j-t.pos.y)**2)
# For a reasonable candidate, lets try "intersection"
if r_base < 5 and r_cbase < 5:
d_test = d_base
# d_base = distance to tree base along ray
while d_test > d_cbase:
# Loop from tree base to crown base
z_test = z + d_test*k;
# Solve z from distance along LiDAR ray
ksii = (treetop.z-z_test)/t.h # Current Rel. height, 0 top, 1 = base
r_trunk = math.sqrt((x+d_test*i-t.pos.x)**2+(y+d_test*j-t.pos.y)**2)
r_true = ta + (ksii**tc) * tb
if abs(r_trunk-r_true) < 0.025:
print "Trunk at h:", z_test-treebase.z, "this close", abs(r_trunk-r_true)
TrunkHit = (d_test, x+d_test*i, y+d_test*j, z+d_test*k,0.3-losses)
break
d_test -= 0.05 # Move at small steps
if CrownHit == () : CrownHit = (1E20,0,0,0,0) # Return a large distance
if TrunkHit == () : TrunkHit = (1E20,0,0,0,0) # and zeroes, if no observation
return list(CrownHit), list(TrunkHit)
4. A sub-ray - checking intersection - return values
SurfaceIntersect(n, case, TRJ.x, TRJ.y, TRJ.z, i,j,k, s)
If there is a hit, returns a list, with
[pulse_number,
ray-number,
Type,
Surf.Refl,
t,
angle,
xt,yt,zt
i,j,k]
0...duration*PRF
0...NumOfRays-1
A, B, C depending in plane orientation
0..1 of the surface
Distance to the intersection point
Incidence angle of the pulse and the ray
Point of intersection
The ray's direction vector
TreeIntersect(n, case, TRJ.x, TRJ.y, TRJ.z, i,j,k, t)
returns list(CrownHit), list(TrunkHit)
If there are observations:
(distance, x,y,z, strength of reflection)
(distance, x,y,z, strength of reflection)
If no observation:
(1E20,0,0,0,0)
(1E20,0,0,0,0)
In TreeIntersect() unseen method for class Tree
class Tree:
def __init__(self, num, pos, h, dbh, hc, sp):
self.num = num
self.pos = pos
self.h
= h
self.dbh = dbh
self.hc = hc
self.sp = sp
def crownradius(self,z):
z_cbase = self.pos.z + self.hc # z at crown base
z_top = self.pos.z + self.h
# z at top
if not((z_cbase <= z) and (z <= z_top)):
return 0.0
# z is outside z-range of crown
else:
crown_l = z_top - z_cbase
# length of the crown
ksii = (z_top-z)/float(crown_l) # 0 = top, 1= crown base
a = 0.25
# Constant, flat top
if self.sp == "coni":
# Select parameters f(species)
b = 15*self.dbh/100.0
# cm to m conversion
c = 0.7
if self.sp == "broa":
b = 20*self.dbh/100.0
c = 0.5
return a + b*ksii**c
# Compute the radius. return it
4. Where are we now? - We are DONE!
def main():
# "Houston - Preparing for takeoff".
Echoes = [] # This list will hold the observations
# Read the parameters and construct Campaign and LiDAR objects
c, l = ReadParameterFile("parameters.txt")
TRJ_list = ComputeTrajectory(c, l) # Trajectory - whole flight
S = readsurfaces("Scene.csv")
# Read planar objects to list S
T = readtrees("Trees.csv")
# Trees to list T
# "HOUSTON - PREPARING FOR SCANNING", Mirror angle == Angle
# increment == move mirror this much between pulses, radians
n = 0; Angle = 0.0;
PulsesPerMirrorCycle = l.PRF / l.ScanFreq
increment = (l.MaxScanAngle*4.0)/ PulsesPerMirrorCycle
for TRJ in TRJ_list: # START FLYING WITH LIDAR ON....
n +=1
# Pulse counter
if Angle > 0 and Angle > l.MaxScanAngle: increment = -increment
if Angle < 0 and Angle < -l.MaxScanAngle: increment = -increment
Angle += increment # Update mirror angle, in zigzag pattern
# Get a bundle of sub-rays for this Pulse
RayList = compute_SubRays(TRJ, Angle,l )
# For each sub-ray, find the closest object with a hit
for case in range(len(RayList)):
Object = FindClosest(n, case, RayList[case], c, l, TRJ)
We have the closest object hit by the ray, for each ray. Alternatively.
tree [pulse-#, ray-#, Type, distance, reflection, distance, 0, x,y,z, i,j,k]
plane [pulse-#, ray-#, Type, Reflectance, distance, angle, x,y,z, i,j,k]
STORE THEM TO A LIST OBSERVATIONS FOR OUTPUT AT THE END.
What do we have now?
Functions are used for abstraction - hiding away
implementation details. Classes are used for that
too, although not as much.
Functions:
Classes:
Point2D
Point3D
BoundingBox
PolygonSurface
Tree
Campaign
LiDAR
Trajectory
_3Dnorm((x,y,z))
vector_angle(vectorA, vectorB)
important storage variables:
sign(number)
list with SurfacePolygon_objs
DotProduct(vectorA, vectorB)
list with Tree_objs
InsidePolygon(VertexList, Point2D)
list with Trajectory_objs
readParameterFile("parameters.txt")
identifier with Campaign_obj
readsurfaces("Scene.csv")
identifier with LiDAR_obj
readtrees("Trees.csv")
list with observations, Echoes
ComputeTrajectory(Campaign, LiDAR)
Compute_SubRays(Trajectory, MirrorAngle, LiDAR )
FindClosest(Pulse#, Ray#, (i,j,k), Campaign, LiDAR, Trajectory)
SurfaceIntersect(Pulse#, Ray#, TRJ.x, TRJ.y, TRJ.z, i, j, k, SurfacePolygon)
TreeIntersect(Pulse#, Ray#, TRJ.x, TRJ.y, TRJ.z, i,j,k, Tree)
main()
INPUT-phase
Create the trajectory
c, l= ReadParameterFile()
S = ReadSurfaces()
T = ReadTrees()
TRJ_LIST = ComputeTrajectory()
Fly in a while loop, let time increase at steps of PRF, till duration of flight is reached:
Set mirror Angle for each time point
Transmit:
Get the Ray bundle
Raylist = Compute_SubRays ()
For each sub-ray:
Hit = FindClosestObject():
Loop surfaces for a hit, keep track of closest surfhitcand = SurfaceIntersect()
Loop crowns and later trunks for a hit.
treehitcand = TreeIntersect()
Store the Hit to a DataList
After transmission, increase time
After Flying, write the DataList to a file
VISUALIZE
Handling the large amount of code
Simulator
LiDAR_Classes.py
LiDAR_Functions_IO.py
LiDAR_Functions_Intersect.py
LiDAR_Functions_Misc.py
LiDAR_Main.py
class definitions
file I/O functions
ray-target intersection functions
Other functions
import statements and the main()
The py-files need the import statements, e.g. if a class is used in a function
in a code module, import LiDAR_Classes must be added in tha code module.
numpy package must be installed (for class PolygonSurface).
input files
params.txt
scene.csv
trees.csv
Output files
lidar.csv (data for all ray's with a hit, header row)
Visualization
graph.py
jetColorMAP.csv (defines 64 nice colors (HSV-colors))
Visualization
graph.py
Download