Skip to content

Python bindings ๐Ÿ Details

han16nah edited this page Aug 29, 2022 · 7 revisions

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.

Transformations

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)

Rotate

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)

Translate

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)

Scale

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.

The survey

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.

The Scanner

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

Scanner head

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

Beam deflector

deflector = scanner.getBeamDeflector()

Beam detector

detector = scanner.getDetector()
detector.accuracy
detector.rangeMin
detector.rangeMax

Scanner fullwave settings

Note, that these can be overwritten in the scannerSettings of a leg.

scanner.fwfSettings.binSize_ns
scanner.fwfSettings.winSize_ns
scanner.fwfSettings.beamSampleQuality

Legs

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

The scene

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

The primitive

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()

The Material

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')

The Scenepart

# To do