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)
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)
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.
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.