ULS toyblocks

Notebook creator: Hannah Weiser, 2026

This demo uses a toy scene, which will be scanned by UAV-based laser scanning (ULS).

[1]:
import helios
import numpy as np
import matplotlib.pyplot as plt

Creating the virtual scene

[2]:
# load objs and create transformations
groundplane = helios.ScenePart.from_obj(
    "../data/sceneparts/basic/groundplane/groundplane.obj"
).scale(80)

parts = helios.ScenePart.from_objs("../data/sceneparts/toyblocks/*.obj")
parts = [p.translate([-40.0, 0, 0]) for p in parts]
# scene
scene = helios.StaticScene(scene_parts=[groundplane] + parts)

Platform and Scanner

[3]:
scanner = helios.scanner_from_name("riegl_vux_1uav22")
platform = helios.platform_from_name("quadcopter")

Scanner and platform settings

[4]:
# these scanner settings will be shared between all legs
scanner_settings = helios.ScannerSettings(
    pulse_frequency="50 kHz",
    scan_frequency="25 Hz",
    scan_angle="90 deg",
    head_rotation="0 deg/s",
    trajectory_time_interval="0.05 s",
)
z = 80.0

Survey Route

[5]:
survey = helios.Survey(scanner=scanner, platform=platform, scene=scene)
[6]:
waypoints = [
    [-70, -60, 10],
    [70, -60, 10],
    [70.0, 60.0, 7],
    [-70.0, 60.0, 10],
    [0.0, -60.0, 4],
    [0.0, 60.0, 10],
]
for x, y, speed in waypoints:
    survey.add_leg(x=x, y=y, z=z, speed_m_s=speed, scanner_settings=scanner_settings)

Running the survey

[7]:
points, trajectories = survey.run(
    verbosity=helios.LogVerbosity.VERBOSE, format=helios.OutputFormat.NPY
)
CRS bounding box (by vertices): Min: dvec3(-80.000000, -80.000000, -3.545861), Max: dvec3(80.000000, 80.000000, 44.024036)
Shift: dvec3(0.000000, 0.000000, 20.239088)
# vertices to translate: 57624
Actual bounding box (by vertices): Min: dvec3(-80.000000, -80.000000, -23.784949), Max: dvec3(80.000000, 80.000000, 23.784948)
Building KD-Grove...
KDTree (num. primitives 19208) :
        Max. # primitives in leaf: 298
        Min. # primitives in leaf: 1
        Max. depth reached: 42
        KDTree axis-aligned surface area: 81644.7
        Interior nodes: 55798
        Leaf nodes: 50334
        Total tree cost: 7.20382
KDGrove stats:
        Number of trees: 1
        Number of static trees: 1
        Number of dynamic trees: 0
        Statistics (min, max, total, mean, stdev):
                Building time: (0.0750, 0.0750, 0.0750, 0.0750, 0.0000)
                Tree primitives: (19208, 19208, 19208, 19208.0000, 0.0000)
                Max primitives in leaf: (298, 298, 298, 298.0000, 0.0000)
                Min primitives in leaf: (1, 1, 1, 1.0000, 0.0000)
                Maximum depth: (42, 42, 42, 42.0000, 0.0000)
                Axis-aligned surface area: (81644.7341, 81644.7341, 81644.7341, 81644.7341, 0.0000)
                Number of interior nodes: (55798, 55798, 55798, 55798.0000, 0.0000)
                Number of leaf nodes: (50334, 50334, 50334, 50334.0000, 0.0000)
                Tree cost: (7.2038, 7.2038, 7.2038, 7.2038, 0.0000)

KDG built in 0.075s
Reading Spectral Library...
10 materials found
Warning: material None of primitive 8Triangle (/home/runner/work/helios/helios/example_notebooks/../data/sceneparts/basic/groundplane/groundplane.mtl) has no spectral definition
Number of subsampling rays (riegl_vux-1uav22): 19
Simulation: Scanner changed!
Pulse frequency set to 50000
Scan angle set to 90
Applying settings for PolygonMirrorBeamDeflector...
Vertical angle min/max nan/nan degrees
 -- verticalAngleMin not set, using the value of -90 degrees
 -- verticalAngleMax not set, using the value of 90 degrees
SOURCE Leg with serial ID:0 waypoints:
        Origin: (-70, -60, 59.7609)
        Target: (70, -60, 59.7609)
        Next: (70, 60, 59.7609)

Starting simulation loop 1 ...
Waypoint reached!
User speed (movePerSec_m) reached.
Pulse frequency set to 50000
Scan angle set to 90
Applying settings for PolygonMirrorBeamDeflector...
Vertical angle min/max nan/nan degrees
 -- verticalAngleMin not set, using the value of -90 degrees
 -- verticalAngleMax not set, using the value of 90 degrees
SOURCE Leg with serial ID:1 waypoints:
        Origin: (70, -60, 59.7609)
        Target: (70, 60, 59.7609)
        Next: (-70, 60, 59.7609)

Waypoint reached!
User speed (movePerSec_m) reached.
Pulse frequency set to 50000
Scan angle set to 90
Applying settings for PolygonMirrorBeamDeflector...
Vertical angle min/max nan/nan degrees
 -- verticalAngleMin not set, using the value of -90 degrees
 -- verticalAngleMax not set, using the value of 90 degrees
SOURCE Leg with serial ID:2 waypoints:
        Origin: (70, 60, 59.7609)
        Target: (-70, 60, 59.7609)
        Next: (0, -60, 59.7609)

Waypoint reached!
User speed (movePerSec_m) reached.
Pulse frequency set to 50000
Scan angle set to 90
Applying settings for PolygonMirrorBeamDeflector...
Vertical angle min/max nan/nan degrees
 -- verticalAngleMin not set, using the value of -90 degrees
 -- verticalAngleMax not set, using the value of 90 degrees
SOURCE Leg with serial ID:3 waypoints:
        Origin: (-70, 60, 59.7609)
        Target: (0, -60, 59.7609)
        Next: (0, 60, 59.7609)

Waypoint reached!
User speed (movePerSec_m) reached.
Pulse frequency set to 50000
Scan angle set to 90
Applying settings for PolygonMirrorBeamDeflector...
Vertical angle min/max nan/nan degrees
 -- verticalAngleMin not set, using the value of -90 degrees
 -- verticalAngleMax not set, using the value of 90 degrees
SOURCE Leg with serial ID:4 waypoints:
        Origin: (0, -60, 59.7609)
        Target: (0, 60, 59.7609)
        Next: (0, 60, 59.7609)

Waypoint reached!
User speed (movePerSec_m) reached.
Pulse frequency set to 50000
Scan angle set to 90
Applying settings for PolygonMirrorBeamDeflector...
Vertical angle min/max nan/nan degrees
 -- verticalAngleMin not set, using the value of -90 degrees
 -- verticalAngleMax not set, using the value of 90 degrees
Waypoint reached!
Leg is too short to achieve the desired (movePerSec_m) speed.
Finishing simulation loop 1 ...
Finished simulation loop 1.
Elapsed simulation steps = 4672186
Elapsed virtual time = 93.4437 sec.
Main thread simulation loop finished in 6.91081 sec.
Waiting for completion of pulse computation tasks...
Pulse computation tasks finished in 6.91082 sec.

Visualizing the results

Color by Object ID

[8]:
fig = plt.figure(figsize=(15, 10))
# 3d plot
ax = fig.add_subplot(projection="3d", computed_zorder=False)

# settings for a discrete colorbar
N = 6
cmap = plt.get_cmap("jet", N)

# scatter plot of points
pos = points["position"]
sc = ax.scatter(
    pos[:, 0],
    pos[:, 1],
    pos[:, 2],
    c=points["hit_object_id"],
    cmap=cmap,
    s=0.02,
    zorder=1,
    vmin=-0.5,
    vmax=N - 0.5,
)

traj = trajectories["position"]
# Plot of trajectory
ax.plot(traj[:, 0], traj[:, 1], traj[:, 2], c="black", linewidth=2, zorder=2)

# Add axis labels.
ax.set_xlabel("$X$")
ax.set_ylabel("$Y$")
ax.set_zlabel("$Z$")

# set equal axes
box = (
    np.ptp(np.hstack((pos[:, 0], traj[:, 0]))),
    np.ptp(np.hstack((pos[:, 1], traj[:, 1]))),
    np.ptp(np.hstack((pos[:, 2], traj[:, 2]))),
)
ax.set_box_aspect(box)

# Set title.
ax.set_title(label="Point cloud and trajectory", fontsize=18)

cbar = plt.colorbar(sc, ticks=[0, 1, 2, 3, 4, 5])
cbar.set_label("Object Id", fontsize=15)
cbar.ax.set_yticklabels(["0", "1", "2", "3", "4", "5"])

# Display results
plt.show()
_images/04-uls_toyblocks_15_0.png
[ ]: