This page will give an introduction on using pyhelios
to access and modify surveys.
pyhelios
allows you to:
import math
import pyhelios
import os
os.chdir("..")
pyhelios.logging_default()
# build simulation parameters
simBuilder = pyhelios.SimulationBuilder(
"data/surveys/toyblocks/als_toyblocks.xml", ["assets/"], "output/"
)
simBuilder.setNumThreads(0)
simBuilder.setLasOutput(True)
simBuilder.setZipOutput(True)
# build the survey
simB = simBuilder.build()
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.survey_path
survey = simB.sim.survey
survey_name = survey.name
print(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.length
survey.calculate_length()
print(survey.length)
Let's have a look at the scanner we are using.
scanner = simB.sim.scanner
# print scanner characteristics
print(scanner.to_string())
The scanner characteristics can also be accessed individually:
print(
f"""
{'Device ID:' : <25}{scanner.device_id : ^8}
{'Average power:' : <25}{scanner.average_power : <8} W
{'Beam divergence:' : <25}{scanner.beam_divergence : <8} rad
{'Wavelength:' : <25}{scanner.wavelength*1000000000 : <8} nm
{'Scanner visibility:' : <25}{scanner.visibility : <8} km
"""
)
The scanner has also some more properties:
if scanner.max_nor == 0:
max_nor = "unlimited"
else:
max_nor = scanner.max_nor
print(
f"""
{'Number of subsampling rays:' : <30}{scanner.num_rays}
{'Pulse length:' : <30}{scanner.pulse_length} ns
{'Supported pulse frequencies:' : <30}{list(scanner.supported_pulse_freqs_hz)} Hz
{'Maximum number of returns:': <30}{max_nor}
"""
)
We can also get information about the scanner head, e.g. the maximum rotation speed in case of TLS scanners.
To demonstrate this, let's load a TLS survey.
pyhelios.logging_default()
# build simulation parameters
simBuilder = pyhelios.SimulationBuilder(
"data/surveys/demo/tls_arbaro_demo.xml", ["assets/"], "output/"
)
simBuilder.setNumThreads(0)
simBuilder.setLasOutput(True)
simBuilder.setZipOutput(True)
# build the survey
simB = simBuilder.build()
scanner = simB.sim.scanner
print(scanner.to_string())
head = scanner.scanner_head
# get scanner rotation speed and range
print(
f"""
Max. rotation speed: {round(head.rotate_per_sec_max * 180 / math.pi)} degrees per second
"""
)
If we want to obtain information about the scanning mechanism, we have to get the beam deflector.
deflector = scanner.beam_deflector
print(
f"""
{'Scanner deflector type:': <25}{deflector.optics_type : <8}
{'Scan frequency range:' : <25}{deflector.scan_freq_min} - {deflector.scan_freq_max} Hz
{'Scan angle range:' : <25}{round(deflector.scan_angle_max * 180 / math.pi)}° FOV
"""
)
From the beam detector, we get information about, e.g., the accuracy of the scanner.
detector = scanner.detector
print(
f"""
{'Accuracy:' : <20}{detector.accuracy} m
{'Minimum range:' : <20}{detector.range_min} m
{'Maximum range:': <20}{detector.range_max} m
"""
)
We can also get the scanner full waveform settings.
Like many of the scanner settings, they can be overwritten in the scannerSettings
of a leg.
print(
f"""Full waveform settings for {scanner.device_id}
{'Bin size:' : <25}{scanner.FWF_settings.bin_size} ns
{'Window size:' : <25}{scanner.FWF_settings.win_size} ns
{'Beam sample quality:' : <25}{scanner.FWF_settings.beam_sample_quality}
"""
)
Each leg of a survey has scanner settings and platform settings, (cf. survey XML file),
which can be accessed and changed with pyhelios
.
# get the first leg
leg = simB.sim.get_leg(0)
# scanner settings
print(
f"""
{'Scanner is active:' : <30}{leg.scanner_settings.is_active}
{'Pulse frequency:' : <30}{leg.scanner_settings.pulse_frequency} Hz
{'Scan angle:' : <30}{leg.scanner_settings.scan_angle * 180 / math.pi}°
{'Minimum vertical angle:' : <30}{leg.scanner_settings.min_vertical_angle * 180 / math.pi:+.1f}°
{'Maximum vertical angle:' : <30}{round(leg.scanner_settings.max_vertical_angle * 180 / math.pi):+.1f}°
{'Scan frequency:' : <30}{leg.scanner_settings.scan_frequency} Hz
{'Beam divergence:' : <30}{leg.scanner_settings.beam_divergence_angle * 1000} mrad
{'Trajectory time interval:' : <30}{leg.scanner_settings.trajectory_time_interval} s
{'Start angle of head rotation:' : <30}{leg.scanner_settings.rotation_start_angle * 180 / math.pi}°
{'Start angle of head rotation:' : <30}{leg.scanner_settings.rotation_stop_angle * 180 / math.pi}°
{'Rotation speed:' : <30}{leg.scanner_settings.head_rotation * 180 / math.pi}° per s
"""
)
Scanner Settings and platform settings may be defined through a template. For this, let's first switch back to the ALS toyblocks demo.
simBuilder = pyhelios.SimulationBuilder(
"data/surveys/toyblocks/als_toyblocks.xml", ["assets/"], "output/"
)
simBuilder.setNumThreads(0)
simBuilder.setLasOutput(True)
simBuilder.setZipOutput(True)
simB = simBuilder.build()
The template can be accessed for a given ScannerSettings
or PlatformSettings
instance:
leg = simB.sim.get_leg(0)
ss = leg.scanner_settings
if ss.has_template():
ss_tmpl = ss.base_template
print(
f"""
Scanner template name: {ss_tmpl.id}
Pulse frequency: {ss_tmpl.pulse_frequency/1000} kHz
"""
) # Print the pulse frequency defined in the template
ps = leg.platform_settings
if ps.has_template():
ps_tmpl = ps.base_template
print(
f"""
{'Platform template name:' : <25}{ps_tmpl.id}
{'Speed:' : <15}{ps_tmpl.speed_m_s} m/s
{'Altitude:' : <15}{ps_tmpl.z} m
"""
)
We can also change the template.
ps_tmpl.z += 20
print(f"New altitude: {ps_tmpl.z} m")
We can also obtain the position at the current leg:
print(
f"""
On ground? {leg.platform_settings.is_on_ground}
Position: ({leg.platform_settings.x}, {leg.platform_settings.y}, {leg.platform_settings.z})
"""
)
If we compare the position here to the position in the XML survey file, we notice that they do not match. The difference is 50 in x direction and 70 in y direction.
When loading a survey, a shift is applied to the scene and to each leg. We can obtain this shift:
scene = simB.sim.scene
shift = scene.shift
print(f"Shift = ({shift[0]},{shift[1]},{shift[2]})")
Using a for-loop, we can get the positions of all legs. Note that we add the shift to obtain the true coordinates as specified in the XML-file:
for i in range(simB.sim.num_legs):
leg = simB.sim.get_leg(i)
print(
f"Leg {i}\tposition = "
f"{leg.platform_settings.x+shift[0]},"
f"{leg.platform_settings.y+shift[1]},"
f"{leg.platform_settings.z+shift[2]}\t"
f"active = {leg.scanner_settings.is_active}"
)
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 (data/surveys/default_survey.xml
) and
then create the legs with Python.
pyhelios.logging_default()
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.scene.shift
for j, wp in enumerate(waypoints):
leg = simB.sim.new_leg(j)
leg.serial_id = j # assigning a serialId is important!
leg.platform_settings.x = wp[0] - shift[0] # don't forget to apply the shift!
leg.platform_settings.y = wp[1] - shift[1]
leg.platform_settings.z = altitude - shift[2]
leg.platform_settings.speed_m_s = speed
leg.scanner_settings.pulse_frequency = pulse_freq
leg.scanner_settings.scan_frequency = scan_freq
leg.scanner_settings.scan_angle = scan_angle
leg.scanner_settings.trajectory_time_interval = (
0.05 # important to get a trajectory output
)
if j % 2 != 0:
leg.scanner_settings.is_active = False
Let's execute this survey!
import time
start_time = time.time()
simB.start()
if simB.isStarted():
print("Simulation is started!")
while simB.isRunning():
duration = time.time() - start_time
mins = duration // 60
secs = duration % 60
print(
"\r"
+ "Simulation is running since {} min and {} sec. Please wait.".format(
int(mins), int(secs)
),
end="",
)
time.sleep(1)
output = simB.join()
print("\nSimulation has finished.")
Now let us also quickly visualize the output. We load the points into numpy arrays using the function outputToNumpy
and then visualize the point cloud with matplotlib as a simple top view with points coloured by the hitObjectId
.
import matplotlib.pyplot as plt
pc, trajectory = pyhelios.outputToNumpy(output)
# Matplotlib figure.
fig = plt.figure(figsize=(5, 5))
# Axes3d axis onto mpl figure.
ax = fig.add_subplot()
# Scatter plot of original and simulated points in different colors
ax.scatter(pc[:, 0], pc[:, 1], c=pc[:, 14], s=0.01)
# Add axis labels.
ax.set_xlabel("$X$")
ax.set_ylabel("$Y$")
ax.axis("equal")
# Set title.
from textwrap import wrap
title = ax.set_title(
"\n".join(
wrap(
"Top view of the simulated point cloud, coloured by " + r"$hitObjectId$", 40
)
)
)
plt.show()