ULS Risley Prism Scanner

Notebook: Hannah Weiser & Sina Zumstein, 2023 - 2026

This demo scene showcases a toyblock scene scanned by a risley prism scanner mounted on a UAV.

[1]:
import helios

Creating the virtual scene

[2]:
# load objs and apply transformations
groundplane = (
    helios.ScenePart.from_obj("../data/sceneparts/basic/groundplane/groundplane.obj")
    .scale(70)
    .translate([20.0, 0.0, 0.0])
)
cube = helios.ScenePart.from_obj("../data/sceneparts/toyblocks/cube.obj")
cube2 = (
    helios.ScenePart.from_obj("../data/sceneparts/toyblocks/cube.obj")
    .scale(0.5)
    .rotate(axis=(0.0, 0.0, 1.0), angle=45)
    .translate([-45.0, 10.0, 10.0])
)
sphere = helios.ScenePart.from_obj("../data/sceneparts/toyblocks/sphere.obj").scale(0.5)
cylinder = helios.ScenePart.from_obj("../data/sceneparts/toyblocks/cylinder.obj")

# create scene
scene = helios.StaticScene(scene_parts=[groundplane, cube, cube2, sphere, cylinder])

Platform and scanner

[3]:
scanner = helios.scanner_from_name("livox_mid70")
platform = helios.platform_from_name("copter_linearpath")

The Livox Mid-70 is a scanner with two rotating risley prisms. For this deflector type, the scan pattern is controlled by the rotation speeds of two rotating risley prisms. This design on which the low-cost Livox scanners are based is described in detail in Liu et al. (2022).

For further reading on rotational risley prisms, see Duma & Schitea (2018).

Survey

[4]:
survey = helios.Survey(scanner=scanner, platform=platform, scene=scene)

scanner_settings = helios.ScannerSettings(
    pulse_frequency=100_000 * helios.units.Hz, trajectory_time_interval=0.05
)

survey.add_leg(x=-80, y=-50, z=35, speed_m_s=5, scanner_settings=scanner_settings)
survey.add_leg(x=80, y=-50, z=35, speed_m_s=5, scanner_settings=scanner_settings)
survey.add_leg(x=-80, y=50, z=35, speed_m_s=5, scanner_settings=scanner_settings)
survey.add_leg(x=80, y=50, z=35, speed_m_s=5, scanner_settings=scanner_settings)

Running the survey

[5]:
points, trajectories = survey.run(
    verbosity=helios.LogVerbosity.VERBOSE, format=helios.OutputFormat.NPY
)
CRS bounding box (by vertices): Min: dvec3(-50.000000, -70.000000, -0.233912), Max: dvec3(90.000000, 70.000000, 22.012018)
Shift: dvec3(20.000000, 0.000000, 10.889053)
# vertices to translate: 3330
Actual bounding box (by vertices): Min: dvec3(-70.000000, -70.000000, -11.122965), Max: dvec3(70.000000, 70.000000, 11.122965)
Building KD-Grove...
KDTree (num. primitives 1110) :
        Max. # primitives in leaf: 34
        Min. # primitives in leaf: 1
        Max. depth reached: 29
        KDTree axis-aligned surface area: 51657.7
        Interior nodes: 3925
        Leaf nodes: 3474
        Total tree cost: 6.58267
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.0050, 0.0050, 0.0050, 0.0050, 0.0000)
                Tree primitives: (1110, 1110, 1110, 1110.0000, 0.0000)
                Max primitives in leaf: (34, 34, 34, 34.0000, 0.0000)
                Min primitives in leaf: (1, 1, 1, 1.0000, 0.0000)
                Maximum depth: (29, 29, 29, 29.0000, 0.0000)
                Axis-aligned surface area: (51657.7208, 51657.7208, 51657.7208, 51657.7208, 0.0000)
                Number of interior nodes: (3925, 3925, 3925, 3925.0000, 0.0000)
                Number of leaf nodes: (3474, 3474, 3474, 3474.0000, 0.0000)
                Tree cost: (6.5827, 6.5827, 6.5827, 6.5827, 0.0000)

KDG built in 0.005s
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 (livox_mid-70): 19
Simulation: Scanner changed!
Pulse frequency set to 100000
Scan angle set to 0
It was not possible to determine attitude with a single computation at MovingPlatform::initLegManual
        angle = 3.14159 but it should be below 0.025
        Using iterative computation instead
Iterative mode was used for manual leg initialization because default one failed for MovingPlatform
SOURCE Leg with serial ID:0 waypoints:
        Origin: (-100, -50, 24.1109)
        Target: (60, -50, 24.1109)
        Next: (-100, 50, 24.1109)

Starting simulation loop 1 ...
Waypoint reached!
Pulse frequency set to 100000
Scan angle set to 0
SOURCE Leg with serial ID:1 waypoints:
        Origin: (60, -50, 24.1109)
        Target: (-100, 50, 24.1109)
        Next: (60, 50, 24.1109)

Waypoint reached!
Pulse frequency set to 100000
Scan angle set to 0
It was not possible to determine attitude with a single computation at MovingPlatform::initLegManual
        angle = 1.1172 but it should be below 0.025
        Using iterative computation instead
Iterative mode was used for manual leg initialization because default one failed for MovingPlatform
SOURCE Leg with serial ID:2 waypoints:
        Origin: (-100, 50, 24.1109)
        Target: (60, 50, 24.1109)
        Next: (60, 50, 24.1109)

Waypoint reached!
Pulse frequency set to 100000
Scan angle set to 0
Waypoint reached!
Finishing simulation loop 1 ...
Finished simulation loop 1.
Elapsed simulation steps = 10173598
Elapsed virtual time = 101.736 sec.
Main thread simulation loop finished in 31.4901 sec.
Waiting for completion of pulse computation tasks...
Pulse computation tasks finished in 31.4901 sec.

Visualizing the results

[6]:
import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(15, 12))
strip_1 = points[points["point_source_id"] == 0]["position"]
strip_2 = points[points["point_source_id"] == 1]["position"]
strip_3 = points[points["point_source_id"] == 2]["position"]
traj = trajectories["position"]

# view from above, colored by strip, including trajectory - show only every 20th measurement
ax.scatter(
    strip_1[::20, 0], strip_1[::20, 1], s=0.05, alpha=0.6
)  # select X and Y coordinates
ax.scatter(strip_2[::20, 0], strip_2[::20, 1], s=0.05, alpha=0.6)
ax.scatter(strip_3[::20, 0], strip_3[::20, 1], s=0.05, alpha=0.6)
ax.scatter(traj[:, 0], traj[:, 1], s=0.5, color="black")
ax.tick_params(labelsize=16)
ax.set_xlabel("X", fontsize=18)
ax.set_ylabel("Y", fontsize=18, rotation=0)

ax.set_title("Point cloud and trajectory (top view)", fontsize=18)

plt.axis("equal")
plt.show()
_images/10-uls_toyblocks_livox_12_0.png
[ ]: