How to rotate Lidar scan to match the real world in SimpleViz live visualization

In our setup the Lidar sensor is tilted, so I want to apply a transformation to make the xy-plane of the lidar scan parallel to the real world ground plane in the SimpleViz live visualization code. I have tried some different approaches with no luck.
Here is my code for just starting a simpleViz, how would you implement the code for rotating the point cloud? :

import argparse
from functools import partial

from ouster.sdk import open_source
from ouster.sdk.client import LidarScan
from ouster.sdk.client import ScanSource, XYZLut
from ouster.sdk.viz import SimpleViz


class ScanIterator(ScanSource):
    def __init__(self, scans: ScanSource):
        self._metadata = scans.metadata
        self._xyzlut = XYZLut(self._metadata)
        self._scans = map(partial(self._process_scan), scans)

    def __iter__(self):
        return self._scans

    def _process_scan(self, scan: LidarScan) -> LidarScan:

        # TODO apply transformation to make the xy-plane of lidar scan parallel to the real world ground plane

        return scan


if __name__ == '__main__':
    parser = argparse.ArgumentParser(prog='Lidar Stream Viewer',
                                     description='Streams LiDAR data')
    parser.add_argument('source', type=str, help='Sensor hostname or path to a sensor PCAP or OSF file')
    args = parser.parse_args()

    scans = ScanIterator(open_source(args.source, sensor_idx=0, cycle=True))

    viz = SimpleViz(open_source(args.source).metadata, pause_at=1)

    viz.run(scans)

And an image of the live view:

I found out I can use extrinsics when loading the scans with the open_source() method. However, I had to manually make the transformation matrix and was not able to use the ones stored in metadata (metadata.lidar_to_sensor_transform and metadata.imu_to_sensor_transform).

The new code:

import argparse

import numpy as np
from ouster.sdk import open_source
from ouster.sdk.client import ScanSource, XYZLut
from ouster.sdk.viz import SimpleViz


class ScanIterator(ScanSource):
    def __init__(self, scans: ScanSource):
        self._metadata = scans.metadata
        self._xyzlut = XYZLut(self._metadata)
        # self._scans = map(partial(self._process_scan), scans)
        self._scans = iter(scans)

    def __iter__(self):
        return self

    def __next__(self) -> np.ndarray:
        """Process the next lidar scan and return rotated point cloud."""

        # skip first 10 scans
        for _ in range(10):
            next(self._scans)

        scan = next(self._scans)  # Get the next scan from the source

        return scan

    # def _process_scan(self, scan: LidarScan) -> LidarScan:
    #     # TODO apply transformation to make the xy-plane of lidar scan parallel to the real world ground plane
    #
    #     return scan


def rotation_matrix_y(theta):
    cos_theta = np.cos(theta)
    sin_theta = np.sin(theta)
    return np.array([
        [cos_theta, 0, sin_theta, 0],
        [0, 1, 0, 0],
        [-sin_theta, 0, cos_theta, 0],
        [0, 0, 0, 1]
    ])


if __name__ == '__main__':
    parser = argparse.ArgumentParser(prog='Lidar Stream Viewer',
                                     description='Streams LiDAR data')
    parser.add_argument('source', type=str, help='Sensor hostname or path to a sensor PCAP or OSF file')
    args = parser.parse_args()

    theta = np.radians(80)
    extrinsics = rotation_matrix_y(theta)

    raw_scans = open_source(args.source, sensor_idx=0, cycle=True, extrinsics=extrinsics)
    scans_iter = ScanIterator(raw_scans)

    viz = SimpleViz(raw_scans.metadata, pause_at=1)
    viz._scan_viz.toggle_scan_axis()

    viz.run(scans_iter)

Hi @TrainFog ,

Supplying the rotation matrix via open_source extrinsics param is the right way. The metadata.lidar_to_sensor_transform is internally applied in addition to the supplied extrinscs when constructing the XYZLut. So if you construct the XYZLut(metadata, use_extrinsics=True) you should get the points rotated properly.

1 Like

You can also overwrite the extrinisics matrix after calling open_source if that is convenient for whatever reason:

raw_scans = open_source(args.source, sensor_idx=0, cycle=True)
# Apply extrinsics after opening the file
raw_scans.metadata.extrinsics = rotation_matrix_y(theta)

Finally, if you wish to store the extrinsics information in the scan source, you can write the scans back out to an OSF file with the OSF Writer interface or use ouster-cli to write the scan source to a new OSF file using the

ouster-cli source --extrinsics [ARRAY] FILENAME save new_file.osf

You can also live record with the extrinsics information written with the same command, where the FILENAME is a live sensor.

2 Likes