-
Notifications
You must be signed in to change notification settings - Fork 44
Python bindings ๐ Details
If you are just getting started with pyhelios
, we recommend to read the pyhelios: Getting-Started page first. In the following, we will explain more details on how to access information about the survey and the scene and how to manipulate these.
With pyhelios
, we can add coordinate transformation filters to the entire scene or to individual sceneparts. They can be applied with the SimulationBuilder
. We first create a SimulationBuilder
object as shown in pyhelios: Getting-Started, using tls_arbaro_demo.xml
as our demo survey.
import pyhelios
pyhelios.loggingQuiet()
# Build simulation parameters
simBuilder = pyhelios.SimulationBuilder(
'data/surveys/demo/tls_arbaro_demo.xml',
'assets/',
'output/'
)
simBuilder.setNumThreads(0)
simBuilder.setLasOutput(True)
simBuilder.setZipOutput(True)
The addRotateFilter()
function takes five arguments: the real term and the three imaginary terms of the rotation quaternion, and the ID of the scenepart to be rotated. If the entire scene should be rotated, an empty string can be provided.
General rotation of the entire scene:
from math import pi, cos, sin
# 90 degree rotation about z axis
q0 = cos(90/2)
q1 = 0
q2 = 0
q3 = sin(90/2)
simBuilder.addRotateFilter(q0, q1, q2, q3, "")
Rotation of a specific scenepart:
from math import pi, cos, sin
q0 = cos(90/2)
q1 = 0
q2 = 0
q3 = sin(90/2)
sp_id = "1" # First element in the scene XML file or the element, for which <part id="1"> was explicitly defined
simBuilder.addRotateFilter(q0, q1, q2, q3, sp_id)
The addTranslateFilter
function takes four arguments: The magnitude of translation in x, y and z direction, and the scenepart ID.
t = [0, 0, 2] #translate upwards by two units
sp_id = "1" # First element in the scene XML file or the element, for which <part id="1"> was explicitly defined
simBuilder.addTranslateFilter(t[0], t[1], t[2], sp_id)
The addScaleFilter
function takes two arguments: The scaling factor and the scenepart ID.
s = 5 # scale by factor 5
sp_id = "1" # First element in the scene XML file or the element, for which <part id="1"> was explicitly defined
simBuilder.addScaleFilter(s, sp_id)
After this, we can build the survey:
simB = simBuilder.build()
The build()
-function returns a SimulationBuild
object.
Once we built a survey, we have numerous options to obtain and change the characteristics of all components of our simulation. Note that after the steps above, simB
is a SimulationBuild
object. To access the Simulation
itself, we have to call simB.sim
.
# obtain survey path and name
survey_path = simB.sim.getSurveyPath()
survey = simB.sim.getSurvey()
survey_name = survey.name
We can also obtain the survey length, i.e. the distance through all waypoints. If the survey has not been running yet, survey.getLength()
will return 0.0. We can calculate the length of a loaded survey of a simulation which was built but not started with survey.calculateLength()
.
survey.calculateLength()
length = survey.getLength()
If the survey was already started, the length will automatically be calculated and survey.getLength()
returns the survey length.
scanner = simB.sim.getScanner()
# print scanner characteristics (device, average power, beam divergence, wavelength, atmospheric visibility)
print(scanner.toString())
Example output:
'Scanner: riegl_vq-880g Power: 4.000000 W Divergence: 0.300000 mrad Wavelength: 1064 nm Visibility: 23.000000 km'
They can also individually accessed:
scanner.deviceId
scanner.averagePower
scanner.beamDivergence
scanner.wavelength*1000000000 # has to be converted from m to nm
scanner.visibility
Some more properties:
scanner.numRays # number of subsampling rays
scanner.pulseLength_ns
list(scanner.getSupportedPulseFrequencies()) # supported pulse frequencies of the scanner
head = scanner.getScannerHead()
# get scanner rotation speed and range (TLS)
head.rotatePerSec
head.rotateRange
head.getMountRelativeAttitude().q0 # getMountRelativeAttitude() yields a pyhelios rotation (quaternion)
head.getMountRelativeAttitude().q1
head.getMountRelativeAttitude().q2
head.getMountRelativeAttitude().q3
head.getMountRelativeAttitude().getAngle()
head.getMountRelativeAttitude().getAxis().x
head.getMountRelativeAttitude().getAxis().y
head.getMountRelativeAttitude().getAxis().z
deflector = scanner.getBeamDeflector()
detector = scanner.getDetector()
detector.accuracy
detector.rangeMin
detector.rangeMax
Note, that these can be overwritten in the scannerSettings
of a leg.
scanner.fwfSettings.binSize_ns
scanner.fwfSettings.winSize_ns
scanner.fwfSettings.beamSampleQuality
Each leg has scanner settings and platform settings, like in the XML file (see XML tag and parameter summary), which can be accessed and changed with pyhelios
.
import numpy as np
# get the first leg
leg = simB.sim.getLeg(0)
# scanner settings
leg.getScannerSettings().active # boolean; True or False
leg.getScannerSettings().pulseFreq
leg.getScannerSettings().scanAngle # in radians
scanAngle_deg = leg.getScannerSettings().scanAngle * 180 / np.pi
leg.getScannerSettings().verticalAngleMin
leg.getScannerSettings().verticalAngleMax
leg.getScannerSettings().scanFreq
leg.getScannerSettings().beamDivAngle
leg.getScannerSettings().trajectoryTimeInterval
leg.getScannerSettings().headRotateStart
leg.getScannerSettings().headRotateStop
leg.getScannerSettings().headRotatePerSec
Scanner Settings and platform settings may be defined through a template (see Scanner settings and platform settings). The template can be accessed for a given ScannerSettings or PlatformSettings instance:
ss = leg.getScannerSettings()
if ss.hasTemplate():
ss_tmpl = ss.getTemplate()
print(ss_tmpl.pulseFreq) # Print the pulse frequency defined in the template
print(ss_tmpl.id) # Print the template's ID
# Do other stuff with tmpl here, e.g. modify tempate scanner settings
ps = leg.getPlatformSettings()
if ps.hasTemplate():
ps_tmpl = ps.getTemplate()
print(ps_tmpl.id) # Print the template's ID
print(ps_tmpl.movePerSec) # Print the platform speed defined in the template
print(ps_tmpl.z) # Print the platform altitude defined in the template
# platform settings
leg.getPlatformSettings().onGround
leg.getPlatformSettings().movePerSec
leg.getPlatformSettings().stopAndTurn # specific to copter platform
leg.getPlatformSettings().yawAtDeparture # specific to copter platform
# position
leg.getPlatformSettings().x
leg.getPlatformSettings().y
leg.getPlatformSettings().z
When loading a survey, a shift is applied to the scene and to each leg. We can obtain this shift:
scene = simB.sim.getScene()
shift = scene.getShift()
print(f'Shift = {shift.x},{shift.y},{shift.z}')
Example output for data/toyblocks/als_toyblocks.xml
:
Shift = -50.0,-70.0,-0.233912
Using a for-loop, we can get all leg positions. Note that we add the shift
to obtain the coordinates as specified in the XML-file:
for i in range(simB.sim.getNumLegs()):
leg = simB.sim.getLeg(i)
print(f'Leg {i}\tposition = '
f'{leg.getPlatformSettings().x+shift.x},'
f'{leg.getPlatformSettings().y+shift.y},'
f'{leg.getPlatformSettings().z+shift.z}\t'
f'active = {leg.getScannerSettings().active}')
Example output for data/toyblocks/als_toyblocks.xml
:
Leg 0 position = -30.0,-50.0,100.0 active = True
Leg 1 position = 70.0,-50.0,100.0 active = False
Leg 2 position = 70.0,0.0,100.0 active = True
Leg 3 position = -30.0,0.0,100.0 active = False
Leg 4 position = -30.0,50.0,100.0 active = True
Leg 5 position = 70.0,50.0,100.0 active = False
We can also use a for-loop to create new legs. Here an example, where we initiate a simulation with a survey with no legs and then create the legs in the python file.
import pyhelios
pyhelios.loggingDefault()
default_survey_path = "data/surveys/default_survey.xml"
# default survey with the toyblocks scene (missing platform and scanner definition and not containing any legs)
survey = """
<?xml version="1.0" encoding="UTF-8"?>
<document>
<survey name="some_survey" scene="data/scenes/toyblocks/toyblocks_scene.xml#toyblocks_scene" platform="data/platforms.xml#copter_linearpath" scanner="data/scanners_als.xml#riegl_vux-1uav">
</survey>
</document>
"""
with open(default_survey_path, "w") as f:
f.write(survey)
simBuilder = pyhelios.SimulationBuilder(default_survey_path, "assets/", "output/")
simBuilder.setCallbackFrequency(10)
simBuilder.setLasOutput(True)
simBuilder.setZipOutput(True)
simBuilder.setRebuildScene(True)
simB = simBuilder.build()
waypoints = [
[100.0, -100.0],
[-100.0, -100.0],
[-100.0, -50.0],
[100.0, -50.0],
[100.0, 0.0],
[-100.0, 0.0],
[-100.0, 50.0],
[100.0, 50.0],
[100.0, 100.0],
[-100.0, 100.0],
]
altitude = 100
speed = 150
pulse_freq = 300_000
scan_freq = 200
scan_angle = 37.5 / 180 * math.pi # convert to rad
shift = simB.sim.getScene().getShift()
for j, wp in enumerate(waypoints):
leg = simB.sim.newLeg(j)
leg.serialId = j # assigning a serialId is important!
leg.getPlatformSettings().x = wp[0] - shift.x # don't forget to apply the shift!
leg.getPlatformSettings().y = wp[1] - shift.y
leg.getPlatformSettings().z = altitude - shift.z
leg.getPlatformSettings().movePerSec = speed
leg.getScannerSettings().pulseFreq = pulse_freq
leg.getScannerSettings().scanFreq = scan_freq
leg.getScannerSettings().scanAngle = scan_angle
leg.getScannerSettings().trajectoryTimeInterval = 0.05 # important to get a trajectory output
if j % 2 != 0:
leg.getScannerSettings().active = False
We already learned how to find out the scene shift. There are many more functions to investigate and manipulate the scene.
scene = simB.sim.getScene()
shift = scene.getShift()
aabb = scene.getAABB() # get the axis aligned bounding box
# print AABB coordinates
minpos = aabb.getMinVertex().getPosition()
maxpos = aabb.getMaxVertex().getPosition()
print(f'minpos = ({minpos.x}, {minpos.y}, {minpos.y}), maxpos = ({maxpos.x}, {maxpos.y}, {maxpos.z})')
We can also create new triangles and detailed voxels
# To do
scene = simB.sim.getScene()
# access a primitive
prim0 = scene.getPrimitive(0) # get first primitive
# we can then access the centroid, the axis aligned bounding box (AABB), the material and more
centr0 = prim0.getCentroid() # coordinates can then be accessed as centr0.x, centr0.y, centr0z
print(f'prim0 centroid = ({centr0.x-shift.x}, {centr0.y-shift.y}, {centr0.z-shift.z})\n')
prim0_aabb = prim0.getAABB()
minpos_p0 = prim0_aabb.getMinVertex().getPosition()
maxpos_p0 = prim0_aabb.getMaxVertex().getPosition()
print(f'AABB of prim0:\n\tminpos = ({minpos_p0.x-shift.x}, {minpos_p0.y-shift.y}, {minpos_p0.z-shift.z}),'
f'\n\tmaxpos = ({maxpos_p0.x-shift.x}, {maxpos_p0.y-shift.y}, {maxpos_p0.z-shift.z})\n')
n_vertices = prim0.getNumVertices()
print(f'Number of vertices: {n_vertices}')
# Each vertex has a position and a normal
for i in range(n_vertices):
v = prim0.getVertex(i).getPosition()
n = prim0.getVertex(i).getNormal()
print(f'Position \tof vertex {i}:\t({v.x-shift.x:.3f}, {v.y-shift.y:.3f}, {v.z-shift.z:.3f})\n'
f'Normal \t\tof vertex {i}:\t({n.x:.3f}, {n.y:.3f}, {n.z:.3f})')
# To do: Update(), getIncidenceAngle(), getRayIntersection(), getRayIntersectionDistance()
scene = simB.sim.getScene()
# access a primitive
prim0 = scene.getPrimitive(0) # get first primitive
mat0 = prim0.getMaterial()
print(f'Material info:\n'
'name:\t\t{mat.name}\n'
'isGround:\t{mat.isGround}\n'
'filepath:\t{mat.matFilePath}\n'
'reflectance:\t{mat.reflectance}\n'
'specularity:\t{mat.specularity}\tspecular exponent:\t{mat.specularExponent}\n'
'classification:\t{mat.classification}\n'
'spectra:\t{mat.spectra}\n'
'ka0: {mat.ka0:.2f}\tkd0: {mat.kd0:.2f}\tks0: {mat.ks0:.2f}\n'
'ka1: {mat.ka1:.2f}\tkd1: {mat.kd1:.2f}\tks1: {mat.ks1:.2f}\n'
'ka2: {mat.ka2:.2f}\tkd2: {mat.kd2:.2f}\tks2: {mat.ks2:.2f}\n'
'ka3: {mat.ka3:.2f}\tkd3: {mat.kd3:.2f}\tks3: {mat.ks3:.2f}\n')
# To do