Simple SLAM scripting with the SDK

SLAM Changes in SDK 0.13
Ouster SDK 0.13 was released last week, and introduces numerous performance enhancements and bug fixes. It also comes with some breaking changes to the SLAMBackend interface in order to support multi-sensor SLAM processing.

Multi-Sensor SLAM
If you have multiple Ouster lidars, you can try out multi-sensor slam with ouster-cli - the command line interface that installs when you pip install ouster-sdk - with the following sequence in your terminal:

ouster-cli source --extrinsics-file FILENAME HOSTNAME_1, HOSTNAME_2 slam viz --accum-num 100
The extrinsics file is required to properly align the two sensors to a common coordinate frame. ouster-cli source --help can provide more info.

To learn more you can visit the SDK docs:
https://static.ouster.dev/sdk-docs/python/slam-api-example.html
https://static.ouster.dev/sdk-docs/cli/mapping-sessions.html#ouster-cli-mapping

If you need to download some sample data for a quick test, here are some links with download buttons in the visualizer:
https://static.ouster.dev/sensor-docs/#sample-data

Updated SLAM Script
Here is an updated version of the original SLAM script that works with 0.13. This script can be saved and run the same as before:

python ./slam_script.py FILE_OR_SENSOR To print slam progress to terminal
python ./slam_script.py --viz FILE_OR_SENSOR To visualize live with SimpleViz

from functools import partial
import numpy as np
import argparse

from ouster.sdk.client import SensorInfo, LidarScan, XYZLut, ScanSource, last_valid_column_pose, last_valid_column_ts, dewarp
from ouster.sdk.viz import SimpleViz
from ouster.sdk.open_source import open_source
from ouster.sdk.mapping.slam import KissBackend


# This iterator mimics a ScanSource object so that SimpleViz can replay the data while applying additional processing
class ScanIterator:
    def __init__(self, scans: ScanSource):
        self._metadata: SensorInfo = scans.metadata
        self._xyzlut = XYZLut(self._metadata, use_extrinsics=True)  # Create the XYZ lookup table
        # Instantiate the slam object
        self._slam = KissBackend([self._metadata])

        # Map the self._update function on to the scans iterator
        # the iterator will now run the self._update command before emitting the modified scan
        self._scans = map(partial(self._update), scans)  # Play on repeat

    @property
    def metadata(self) -> SensorInfo:
        return self._metadata

    # Return the scans iterator when instantiating the class
    def __iter__(self):
        return self._scans

    def _update(self, scan: LidarScan) -> LidarScan:
        # Run the slam update command to calculate column poses for the scan
        scan = self._slam.update([scan])[0]
        # scan.pose now contains an (N, 4, 4) array of pose transformations

        # To get the dewarped XYZ points, use the dewarp command and the poses calculated from SLAM. The following lines are provided only as an example and can be commented out.
        xyz = self._xyzlut(scan.field("RANGE"))
        xyz_dewarped = dewarp(xyz, scan.pose)
        # For fun, we can add this (m, n, 3) shaped array to the LidarScan for visualizing in SimpleViz
        scan.add_field("XYZ_DEWARPED", xyz_dewarped)
        return scan


if __name__ == "__main__":
    # parse the command arguments
    parser = argparse.ArgumentParser(prog='sdk slam demo',
                                     description='Runs a minimal demo of SDK slam post-processing')
    parser.add_argument('source', type=str, help='Sensor hostname or path to a sensor PCAP or OSF file')
    parser.add_argument('--viz', help='Visualize the result.', action='store_true', default=False)
    parser.add_argument('--accum-num', type=int, help='Number of scans to vizualize', default=50)
    args = parser.parse_args()

    # Open the file or live sensor
    # This interface supports multiple sensors at once so we supply the sensor_idx to get just one ScanSource back
    scans = open_source(source_url=args.source, sensor_idx=0)

    # One option: Pass an interator that applies the slam to SimpleViz for live viewing.
    if args.viz:
        scans = ScanIterator(scans)
        # Pass the scans iterator to SimpleViz
        SimpleViz(scans.metadata, rate=0, accum_max_num=args.accum_num).run(scans)

    # Alternative option: Run slam in a simple loop and print some information
    else:
        prev_scan_pose, prev_scan_time_sec = None, None
        slam = KissBackend([scans.metadata])

        for scan in scans:
            scan = slam.update([scan])[0]
            curr_scan_pose = last_valid_column_pose(scan)
            curr_scan_time = last_valid_column_ts(scan)/1e9
            if prev_scan_time_sec is None:
                prev_scan_pose = curr_scan_pose
                prev_scan_time_sec = curr_scan_time
                continue

            # Calculate and print the translation and linear velocity for each frame as an example
            delta_pose = np.linalg.inv(prev_scan_pose) @ curr_scan_pose
            delta_distance_m = np.linalg.norm(delta_pose[0:3, -1])
            delta_time_sec = curr_scan_time - prev_scan_time_sec

            prev_scan_pose = curr_scan_pose.copy()
            prev_scan_time_sec = curr_scan_time

            print(f"frame id {scan.frame_id}: {delta_distance_m:0.3f} meters moved at {delta_distance_m/delta_time_sec:0.3f}m/s")

1 Like