Ground Segmentation

Ground Segmentation classifies lidar returns as ground or non-ground. The result is stored directly on each frame as a uint8 mask: 1 means ground and 0 means non-ground. The first-return mask is named GROUND and the optional second-return mask is named GROUND2.

Users can visualize the mask, remove ground points before object detection or mapping, or reuse it during point cloud alignment. Use the ground CLI command to annotate a stream of frames, or use GroundSegEngine to add the mask inside a Python or C++ processing pipeline.

When used with viz, the visualizer can highlight the segmented ground in the point cloud while keeping the image panels available for comparison:

A screenshot of Ouster Viz displaying ground segmentation results in the point cloud view

Ouster Viz displaying the output of the ground command.

Using the CLI

The chainable ground command segments every frame and makes the mask available to downstream commands such as viz:

ouster-cli source {OSF_PATH} ground viz

For the best results, ground expects the input sensor extrinsics to be aligned with gravity. If the recording contains IMU data from sensors running FW 3.2 or later, run plumb before ground to calculate the gravity-aligned extrinsics:

ouster-cli source {SOURCE_PATH} plumb ground viz

For moving recordings, especially handheld recordings, run slam before ground segmentation. SLAM supplies a pose for each frame column so the ground model is built in a consistent global frame:

ouster-cli source {SOURCE_PATH} plumb slam ground viz

Using the API

Create a GroundSegEngine and pass each FrameSet to update(). The engine modifies the frames in place but does not calculate gravity alignment. Before reading frames, assign each sensor a SensorInfo.sensor_to_body whose frame is aligned with gravity. In this frame, ground surface normals should point along the positive Z axis. The examples assume plumb_extrinsics contains these gravity-aligned transforms.

source = open_source(source_file)
for info, extrinsic in zip(source.sensor_info, plumb_extrinsics):
    info.sensor_to_body = extrinsic

frame_set = next(iter(source))
engine = GroundSegEngine.create()

engine.update(frame_set)
ground_mask = frame_set[0].field(  # type: ignore[union-attr]
    core.ChanField.GROUND)
auto source = open_source(source_file);
for (std::size_t i = 0; i < source.sensor_info().size(); ++i) {
    source.sensor_info()[i]->sensor_to_body = plumb_extrinsics[i];
}

auto frames = *source.begin();
auto engine = GroundSegEngine::create();

engine->update(frames);
auto ground_mask = frames[0]->field<uint8_t>(core::ChanField::GROUND);